Explorar o código

add feature admin give province to sales & schedule notification

pearlgw hai 4 días
pai
achega
f72769e55a

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 14 - 0
catatan.txt


+ 1 - 8
config/config.ts

@@ -5,11 +5,4 @@ dotenv.config();
5 5
 export const config = {
6 6
   port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
7 7
   BASE_URL: process.env.BASE_URL || 'http://localhost:3200',
8
-};
9
-
10
-// require('dotenv').config();
11
-
12
-// module.exports = {
13
-//   port: process.env.PORT || 3000,
14
-//   BASE_URL: process.env.BASE_URL || 'http://localhost:3200'
15
-// };
8
+};

+ 1 - 12
config/keycloak.ts

@@ -7,15 +7,4 @@ export const CLIENT_ID = process.env.CLIENT_ID as string;
7 7
 export const CLIENT_SECRET = process.env.CLIENT_SECRET as string;
8 8
 export const JWT_SECRET = process.env.JWT_SECRET as string;
9 9
 export const KEYCLOAK_ADMIN_URL = process.env.KEYCLOAK_ADMIN_URL as string;
10
-export const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM as string;
11
-
12
-// require('dotenv').config();
13
-
14
-// module.exports = {
15
-//     KEYCLOAK_TOKEN_URL: process.env.KEYCLOAK_TOKEN_URL,
16
-//     CLIENT_ID: process.env.CLIENT_ID,
17
-//     CLIENT_SECRET: process.env.CLIENT_SECRET,
18
-//     JWT_SECRET: process.env.JWT_SECRET,
19
-//     KEYCLOAK_ADMIN_URL: process.env.KEYCLOAK_ADMIN_URL,
20
-//     KEYCLOAK_REALM: process.env.KEYCLOAK_REALM,
21
-// };
10
+export const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM as string;

+ 3 - 0
index.ts

@@ -17,8 +17,10 @@ import CategoryRoutes from './src/routes/admin/CategoryRoute';
17 17
 import ScheduleVisitationRoutes from './src/routes/admin/ScheduleVisitationRoute';
18 18
 import ScheduleVisitationForSalesRoutes from './src/routes/sales/ScheduleVisitationRoute';
19 19
 import salesRoutes from './src/routes/admin/SalesRoute';
20
+import userAreaRoutes from './src/routes/admin/UserAreaRoute';
20 21
 
21 22
 import './src/utils/Scheduler';
23
+import "./src/utils/ScheduleNotification";
22 24
 
23 25
 const app: Application = express();
24 26
 
@@ -47,6 +49,7 @@ app.use('/storage/', express.static(path.join(__dirname, 'storage/')));
47 49
 const apiV1 = express.Router();
48 50
 apiV1.use('/province', provinceRoutes);
49 51
 apiV1.use('/sales', salesRoutes);
52
+apiV1.use('/user-area', userAreaRoutes);
50 53
 apiV1.use('/city', cityRoutes);
51 54
 apiV1.use('/hospital', hospitalRoutes);
52 55
 apiV1.use('/hospital-area', salesHospitalRoutes);

+ 2 - 0
prisma/migrations/20250902131957_add_field_in_userkeycloak/migration.sql

@@ -0,0 +1,2 @@
1
+-- AlterTable
2
+ALTER TABLE "keycloak_users" ADD COLUMN     "phone" TEXT;

+ 2 - 0
prisma/migrations/20250903020800_add_role_in_userkeycloak/migration.sql

@@ -0,0 +1,2 @@
1
+-- AlterTable
2
+ALTER TABLE "keycloak_users" ADD COLUMN     "role" TEXT;

+ 2 - 0
prisma/schema.prisma

@@ -61,6 +61,8 @@ model User {
61 61
 model UserKeycloak {
62 62
   id                 String               @id @default(uuid())
63 63
   fullname           String
64
+  phone              String?
65
+  role               String?
64 66
   Hospital           Hospital[]
65 67
   UserArea           UserArea[]
66 68
   Vendor             Vendor[]

+ 1 - 63
src/controllers/admin/CityController.ts

@@ -57,66 +57,4 @@ export const deleteCity = async (req: Request, res: Response): Promise<Response>
57 57
     } catch (err) {
58 58
         return errorResponse(res, err);
59 59
     }
60
-};
61
-
62
-// const { CityCollection } = require('../../resources/admin/city/CityCollection.js');
63
-// const { CityResource } = require('../../resources/admin/city/CityResource.js');
64
-// const cityService = require('../../services/admin/CityService.js');
65
-// const { PaginationParam } = require('../../utils/PaginationParams.js');
66
-// const { errorResponse, messageSuccessResponse } = require('../../utils/Response.js');
67
-// const { validateStoreCityRequest, validateUpdateCityRequest } = require('../../validators/admin/city/CityValidators.js');
68
-
69
-// exports.getAllCity = async (req, res) => {
70
-//     try {
71
-//         const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
72
-
73
-//         const { cities, total } = await cityService.getAllCityService({
74
-//             page, limit, search, sortBy, orderBy
75
-//         });
76
-
77
-//         return CityCollection(req, res, cities, total, page, limit, 'City data successfully retrieved');
78
-//     } catch (err) {
79
-//         return errorResponse(res, err);
80
-//     }
81
-// };
82
-
83
-// exports.showCity = async (req, res) => {
84
-//     try {
85
-//         const id = req.params.id;
86
-//         const data = await cityService.showCityService(id);
87
-//         return CityResource(res, data, 'Success show city');
88
-//     } catch (err) {
89
-//         return errorResponse(res, err);
90
-//     }
91
-// };
92
-
93
-// exports.storeCity = async (req, res) => {
94
-//     try {
95
-//         const validatedData = validateStoreCityRequest(req.body);
96
-//         await cityService.storeCityService(validatedData, req);
97
-//         return messageSuccessResponse(res, 'Success added city', 201);
98
-//     } catch (err) {
99
-//         return errorResponse(res, err);
100
-//     }
101
-// }
102
-
103
-// exports.updateCity = async (req, res) => {
104
-//     try {
105
-//         const id = req.params.id;
106
-//         const validatedData = validateUpdateCityRequest(req.body);
107
-//         await cityService.updateCityService(validatedData, id, req);
108
-//         return messageSuccessResponse(res, 'Success update city');
109
-//     } catch (err) {
110
-//         return errorResponse(res, err);
111
-//     }
112
-// }
113
-
114
-// exports.deleteCity = async (req, res) => {
115
-//     try {
116
-//         const id = req.params.id;
117
-//         await cityService.deleteCityService(id, req);
118
-//         return messageSuccessResponse(res, 'Success delete city');
119
-//     } catch (err) {
120
-//         return errorResponse(res, err);
121
-//     }
122
-// };
60
+};

