diff --git a/darmasaba-api-ai.yml b/darmasaba-api-ai.yml new file mode 100644 index 0000000..cba55c6 --- /dev/null +++ b/darmasaba-api-ai.yml @@ -0,0 +1,2514 @@ +openapi: 3.0.3 +info: + title: AI Desa+ API + description: API for the AI Desa+ management system, providing endpoints for managing announcements, banners, calendar events, discussions, divisions, documents, groups, positions, projects, tasks, users, and division reports. + version: 1.0.0 + contact: + name: API Support + email: support@desa-plus.com + +servers: + - url: http://localhost:3000/api/ai + description: Development server + - url: https://api.desa-plus.com/api/ai + description: Production server + +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + BaseResponse: + type: object + required: + - success + - message + properties: + success: + type: boolean + description: Indicates whether the request was successful + message: + type: string + description: Response message or error description + meta: + type: object + properties: + total: + type: integer + description: Total number of items + page: + type: integer + description: Current page number + get: + type: integer + description: Number of items per page + description: Pagination metadata + + # Banner + BannerBase: + type: object + required: + - id + - idVillage + - title + - image + - isActive + properties: + id: + type: string + description: Unique identifier for the banner + idVillage: + type: string + description: ID of the village associated with the banner + title: + type: string + description: Title of the banner + image: + type: string + description: URL or path to the banner image + extension: + type: string + description: File extension of the banner image (e.g., jpg, png) + nullable: true + isActive: + type: boolean + description: Indicates whether the banner is active + createdAt: + type: string + format: date-time + description: Timestamp when the banner was created + updatedAt: + type: string + format: date-time + description: Timestamp when the banner was last updated + + BannerListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/BannerBase" + description: List of banners + + BannerDetailResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + $ref: "#/components/schemas/BannerBase" + description: Details of a specific banner + + # Announcement + AnnouncementBase: + type: object + required: + - id + - idVillage + - title + - desc + - isActive + properties: + id: + type: string + description: Unique identifier for the announcement + idVillage: + type: string + description: ID of the village associated with the announcement + title: + type: string + description: Title of the announcement + desc: + type: string + description: Description or content of the announcement + isActive: + type: boolean + description: Indicates whether the announcement is active + createdBy: + type: string + description: ID of the user who created the announcement + createdAt: + type: string + format: date-time + description: Timestamp when the announcement was created + updatedAt: + type: string + format: date-time + description: Timestamp when the announcement was last updated + + AnnouncementMember: + type: object + required: + - idGroup + - idDivision + properties: + idGroup: + type: string + description: ID of the group associated with the announcement + idDivision: + type: string + description: ID of the division associated with the announcement + group: + type: string + description: Name of the group + division: + type: string + description: Name of the division + + AnnouncementListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/AnnouncementBase" + description: List of announcements + + AnnouncementDetailResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + properties: + id: + type: string + description: Unique identifier for the announcement + title: + type: string + description: Title of the announcement + desc: + type: string + description: Description or content of the announcement + isActive: + type: boolean + description: Indicates whether the announcement is active + createdBy: + type: string + description: ID of the user who created the announcement + createdAt: + type: string + format: date-time + description: Timestamp when the announcement was created + updatedAt: + type: string + format: date-time + description: Timestamp when the announcement was last updated + member: + type: array + items: + $ref: "#/components/schemas/AnnouncementMember" + description: List of members associated with the announcement + description: Details of a specific announcement + + # Calendar + CalendarBase: + type: object + required: + - id + - dateStart + - title + - isActive + properties: + id: + type: string + description: Unique identifier for the calendar event + dateStart: + type: string + format: date-time + description: Start date and time of the event + timeStart: + type: string + description: Start time of the event (e.g., HH:MM) + nullable: true + timeEnd: + type: string + description: End time of the event (e.g., HH:MM) + nullable: true + title: + type: string + description: Title of the event + desc: + type: string + description: Description of the event + nullable: true + createdBy: + type: string + description: ID of the user who created the event + isActive: + type: boolean + description: Indicates whether the event is active + createdAt: + type: string + format: date-time + description: Timestamp when the event was created + updatedAt: + type: string + format: date-time + description: Timestamp when the event was last updated + + CalendarMember: + type: object + required: + - id + - idUser + - name + - email + properties: + id: + type: string + description: Unique identifier for the member + idUser: + type: string + description: ID of the user + name: + type: string + description: Name of the user + email: + type: string + description: Email of the user + img: + type: string + description: URL or path to the user's image + nullable: true + + CalendarListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/CalendarBase" + description: List of calendar events + + CalendarDetailResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + properties: + id: + type: string + description: Unique identifier for the event + dateStart: + type: string + format: date-time + description: Start date and time of the event + timeStart: + type: string + description: Start time of the event (e.g., HH:MM) + nullable: true + timeEnd: + type: string + description: End time of the event (e.g., HH:MM) + nullable: true + title: + type: string + description: Title of the event + desc: + type: string + description: Description of the event + nullable: true + linkMeet: + type: string + description: URL for the meeting (if applicable) + nullable: true + repeatEventType: + type: string + description: Type of event repetition (e.g., daily, weekly) + nullable: true + repeatValue: + type: integer + description: Number of repetitions or interval + nullable: true + createdBy: + type: string + description: ID of the user who created the event + isActive: + type: boolean + description: Indicates whether the event is active + createdAt: + type: string + format: date-time + description: Timestamp when the event was created + updatedAt: + type: string + format: date-time + description: Timestamp when the event was last updated + member: + type: array + items: + $ref: "#/components/schemas/CalendarMember" + description: List of members associated with the event + description: Details of a specific calendar event + + # Discussion + DiscussionBase: + type: object + required: + - id + - desc + - idDivision + - status + properties: + id: + type: string + description: Unique identifier for the discussion + desc: + type: string + description: Description of the discussion + idDivision: + type: string + description: ID of the division associated with the discussion + division: + type: string + description: Name of the division + status: + type: string + enum: [open, closed] + description: Status of the discussion + totalKomentar: + type: integer + description: Total number of comments in the discussion + createdAt: + type: string + format: date-time + description: Timestamp when the discussion was created + updatedAt: + type: string + format: date-time + description: Timestamp when the discussion was last updated + + DiscussionComment: + type: object + required: + - id + - comment + - username + properties: + id: + type: string + description: Unique identifier for the comment + comment: + type: string + description: Content of the comment + username: + type: string + description: Username of the commenter + userimg: + type: string + description: URL or path to the commenter's image + nullable: true + createdAt: + type: string + format: date-time + description: Timestamp when the comment was created + + DiscussionListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/DiscussionBase" + description: List of discussions + + DiscussionDetailResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + properties: + id: + type: string + description: Unique identifier for the discussion + idDivision: + type: string + description: ID of the division associated with the discussion + division: + type: string + description: Name of the division + desc: + type: string + description: Description of the discussion + status: + type: string + enum: [open, closed] + description: Status of the discussion + isActive: + type: boolean + description: Indicates whether the discussion is active + createdBy: + type: string + description: ID of the user who created the discussion + createdAt: + type: string + format: date-time + description: Timestamp when the discussion was created + updatedAt: + type: string + format: date-time + description: Timestamp when the discussion was last updated + komentar: + type: array + items: + $ref: "#/components/schemas/DiscussionComment" + description: List of comments in the discussion + description: Details of a specific discussion + + # Discussion General + DiskusiUmumBase: + type: object + required: + - id + - title + - desc + - status + properties: + id: + type: string + description: Unique identifier for the general discussion + title: + type: string + description: Title of the general discussion + desc: + type: string + description: Description of the general discussion + status: + type: string + enum: [open, closed] + description: Status of the general discussion + group: + type: string + description: Name of the group associated with the discussion + totalKomentar: + type: integer + description: Total number of comments in the discussion + createdAt: + type: string + format: date-time + description: Timestamp when the discussion was created + updatedAt: + type: string + format: date-time + description: Timestamp when the discussion was last updated + + DiskusiUmumListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/DiskusiUmumBase" + description: List of general discussions + + DiskusiUmumDetailResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + required: + - id + - title + - desc + - status + properties: + id: + type: string + description: Unique identifier for the general discussion + idGroup: + type: string + description: ID of the group associated with the discussion + group: + type: string + description: Name of the group + title: + type: string + description: Title of the general discussion + desc: + type: string + description: Description of the general discussion + status: + type: string + enum: [open, closed] + description: Status of the general discussion + isActive: + type: boolean + description: Indicates whether the discussion is active + createdAt: + type: string + format: date-time + description: Timestamp when the discussion was created + updatedAt: + type: string + format: date-time + description: Timestamp when the discussion was last updated + description: Details of a specific general discussion + + DiskusiUmumMemberResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + type: object + required: + - idUser + - name + properties: + idUser: + type: string + description: ID of the user + name: + type: string + description: Name of the user + img: + type: string + description: URL or path to the user's image + nullable: true + description: List of members in the general discussion + + DiskusiUmumCommentResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + type: object + required: + - id + - comment + - idUser + - username + properties: + id: + type: string + description: Unique identifier for the comment + comment: + type: string + description: Content of the comment + idUser: + type: string + description: ID of the user who made the comment + username: + type: string + description: Username of the commenter + img: + type: string + description: URL or path to the commenter's image + nullable: true + createdAt: + type: string + format: date-time + description: Timestamp when the comment was created + description: List of comments in the general discussion + + # Division + DivisiBase: + type: object + required: + - id + - name + properties: + id: + type: string + description: Unique identifier for the division + name: + type: string + description: Name of the division + desc: + type: string + description: Description of the division + nullable: true + idGroup: + type: string + description: ID of the group associated with the division + group: + type: string + description: Name of the group + jumlahMember: + type: integer + description: Number of members in the division + + DivisiListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/DivisiBase" + description: List of divisions + + DivisiDetailResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + required: + - id + - name + - isActive + properties: + id: + type: string + description: Unique identifier for the division + idVillage: + type: string + description: ID of the village associated with the division + idGroup: + type: string + description: ID of the group associated with the division + name: + type: string + description: Name of the division + desc: + type: string + description: Description of the division + nullable: true + isActive: + type: boolean + description: Indicates whether the division is active + createdBy: + type: string + description: ID of the user who created the division + createdAt: + type: string + format: date-time + description: Timestamp when the division was created + updatedAt: + type: string + format: date-time + description: Timestamp when the division was last updated + member: + type: array + items: + type: object + required: + - id + - idUser + - name + properties: + id: + type: string + description: Unique identifier for the member + idUser: + type: string + description: ID of the user + name: + type: string + description: Name of the user + isAdmin: + type: boolean + description: Indicates whether the user is an admin + img: + type: string + description: URL or path to the user's image + nullable: true + description: List of members in the division + description: Details of a specific division + + DivisiReportDokumenResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + properties: + gambar: + type: integer + description: Number of images in the division + dokumen: + type: integer + description: Number of documents in the division + description: Document report for the division + + DivisiReportEventResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + properties: + selesai: + type: integer + description: Number of completed events + akan_datang: + type: integer + description: Number of upcoming events + description: Event report for the division + + DivisiReportProgressResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + properties: + Segera: + type: string + description: Percentage of tasks in 'Segera' status + Dikerjakan: + type: string + description: Percentage of tasks in 'Dikerjakan' status + Selesai: + type: integer + description: Percentage of tasks in 'Selesai' status + Dibatalkan: + type: string + description: Percentage of tasks in 'Dibatalkan' status + description: Progress report for the division + + # Document + DocumentItem: + type: object + required: + - id + - category + - name + - path + properties: + id: + type: string + description: Unique identifier for the document or folder + category: + type: string + enum: [FILE, FOLDER] + description: Type of item (file or folder) + name: + type: string + description: Name of the document or folder + extension: + type: string + description: File extension (e.g., pdf, docx) + nullable: true + idStorage: + type: string + description: ID of the storage location + nullable: true + path: + type: string + description: Path to the document or folder + createdBy: + type: string + description: ID of the user who created the document or folder + createdAt: + type: string + format: date-time + description: Timestamp when the document or folder was created + updatedAt: + type: string + format: date-time + description: Timestamp when the document or folder was last updated + share: + type: boolean + description: Indicates whether the document or folder is shared + + DocumentListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/DocumentItem" + description: List of documents and folders + + # Group + GroupItem: + type: object + required: + - id + - idVillage + - name + - isActive + properties: + id: + type: string + description: Unique identifier for the group + idVillage: + type: string + description: ID of the village associated with the group + name: + type: string + description: Name of the group + isActive: + type: boolean + description: Indicates whether the group is active + createdAt: + type: string + format: date-time + description: Timestamp when the group was created + updatedAt: + type: string + format: date-time + description: Timestamp when the group was last updated + + GroupListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/GroupItem" + description: List of groups + + # Position + PositionItem: + type: object + required: + - id + - name + - idGroup + - isActive + properties: + id: + type: string + description: Unique identifier for the position + name: + type: string + description: Name of the position + idGroup: + type: string + description: ID of the group associated with the position + group: + type: string + description: Name of the group + isActive: + type: boolean + description: Indicates whether the position is active + createdAt: + type: string + format: date-time + description: Timestamp when the position was created + updatedAt: + type: string + format: date-time + description: Timestamp when the position was last updated + + PositionListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/PositionItem" + description: List of positions + + # Project + ProjectBase: + type: object + required: + - id + - idGroup + - title + - status + properties: + id: + type: string + description: Unique identifier for the project + idGroup: + type: string + description: ID of the group associated with the project + title: + type: string + description: Title of the project + desc: + type: string + description: Description of the project + nullable: true + group: + type: string + description: Name of the group + status: + type: string + enum: [segera, dikerjakan, selesai, batal] + description: Status of the project + progress: + type: integer + description: Progress percentage of the project + member: + type: integer + description: Number of members in the project + + BaseResponseProjectList: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/ProjectBase" + description: List of projects + + BaseResponseProjectDetail: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + required: + - id + - idVillage + - idGroup + - title + - status + - isActive + properties: + id: + type: string + description: Unique identifier for the project + idVillage: + type: string + description: ID of the village associated with the project + idGroup: + type: string + description: ID of the group associated with the project + group: + type: string + description: Name of the group + title: + type: string + description: Title of the project + status: + type: string + enum: [segera, dikerjakan, selesai, batal] + description: Status of the project + desc: + type: string + description: Description of the project + nullable: true + reason: + type: string + description: Reason for project status (e.g., cancellation) + nullable: true + report: + type: string + description: Project report or summary + nullable: true + isActive: + type: boolean + description: Indicates whether the project is active + progress: + type: integer + description: Progress percentage of the project + createdAt: + type: string + format: date-time + description: Timestamp when the project was created + updatedAt: + type: string + format: date-time + description: Timestamp when the project was last updated + description: Details of a specific project + + # Task + TaskBase: + type: object + required: + - id + - idDivision + - title + - status + properties: + id: + type: string + description: Unique identifier for the task + idDivision: + type: string + description: ID of the division associated with the task + title: + type: string + description: Title of the task + desc: + type: string + description: Description of the task + nullable: true + division: + type: string + description: Name of the division + status: + type: string + enum: [segera, dikerjakan, selesai, batal] + description: Status of the task + progress: + type: integer + description: Progress percentage of the task + member: + type: integer + description: Number of members assigned to the task + + BaseResponseTaskList: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/TaskBase" + description: List of tasks + + BaseResponseTaskDetail: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + required: + - id + - idDivision + - title + - status + - isActive + properties: + id: + type: string + description: Unique identifier for the task + idDivision: + type: string + description: ID of the division associated with the task + division: + type: string + description: Name of the division + title: + type: string + description: Title of the task + status: + type: string + enum: [segera, dikerjakan, selesai, batal] + description: Status of the task + desc: + type: string + description: Description of the task + nullable: true + reason: + type: string + description: Reason for task status (e.g., cancellation) + nullable: true + report: + type: string + description: Task report or summary + nullable: true + isActive: + type: boolean + description: Indicates whether the task is active + progress: + type: integer + description: Progress percentage of the task + createdAt: + type: string + format: date-time + description: Timestamp when the task was created + updatedAt: + type: string + format: date-time + description: Timestamp when the task was last updated + description: Details of a specific task + + BaseResponseSubTaskList: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + type: object + required: + - id + - title + - status + properties: + id: + type: string + description: Unique identifier for the subtask + title: + type: string + description: Title of the subtask + status: + type: string + enum: [belum_selesai, selesai] + description: Status of the subtask + dateStart: + type: string + format: date-time + description: Start date and time of the subtask + dateEnd: + type: string + format: date-time + description: End date and time of the subtask + description: List of subtasks + + BaseResponseMemberList: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + type: object + required: + - id + - idUser + - name + - email + properties: + id: + type: string + description: Unique identifier for the member + idUser: + type: string + description: ID of the user + name: + type: string + description: Name of the user + email: + type: string + description: Email of the user + img: + type: string + description: URL or path to the user's image + nullable: true + position: + type: string + description: Position of the user + nullable: true + description: List of members + + BaseResponseFileList: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/DocumentItem" + description: List of files + + BaseResponseLinkList: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + type: object + required: + - id + - idProject + - link + - isActive + properties: + id: + type: string + description: Unique identifier for the link + idProject: + type: string + description: ID of the project associated with the link + link: + type: string + description: URL of the link + isActive: + type: boolean + description: Indicates whether the link is active + createdAt: + type: string + format: date-time + description: Timestamp when the link was created + updatedAt: + type: string + format: date-time + description: Timestamp when the link was last updated + description: List of links + + # User + UserBase: + type: object + required: + - id + - idUserRole + - nik + - name + - gender + - phone + - isActive + properties: + id: + type: string + description: Unique identifier for the user + idUserRole: + type: string + description: ID of the user's role + nik: + type: string + description: National identification number of the user + name: + type: string + description: Name of the user + gender: + type: string + description: Gender of the user + phone: + type: string + description: Phone number of the user + position: + type: string + description: Position of the user + nullable: true + group: + type: string + description: Name of the group the user belongs to + nullable: true + isActive: + type: boolean + description: Indicates whether the user is active + + UserListResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: array + items: + $ref: "#/components/schemas/UserBase" + description: List of users + + UserDetailResponse: + allOf: + - $ref: "#/components/schemas/BaseResponse" + - type: object + properties: + data: + type: object + required: + - id + - idUserRole + - nik + - name + - phone + - email + - gender + - isActive + properties: + id: + type: string + description: Unique identifier for the user + idUserRole: + type: string + description: ID of the user's role + nik: + type: string + description: National identification number of the user + name: + type: string + description: Name of the user + email: + type: string + description: Email of the user + gender: + type: string + description: Gender of the user + phone: + type: string + description: Phone number of the user + img: + type: string + description: URL or path to the user's image + nullable: true + idGroup: + type: string + description: ID of the group the user belongs to + nullable: true + idPosition: + type: string + description: ID of the user's position + nullable: true + role: + type: string + description: Role of the user + nullable: true + position: + type: string + description: Position of the user + nullable: true + group: + type: string + description: Name of the group the user belongs to + nullable: true + isActive: + type: boolean + description: Indicates whether the user is active + createdAt: + type: string + format: date-time + description: Timestamp when the user was created + updatedAt: + type: string + format: date-time + description: Timestamp when the user was last updated + description: Details of a specific user + +paths: + # Announcement + /announcement: + get: + tags: + - Announcement + summary: Get list of announcements + description: Retrieves a paginated list of announcements filtered by village, search term, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: search + in: query + description: Search term for announcement title or description + schema: + type: string + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + responses: + "200": + description: List of announcements + content: + application/json: + schema: + $ref: "#/components/schemas/AnnouncementListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /announcement/{id}: + get: + tags: + - Announcement + summary: Get announcement details + description: Retrieves details of a specific announcement + parameters: + - name: id + in: path + required: true + description: Announcement ID + schema: + type: string + responses: + "200": + description: Announcement details + content: + application/json: + schema: + $ref: "#/components/schemas/AnnouncementDetailResponse" + "404": + description: Announcement not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Banner + /banner: + get: + tags: + - Banner + summary: Get list of banners + description: Retrieves a paginated list of banners filtered by village, search term, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: search + in: query + description: Search term for banner title + schema: + type: string + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + responses: + "200": + description: List of banners + content: + application/json: + schema: + $ref: "#/components/schemas/BannerListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /banner/{id}: + get: + tags: + - Banner + summary: Get banner details + description: Retrieves details of a specific banner + parameters: + - name: id + in: path + required: true + description: Banner ID + schema: + type: string + responses: + "200": + description: Banner details + content: + application/json: + schema: + $ref: "#/components/schemas/BannerDetailResponse" + "404": + description: Banner not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Calendar + /calendar: + get: + tags: + - Calendar + summary: Get calendar events + description: Retrieves a paginated list of calendar events filtered by village, date, division, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: date + in: query + description: Filter by event date + schema: + type: string + format: date + nullable: true + - name: division + in: query + description: Filter by division ID + schema: + type: string + nullable: true + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + - name: search + in: query + description: Search term for event title or description + schema: + type: string + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of calendar events + content: + application/json: + schema: + $ref: "#/components/schemas/CalendarListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /calendar/{id}: + get: + tags: + - Calendar + summary: Get calendar event details + description: Retrieves details of a specific calendar event + parameters: + - name: id + in: path + required: true + description: Event ID + schema: + type: string + responses: + "200": + description: Event details + content: + application/json: + schema: + $ref: "#/components/schemas/CalendarDetailResponse" + "404": + description: Event not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Discussion + /discussion: + get: + tags: + - Discussion + summary: Get division discussions + description: Retrieves a paginated list of discussions filtered by village, division, status, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: division + in: query + description: Filter by division ID + schema: + type: string + nullable: true + - name: status + in: query + description: Filter by discussion status + schema: + type: string + enum: [open, closed] + nullable: true + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + - name: search + in: query + description: Search term for discussion description + schema: + type: string + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of discussions + content: + application/json: + schema: + $ref: "#/components/schemas/DiscussionListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /discussion/{id}: + get: + tags: + - Discussion + summary: Get division discussion details + description: Retrieves details of a specific division discussion + parameters: + - name: id + in: path + required: true + description: Discussion ID + schema: + type: string + responses: + "200": + description: Discussion details + content: + application/json: + schema: + $ref: "#/components/schemas/DiscussionDetailResponse" + "404": + description: Discussion not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Discussion General + /discussion-general: + get: + tags: + - DiscussionGeneral + summary: Get general discussions + description: Retrieves a paginated list of general discussions filtered by village, group, status, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: group + in: query + description: Filter by group ID + schema: + type: string + nullable: true + - name: search + in: query + description: Search term for discussion title or description + schema: + type: string + nullable: true + - name: status + in: query + description: Filter by discussion status + schema: + type: string + enum: [open, closed] + nullable: true + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of general discussions + content: + application/json: + schema: + $ref: "#/components/schemas/DiskusiUmumListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /discussion-general/{id}: + get: + tags: + - DiscussionGeneral + summary: Get general discussion details + description: Retrieves details of a specific general discussion, including members or comments based on category + parameters: + - name: id + in: path + required: true + description: Discussion ID + schema: + type: string + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: cat + in: query + description: Category of data to retrieve (detail, member, or comment) + schema: + type: string + enum: [detail, member, comment] + nullable: true + responses: + "200": + description: Discussion details, members, or comments + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/DiskusiUmumDetailResponse" + - $ref: "#/components/schemas/DiskusiUmumMemberResponse" + - $ref: "#/components/schemas/DiskusiUmumCommentResponse" + "400": + description: Bad request (e.g., invalid parameters) + "404": + description: Discussion not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Division + /division: + get: + tags: + - Division + summary: Get divisions + description: Retrieves a paginated list of divisions filtered by village, group, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: group + in: query + description: Filter by group ID + schema: + type: string + nullable: true + - name: search + in: query + description: Search term for division name or description + schema: + type: string + nullable: true + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of divisions + content: + application/json: + schema: + $ref: "#/components/schemas/DivisiListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /division/{id}: + get: + tags: + - Division + summary: Get division details + description: Retrieves details of a specific division + parameters: + - name: id + in: path + required: true + description: Division ID + schema: + type: string + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + responses: + "200": + description: Division details + content: + application/json: + schema: + $ref: "#/components/schemas/DivisiDetailResponse" + "404": + description: Division not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /division/report: + get: + tags: + - Division + summary: Get division reports + description: Retrieves reports for a specific division based on category (dokumen, event, or progress) + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: cat + in: query + required: true + description: Category of report to retrieve (dokumen, event, or progress) + schema: + type: string + enum: [dokumen, event, progress] + - name: group + in: query + description: Group ID + schema: + type: string + nullable: true + - name: division + in: query + description: Division ID + schema: + type: string + nullable: true + - name: date-start + in: query + required: true + description: Start date for filtering + schema: + type: string + format: date + - name: date-end + in: query + required: true + description: End date for filtering + schema: + type: string + format: date + responses: + "200": + description: Division report data + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/DivisiReportDokumenResponse" + - $ref: "#/components/schemas/DivisiReportEventResponse" + - $ref: "#/components/schemas/DivisiReportProgressResponse" + "400": + description: Bad request (e.g., invalid parameters) + "404": + description: Division not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Document + /document: + get: + tags: + - Document + summary: Get documents + description: Retrieves a paginated list of documents and folders filtered by village, division, path, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: division + in: query + description: Filter by division ID + schema: + type: string + nullable: true + - name: path + in: query + description: Filter by document path + schema: + type: string + nullable: true + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + - name: search + in: query + description: Search term for document name + schema: + type: string + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of documents and folders + content: + application/json: + schema: + $ref: "#/components/schemas/DocumentListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Group + /group: + get: + tags: + - Group + summary: Get groups + description: Retrieves a paginated list of groups filtered by village, search term, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: search + in: query + description: Search term for group name + schema: + type: string + nullable: true + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of groups + content: + application/json: + schema: + $ref: "#/components/schemas/GroupListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Position + /position: + get: + tags: + - Position + summary: Get positions + description: Retrieves a paginated list of positions filtered by village, group, search term, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: group + in: query + description: Filter by group ID + schema: + type: string + nullable: true + - name: search + in: query + description: Search term for position name + schema: + type: string + nullable: true + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of positions + content: + application/json: + schema: + $ref: "#/components/schemas/PositionListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Project + /project: + get: + tags: + - Project + summary: Get projects + description: Retrieves a paginated list of projects filtered by village, group, status, and search term + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: group + in: query + description: Filter by group ID + schema: + type: string + nullable: true + - name: status + in: query + description: Filter by project status + schema: + type: string + enum: [segera, dikerjakan, selesai, batal] + nullable: true + - name: search + in: query + description: Search term for project title or description + schema: + type: string + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of projects + content: + application/json: + schema: + $ref: "#/components/schemas/BaseResponseProjectList" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /project/{id}: + get: + tags: + - Project + summary: Get project details + description: Retrieves details of a specific project based on category (data, task, file, member, or link) + parameters: + - name: id + in: path + required: true + description: Project ID + schema: + type: string + - name: cat + in: query + required: true + description: Category of project data to retrieve + schema: + type: string + enum: [data, task, file, member, link] + responses: + "200": + description: Project details + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/BaseResponseProjectDetail" + - $ref: "#/components/schemas/BaseResponseTaskList" + - $ref: "#/components/schemas/BaseResponseFileList" + - $ref: "#/components/schemas/BaseResponseMemberList" + - $ref: "#/components/schemas/BaseResponseLinkList" + "400": + description: Bad request (e.g., invalid parameters) + "404": + description: Project not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # Task + /task: + get: + tags: + - Task + summary: Get tasks + description: Retrieves a paginated list of tasks filtered by village, division, status, and search term + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: division + in: query + description: Filter by division ID + schema: + type: string + nullable: true + - name: status + in: query + description: Filter by task status + schema: + type: string + enum: [segera, dikerjakan, selesai, batal] + nullable: true + - name: search + in: query + description: Search term for task title or description + schema: + type: string + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of tasks + content: + application/json: + schema: + $ref: "#/components/schemas/BaseResponseTaskList" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /task/{id}: + get: + tags: + - Task + summary: Get task details + description: Retrieves details of a specific task based on category (data, task, file, member, or link) + parameters: + - name: id + in: path + required: true + description: Task ID + schema: + type: string + - name: cat + in: query + required: true + description: Category of task data to retrieve + schema: + type: string + enum: [data, task, file, member, link] + responses: + "200": + description: Task details + content: + application/json: + schema: + oneOf: + - $ref: "#/components/schemas/BaseResponseTaskDetail" + - $ref: "#/components/schemas/BaseResponseSubTaskList" + - $ref: "#/components/schemas/BaseResponseFileList" + - $ref: "#/components/schemas/BaseResponseMemberList" + - $ref: "#/components/schemas/BaseResponseLinkList" + "400": + description: Bad request (e.g., invalid parameters) + "404": + description: Task not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + # User + /user: + get: + tags: + - User + summary: Get users + description: Retrieves a paginated list of users filtered by village, group, search term, and active status + parameters: + - name: desa + in: query + required: true + description: Village ID + schema: + type: string + - name: group + in: query + description: Filter by group ID + schema: + type: string + nullable: true + - name: search + in: query + description: Search term for user name + schema: + type: string + nullable: true + - name: active + in: query + description: Filter by active status + schema: + type: boolean + nullable: true + - name: page + in: query + description: Page number for pagination + schema: + type: integer + minimum: 1 + default: 1 + - name: perPage + in: query + description: Number of items per page + schema: + type: integer + minimum: 1 + default: 10 + responses: + "200": + description: List of users + content: + application/json: + schema: + $ref: "#/components/schemas/UserListResponse" + "400": + description: Bad request (e.g., invalid parameters) + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + + /user/{id}: + get: + tags: + - User + summary: Get user details + description: Retrieves details of a specific user + parameters: + - name: id + in: path + required: true + description: User ID + schema: + type: string + responses: + "200": + description: User details + content: + application/json: + schema: + $ref: "#/components/schemas/UserDetailResponse" + "404": + description: User not found + "401": + description: Unauthorized (invalid or missing token) + "500": + description: Internal server error + +security: + - bearerAuth: [] + +tags: + - name: Announcement + description: Operations related to announcements + - name: Banner + description: Operations related to banner management + - name: Calendar + description: Operations related to calendar events + - name: Discussion + description: Operations related to division-specific discussions + - name: DiscussionGeneral + description: Operations related to general discussions + - name: Division + description: Operations related to division management and reports + - name: Document + description: Operations related to document and folder management + - name: Group + description: Operations related to group management + - name: Position + description: Operations related to position management + - name: Project + description: Operations related to project management + - name: Task + description: Operations related to task management + - name: User + description: Operations related to user managementØ diff --git a/public/util/privacy-policy.html b/public/util/privacy-policy.html new file mode 100644 index 0000000..be39202 --- /dev/null +++ b/public/util/privacy-policy.html @@ -0,0 +1,253 @@ + + + + + + Privacy Policy — Bali Interaktif Perkasa + + + +
+
+

Privacy Policy

+
Last updated September 01, 2025
+
+
+ +
+

+ This Privacy Notice for Bali Interaktif Perkasa ("we," "us," or "our") describes how and why we might access, collect, store, use, and/or share ("process") your personal information when you use our services ("Services"), including when you: +

+ +

Questions or concerns? Reading this Privacy Notice will help you understand your privacy rights and choices. We are responsible for making decisions about how your personal information is processed. If you do not agree with our policies and practices, please do not use our Services. If you still have any questions or concerns, please contact us at bip.baliinteraktifperkasa@gmail.com.

+ +
+

Summary of Key Points

+

This summary provides key points from our Privacy Notice, but you can find out more details about any of these topics by using the table of contents below.

+ +

Want to learn more about what we do with information we collect? Review the Privacy Notice in full below.

+
+ + + +
+

1. WHAT INFORMATION DO WE COLLECT?

+

Personal information you disclose to us

+

In Short: We collect personal information that you provide to us.

+

We collect personal information that you voluntarily provide to us when you express an interest in obtaining information about us or our products and Services, when you participate in activities on the Services, or otherwise when you contact us.

+

Personal Information Provided by You

+

The personal information we collect depends on the context of your interactions with us and the Services, the choices you make, and the products and features you use. This may include:

+ +