+ 33 - 1
src/controllers/admin/SalesController.ts

@@ -13,4 +13,36 @@ export const getAllSales = async (req: Request, res: Response): Promise<Response
13 13
     } catch (err) {
14 14
         return errorResponse(res, err);
15 15
     }
16
-};
16
+};
17
+
18
+// export const storeCity = async (req: Request, res: Response): Promise<Response> => {
19
+//     try {
20
+//         const validatedData = validateStoreCityRequest(req.body);
21
+//         await CityService.storeCityService(validatedData, req as CustomRequest);
22
+//         return messageSuccessResponse(res, 'Success added city', 201);
23
+//     } catch (err) {
24
+//         return errorResponse(res, err);
25
+//     }
26
+// };
27
+
28
+
29
+// export const showCity = async (req: Request, res: Response): Promise<Response> => {
30
+//     try {
31
+//         const id = req.params.id;
32
+//         const data = await CityService.showCityService(id);
33
+//         return CityResource(res, data, 'Success show city');
34
+//     } catch (err) {
35
+//         return errorResponse(res, err);
36
+//     }
37
+// };
38
+
39
+// export const updateCity = async (req: Request, res: Response): Promise<Response> => {
40
+//     try {
41
+//         const id = req.params.id;
42
+//         const validatedData = validateUpdateCityRequest(req.body);
43
+//         await CityService.updateCityService(validatedData, id, req as CustomRequest);
44
+//         return messageSuccessResponse(res, 'Success update city');
45
+//     } catch (err) {
46
+//         return errorResponse(res, err);
47
+//     }
48
+// };

+ 65 - 0
src/controllers/admin/UserAreaController.ts

@@ -0,0 +1,65 @@
1
+import { Request, Response } from 'express';
2
+import * as UserAreaService from '../../services/admin/UserAreaService';
3
+import { PaginationParam } from '../../utils/PaginationParams';
4
+import { errorResponse, messageSuccessResponse } from '../../utils/Response';
5
+import { UserAreaCollection } from '../../resources/admin/user_area/UserAreaCollection';
6
+import { CustomRequest } from '../../types/token/CustomRequest';
7
+import { validateStoreUserAreaRequest, validateUpdateUserAreaRequest } from '../../validators/admin/user_area/UserAreaValidators';
8
+import { UserAreaResource } from '../../resources/admin/user_area/UserAreaResource';
9
+
10
+export const getAllUserArea = async (req: Request, res: Response): Promise<Response | void> => {
11
+    try {
12
+        const { page, limit, search, sortBy, orderBy } = PaginationParam(req);
13
+
14
+        const { areas, total } = await UserAreaService.getAllUserAreaService(
15
+            { page, limit, search, sortBy, orderBy },
16
+        );
17
+
18
+        return UserAreaCollection(req, res, areas, total, page, limit, 'User area data successfully retrieved',);
19
+    } catch (err) {
20
+        return errorResponse(res, err);
21
+    }
22
+};
23
+
24
+
25
+export const CreateUserArea = async (req: Request, res: Response): Promise<Response> => {
26
+    try {
27
+        const validatedData = validateStoreUserAreaRequest(req.body);
28
+        await UserAreaService.storeUserAreaService(validatedData, req as CustomRequest);
29
+        return messageSuccessResponse(res, 'Success added user area', 201);
30
+    } catch (err) {
31
+        return errorResponse(res, err);
32
+    }
33
+};
34
+
35
+export const showUserArea = async (req: Request, res: Response): Promise<Response> => {
36
+    try {
37
+        const id = req.params.id;
38
+        const data = await UserAreaService.showUserAreaService(id);
39
+        return UserAreaResource(res, data, 'Success show user area');
40
+    } catch (err) {
41
+        return errorResponse(res, err);
42
+    }
43
+};
44
+
45
+export const updateUserArea = async (req: Request, res: Response): Promise<Response> => {
46
+    try {
47
+        const id = req.params.id;
48
+        const validatedData = validateUpdateUserAreaRequest(req.body);
49
+        await UserAreaService.updateUserAreaService(validatedData, id, req as CustomRequest);
50
+        return messageSuccessResponse(res, 'Success update user area');
51
+    } catch (err) {
52
+        return errorResponse(res, err);
53
+    }
54
+};
55
+
56
+
57
+export const DeleteUserArea = async (req: Request, res: Response): Promise<Response> => {
58
+    try {
59
+        const id = req.params.id;
60
+        await UserAreaService.deleteUserAreaService(id, req as CustomRequest);
61
+        return messageSuccessResponse(res, 'Success delete user area');
62
+    } catch (err) {
63
+        return errorResponse(res, err);
64
+    }
65
+};

+ 9 - 12
src/repository/admin/SalesRepository.ts

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

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

@@ -1,6 +1,5 @@
1 1
 import prisma from '../../prisma/PrismaClient';
2 2
 import { Prisma } from '@prisma/client';
3
-import { ScheduleVisitationDTO } from '../../types/admin/schedule_visitation/ScheduleVisitationDTO';
4 3
 
5 4
 interface FindAllParams {
6 5
     skip: number;

+ 94 - 0
src/repository/admin/UserAreaRepository.ts

@@ -0,0 +1,94 @@
1
+// AreaRepository.ts
2
+import prisma from '../../prisma/PrismaClient';
3
+import { Prisma } from '@prisma/client';
4
+
5
+interface FindAllParams {
6
+    skip?: number;
7
+    take?: number;
8
+    where?: Prisma.UserAreaWhereInput;
9
+    orderBy?: Prisma.UserAreaOrderByWithRelationInput;
10
+}
11
+
12
+const UserAreaRepository = {
13
+    findAll: async ({ skip, take, where, orderBy }: FindAllParams) => {
14
+        return await prisma.userArea.findMany({
15
+            where,
16
+            skip,
17
+            take,
18
+            orderBy,
19
+            select: {
20
+                id: true,
21
+                user: {
22
+                    select: {
23
+                        id: true,
24
+                        fullname: true,
25
+                        phone: true,
26
+                        role: true,
27
+                    },
28
+                },
29
+                province: {
30
+                    select: {
31
+                        id: true,
32
+                        name: true,
33
+                    },
34
+                },
35
+                createdAt: true,
36
+                updatedAt: true
37
+            },
38
+        });
39
+    },
40
+
41
+    countAll: async (where?: Prisma.UserAreaWhereInput) => {
42
+        return prisma.userArea.count({ where });
43
+    },
44
+
45
+    findById: async (id: string) => {
46
+        return prisma.userArea.findFirst({
47
+            where: {
48
+                id,
49
+                deletedAt: null,
50
+            },
51
+            select: {
52
+                id: true,
53
+                user: {
54
+                    select: {
55
+                        id: true,
56
+                        fullname: true,
57
+                        phone: true,
58
+                        role: true
59
+                    }
60
+                },
61
+                province: {
62
+                    select: {
63
+                        id: true,
64
+                        name: true
65
+                    }
66
+                },
67
+                createdAt: true,
68
+                updatedAt: true,
69
+            }
70
+        });
71
+    },
72
+
73
+    create: async (data: Prisma.UserAreaCreateInput) => {
74
+        return prisma.userArea.create({
75
+            data,
76
+        });
77
+    },
78
+
79
+    update: async (id: string, data: Prisma.UserAreaUpdateInput) => {
80
+        return prisma.userArea.update({
81
+            where: { id },
82
+            data,
83
+        });
84
+    },
85
+
86
+    deleteByUserId: async (userId: string) => {
87
+        return prisma.userArea.deleteMany({
88
+            where: { user_id: userId },
89
+        });
90
+    },
91
+
92
+};
93
+
94
+export default UserAreaRepository;

+ 1 - 28
src/resources/admin/sales/SalesCollection.ts

@@ -1,12 +1,9 @@
1 1
 import { Request, Response } from 'express';
2 2
 import { ListResponse } from '../../../utils/ListResponse';
3
-import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4 3
 import { SalesDTO } from '../../../types/admin/sales/SalesDTO';
5 4
 
6 5
 const formatItem = (item: SalesDTO) => ({
7 6
     ...item,
8
-    createdAt: formatISOWithoutTimezone(item.createdAt),
9
-    updatedAt: formatISOWithoutTimezone(item.updatedAt),
10 7
 });
11 8
 
12 9
 export const SalesCollection = (req: Request, res: Response, data: SalesDTO[] = [], total: number | null = null, page: number = 1, limit: number = 10, message: string = 'Success'): Response => {
@@ -29,28 +26,4 @@ export const SalesCollection = (req: Request, res: Response, data: SalesDTO[] =
29 26
         limit,
30 27
         message,
31 28
     });
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
-// };
29
+};

+ 32 - 0
src/resources/admin/user_area/UserAreaCollection.ts

@@ -0,0 +1,32 @@
1
+import { Request, Response } from 'express';
2
+import { ListResponse } from '../../../utils/ListResponse';
3
+import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
4
+import { UserAreaDTO } from '../../../types/admin/user_area/UserAreaDTO';
5
+
6
+const formatItem = (item: UserAreaDTO) => ({
7
+    ...item,
8
+    createdAt: formatISOWithoutTimezone(item.createdAt),
9
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
10
+});
11
+
12
+export const UserAreaCollection = (req: Request, res: Response, data: UserAreaDTO[] = [], 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
+};