Sensitive Information

+

We do not process sensitive information.

+

Application Data

+

If you use our application(s), we may also collect the following information if you choose to provide us with access or permission:

+ +

This information is primarily needed to maintain the security and operation of our application(s), for troubleshooting, and for internal analytics and reporting purposes.

+

All personal information that you provide to us must be true, complete, and accurate, and you must notify us of any changes to such personal information.

+
+ +
+

2. HOW DO WE PROCESS YOUR INFORMATION?

+

In Short: We process your information to provide, improve, and administer our Services, communicate with you, for security and fraud prevention, and to comply with law. We may also process your information for other purposes with your consent.

+

We process your personal information for a variety of reasons, depending on how you interact with our Services, including:

+ +
+ +
+

3. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION?

+

In Short: We may share information in specific situations described in this section and/or with the following third parties.

+

Vendors, Consultants, and Other Third-Party Service Providers. We may share your data with third-party vendors, service providers, contractors, or agents ("third parties") who perform services for us or on our behalf and require access to such information to do that work.

+

The third parties we may share personal information with include:

+ +

We may also need to share your personal information in the following situations:

+ +
+ +
+

4. HOW LONG DO WE KEEP YOUR INFORMATION?

+

In Short: We keep your information for as long as necessary to fulfill the purposes outlined in this Privacy Notice unless otherwise required by law.