+ 19 - 0
src/resources/admin/user_area/UserAreaResource.ts

@@ -0,0 +1,19 @@
1
+import { Response } from 'express';
2
+import { formatISOWithoutTimezone } from '../../../utils/FormatDate';
3
+import { UserAreaDTO } from '../../../types/admin/user_area/UserAreaDTO';
4
+
5
+const formatItem = (item: UserAreaDTO) => ({
6
+    ...item,
7
+    createdAt: formatISOWithoutTimezone(item.createdAt),
8
+    updatedAt: formatISOWithoutTimezone(item.updatedAt),
9
+});
10
+
11
+export const UserAreaResource = (res: Response, data: UserAreaDTO, message: string = 'Success'): Response => {
12
+    const formattedData = formatItem(data);
13
+
14
+    return res.status(200).json({
15
+        success: true,
16
+        message,
17
+        data: formattedData,
18
+    });
19
+};

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

@@ -0,0 +1,15 @@
1
+import express, { Router } from 'express';
2
+import * as UserAreaController from '../../controllers/admin/UserAreaController';
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"])], UserAreaController.getAllUserArea);
10
+router.post('/', [keycloak.protect(), extractToken, checkRoles(["admin"])], UserAreaController.CreateUserArea);
11
+router.get('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], UserAreaController.showUserArea);
12
+router.patch('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], UserAreaController.updateUserArea);
13
+router.delete('/:id', [keycloak.protect(), extractToken, checkRoles(["admin"])], UserAreaController.DeleteUserArea);
14
+
15
+export default router;

+ 1 - 2
src/services/admin/SalesService.ts

@@ -13,9 +13,8 @@ interface GetAllSalesParams {
13 13
 export const getAllSalesService = async ({ page, limit, search = '', sortBy, orderBy }: GetAllSalesParams) => {
14 14
     const skip = (page - 1) * limit;
15 15
 
16
-    const where: Prisma.UserWhereInput = {
16
+    const where: Prisma.UserKeycloakWhereInput = {
17 17
         ...SearchFilter(search, ['id', 'firstname', 'lastname']),
18
-        deletedAt: null,
19 18
     };
20 19
 
21 20
     const [sales, total] = await Promise.all([

+ 2 - 2
src/services/admin/ScheduleVisitationService.ts

@@ -89,7 +89,7 @@ export const getAllScheduleVisitationService = async ({ page, limit, search, sor
89 89
 export const storeScheduleVisitationService = async (validateData: ScheduleVisitationRequestDTO, req: CustomRequest) => {
90 90
     const creatorId = req.tokenData.sub;
91 91
 
92
-    const sales = await prisma.user.findFirst({
92
+    const sales = await prisma.userKeycloak.findFirst({
93 93
         where: {
94 94
             id: validateData.sales_id,
95 95
         }
@@ -150,7 +150,7 @@ export const updateScheduleVisitationService = async (validateData: Partial<Sche
150 150
     }
151 151
 
152 152
     if (validateData.sales_id) {
153
-        const sales = await prisma.user.findFirst({ where: { id: validateData.sales_id } });
153
+        const sales = await prisma.userKeycloak.findFirst({ where: { id: validateData.sales_id } });
154 154
         if (!sales) throw new HttpException("Sales user not found", 404);
155 155
 
156 156
         payload.sales_visit = { connect: { id: validateData.sales_id } };

+ 133 - 0
src/services/admin/UserAreaService.ts

@@ -0,0 +1,133 @@
1
+import { Prisma } from '@prisma/client';
2
+import prisma from '../../prisma/PrismaClient';
3
+import ProvinceRepository from '../../repository/admin/ProvinceRepository';
4
+import UserAreaRepository from '../../repository/admin/UserAreaRepository';
5
+import { RequestUpdateUserAreaDTO, RequestUserAreaDTO } from '../../types/admin/user_area/UserAreaDTO';
6
+import { CustomRequest } from '../../types/token/CustomRequest';
7
+import { HttpException } from '../../utils/HttpException';
8
+import { createLog, deleteLog, updateLog } from '../../utils/LogActivity';
9
+import { SearchFilter } from '../../utils/SearchFilter';
10
+import { now } from '../../utils/TimeLocal';
11
+
12
+interface GetAllAreaParams {
13
+    page: number;
14
+    limit: number;
15
+    search?: string;
16
+    sortBy: string;
17
+    orderBy: 'asc' | 'desc';
18
+}
19
+
20
+export const getAllUserAreaService = async ({ page, limit, search, sortBy, orderBy }: GetAllAreaParams): Promise<{ areas: any[]; total: number; }> => {
21
+    const skip = (page - 1) * limit;
22
+
23
+    const where = {
24
+        deletedAt: null,
25
+        ...SearchFilter(search, ['user.fullname', 'province.name']),
26
+    };
27
+
28
+    const [areas, total] = await Promise.all([
29
+        UserAreaRepository.findAll({
30
+            skip,
31
+            take: limit,
32
+            where,
33
+            orderBy: { [sortBy]: orderBy },
34
+        }),
35
+        UserAreaRepository.countAll(where),
36
+    ]);
37
+
38
+    return { areas, total };
39
+};
40
+
41
+export const storeUserAreaService = async (validateData: RequestUserAreaDTO, req: CustomRequest) => {
42
+    const { user_id, province_id } = validateData;
43
+
44
+    const userKeycloak = await prisma.userKeycloak.findFirst({ where: { id: user_id } });
45
+    if (!userKeycloak) {
46
+        throw new HttpException('User not found', 404);
47
+    }
48
+
49
+    // cek semua province id valid
50
+    for (const pid of province_id) {
51
+        const province = await ProvinceRepository.findById(pid);
52
+        if (!province) {
53
+            throw new HttpException(`Province with id ${pid} not found`, 404);
54
+        }
55
+    }
56
+
57
+    // simpan banyak province untuk user
58
+    const results = await Promise.all(
59
+        province_id.map(pid =>
60
+            UserAreaRepository.create({
61
+                user: { connect: { id: user_id } },
62
+                province: { connect: { id: pid } },
63
+            })
64
+        )
65
+    );
66
+
67
+    // bentuk object gabungan untuk response + log
68
+    const data = {
69
+        user_id,
70
+        provinces: province_id,
71
+        createdAt: results[0]?.createdAt,
72
+        updatedAt: results[0]?.updatedAt,
73
+    };
74
+
75
+    await createLog(req, data);
76
+};
77
+
78
+export const showUserAreaService = async (id: string) => {
79
+    const user_area = await UserAreaRepository.findById(id);
80
+    if (!user_area) {
81
+        throw new HttpException('Data user area not found', 404);
82
+    }
83
+    return user_area;
84
+};
85
+
86
+export const updateUserAreaService = async (validateData: Partial<RequestUpdateUserAreaDTO>, id: string, req: CustomRequest) => {
87
+    const user_area = await UserAreaRepository.findById(id);
88
+    if (!user_area) {
89
+        throw new HttpException('Data user area not found', 404);
90
+    }
91
+
92
+    const updatePayload = validateData;
93
+
94
+    if (updatePayload.user_id) {
95
+        const userKeycloak = await prisma.userKeycloak.findFirst({ where: { id: updatePayload.user_id } });
96
+        if (!userKeycloak) {
97
+            throw new HttpException('User not found', 404);
98
+        }
99
+    }
100
+
101
+    if (updatePayload.province_id) {
102
+        const province = await prisma.province.findFirst({ where: { id: updatePayload.province_id } });
103
+        if (!province) {
104
+            throw new HttpException('Province not found', 404);
105
+        }
106
+    }
107
+
108
+    const prismaPayload: Prisma.UserAreaUpdateInput = {};
109
+    if (validateData.user_id) {
110
+        prismaPayload.user = { connect: { id: validateData.user_id } };
111
+    }
112
+    if (validateData.province_id) {
113
+        prismaPayload.province = { connect: { id: validateData.province_id } };
114
+    }
115
+
116
+    const data = await UserAreaRepository.update(id, prismaPayload);
117
+
118
+    await updateLog(req, data);
119
+};
120
+
121
+export const deleteUserAreaService = async (id: string, req: CustomRequest) => {
122
+    const user_area = await UserAreaRepository.findById(id);
123
+    if (!user_area) {
124
+        throw new HttpException('User area not found', 404);
125
+    }
126
+
127
+    const data = await UserAreaRepository.update(id, {
128
+        deletedAt: now().toDate(),
129
+    });
130
+
131
+    await deleteLog(req, data);
132
+    return data;
133
+};

+ 1 - 10
src/services/sales/ScheduleVisitationService.ts

@@ -93,16 +93,7 @@ export const getAllScheduleVisitationService = async (req: CustomRequest,
93 93
 
94 94
 export const storeScheduleVisitationService = async (validateData: ScheduleVisitationRequestDTO, req: CustomRequest) => {
95 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
-
96
+    
106 97
     const hospital = await HospitalRepository.findById(validateData.hospital_id);
107 98
     if (!hospital) {
108 99
         throw new HttpException('Hospital not found', 404);

+ 3 - 6
src/types/admin/sales/SalesDTO.ts

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

+ 25 - 0
src/types/admin/user_area/UserAreaDTO.ts

@@ -0,0 +1,25 @@
1
+export interface UserAreaDTO {
2
+    id: string;
3
+    user: {
4
+        id: string;
5
+        fullname: string;
6
+        phone: string | null;
7
+        role: string | null;
8
+    }
9
+    province: {
10
+        id: string;
11
+        name: string;
12
+    };
13
+    createdAt: Date;
14
+    updatedAt: Date;
15
+}
16
+
17
+export interface RequestUserAreaDTO {
18
+    user_id: string;
19
+    province_id: string[];
20
+}
21
+
22
+export interface RequestUpdateUserAreaDTO {
23
+    user_id?: string;
24
+    province_id?: string;
25
+}

+ 1 - 48
src/utils/LogActivity.ts

@@ -55,51 +55,4 @@ export const createLog = (req: CustomRequest, data: LogData) => baseLog({ req, a
55 55
 export const updateLog = (req: CustomRequest, data: LogData) => baseLog({ req, action: 'update', data });
56 56
 export const deleteLog = (req: CustomRequest, data: LogData) => baseLog({ req, action: 'delete', data });
57 57
 export const loginLog = (req: CustomRequest, data: LogData) => baseLog({ req, action: 'login', data });
58
-export const logoutLog = (req: CustomRequest, data: LogData) => baseLog({ req, action: 'logout', data });
59
-
60
-
61
-// const jwt = require('jsonwebtoken');
62
-// const prisma = require('../prisma/PrismaClient.js');
63
-// const timeLocal = require('../utils/TimeLocal.js');
64
-// const { getUserNameById } = require('./CheckUserKeycloak.js');
65
-
66
-// const baseLog = async ({ req, action, data }) => {
67
-//     try {
68
-//         let userId, username;
69
-
70
-//         // Ambil userId dari token Keycloak
71
-//         if (req?.tokenData?.sub) {
72
-//             userId = req.tokenData.sub;
73
-//             username = await getUserNameById(userId);
74
-//         }
75
-
76
-//         // Fallback kalau tokenData tidak ada
77
-//         if (!userId || !username) {
78
-//             userId = data?.id;
79
-//             username = data?.username;
80
-//         }
81
-
82
-//         if (!userId || !username) return;
83
-
84
-//         await prisma.activityLog.create({
85
-//             data: {
86
-//                 user_id: userId,
87
-//                 username,
88
-//                 action: JSON.stringify({ [action]: data }),
89
-//                 createdAt: timeLocal.now().toDate(),
90
-//                 updatedAt: timeLocal.now().toDate(),
91
-//                 deletedAt: null,
92
-//             }
93
-//         });
94
-//     } catch (err) {
95
-//         console.error('Failed to log activity:', err.message);
96
-//     }
97
-// };
98
-
99
-// const createLog = (req, data) => baseLog({ req, action: 'create', data });
100
-// const updateLog = (req, data) => baseLog({ req, action: 'update', data });
101
-// const deleteLog = (req, data) => baseLog({ req, action: 'delete', data });
102
-// const loginLog = (req, data) => baseLog({ req, action: 'login', data });
103
-// const logoutLog = (req, data) => baseLog({ req, action: 'logout', data });
104
-
105
-// module.exports = { createLog, updateLog, deleteLog, loginLog, logoutLog };
58
+export const logoutLog = (req: CustomRequest, data: LogData) => baseLog({ req, action: 'logout', data });

+ 154 - 0
src/utils/ScheduleNotification.ts

@@ -0,0 +1,154 @@
1
+import 'dotenv/config'
2
+import cron from "node-cron";
3
+import dayjs from "dayjs";
4
+import axios from "axios";
5
+import prisma from "../prisma/PrismaClient";
6
+import { formatISOWithoutTimezone } from './FormatDate';
7
+
8
+const queue: (() => Promise<void>)[] = [];
9
+let processing = false;
10
+
11
+const API_URL_NOTIFICATION = process.env.API_TOKEN_NOTIFICATION;
12
+const API_TOKEN_NOTIFICATTION = process.env.API_TOKEN_NOTIFICATTION;
13
+
14
+const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
15
+
16
+const randomDelay = () => {
17
+    const min = 60 * 1000;  // 60 detik
18
+    const max = 180 * 1000; // 180 detik
19
+    return Math.floor(Math.random() * (max - min + 1)) + min;
20
+};
21
+
22
+// Worker untuk memproses queue
23
+const processQueue = async () => {
24
+    if (processing || queue.length === 0) return;
25
+    processing = true;
26
+
27
+    while (queue.length > 0) {
28
+        console.log(`▶️ Mulai memproses antrean, sisa job: ${queue.length}`);
29
+        const job = queue.shift();
30
+        if (job) {
31
+            try {
32
+                await job();
33
+                if (queue.length > 0) {
34
+                    const delayMs = randomDelay();
35
+                    console.log(`⏳ Tunggu ${Math.round(delayMs / 1000)} detik sebelum job berikutnya...`);
36
+                    await delay(delayMs);
37
+                }
38
+            } catch (err: any) {
39
+                console.error("❌ Job gagal:", err.message);
40
+            }
41
+        }
42
+    }
43
+
44
+    console.log("✅ Semua job selesai diproses.");
45
+    processing = false;
46
+};
47
+
48
+// Tambah job ke antrean
49
+const addToQueue = (fn: () => Promise<void>) => {
50
+    queue.push(fn);
51
+    console.log(`➕ Job ditambahkan ke antrean. Total sekarang: ${queue.length}`);
52
+    console.log("📦 Isi antrean saat ini:", queue.map((_, i) => `Job#${i + 1}`));
53
+    processQueue();
54
+};
55
+
56
+// Cron: jalan tiap hari
57
+cron.schedule("00 08 * * *", async () => {
58
+    console.log("⏰ [CRON] Checking schedules for today...");
59
+
60
+    try {
61
+        const today = dayjs().startOf("day").toDate();
62
+        const tomorrow = dayjs().add(1, "day").startOf("day").toDate();
63
+
64
+        // Ambil semua schedule untuk hari ini
65
+        const schedules = await prisma.scheduleVisitation.findMany({
66
+            where: {
67
+                deletedAt: null,
68
+                date_visit: {
69
+                    gte: today,
70
+                    lt: tomorrow,
71
+                },
72
+            },
73
+            include: {
74
+                sales_visit: true,
75
+                hospital: {
76
+                    include: {
77
+                        province: true,
78
+                        city: true,
79
+                    }
80
+                }
81
+            },
82
+        });
83
+
84
+        if (schedules.length === 0) {
85
+            console.log("ℹ️ No schedules for today.");
86
+            return;
87
+        }
88
+
89
+        console.log(`📌 Ditemukan ${schedules.length} schedule untuk hari ini:`);
90
+        schedules.forEach((s, idx) => {
91
+            console.log(`   #${idx + 1} SalesID=${s.sales_id}, Phone=${s.sales_visit?.phone ?? "❌ kosong"}`);
92
+        });
93
+
94
+        schedules.forEach((schedule, idx) => {
95
+            const phone = schedule.sales_visit?.phone as string | undefined;
96
+            if (!phone) {
97
+                console.warn(`⚠️ Sales ${schedule.sales_id} tidak punya nomor telepon.`);
98
+                return;
99
+            }
100
+
101
+            const fullname = schedule.sales_visit?.fullname || '-';
102
+            const hospitalName = schedule.hospital?.name || '-';
103
+            const hospitalAddress = schedule.hospital?.address || '-';
104
+            const hospitalContact = schedule.hospital?.contact || '-';
105
+            const hospitalEmail = schedule.hospital?.email || '-';
106
+            const hospitalLat = schedule.hospital?.latitude || '-';
107
+            const hospitalLong = schedule.hospital?.longitude || '-';
108
+            const hospitalGmaps = schedule.hospital?.gmaps_url || '-';
109
+            const hospitalProv = schedule.hospital?.province.name || '-';
110
+            const hospitalCity = schedule.hospital?.city.name || '-';
111
+            const scheduleCreate = schedule.createdAt || '-';
112
+
113
+            const message = `
114
+Halo ${fullname},
115
+
116
+[[[ maaf, ini sedang testing - gayuh ]]]
117
+Ini adalah pengingat jadwal kunjungan yang dibuat pada tanggal ${formatISOWithoutTimezone(scheduleCreate)}.
118
+
119
+Anda dijadwalkan untuk mengunjungi:
120
+Rumah Sakit: ${hospitalName}
121
+Alamat: ${hospitalAddress}
122
+Kota: ${hospitalCity}, Provinsi: ${hospitalProv}
123
+Kontak: ${hospitalContact}, Email: ${hospitalEmail}
124
+Lokasi Google Maps: ${hospitalGmaps}
125
+Koordinat: Latitude ${hospitalLat}, Longitude ${hospitalLong}
126
+
127
+Nomor telepon sales terkait: ${phone}
128
+
129
+Pastikan semua persiapan kunjungan sudah lengkap! Semangat bertugas!
130
+`.trim();
131
+
132
+            addToQueue(async () => {
133
+                console.log(
134
+                    `[${dayjs().format("HH:mm:ss")}] 📩 (Job#${idx + 1}) Sending message to ${phone}...`
135
+                );
136
+                await axios.post(
137
+                    API_URL_NOTIFICATION!,
138
+                    { to: phone, message },
139
+                    {
140
+                        headers: {
141
+                            "x-api-token": API_TOKEN_NOTIFICATTION!,
142
+                            "Content-Type": "application/json",
143
+                        },
144
+                    }
145
+                );
146
+                console.log(
147
+                    `[${dayjs().format("HH:mm:ss")}] ✔️ (Job#${idx + 1}) Sent message to ${phone}`
148
+                );
149
+            });
150
+        });
151
+    } catch (err: any) {
152
+        console.error("❌ Cron job error:", err.message);
153
+    }
154
+});

+ 32 - 72
src/utils/Scheduler.ts

@@ -1,73 +1,3 @@
1
-// const cron = require('node-cron');
2
-// const axios = require('axios');
3
-// const dayjs = require('dayjs');
4
-// const prisma = require('../prisma/PrismaClient.js');
5
-
6
-// const getAdminToken = async () => {
7
-//     const response = await axios.post(`${process.env.KEYCLOAK_URL}/realms/master/protocol/openid-connect/token`, new URLSearchParams({
8
-//         client_id: 'admin-cli',
9
-//         username: process.env.KEYCLOAK_ADMIN_USERNAME,
10
-//         password: process.env.KEYCLOAK_ADMIN_PASSWORD,
11
-//         grant_type: 'password'
12
-//     }), {
13
-//         headers: {
14
-//             'Content-Type': 'application/x-www-form-urlencoded'
15
-//         }
16
-//     });
17
-
18
-//     return response.data.access_token;
19
-// };
20
-
21
-// async function getAllUsers(token) {
22
-//     const res = await axios.get(`${process.env.KEYCLOAK_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users`, {
23
-//         headers: {
24
-//             Authorization: `Bearer ${token}`
25
-//         }
26
-//     });
27
-
28
-//     return res.data;
29
-// }
30
-
31
-// async function syncUserToDB(user) {
32
-//     const fullname = `${user.firstName || ''} ${user.lastName || ''}`.trim();
33
-//     const existing = await prisma.userKeycloak.findUnique({ where: { id: user.id } });
34
-//     if (!existing) {
35
-//         await prisma.userKeycloak.create({
36
-//             data: {
37
-//                 id: user.id,
38
-//                 fullname
39
-//             }
40
-//         });
41
-//         console.log('✔️ Synced user:', fullname);
42
-//     }
43
-// }
44
-
45
-// // Cron job setiap 1 menit
46
-// cron.schedule('* * * * *', async () => {
47
-//     console.log('[SYNC] Checking for new users...');
48
-
49
-//     try {
50
-//         const token = await getAdminToken();
51
-//         const users = await getAllUsers(token);
52
-
53
-//         const now = dayjs();
54
-//         const oneMinuteAgo = now.subtract(1, 'minute');
55
-
56
-//         // Filter user yang dibuat dalam 1 menit terakhir
57
-//         const newUsers = users.filter(u => u.createdTimestamp && dayjs(u.createdTimestamp).isAfter(oneMinuteAgo));
58
-
59
-//         for (const user of newUsers) {
60
-//             await syncUserToDB(user);
61
-//         }
62
-
63
-//         if (newUsers.length === 0) {
64
-//             console.log('ℹ️ No new users.');
65
-//         }
66
-//     } catch (err) {
67
-//         console.error('❌ Sync error:', err.message);
68
-//     }
69
-// });
70
-
71 1
 import cron from 'node-cron';
72 2
 import axios from 'axios';
73 3
 import dayjs from 'dayjs';
@@ -79,6 +9,17 @@ interface KeycloakUser {
79 9
     firstName?: string;
80 10
     lastName?: string;
81 11
     createdTimestamp?: number;
12
+    attributes?: {
13
+        phone?: string[];
14
+    };
15
+}
16
+interface KeycloakRole {
17
+    id: string;
18
+    name: string;
19
+    description?: string;
20
+    composite?: boolean;
21
+    clientRole?: boolean;
22
+    containerId?: string;
82 23
 }
83 24
 
84 25
 const getAdminToken = async (): Promise<string> => {
@@ -113,8 +54,25 @@ const getAllUsers = async (token: string): Promise<KeycloakUser[]> => {
113 54
     return res.data;
114 55
 };
115 56
 
116
-const syncUserToDB = async (user: KeycloakUser): Promise<void> => {
57
+const getUserRoles = async (token: string, userId: string): Promise<KeycloakRole[]> => {
58
+    const res = await axios.get(
59
+        `${process.env.KEYCLOAK_URL}/admin/realms/${process.env.KEYCLOAK_REALM}/users/${userId}/role-mappings/realm`,
60
+        {
61
+            headers: {
62
+                Authorization: `Bearer ${token}`,
63
+            },
64
+        }
65
+    );
66
+
67
+    return res.data;
68
+};
69
+
70
+const syncUserToDB = async (user: KeycloakUser, token: string): Promise<void> => {
117 71
     const fullname = `${user.firstName || ''} ${user.lastName || ''}`.trim();
72
+    const phone = user.attributes?.phone?.[0] || null;
73
+
74
+    const roles = await getUserRoles(token, user.id);
75
+    const role = roles[0]?.name || null;
118 76
 
119 77
     const existing = await prisma.userKeycloak.findUnique({
120 78
         where: { id: user.id },
@@ -125,6 +83,8 @@ const syncUserToDB = async (user: KeycloakUser): Promise<void> => {
125 83
             data: {
126 84
                 id: user.id,
127 85
                 fullname,
86
+                phone,
87
+                role
128 88
             },
129 89
         });
130 90
         console.log('✔️ Synced user:', fullname);
@@ -149,7 +109,7 @@ cron.schedule('* * * * *', async () => {
149 109
         );
150 110
 
151 111
         for (const user of newUsers) {
152
-            await syncUserToDB(user);
112
+            await syncUserToDB(user, token);
153 113
         }
154 114
 
155 115
         if (newUsers.length === 0) {

+ 32 - 0
src/validators/admin/user_area/UserAreaValidators.ts

@@ -0,0 +1,32 @@
1
+import Joi from 'joi';
2
+import { validateWithSchema } from '../../ValidateSchema';
3
+import { RequestUpdateUserAreaDTO, RequestUserAreaDTO } from '../../../types/admin/user_area/UserAreaDTO';
4
+
5
+const storeUserAreaSchema = Joi.object<RequestUserAreaDTO>({
6
+    user_id: Joi.string().trim().required().messages({
7
+        'string.empty': 'User ID is required',
8
+    }),
9
+    province_id: Joi.array().items(
10
+        Joi.string().trim().required()
11
+    ).min(1).required().messages({
12
+        'array.base': 'Province ID must be an array',
13
+        'array.min': 'At least one Province ID is required',
14
+    }),
15
+});
16
+
17
+const updateUserAreaSchema = Joi.object<Partial<RequestUpdateUserAreaDTO>>({
18
+    user_id: Joi.string().trim().optional().messages({
19
+        'string.empty': 'User ID is required',
20
+    }),
21
+    province_id: Joi.string().trim().optional().messages({
22
+        'string.empty': 'Province ID is required',
23
+    }),
24
+});
25
+
26
+export const validateStoreUserAreaRequest = (body: unknown): RequestUserAreaDTO => {
27
+    return validateWithSchema<RequestUserAreaDTO>(storeUserAreaSchema, body);
28
+};
29
+
30
+export const validateUpdateUserAreaRequest = (body: unknown): Partial<RequestUpdateUserAreaDTO> => {
31
+    return validateWithSchema<Partial<RequestUpdateUserAreaDTO>>(updateUserAreaSchema, body);
32
+};