+

We will only keep your personal information as long as necessary for the purposes set out in this Privacy Notice, unless a longer retention period is required or permitted by law (such as tax, accounting, or other legal requirements).

+

When we have no ongoing legitimate business need to process your personal information, we will delete or anonymize such information. If deletion is not possible (for example, if your personal information is stored in backup archives), we will securely store your personal information and isolate it from any further processing until deletion is possible.

+
+ +
+

5. HOW DO WE KEEP YOUR INFORMATION SAFE?

+

In Short: We aim to protect your personal information through a system of organizational and technical security measures.

+

We have implemented appropriate and reasonable technical and organizational security measures designed to protect the security of any personal information we process. However, despite our safeguards and efforts to secure your information, no electronic transmission over the Internet or information storage technology can be guaranteed to be 100% secure. Transmission of personal information to and from our Services is at your own risk. You should only access the Services within a secure environment.

+
+ +
+

6. DO WE COLLECT INFORMATION FROM MINORS?

+

In Short: We do not knowingly collect data from or market to children under 18 years of age.

+

We do not knowingly collect, solicit data from, or market to children under 18 years of age, nor do we knowingly sell such personal information. By using the Services, you represent that you are at least 18 or that you are the parent or guardian of such a minor and consent to such minor dependent’s use of the Services. If we learn that personal information from users less than 18 years of age has been collected, we will deactivate the account and take reasonable measures to promptly delete such data from our records. If you become aware of any data we may have collected from children under age 18, please contact us at bip.baliinteraktifperkasa@gmail.com.

+
+ +
+

7. WHAT ARE YOUR PRIVACY RIGHTS?

+

In Short: You may review, change, or terminate your account at any time, depending on your country, province, or state of residence.

+

Withdrawing your consent: If we are relying on your consent to process your personal information (which may be express and/or implied consent depending on the applicable law), you have the right to withdraw your consent at any time. You can do so by contacting us using the details in the section "HOW CAN YOU CONTACT US ABOUT THIS NOTICE?" below. This will not affect the lawfulness of the processing before its withdrawal, nor will it affect processing conducted in reliance on lawful processing grounds other than consent where permitted by law.

+

If you have questions or comments about your privacy rights, email us at bip.baliinteraktifperkasa@gmail.com.

+
+ +
+

8. CONTROLS FOR DO-NOT-TRACK FEATURES

+

Most web browsers and some mobile operating systems and applications include a Do-Not-Track ("DNT") setting you can activate to signal your privacy preference not to have data about your online browsing activities monitored and collected. At this stage, no uniform technology standard for recognizing and implementing DNT signals has been finalized. As such, we do not currently respond to DNT browser signals or any other mechanism that automatically communicates your choice not to be tracked online. If a standard for online tracking is adopted that we must follow in the future, we will inform you about that practice in a revised version of this Privacy Notice.

+
+ +
+

9. DO WE MAKE UPDATES TO THIS NOTICE?

+

In Short: Yes, we will update this notice as necessary to stay compliant with relevant laws.

+

We may update this Privacy Notice from time to time. The updated version will be indicated by an updated "Revised" date at the top of this Privacy Notice. If we make material changes, we may notify you by prominently posting a notice of such changes or by directly sending you a notification. We encourage you to review this Privacy Notice frequently to stay informed of how we are protecting your information.

+
+ +
+

10. HOW CAN YOU CONTACT US ABOUT THIS NOTICE?

+
+Bali Interaktif Perkasa +Park23 Creative Hub, Bali Interaktif Perkasa - Private Office +Jl. Kediri 3rd Floor, Number 01 - 02, Tuban +Badung, Bali 80361 +Indonesia +
+

Email: bip.baliinteraktifperkasa@gmail.com

+
+ +
+

11. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU?

+

You have the right to request access to the personal information we collect from you, details about how we have processed it, correct inaccuracies, or delete your personal information. You may also have the right to withdraw your consent to our processing of your personal information. These rights may be limited in some circumstances by applicable law.

+

To make a request, please contact us at bip.baliinteraktifperkasa@gmail.com.

+
+ + +
+ + + + diff --git a/src/app/api/ai/announcement/[id]/route.ts b/src/app/api/ai/announcement/[id]/route.ts new file mode 100644 index 0000000..e301cc9 --- /dev/null +++ b/src/app/api/ai/announcement/[id]/route.ts @@ -0,0 +1,95 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + + +// GET ONE PENGUMUMAN, UNTUK TAMPIL DETAIL PENGUMUMAN +export async function GET(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + + const data = await prisma.announcement.count({ + where: { + id: id, + }, + }); + + if (data == 0) { + return NextResponse.json( + { + success: false, + message: "Gagal mendapatkan pengumuman, data tidak ditemukan", + }, + { status: 404 } + ); + } + + const announcement = await prisma.announcement.findUnique({ + where: { + id: id, + }, + select: { + id: true, + title: true, + desc: true, + }, + }); + + if (!announcement) { + return NextResponse.json( + { + success: false, + message: "Gagal mendapatkan pengumuman, data tidak ditemukan", + }, + { status: 404 } + ); + } + + let dataFix = { ...announcement, member: {} }; + + const announcementMember = await prisma.announcementMember.findMany({ + where: { + idAnnouncement: id, + }, + select: { + idGroup: true, + idDivision: true, + Group: { + select: { + name: true, + }, + }, + Division: { + select: { + name: true, + }, + }, + }, + }); + + const formatMember = announcementMember.map((v: any) => ({ + ..._.omit(v, ["Group", "Division"]), + idGroup: v.idGroup, + idDivision: v.idDivision, + group: v.Group.name, + division: v.Division.name + })) + + dataFix.member = formatMember + + return NextResponse.json( + { + success: true, + message: "Berhasil mendapatkan pengumuman", + data: dataFix, + }, + { status: 200 } + ); + + + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan pengumuman, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/announcement/route.ts b/src/app/api/ai/announcement/route.ts new file mode 100644 index 0000000..a4a1d4a --- /dev/null +++ b/src/app/api/ai/announcement/route.ts @@ -0,0 +1,52 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import "moment/locale/id"; +import { NextResponse } from "next/server"; +export const dynamic = 'force-dynamic' + + + +// GET ALL PENGUMUMAN +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const judul = searchParams.get('search'); + const page = searchParams.get('page'); + const get = searchParams.get('get'); + const villageId = searchParams.get('desa'); + const active = searchParams.get('active'); + + let getFix = 0; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + + let kondisi: any = { + idVillage: String(villageId), + isActive: (active == "false" || active == undefined) ? false : true, + title: { + contains: (judul == undefined || judul == null) ? "" : judul, + mode: "insensitive" + } + } + + const data = await prisma.announcement.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + orderBy: { + createdAt: 'desc' + } + }); + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan pengumuman", data, }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan pengumuman, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/banner/[id]/route.ts b/src/app/api/ai/banner/[id]/route.ts new file mode 100644 index 0000000..01f91ac --- /dev/null +++ b/src/app/api/ai/banner/[id]/route.ts @@ -0,0 +1,21 @@ +import { prisma } from "@/module/_global"; +import { NextResponse } from "next/server"; + + +// GET ONE BANNER +export async function GET(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + + const data = await prisma.bannerImage.findUnique({ + where: { + id: String(id) + } + }) + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan banner", data }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan banner, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/banner/route.ts b/src/app/api/ai/banner/route.ts new file mode 100644 index 0000000..93704b4 --- /dev/null +++ b/src/app/api/ai/banner/route.ts @@ -0,0 +1,48 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + + +// GET ALL BANNER +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const judul = searchParams.get('search'); + const page = searchParams.get('page'); + const get = searchParams.get('get'); + const villageId = searchParams.get('desa'); + const active = searchParams.get('active'); + + let getFix = 0; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + let kondisi: any = { + idVillage: String(villageId), + isActive: (active == "false" || active == undefined) ? false : true, + title: { + contains: (judul == undefined || judul == null) ? "" : judul, + mode: "insensitive" + } + } + + const data = await prisma.bannerImage.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + orderBy: { + createdAt: 'desc' + } + }); + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan banner", data }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan data banner, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/calendar/[id]/route.ts b/src/app/api/ai/calendar/[id]/route.ts new file mode 100644 index 0000000..9d43c90 --- /dev/null +++ b/src/app/api/ai/calendar/[id]/route.ts @@ -0,0 +1,99 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import moment from "moment"; +import { NextResponse } from "next/server"; + +// GET ONE CALENDER BY ID KALENDER REMINDER +export async function GET(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params + + const cek = await prisma.divisionCalendarReminder.count({ + where: { + id: id + } + }) + + if (cek == 0) { + return NextResponse.json( + { + success: false, + message: "Gagal mendapatkan acara, data tidak ditemukan", + }, + { status: 404 } + ); + } + + const data: any = await prisma.divisionCalendarReminder.findUnique({ + where: { + id: id + }, + select: { + id: true, + timeStart: true, + dateStart: true, + timeEnd: true, + createdAt: true, + DivisionCalendar: { + select: { + id: true, + title: true, + desc: true, + linkMeet: true, + repeatEventTyper: true, + repeatValue: true, + } + } + } + }); + const { DivisionCalendar, ...dataCalender } = data + const timeStart = moment.utc(dataCalender?.timeStart).format("HH:mm") + const timeEnd = moment.utc(dataCalender?.timeEnd).format("HH:mm") + const idCalendar = data?.DivisionCalendar.id + const title = data?.DivisionCalendar?.title + const desc = data?.DivisionCalendar?.desc + const linkMeet = data?.DivisionCalendar?.linkMeet + const repeatEventTyper = data?.DivisionCalendar?.repeatEventTyper + const repeatValue = data?.DivisionCalendar?.repeatValue + + + const result = { ...dataCalender, timeStart, timeEnd, title, desc, linkMeet, repeatEventTyper, repeatValue } + + + const member = await prisma.divisionCalendarMember.findMany({ + where: { + idCalendar + }, + select: { + id: true, + idUser: true, + User: { + select: { + id: true, + name: true, + email: true, + img: true + } + } + } + }) + const fixMember = member.map((v: any) => ({ + ..._.omit(v, ["User"]), + name: v.User.name, + email: v.User.email, + img: v.User.img + })) + + + const dataFix = { + ...result, + member: fixMember, + } + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan kalender", data: dataFix }, { status: 200 }); + + } catch (error) { + return NextResponse.json({ success: false, message: "Gagal mendapatkan kalender, data tidak ditemukan (error: 500)", }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/calendar/route.ts b/src/app/api/ai/calendar/route.ts new file mode 100644 index 0000000..c07d011 --- /dev/null +++ b/src/app/api/ai/calendar/route.ts @@ -0,0 +1,149 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import moment from "moment"; +import "moment/locale/id"; +import { NextResponse } from "next/server"; + +//GET ALL CALENDER +export async function GET(request: Request) { + try { + + const { searchParams } = new URL(request.url); + const idDivision = searchParams.get("division"); + const isDate = searchParams.get("date") + const villageId = searchParams.get("desa") + const active = searchParams.get("active") + const search = searchParams.get("search") + const page = searchParams.get("page") + const get = searchParams.get("get") + + let getFix = 0; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + + let kondisi: any = {} + + if (idDivision != "" && idDivision != null && idDivision != undefined) { + if (isDate != null && isDate != undefined && isDate != "") { + kondisi = { + idDivision: String(idDivision), + dateStart: new Date(String(isDate)), + DivisionCalendar: { + title: { + contains: (search == undefined || search == null) ? "" : search, + mode: "insensitive" + }, + isActive: (active == "false" || active == undefined) ? false : true, + Division: { + idVillage: String(villageId) + } + } + } + } else { + kondisi = { + idDivision: String(idDivision), + DivisionCalendar: { + title: { + contains: (search == undefined || search == null) ? "" : search, + mode: "insensitive" + }, + isActive: (active == "false" || active == undefined) ? false : true, + Division: { + idVillage: String(villageId) + } + } + } + } + } else { + if (isDate != null && isDate != undefined && isDate != "") { + kondisi = { + dateStart: new Date(String(isDate)), + DivisionCalendar: { + title: { + contains: (search == undefined || search == null) ? "" : search, + mode: "insensitive" + }, + isActive: (active == "false" || active == undefined) ? false : true, + Division: { + idVillage: String(villageId) + } + } + } + } else { + kondisi = { + DivisionCalendar: { + title: { + contains: (search == undefined || search == null) ? "" : search, + mode: "insensitive" + }, + isActive: (active == "false" || active == undefined) ? false : true, + Division: { + idVillage: String(villageId) + } + } + } + } + } + + const data = await prisma.divisionCalendarReminder.findMany({ + where: kondisi, + skip: dataSkip, + take: getFix, + select: { + id: true, + dateStart: true, + timeStart: true, + timeEnd: true, + createdAt: true, + DivisionCalendar: { + select: { + isActive: true, + title: true, + desc: true, + User: { + select: { + name: true + } + } + } + } + }, + orderBy: [ + { + dateStart: 'asc' + }, + { + timeStart: 'asc' + }, + { + timeEnd: 'asc' + } + ] + }); + + const allOmit = data.map((v: any) => ({ + ..._.omit(v, ["DivisionCalendar", "User"]), + title: v.DivisionCalendar.title, + desc: v.DivisionCalendar.desc, + createdBy: v.DivisionCalendar.User.name, + isActive: v.DivisionCalendar.isActive, + timeStart: moment.utc(v.timeStart).format('HH:mm'), + timeEnd: moment.utc(v.timeEnd).format('HH:mm') + })) + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan kalender", data: allOmit }, { status: 200 }); + + } catch (error) { + console.error(error) + return NextResponse.json({ success: false, message: "Gagal mendapatkan kalender, data tidak ditemukan (error: 500)" }, { status: 404 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/discussion-general/[id]/route.ts b/src/app/api/ai/discussion-general/[id]/route.ts new file mode 100644 index 0000000..aae67dd --- /dev/null +++ b/src/app/api/ai/discussion-general/[id]/route.ts @@ -0,0 +1,117 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + + +// GET ONE DETAIL DISKUSI UMUM +export async function GET(request: Request, context: { params: { id: string } }) { + try { + let dataFix + const { id } = context.params + + const { searchParams } = new URL(request.url); + const kategori = searchParams.get("cat"); + const idVillage = searchParams.get("desa"); + + const cek = await prisma.discussion.count({ + where: { + id, + idVillage: String(idVillage) + } + }) + + if (cek == 0) { + return NextResponse.json({ success: false, message: "Gagal mendapatkan diskusi, data tidak ditemukan" }, { status: 404 }); + } + + if (kategori == "comment") { + const data = await prisma.discussionComment.findMany({ + where: { + idDiscussion: id, + isActive: true + }, + select: { + id: true, + comment: true, + createdAt: true, + idUser: true, + User: { + select: { + name: true, + img: true + } + } + } + }) + + dataFix = data.map((v: any) => ({ + ..._.omit(v, ["User",]), + username: v.User.name, + img: v.User.img + })) + + } else if (kategori == "member") { + const data = await prisma.discussionMember.findMany({ + where: { + idDiscussion: id, + isActive: true + }, + select: { + idUser: true, + User: { + select: { + name: true, + img: true + } + } + } + }) + + dataFix = data.map((v: any) => ({ + ..._.omit(v, ["User",]), + name: v.User.name, + img: v.User.img + })) + } else { + const data = await prisma.discussion.findUnique({ + where: { + id, + idVillage: String(idVillage) + }, + select: { + isActive: true, + id: true, + title: true, + idGroup: true, + desc: true, + status: true, + createdAt: true, + Group: { + select: { + name: true, + } + } + } + }) + + dataFix = { + id: data?.id, + isActive: data?.isActive, + idGroup: data?.idGroup, + group: data?.Group.name, + title: data?.title, + desc: data?.desc, + status: data?.status == 1 ? "Open" : "Close", + createdAt: data?.createdAt + } + } + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan diskusi", data: dataFix }, { status: 200 }); + + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan diskusi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} diff --git a/src/app/api/ai/discussion-general/route.ts b/src/app/api/ai/discussion-general/route.ts new file mode 100644 index 0000000..1e26ea7 --- /dev/null +++ b/src/app/api/ai/discussion-general/route.ts @@ -0,0 +1,92 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import "moment/locale/id"; +import { NextResponse } from "next/server"; + + +// GET ALL DISCUSSION GENERAL +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const idGroup = searchParams.get("group"); + const idVillage = searchParams.get("desa"); + const search = searchParams.get('search'); + const page = searchParams.get('page'); + const status = searchParams.get('status'); + const active = searchParams.get('active'); + const get = searchParams.get('get') + + + let getFix = 10; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + let kondisi: any = { + isActive: active == "false" ? false : true, + status: status == "close" ? 2 : 1, + idVillage: String(idVillage), + title: { + contains: (search == undefined || search == "null") ? "" : search, + mode: "insensitive" + }, + } + + if (idGroup != "null" && idGroup != undefined && idGroup != "") { + kondisi = { + ...kondisi, + idGroup: String(idGroup) + } + } + + + const data = await prisma.discussion.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + orderBy: [ + { + status: 'desc' + }, + { + createdAt: 'desc' + } + ], + + select: { + id: true, + title: true, + desc: true, + status: true, + createdAt: true, + DiscussionComment: { + select: { + id: true, + } + }, + Group: { + select: { + name: true, + } + } + } + }); + + const fixData = data.map((v: any) => ({ + ..._.omit(v, ["DiscussionComment", "status", "Group"]), + totalKomentar: v.DiscussionComment.length, + status: v.status == 1 ? "Open" : "Close", + group: v.Group.name, + })) + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan diskusi", data: fixData }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan diskusi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} diff --git a/src/app/api/ai/discussion/[id]/route.ts b/src/app/api/ai/discussion/[id]/route.ts new file mode 100644 index 0000000..c7d9876 --- /dev/null +++ b/src/app/api/ai/discussion/[id]/route.ts @@ -0,0 +1,92 @@ +import { prisma } from "@/module/_global"; +import { NextResponse } from "next/server"; + +// GET ONE DISCUSSION BY ID +export async function GET(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params + + const cek = await prisma.divisionDisscussion.count({ + where: { id } + }) + + if (cek == 0) { + return NextResponse.json( + { success: false, message: "Gagal mendapatkan diskusi, data tidak ditemukan" }, + { status: 404 } + ); + } + + const data = await prisma.divisionDisscussion.findUnique({ + where: { id }, + select: { + isActive: true, + id: true, + desc: true, + status: true, + createdAt: true, + idDivision: true, + Division: { + select: { + name: true, + } + }, + User: { select: { name: true, img: true } }, + DivisionDisscussionComment: { + select: { + id: true, + comment: true, + createdAt: true, + User: { select: { name: true, img: true } } + } + }, + } + }); + + if (!data) { + return NextResponse.json( + { success: false, message: "Diskusi tidak ditemukan" }, + { status: 404 } + ); + } + + // ambil nama creator + const createdBy = data.User.name; + const status = data.status == 1 ? "Open" : "Close" + const division = data.Division.name + + // mapping komentar → hilangkan nested User + const komentar = data.DivisionDisscussionComment.map((comment: any) => ({ + id: comment.id, + comment: comment.comment, + createdAt: comment.createdAt, + username: comment.User.name, + userimg: comment.User.img, + })); + + // bentuk hasil akhir sesuai request + const result = { + id: data.id, + idDivision: data.idDivision, + division, + isActive: data.isActive, + desc: data.desc, + status, + createdAt: data.createdAt, + createdBy, + komentar, + }; + + return NextResponse.json( + { success: true, message: "Berhasil mendapatkan diskusi", data: result }, + { status: 200 } + ); + + } catch (error) { + console.error(error); + return NextResponse.json( + { success: false, message: "Gagal mendapatkan diskusi, coba lagi nanti (error: 500)", reason: (error as Error).message }, + { status: 500 } + ); + } +} diff --git a/src/app/api/ai/discussion/route.ts b/src/app/api/ai/discussion/route.ts new file mode 100644 index 0000000..a37f0b9 --- /dev/null +++ b/src/app/api/ai/discussion/route.ts @@ -0,0 +1,95 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import "moment/locale/id"; +import { NextResponse } from "next/server"; + + +// GET ALL DISCUSSION DIVISION ACTIVE = TRUE +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const idDivision = searchParams.get("division"); + const search = searchParams.get('search'); + const page = searchParams.get('page'); + const status = searchParams.get('status'); + const isActive = searchParams.get('active'); + const villageId = searchParams.get('desa'); + const get = searchParams.get('get'); + + let getFix = 0; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + let kondisi: any = { + isActive: isActive == "false" ? false : true, + status: status == "close" ? 2 : 1, + Division: { + idVillage: String(villageId) + }, + desc: { + contains: (search == undefined || search == "null") ? "" : search, + mode: "insensitive" + }, + } + + + if (idDivision != "null" && idDivision != null && idDivision != undefined) { + kondisi = { + isActive: isActive == "false" ? false : true, + status: status == "close" ? 2 : 1, + idDivision: idDivision, + Division: { + idVillage: String(villageId) + }, + desc: { + contains: (search == undefined || search == "null") ? "" : search, + mode: "insensitive" + }, + } + } + + const data = await prisma.divisionDisscussion.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + orderBy: { + createdAt: 'desc' + }, + select: { + id: true, + desc: true, + status: true, + createdAt: true, + idDivision: true, + Division: { + select: { + name: true, + } + }, + DivisionDisscussionComment: { + select: { + id: true, + } + } + } + }); + + const fixData = data.map((v: any) => ({ + ..._.omit(v, ["DivisionDisscussionComment", "status", "Division"]), + totalKomentar: v.DivisionDisscussionComment.length, + status: v.status == 1 ? "Open" : "Close", + division: v.Division.name + })) + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan diskusi", data: fixData, }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan diskusi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/division/[id]/route.ts b/src/app/api/ai/division/[id]/route.ts new file mode 100644 index 0000000..5e5d475 --- /dev/null +++ b/src/app/api/ai/division/[id]/route.ts @@ -0,0 +1,63 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + + +// GET ONE DATA DIVISI :: UNTUK TAMPIL DATA DI HALAMAN EDIT DAN INFO +export async function GET(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + const { searchParams } = new URL(request.url); + const idVillage = searchParams.get("desa"); + + const data = await prisma.division.findUnique({ + where: { + id: String(id), + idVillage: String(idVillage) + } + }); + + if (!data) { + return NextResponse.json({ success: false, message: "Gagal mendapatkan divisi, data tidak ditemukan", }, { status: 404 }); + } + + const member = await prisma.divisionMember.findMany({ + where: { + idDivision: String(id), + isActive: true, + }, + select: { + id: true, + isAdmin: true, + idUser: true, + User: { + select: { + name: true, + img: true + } + } + }, + orderBy: { + isAdmin: 'desc', + } + }) + + const fixMember = member.map((v: any) => ({ + ..._.omit(v, ["User"]), + name: v.User.name, + img: v.User.img + })) + + const dataFix = { + ...data, + member: fixMember + } + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan divisi", data: dataFix, }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan divisi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/division/report/route.ts b/src/app/api/ai/division/report/route.ts new file mode 100644 index 0000000..2d6bbee --- /dev/null +++ b/src/app/api/ai/division/report/route.ts @@ -0,0 +1,269 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url) + const idVillage = searchParams.get("desa") + const idGroup = searchParams.get("group") + const division = searchParams.get("division") + const date = searchParams.get("date-start") + const dateAkhir = searchParams.get("date-end") + const kat = searchParams.get("cat") + + + // CHART PROGRESS + if (kat == "dokumen") { + // CHART DOKUMEN + let kondisi: any = { + isActive: true, + category: 'FILE', + Division: { + idVillage: String(idVillage) + }, + createdAt: { + gte: new Date(String(date)), + lte: new Date(String(dateAkhir)) + }, + } + + if (idGroup != undefined && idGroup != null && idGroup != "") { + kondisi = { + isActive: true, + category: 'FILE', + Division: { + idGroup: String(idGroup) + }, + createdAt: { + gte: new Date(String(date)), + lte: new Date(String(dateAkhir)) + }, + } + } + + + if (division != undefined && division != null && division != "") { + kondisi = { + ...kondisi, + idDivision: String(division) + } + } + + + const dataDokumen = await prisma.divisionDocumentFolderFile.findMany({ + where: kondisi, + }) + + const groupData = _.map(_.groupBy(dataDokumen, "extension"), (v: any) => ({ + file: v[0].extension, + jumlah: v.length, + })) + + const image = ['jpg', 'jpeg', 'png', 'heic'] + + let hasilImage = { + name: 'Gambar', + value: 0 + } + + let hasilFile = { + name: 'Dokumen', + value: 0 + } + + groupData.map((v: any) => { + if (image.some((i: any) => i == v.file)) { + hasilImage = { + name: 'Gambar', + value: hasilImage.value + v.jumlah + } + } else { + hasilFile = { + name: 'Dokumen', + value: hasilFile.value + v.jumlah + } + } + }) + + const hasilDokumen = { gambar: hasilImage.value, dokumen: hasilFile.value } + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan data", data: hasilDokumen }, { status: 200 }); + } else if (kat == "event") { + // CHART EVENT + let kondisiSelesai: any = { + isActive: true, + Division: { + idVillage: String(idVillage) + }, + DivisionCalendarReminder: { + some: { + dateStart: { + gte: new Date(String(date)), + lte: new Date() + } + } + } + } + + let kondisiComingSoon: any = { + isActive: true, + Division: { + idVillage: String(idVillage) + }, + DivisionCalendarReminder: { + some: { + dateStart: { + gt: new Date(), + lte: new Date(String(dateAkhir)) + } + } + } + } + + if (idGroup != undefined && idGroup != null && idGroup != "") { + kondisiSelesai = { + isActive: true, + Division: { + idGroup: String(idGroup) + }, + DivisionCalendarReminder: { + some: { + dateStart: { + gte: new Date(String(date)), + lte: new Date() + } + } + } + } + + kondisiComingSoon = { + isActive: true, + Division: { + idGroup: String(idGroup) + }, + DivisionCalendarReminder: { + some: { + dateStart: { + gt: new Date(), + lte: new Date(String(dateAkhir)) + } + } + } + } + } + + if (division != undefined && division != null && division != "") { + kondisiSelesai = { + ...kondisiSelesai, + idDivision: String(division) + } + + kondisiComingSoon = { + ...kondisiComingSoon, + idDivision: String(division) + } + } + + const eventSelesai = await prisma.divisionCalendar.count({ + where: kondisiSelesai + }) + + const eventComingSoon = await prisma.divisionCalendar.count({ + where: kondisiComingSoon + }) + + const hasilEvent = { + selesai: eventSelesai, + akan_datang: eventComingSoon + } + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan data", data: hasilEvent }, { status: 200 }); + + } else { + let kondisiProgress: any = { + isActive: true, + Division: { + idVillage: String(idVillage) + }, + DivisionProjectTask: { + some: { + dateStart: { + gte: new Date(String(date)) + }, + dateEnd: { + lte: new Date(String(dateAkhir)) + } + } + } + } + + if (idGroup != undefined && idGroup != null && idGroup != "") { + kondisiProgress = { + isActive: true, + Division: { + idGroup: String(idGroup) + }, + DivisionProjectTask: { + some: { + dateStart: { + gte: new Date(String(date)) + }, + dateEnd: { + lte: new Date(String(dateAkhir)) + } + } + } + } + } + + if (division != undefined && division != null && division != "") { + kondisiProgress = { + ...kondisiProgress, + idDivision: String(division) + } + } + + const data = await prisma.divisionProject.groupBy({ + where: kondisiProgress, + by: ["status"], + _count: true + }) + + const dataStatus = [{ name: 'Segera', status: 0 }, { name: 'Dikerjakan', status: 1 }, { name: 'Selesai', status: 2 }, { name: 'Dibatalkan', status: 3 }] + const hasilProgres: any[] = [] + let input + for (let index = 0; index < dataStatus.length; index++) { + const cek = data.some((i: any) => i.status == dataStatus[index].status) + if (cek) { + const find = ((Number(data.find((i: any) => i.status == dataStatus[index].status)?._count) * 100) / data.reduce((n, { _count }) => n + _count, 0)).toFixed(2) + const fix = find != "100.00" ? find.substr(-2, 2) == "00" ? find.substr(0, 2) : find : "100" + input = { + name: dataStatus[index].name, + value: fix + } + } else { + input = { + name: dataStatus[index].name, + value: 0 + } + } + hasilProgres.push(input) + } + + const dataFixProgress = hasilProgres.reduce((acc: any, curr: any) => { + acc[curr.name] = curr.value + return acc + }, {}) + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan data", data: dataFixProgress }, { status: 200 }); + + } + + } + catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan data, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/division/route.ts b/src/app/api/ai/division/route.ts new file mode 100644 index 0000000..7c65213 --- /dev/null +++ b/src/app/api/ai/division/route.ts @@ -0,0 +1,84 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + + +// GET ALL DATA DIVISI == LIST DATA DIVISI +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const idVillage = searchParams.get("desa"); + const idGroup = searchParams.get("group"); + const name = searchParams.get('search'); + const page = searchParams.get('page'); + const active = searchParams.get("active"); + const get = searchParams.get('get') + + let getFix = 10; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + let kondisi: any = { + isActive: active == 'false' ? false : true, + idVillage: String(idVillage), + name: { + contains: (name == undefined || name == "null") ? "" : name, + mode: "insensitive" + } + } + + if (idGroup != "null" && idGroup != undefined && idGroup != "") { + kondisi = { + ...kondisi, + idGroup: String(idGroup) + } + } + + + const data = await prisma.division.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + select: { + id: true, + name: true, + desc: true, + idGroup: true, + Group: { + select: { + name: true + } + }, + DivisionMember: { + where: { + isActive: true + }, + select: { + idUser: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + const allData = data.map((v: any) => ({ + ..._.omit(v, ["DivisionMember", "Group"]), + group: v.Group.name, + jumlahMember: v.DivisionMember.length, + })) + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan divisi", data: allData }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan divisi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/document/route.ts b/src/app/api/ai/document/route.ts new file mode 100644 index 0000000..2c52ec7 --- /dev/null +++ b/src/app/api/ai/document/route.ts @@ -0,0 +1,145 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import moment from "moment"; +import { NextResponse } from "next/server"; + + +// GET ALL DOCUMENT +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const idDivision = searchParams.get("division"); + const villageId = searchParams.get("desa"); + const path = searchParams.get("path"); + const active = searchParams.get("active"); + const search = searchParams.get("search"); + const page = searchParams.get("page"); + const get = searchParams.get("get"); + + let getFix = 10; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + let kondisi: any = { + Division: { + idVillage: String(villageId) + }, + isActive: active == 'false' ? false : true, + path: (path == "undefined" || path == "null" || path == "" || path == null || path == undefined) ? "home" : path, + name: { + contains: (search == undefined || search == "null") ? "" : search, + mode: "insensitive" + } + } + + if (idDivision != "null" && idDivision != undefined && idDivision != "") { + kondisi = { + ...kondisi, + idDivision: String(idDivision) + } + } + + let formatDataShare: any[] = []; + + if (path == "home" || path == "null" || path == "undefined" || path == null || path == undefined || path == "") { + const dataShare = await prisma.divisionDocumentShare.findMany({ + where: { + isActive: true, + idDivision: String(idDivision), + DivisionDocumentFolderFile: { + isActive: true + } + }, + select: { + DivisionDocumentFolderFile: { + select: { + idStorage: true, + id: true, + category: true, + name: true, + extension: true, + path: true, + User: { + select: { + name: true + } + }, + createdAt: true, + updatedAt: true + } + } + }, + orderBy: { + DivisionDocumentFolderFile: { + createdAt: 'desc' + } + } + }) + + formatDataShare = dataShare.map((v: any) => ({ + ..._.omit(v, ["DivisionDocumentFolderFile"]), + idStorage: v.DivisionDocumentFolderFile.idStorage, + id: v.DivisionDocumentFolderFile.id, + category: v.DivisionDocumentFolderFile.category, + name: v.DivisionDocumentFolderFile.name, + extension: v.DivisionDocumentFolderFile.extension, + path: v.DivisionDocumentFolderFile.path, + createdBy: v.DivisionDocumentFolderFile.User.name, + createdAt: v.DivisionDocumentFolderFile.createdAt, + updatedAt: v.DivisionDocumentFolderFile.updatedAt, + share: true + })) + } + + + const data = await prisma.divisionDocumentFolderFile.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + select: { + id: true, + category: true, + name: true, + extension: true, + idStorage: true, + path: true, + User: { + select: { + name: true + } + }, + createdAt: true, + updatedAt: true + }, + orderBy: { + createdAt: 'desc' + } + }) + + const allData = data.map((v: any) => ({ + ..._.omit(v, ["User", "createdAt", "updatedAt"]), + createdBy: v.User.name, + createdAt: v.createdAt, + updatedAt: v.updatedAt, + share: false + })) + + if (formatDataShare.length > 0) { + allData.push(...formatDataShare) + } + + const formatData = _.orderBy(allData, ['category', 'createdAt'], ['desc', 'desc']); + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan item", data: formatData }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan item, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/group/route.ts b/src/app/api/ai/group/route.ts new file mode 100644 index 0000000..3b85c37 --- /dev/null +++ b/src/app/api/ai/group/route.ts @@ -0,0 +1,45 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const villageId = searchParams.get("desa"); + const isActive = searchParams.get("active"); + const search = searchParams.get('search'); + const page = searchParams.get('page') + const get = searchParams.get('get') + + let getFix = 10; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + const data = await prisma.group.findMany({ + skip: dataSkip, + take: getFix, + where: { + isActive: isActive == 'false' ? false : true, + idVillage: String(villageId), + name: { + contains: (search == undefined || search == null) ? "" : search, + mode: "insensitive" + } + }, + orderBy: { + name: 'asc' + } + }); + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan grup", data, }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan grup, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/position/route.ts b/src/app/api/ai/position/route.ts new file mode 100644 index 0000000..3aaca1f --- /dev/null +++ b/src/app/api/ai/position/route.ts @@ -0,0 +1,79 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + + +// GET ALL POSITION +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const idVillage = searchParams.get("desa"); + const idGroup = searchParams.get("group"); + const active = searchParams.get('active'); + const search = searchParams.get('search') + const page = searchParams.get('page') + const get = searchParams.get('get') + + let getFix = 10; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + let kondisi: any = { + isActive: active == 'false' ? false : true, + Group: { + idVillage: String(idVillage) + }, + name: { + contains: (search == undefined || search == null) ? "" : search, + mode: "insensitive" + } + } + + if (idGroup != "null" && idGroup != undefined && idGroup != "") { + kondisi = { + ...kondisi, + idGroup: String(idGroup) + } + } + + + + const positions = await prisma.position.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + select: { + id: true, + name: true, + idGroup: true, + isActive: true, + createdAt: true, + updatedAt: true, + Group: { + select: { + name: true + } + } + }, + orderBy: { + name: 'asc' + } + }); + + const allData = positions.map((v: any) => ({ + ..._.omit(v, ["Group"]), + group: v.Group.name + })) + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan jabatan", data: allData }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan jabatan, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/project/[id]/route.ts b/src/app/api/ai/project/[id]/route.ts new file mode 100644 index 0000000..83853a9 --- /dev/null +++ b/src/app/api/ai/project/[id]/route.ts @@ -0,0 +1,172 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import moment from "moment"; +import { NextResponse } from "next/server"; + + +// GET DETAIL PROJECT / GET ONE PROJECT +export async function GET(request: Request, context: { params: { id: string } }) { + try { + let allData + const { id } = context.params; + const { searchParams } = new URL(request.url); + const kategori = searchParams.get("cat"); + + const data = await prisma.project.findUnique({ + where: { + id: String(id), + }, + select: { + id: true, + idVillage: true, + idGroup: true, + title: true, + status: true, + desc: true, + reason: true, + report: true, + isActive: true, + Group: { + select: { + name: true + } + } + } + }); + + if (!data) { + return NextResponse.json({ success: false, message: "Gagal mendapatkan kegiatan, data tidak ditemukan", }, { status: 404 }); + } + + + if (kategori == "data") { + const dataProgress = await prisma.projectTask.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + orderBy: { + updatedAt: 'desc' + } + }) + + const semua = dataProgress.length + const selesai = _.filter(dataProgress, { status: 1 }).length + const progress = Math.ceil((selesai / semua) * 100) + + allData = { + id: data.id, + idVillage: data.idVillage, + idGroup: data.idGroup, + group: data.Group.name, + title: data.title, + status: data.status == 3 ? "batal" : data.status == 2 ? "selesai" : data.status == 1 ? "dikerjakan" : "segera", + desc: data.desc, + reason: data.reason, + report: data.report, + isActive: data.isActive, + progress: (_.isNaN(progress)) ? 0 : progress, + } + + } else if (kategori == "task") { + const dataProgress = await prisma.projectTask.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + select: { + id: true, + title: true, + desc: true, + status: true, + dateStart: true, + dateEnd: true, + createdAt: true + }, + orderBy: { + createdAt: 'asc' + } + }) + + const formatData = dataProgress.map((v: any) => ({ + ..._.omit(v, ["dateStart", "dateEnd", "createdAt", "status"]), + status: v.status == 1 ? "selesai" : "belum selesai", + dateStart: moment(v.dateStart).format("DD-MM-YYYY"), + dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"), + createdAt: moment(v.createdAt).format("DD-MM-YYYY HH:mm"), + })) + const dataFix = _.orderBy(formatData, 'createdAt', 'asc') + allData = dataFix + + } else if (kategori == "file") { + const dataFile = await prisma.projectFile.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + orderBy: { + createdAt: 'asc' + }, + select: { + id: true, + name: true, + extension: true, + idStorage: true + } + }) + + allData = dataFile + + } else if (kategori == "member") { + const dataMember = await prisma.projectMember.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + select: { + id: true, + idUser: true, + User: { + select: { + name: true, + email: true, + img: true, + Position: { + select: { + name: true + } + } + } + }, + } + }) + + const fix = dataMember.map((v: any) => ({ + ..._.omit(v, ["User"]), + name: v.User.name, + email: v.User.email, + img: v.User.img, + position: v.User.Position.name + })) + + allData = fix + } else if (kategori == "link") { + const dataLink = await prisma.projectLink.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + orderBy: { + createdAt: 'asc' + } + }) + allData = dataLink + } + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan kegiatan", data: allData }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan kegiatan, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/project/route.ts b/src/app/api/ai/project/route.ts new file mode 100644 index 0000000..7715ee5 --- /dev/null +++ b/src/app/api/ai/project/route.ts @@ -0,0 +1,107 @@ +import { prisma } from "@/module/_global"; +import _, { ceil } from "lodash"; +import { NextResponse } from "next/server"; + + +// GET ALL DATA PROJECT +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const idVillage = searchParams.get('desa'); + const name = searchParams.get('search'); + const status = searchParams.get('status'); + const idGroup = searchParams.get("group"); + const page = searchParams.get('page'); + const get = searchParams.get('get'); + + let getFix = 10; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + + + + let kondisi: any = { + isActive: true, + idVillage: String(idVillage), + title: { + contains: (name == undefined || name == "null") ? "" : name, + mode: "insensitive" + }, + } + + if (status != "null" && status != undefined && status != "") { + kondisi = { + ...kondisi, + status: status == "segera" ? 0 : status == "dikerjakan" ? 1 : status == "selesai" ? 2 : status == "batal" ? 3 : 0 + } + } + + + if (idGroup != "null" && idGroup != undefined && idGroup != "") { + kondisi = { + ...kondisi, + idGroup: String(idGroup) + } + } + + + const data = await prisma.project.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + select: { + id: true, + idGroup: true, + title: true, + desc: true, + status: true, + ProjectMember: { + where: { + isActive: true + }, + select: { + idUser: true + } + }, + ProjectTask: { + where: { + isActive: true + }, + select: { + title: true, + status: true + } + }, + Group: { + select: { + name: true + } + } + }, + orderBy: { + createdAt: 'desc' + } + }) + + const omitData = data.map((v: any) => ({ + ..._.omit(v, ["ProjectMember", "ProjectTask", "status", "Group"]), + group: v.Group.name, + status: v.status == 1 ? "dikerjakan" : v.status == 2 ? "selesai" : v.status == 3 ? "batal" : "segera", + progress: ceil((v.ProjectTask.filter((i: any) => i.status == 1).length * 100) / v.ProjectTask.length), + member: v.ProjectMember.length + })) + + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan kegiatan", data: omitData }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan kegiatan, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/task/[id]/route.ts b/src/app/api/ai/task/[id]/route.ts new file mode 100644 index 0000000..75b3261 --- /dev/null +++ b/src/app/api/ai/task/[id]/route.ts @@ -0,0 +1,180 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import moment from "moment"; +import "moment/locale/id"; +import { NextResponse } from "next/server"; + + +// GET DETAIL TASK DIVISI / GET ONE +export async function GET(request: Request, context: { params: { id: string } }) { + try { + let allData + const { id } = context.params; + const { searchParams } = new URL(request.url); + const kategori = searchParams.get("cat"); + + const data = await prisma.divisionProject.findUnique({ + where: { + id: String(id), + }, + select: { + id: true, + idDivision: true, + title: true, + status: true, + desc: true, + reason: true, + report: true, + isActive: true, + Division: { + select: { + name: true + } + } + } + }); + + if (!data) { + return NextResponse.json({ success: false, message: "Gagal mendapatkan kegiatan, data tidak ditemukan", }, { status: 404 }); + } + + if (kategori == "data") { + const dataProgress = await prisma.divisionProjectTask.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + orderBy: { + updatedAt: 'desc' + } + }) + + const semua = dataProgress.length + const selesai = _.filter(dataProgress, { status: 1 }).length + const progress = Math.ceil((selesai / semua) * 100) + + + allData = { + id: data.id, + idDivision: data.idDivision, + division: data.Division.name, + title: data.title, + status: data.status == 3 ? "batal" : data.status == 2 ? "selesai" : data.status == 1 ? "dikerjakan" : "segera", + desc: data.desc, + reason: data.reason, + report: data.report, + isActive: data.isActive, + progress: progress, + } + } else if (kategori == "task") { + const dataProgress = await prisma.divisionProjectTask.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + select: { + id: true, + title: true, + status: true, + dateStart: true, + dateEnd: true, + }, + orderBy: { + createdAt: 'asc' + } + }) + + const fix = dataProgress.map((v: any) => ({ + ..._.omit(v, ["dateStart", "dateEnd", "status"]), + status: v.status == 1 ? "selesai" : "belum selesai", + dateStart: moment(v.dateStart).format("DD-MM-YYYY"), + dateEnd: moment(v.dateEnd).format("DD-MM-YYYY"), + })) + + allData = fix + + } else if (kategori == "file") { + const dataFile = await prisma.divisionProjectFile.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + select: { + id: true, + ContainerFileDivision: { + select: { + id: true, + name: true, + extension: true, + idStorage: true + } + } + } + }) + + const fix = dataFile.map((v: any) => ({ + ..._.omit(v, ["ContainerFileDivision"]), + nameInStorage: v.ContainerFileDivision.id, + name: v.ContainerFileDivision.name, + extension: v.ContainerFileDivision.extension, + idStorage: v.ContainerFileDivision.idStorage, + })) + + allData = fix + + } else if (kategori == "member") { + const dataMember = await prisma.divisionProjectMember.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + select: { + id: true, + idUser: true, + User: { + select: { + name: true, + email: true, + img: true, + Position: { + select: { + name: true + } + } + } + } + } + }) + + + const fix = dataMember.map((v: any) => ({ + ..._.omit(v, ["User"]), + name: v.User.name, + email: v.User.email, + img: v.User.img, + position: v.User.Position.name + })) + + allData = fix + } else if (kategori == "link") { + const dataLink = await prisma.divisionProjectLink.findMany({ + where: { + isActive: true, + idProject: String(id) + }, + orderBy: { + createdAt: 'asc' + } + }) + + allData = dataLink + } + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan tugas divisi", data: allData }, { status: 200 }); + + } + catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan tugas divisi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/task/route.ts b/src/app/api/ai/task/route.ts new file mode 100644 index 0000000..3fd53dc --- /dev/null +++ b/src/app/api/ai/task/route.ts @@ -0,0 +1,104 @@ +import { prisma } from "@/module/_global"; +import _, { ceil } from "lodash"; +import { NextResponse } from "next/server"; + + +// GET ALL DATA TUGAS DIVISI +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const villageId = searchParams.get('desa'); + const division = searchParams.get('division'); + const search = searchParams.get('search'); + const status = searchParams.get('status'); + const page = searchParams.get('page'); + const get = searchParams.get('get'); + + let getFix = 10; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + let kondisi: any = { + isActive: true, + Division: { + idVillage: String(villageId) + }, + title: { + contains: (search == undefined || search == "null") ? "" : search, + mode: "insensitive" + } + } + + if (status != "null" && status != undefined && status != "" && status != null) { + kondisi = { + ...kondisi, + status: status == "segera" ? 0 : status == "dikerjakan" ? 1 : status == "selesai" ? 2 : status == "batal" ? 3 : 0 + } + } + + if (division != "null" && division != undefined && division != "" && division != null) { + kondisi = { + ...kondisi, + idDivision: String(division) + } + } + + + const data = await prisma.divisionProject.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + select: { + id: true, + idDivision: true, + title: true, + desc: true, + status: true, + DivisionProjectTask: { + where: { + isActive: true + }, + select: { + title: true, + status: true + } + }, + DivisionProjectMember: { + where: { + isActive: true + }, + select: { + idUser: true + } + }, + Division: { + select: { + name: true + } + } + }, + orderBy: { + createdAt: "desc" + } + }); + + const formatData = data.map((v: any) => ({ + ..._.omit(v, ["DivisionProjectTask", "DivisionProjectMember", "status", "Division"]), + division: v.Division.name, + status: v.status == 1 ? "dikerjakan" : v.status == 2 ? "selesai" : v.status == 3 ? "batal" : "segera", + progress: ceil((v.DivisionProjectTask.filter((i: any) => i.status == 1).length * 100) / v.DivisionProjectTask.length), + member: v.DivisionProjectMember.length, + })) + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan divisi", data: formatData }, { status: 200 }); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan divisi, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/user/[id]/route.ts b/src/app/api/ai/user/[id]/route.ts new file mode 100644 index 0000000..bb8048c --- /dev/null +++ b/src/app/api/ai/user/[id]/route.ts @@ -0,0 +1,75 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + +// GET ONE MEMBER / USER +export async function GET(request: Request, context: { params: { id: string } }) { + try { + const { id } = context.params; + + const users = await prisma.user.findUnique({ + where: { + id: id, + }, + select: { + id: true, + nik: true, + name: true, + phone: true, + email: true, + gender: true, + img: true, + idGroup: true, + isActive: true, + idPosition: true, + createdAt: true, + updatedAt: true, + UserRole: { + select: { + name: true, + id: true + } + }, + Position: { + select: { + name: true, + id: true + }, + }, + Group: { + select: { + name: true, + id: true + }, + }, + }, + }); + + const { ...userData } = users; + const group = users?.Group.name + const position = users?.Position?.name + const idUserRole = users?.UserRole.id + const phone = '+62' + users?.phone + const role = users?.UserRole.name + const gender = users?.gender == "F" ? "Perempuan" : "Laki-Laki" + + const result = { ...userData, gender, group, position, idUserRole, phone, role }; + + const omitData = _.omit(result, ["Group", "Position", "UserRole"]); + + + + return NextResponse.json( + { + success: true, + message: "Berhasil mendapatkan anggota", + data: omitData, + }, + { status: 200 } + ); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan anggota, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/ai/user/route.ts b/src/app/api/ai/user/route.ts new file mode 100644 index 0000000..0f5908e --- /dev/null +++ b/src/app/api/ai/user/route.ts @@ -0,0 +1,88 @@ +import { prisma } from "@/module/_global"; +import _ from "lodash"; +import { NextResponse } from "next/server"; + +// GET ALL MEMBER / USER +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const search = searchParams.get('search') + const idVillage = searchParams.get("desa"); + const idGroup = searchParams.get("group"); + const active = searchParams.get("active"); + const page = searchParams.get('page'); + const get = searchParams.get('get'); + + let getFix = 10; + if (get == null || get == undefined || get == "" || _.isNaN(Number(get))) { + getFix = 10; + } else { + getFix = Number(get); + } + + const dataSkip = page == null || page == undefined ? 0 : Number(page) * getFix - getFix; + + let kondisi: any = { + isActive: active == 'false' ? false : true, + idVillage: String(idVillage), + name: { + contains: (search == undefined || search == null) ? "" : search, + mode: "insensitive", + }, + NOT: { + idUserRole: 'developer' + } + } + + if (idGroup != "null" && idGroup != undefined && idGroup != "" && idGroup != null) { + kondisi = { + ...kondisi, + idGroup: String(idGroup) + } + } + + + const users = await prisma.user.findMany({ + skip: dataSkip, + take: getFix, + where: kondisi, + select: { + id: true, + idUserRole: true, + isActive: true, + nik: true, + name: true, + phone: true, + Position: { + select: { + name: true, + }, + }, + Group: { + select: { + name: true, + }, + }, + }, + orderBy: { + name: 'asc' + } + }); + + const allData = users.map((v: any) => ({ + ..._.omit(v, ["phone", "gender", "Group", "Position"]), + gender: v.gender == "F" ? "Perempuan" : "Laki-Laki", + phone: "+" + v.phone, + group: v.Group.name, + position: v?.Position?.name + })) + + return NextResponse.json({ success: true, message: "Berhasil member", data: allData }, { status: 200 }); + + + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan anggota, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/version-app/route.ts b/src/app/api/version-app/route.ts index 92ada2d..1d58873 100644 --- a/src/app/api/version-app/route.ts +++ b/src/app/api/version-app/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; export async function GET(request: Request) { try { - return NextResponse.json({ success: true, version: "1.8.0", tahap: "beta", update: "-api mobile; -login tanpa otp (mobile app); -tambah laporan pada project dan tugas divisi; -tambah upload link pada project dan tugas divisi; -tambah detail tanggal dan jam pada project dan tugas divisi" }, { status: 200 }); + return NextResponse.json({ success: true, version: "2.0.0", tahap: "beta", update: "-api mobile; -login tanpa otp (mobile app); -tambah laporan pada project dan tugas divisi; -tambah upload link pada project dan tugas divisi; -tambah detail tanggal dan jam pada project dan tugas divisi; -api jenna ai; -privacy policy" }, { status: 200 }); } catch (error) { console.error(error); return NextResponse.json({ success: false, version: "Gagal mendapatkan version, coba lagi nanti (error: 500)", reason: (error as Error).message, }, { status: 500 });