From 433e6889cdcbf9326ad90f599b849c142ccd614a Mon Sep 17 00:00:00 2001 From: yosaphatprs Date: Tue, 25 Nov 2025 16:26:00 +0700 Subject: [PATCH] feat: (+) add total audit data, jumlah rekam medis 7 hari terakhir, data audit trail (tampered & not tampered), data yang perlu divalidasi, tamtampered data per kelompok data pada dashboard. (+) fix validation payload on rekam medis, add validation on rekam medis create, update, and delete --- backend/api/backfill-state.json | 14 +- .../migration.sql | 29 ++ .../migration.sql | 2 + .../migration.sql | 2 + backend/api/prisma/schema.prisma | 37 +- backend/api/src/app.module.ts | 4 +- backend/api/src/app.service.ts | 19 +- backend/api/src/main.ts | 1 + .../api/src/modules/audit/audit.controller.ts | 17 +- .../api/src/modules/audit/audit.gateway.ts | 6 +- backend/api/src/modules/audit/audit.module.ts | 1 + .../api/src/modules/audit/audit.service.ts | 53 ++- backend/api/src/modules/auth/auth.module.ts | 2 +- backend/api/src/modules/log/log.service.ts | 104 ++--- .../api/src/modules/proof/proof.service.ts | 26 +- .../rekammedis/dto/create-rekammedis.dto.ts | 4 + .../rekammedis/rekammedis.controller.ts | 11 + .../modules/rekammedis/rekammedis.module.ts | 1 - .../modules/rekammedis/rekammedis.service.ts | 147 +++++- .../tindakandokter.controller.ts | 1 + .../tindakandokter/tindakandokter.module.ts | 1 + .../tindakandokter/tindakandokter.service.ts | 130 ++++-- .../dto/store-validation-queue.dto.ts | 49 ++ .../validation/validation.controller.spec.ts | 18 + .../validation/validation.controller.ts | 62 +++ .../modules/validation/validation.module.ts | 14 + .../validation/validation.service.spec.ts | 18 + .../modules/validation/validation.service.ts | 168 +++++++ .../network/channel-artifacts/mychannel.block | Bin 14402 -> 14404 bytes frontend/hospital-log/package-lock.json | 30 ++ frontend/hospital-log/package.json | 2 + .../src/components/dashboard/DataTable.vue | 13 + .../src/components/dashboard/Sidebar.vue | 32 ++ .../hospital-log/src/constants/interfaces.ts | 14 + .../hospital-log/src/constants/pagination.ts | 54 +++ frontend/hospital-log/src/routes/index.ts | 21 + frontend/hospital-log/src/style/style.css | 8 +- .../src/views/dashboard/AuditTrailView.vue | 5 +- .../src/views/dashboard/DashboardView.vue | 342 +++++++++++++- .../dashboard/PemberianObatDetailView.vue | 162 ++++--- .../views/dashboard/PemberianObatEditView.vue | 155 +++++-- .../dashboard/TindakanDokterDetailsView.vue | 2 +- .../views/dashboard/users/CreateUserView.vue | 5 + .../dashboard/validasi/ReviewValidasiView.vue | 427 ++++++++++++++++++ .../views/dashboard/validasi/ValidasiView.vue | 321 +++++++++++++ 45 files changed, 2280 insertions(+), 254 deletions(-) create mode 100644 backend/api/prisma/migrations/20251117091815_add_validation_queue_table/migration.sql create mode 100644 backend/api/prisma/migrations/20251117092351_alter_table_validation_add_status/migration.sql create mode 100644 backend/api/prisma/migrations/20251121043536_change_record_id_on_validation_queue_into_nullable/migration.sql create mode 100644 backend/api/src/modules/validation/dto/store-validation-queue.dto.ts create mode 100644 backend/api/src/modules/validation/validation.controller.spec.ts create mode 100644 backend/api/src/modules/validation/validation.controller.ts create mode 100644 backend/api/src/modules/validation/validation.module.ts create mode 100644 backend/api/src/modules/validation/validation.service.spec.ts create mode 100644 backend/api/src/modules/validation/validation.service.ts create mode 100644 frontend/hospital-log/src/views/dashboard/users/CreateUserView.vue create mode 100644 frontend/hospital-log/src/views/dashboard/validasi/ReviewValidasiView.vue create mode 100644 frontend/hospital-log/src/views/dashboard/validasi/ValidasiView.vue diff --git a/backend/api/backfill-state.json b/backend/api/backfill-state.json index 0ac4125..66061de 100644 --- a/backend/api/backfill-state.json +++ b/backend/api/backfill-state.json @@ -1,28 +1,28 @@ { "cursors": { - "pemberian_obat": "5", - "rekam_medis": "100079", - "pemberian_tindakan": "5" + "pemberian_obat": "10", + "rekam_medis": "1001724", + "pemberian_tindakan": "10" }, "failures": {}, "metadata": { "pemberian_obat": { - "lastRunAt": "2025-11-14T03:35:57.788Z", + "lastRunAt": "2025-11-21T02:36:13.102Z", "processed": 5, "success": 5, "failed": 0 }, "rekam_medis": { - "lastRunAt": "2025-11-14T03:35:57.788Z", + "lastRunAt": "2025-11-21T02:36:13.102Z", "processed": 5, "success": 5, "failed": 0 }, "pemberian_tindakan": { - "lastRunAt": "2025-11-14T03:35:57.788Z", + "lastRunAt": "2025-11-21T02:36:13.102Z", "processed": 5, "success": 5, "failed": 0 } } -} +} \ No newline at end of file diff --git a/backend/api/prisma/migrations/20251117091815_add_validation_queue_table/migration.sql b/backend/api/prisma/migrations/20251117091815_add_validation_queue_table/migration.sql new file mode 100644 index 0000000..04ac736 --- /dev/null +++ b/backend/api/prisma/migrations/20251117091815_add_validation_queue_table/migration.sql @@ -0,0 +1,29 @@ +-- CreateEnum +CREATE TYPE "TableName" AS ENUM ('pemberian_obat', 'pemberian_tindakan', 'rekam_medis'); + +-- CreateEnum +CREATE TYPE "ValidationStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "ValidationAction" AS ENUM ('CREATE', 'UPDATE', 'DELETE'); + +-- CreateTable +CREATE TABLE "validation_queue" ( + "id" BIGSERIAL NOT NULL, + "table_name" "TableName" NOT NULL, + "record_id" VARCHAR(50) NOT NULL, + "action" "ValidationAction" NOT NULL, + "dataPayload" JSONB NOT NULL, + "user_id_request" BIGINT NOT NULL, + "user_id_process" BIGINT, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "processed_at" TIMESTAMPTZ(6), + + CONSTRAINT "validation_queue_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "validation_queue" ADD CONSTRAINT "fk_validation_user" FOREIGN KEY ("user_id_request") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "validation_queue" ADD CONSTRAINT "fk_validation_user_process" FOREIGN KEY ("user_id_process") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/backend/api/prisma/migrations/20251117092351_alter_table_validation_add_status/migration.sql b/backend/api/prisma/migrations/20251117092351_alter_table_validation_add_status/migration.sql new file mode 100644 index 0000000..6dd7339 --- /dev/null +++ b/backend/api/prisma/migrations/20251117092351_alter_table_validation_add_status/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "validation_queue" ADD COLUMN "status" "ValidationStatus" NOT NULL DEFAULT 'PENDING'; diff --git a/backend/api/prisma/migrations/20251121043536_change_record_id_on_validation_queue_into_nullable/migration.sql b/backend/api/prisma/migrations/20251121043536_change_record_id_on_validation_queue_into_nullable/migration.sql new file mode 100644 index 0000000..a4f409a --- /dev/null +++ b/backend/api/prisma/migrations/20251121043536_change_record_id_on_validation_queue_into_nullable/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "validation_queue" ALTER COLUMN "record_id" SET DEFAULT ''; diff --git a/backend/api/prisma/schema.prisma b/backend/api/prisma/schema.prisma index ddb8860..3d19d1d 100644 --- a/backend/api/prisma/schema.prisma +++ b/backend/api/prisma/schema.prisma @@ -74,6 +74,8 @@ model users { created_at DateTime? @default(now()) @db.Timestamptz(6) updated_at DateTime? @default(now()) @db.Timestamptz(6) blockchain_log_queue blockchain_log_queue[] + validation_queue_requested validation_queue[] @relation("ValidationRequester") + validation_queue_processed validation_queue[] @relation("ValidationProcessor") } enum AuditEvent { @@ -101,4 +103,37 @@ model audit { user_id BigInt last_sync DateTime @default(now()) @db.Timestamptz(6) result resultStatus -} \ No newline at end of file +} + +enum TableName { + pemberian_obat + pemberian_tindakan + rekam_medis +} + +enum ValidationStatus { + PENDING + APPROVED + REJECTED +} + +enum ValidationAction { + CREATE + UPDATE + DELETE +} + +model validation_queue { + id BigInt @id @default(autoincrement()) + table_name TableName + record_id String @db.VarChar(50) @default("") + action ValidationAction + dataPayload Json + user_id_request BigInt + user_id_process BigInt? + status ValidationStatus @default(PENDING) + created_at DateTime @default(now()) @db.Timestamptz(6) + processed_at DateTime? @db.Timestamptz(6) + users users @relation("ValidationRequester", fields: [user_id_request], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_validation_user") + users_process users? @relation("ValidationProcessor", fields: [user_id_process], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_validation_user_process") +} \ No newline at end of file diff --git a/backend/api/src/app.module.ts b/backend/api/src/app.module.ts index 8b51da2..9695c32 100644 --- a/backend/api/src/app.module.ts +++ b/backend/api/src/app.module.ts @@ -12,7 +12,7 @@ import { AuthModule } from './modules/auth/auth.module'; import { FabricModule } from './modules/fabric/fabric.module'; import { AuditModule } from './modules/audit/audit.module'; import { ProofModule } from './modules/proof/proof.module'; -import { EventEmitterModule } from '@nestjs/event-emitter'; +import { ValidationModule } from './modules/validation/validation.module'; @Module({ imports: [ @@ -29,7 +29,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter'; FabricModule, AuditModule, ProofModule, - EventEmitterModule.forRoot(), + ValidationModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/api/src/app.service.ts b/backend/api/src/app.service.ts index 6fe1c4f..0d50cca 100644 --- a/backend/api/src/app.service.ts +++ b/backend/api/src/app.service.ts @@ -4,6 +4,8 @@ import { TindakanDokterService } from './modules/tindakandokter/tindakandokter.s import { RekammedisService } from './modules/rekammedis/rekammedis.service'; import { ObatService } from './modules/obat/obat.service'; import { LogService } from './modules/log/log.service'; +import { AuditService } from './modules/audit/audit.service'; +import { ValidationService } from './modules/validation/validation.service'; @Injectable() export class AppService { @@ -13,6 +15,8 @@ export class AppService { private tindakanDokterService: TindakanDokterService, private obatService: ObatService, private logService: LogService, + private auditService: AuditService, + private validationService: ValidationService, ) {} getHello(): string { @@ -24,6 +28,19 @@ export class AppService { const countTindakanDokter = await this.tindakanDokterService.countTindakanDokter(); const countObat = await this.obatService.countObat(); - return { countRekamMedis, countTindakanDokter, countObat }; + const auditTrailData = await this.auditService.getCountAuditTamperedData(); + const validasiData = + await this.validationService.getAllValidationQueueDashboard(); + const last7DaysRekamMedis = + await this.rekamMedisService.getLast7DaysCount(); + + return { + countRekamMedis, + countTindakanDokter, + countObat, + auditTrailData, + validasiData, + last7DaysRekamMedis, + }; } } diff --git a/backend/api/src/main.ts b/backend/api/src/main.ts index ba67f11..fc9894c 100644 --- a/backend/api/src/main.ts +++ b/backend/api/src/main.ts @@ -19,6 +19,7 @@ async function bootstrap() { 'https://64spbch3-5174.asse.devtunnels.ms', 'http://localhost:5173', 'http://localhost:5174', + 'http://localhost:5175', ], credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], diff --git a/backend/api/src/modules/audit/audit.controller.ts b/backend/api/src/modules/audit/audit.controller.ts index 390f870..a722be2 100644 --- a/backend/api/src/modules/audit/audit.controller.ts +++ b/backend/api/src/modules/audit/audit.controller.ts @@ -9,15 +9,10 @@ import { } from '@nestjs/common'; import { AuthGuard } from '../auth/guard/auth.guard'; import { AuditService } from './audit.service'; -import { fromEvent, interval, map, Observable } from 'rxjs'; -import { EventEmitter2 } from '@nestjs/event-emitter'; @Controller('audit') export class AuditController { - constructor( - private readonly auditService: AuditService, - private eventEmitter: EventEmitter2, - ) {} + constructor(private readonly auditService: AuditService) {} @Get('/trail') @UseGuards(AuthGuard) @@ -44,14 +39,4 @@ export class AuditController { this.auditService.storeAuditTrail(); return { message: 'Proses audit trail dijalankan', status: 'STARTED' }; } - - @Sse('stream') - @UseGuards(AuthGuard) - auditStream(): Observable { - return fromEvent(this.eventEmitter, 'audit.*').pipe( - map((data: any) => { - return new MessageEvent('message', { data: data }); - }), - ); - } } diff --git a/backend/api/src/modules/audit/audit.gateway.ts b/backend/api/src/modules/audit/audit.gateway.ts index c27a006..c850c31 100644 --- a/backend/api/src/modules/audit/audit.gateway.ts +++ b/backend/api/src/modules/audit/audit.gateway.ts @@ -5,7 +5,11 @@ import { WebsocketGuard } from '../auth/guard/websocket.guard'; @WebSocketGateway({ cors: { - origin: 'https://64spbch3-5173.asse.devtunnels.ms', + origin: [ + 'https://64spbch3-5173.asse.devtunnels.ms', + 'http://localhost:5173', + 'http://localhost:5175', + ], credentials: true, }, }) diff --git a/backend/api/src/modules/audit/audit.module.ts b/backend/api/src/modules/audit/audit.module.ts index b051182..1724309 100644 --- a/backend/api/src/modules/audit/audit.module.ts +++ b/backend/api/src/modules/audit/audit.module.ts @@ -21,5 +21,6 @@ import { WebsocketGuard } from '../auth/guard/websocket.guard'; ], providers: [AuditService, AuditGateway, WebsocketGuard], controllers: [AuditController], + exports: [AuditService], }) export class AuditModule {} diff --git a/backend/api/src/modules/audit/audit.service.ts b/backend/api/src/modules/audit/audit.service.ts index a378071..e4eb9b3 100644 --- a/backend/api/src/modules/audit/audit.service.ts +++ b/backend/api/src/modules/audit/audit.service.ts @@ -4,7 +4,7 @@ import { LogService } from '../log/log.service'; import { ObatService } from '../obat/obat.service'; import { RekammedisService } from '../rekammedis/rekammedis.service'; import { TindakanDokterService } from '../tindakandokter/tindakandokter.service'; -import { sha256 } from '@api/common/crypto/hash'; + import type { AuditEvent, resultStatus as ResultStatus, @@ -54,7 +54,6 @@ export class AuditService { tampered = undefined; } - console.log(type, tampered); const auditLogs = await this.prisma.audit.findMany({ take: pageSize, skip: (page - 1) * pageSize, @@ -66,8 +65,6 @@ export class AuditService { }, }); - console.log(auditLogs); - const count = await this.prisma.audit.count({ where: { id: type && type !== 'all' ? { startsWith: type } : undefined, @@ -182,6 +179,54 @@ export class AuditService { } } + async getCountAuditTamperedData() { + const auditTamperedCount = await this.prisma.audit.count({ + where: { + result: 'tampered', + }, + }); + + const auditNonTamperedCount = await this.prisma.audit.count({ + where: { + result: 'non_tampered', + }, + }); + + const rekamMedisTamperedCount = await this.prisma.audit.count({ + where: { + result: 'tampered', + id: { + startsWith: 'REKAM', + }, + }, + }); + + const tindakanDokterTamperedCount = await this.prisma.audit.count({ + where: { + result: 'tampered', + id: { + startsWith: 'TINDAKAN', + }, + }, + }); + + const obatTamperedCount = await this.prisma.audit.count({ + where: { + result: 'tampered', + id: { + startsWith: 'OBAT', + }, + }, + }); + return { + auditTamperedCount, + auditNonTamperedCount, + rekamMedisTamperedCount, + tindakanDokterTamperedCount, + obatTamperedCount, + }; + } + private async buildAuditRecord( logEntry: any, index?: number, diff --git a/backend/api/src/modules/auth/auth.module.ts b/backend/api/src/modules/auth/auth.module.ts index 8dd7bc7..14378f7 100644 --- a/backend/api/src/modules/auth/auth.module.ts +++ b/backend/api/src/modules/auth/auth.module.ts @@ -16,7 +16,7 @@ import { JwtModule } from '@nestjs/jwt'; inject: [ConfigService], useFactory: (configService: ConfigService) => ({ secret: configService.get('JWT_SECRET'), - signOptions: { expiresIn: '60m' }, + signOptions: { expiresIn: '120m' }, }), }), ], diff --git a/backend/api/src/modules/log/log.service.ts b/backend/api/src/modules/log/log.service.ts index 905741d..de9a6e5 100644 --- a/backend/api/src/modules/log/log.service.ts +++ b/backend/api/src/modules/log/log.service.ts @@ -71,63 +71,63 @@ export class LogService { return this.fabricService.getLogsWithPagination(pageSize, bookmark); } - async storeFromDBToBlockchain() {} + // async storeFromDBToBlockchain() {} - // async storeFromDBToBlockchain( - // limitPerEntity = 5, - // batchSize = 1, - // ): Promise<{ - // summaries: Record; - // checkpointFile: string; - // }> { - // const state = await this.loadState(); + async storeFromDBToBlockchain( + limitPerEntity = 5, + batchSize = 1, + ): Promise<{ + summaries: Record; + checkpointFile: string; + }> { + const state = await this.loadState(); - // const summaries = { - // pemberian_obat: await this.syncPemberianObat( - // state, - // limitPerEntity, - // batchSize, - // ), - // rekam_medis: await this.syncRekamMedis(state, limitPerEntity, batchSize), - // pemberian_tindakan: await this.syncPemberianTindakan( - // state, - // limitPerEntity, - // batchSize, - // ), - // } as Record; + const summaries = { + pemberian_obat: await this.syncPemberianObat( + state, + limitPerEntity, + batchSize, + ), + rekam_medis: await this.syncRekamMedis(state, limitPerEntity, batchSize), + pemberian_tindakan: await this.syncPemberianTindakan( + state, + limitPerEntity, + batchSize, + ), + } as Record; - // const timestamp = new Date().toISOString(); + const timestamp = new Date().toISOString(); - // await this.persistState({ - // ...state, - // metadata: { - // ...(state.metadata ?? {}), - // pemberian_obat: { - // lastRunAt: timestamp, - // processed: summaries.pemberian_obat.processed, - // success: summaries.pemberian_obat.success, - // failed: summaries.pemberian_obat.failed, - // }, - // rekam_medis: { - // lastRunAt: timestamp, - // processed: summaries.rekam_medis.processed, - // success: summaries.rekam_medis.success, - // failed: summaries.rekam_medis.failed, - // }, - // pemberian_tindakan: { - // lastRunAt: timestamp, - // processed: summaries.pemberian_tindakan.processed, - // success: summaries.pemberian_tindakan.success, - // failed: summaries.pemberian_tindakan.failed, - // }, - // }, - // }); + await this.persistState({ + ...state, + metadata: { + ...(state.metadata ?? {}), + pemberian_obat: { + lastRunAt: timestamp, + processed: summaries.pemberian_obat.processed, + success: summaries.pemberian_obat.success, + failed: summaries.pemberian_obat.failed, + }, + rekam_medis: { + lastRunAt: timestamp, + processed: summaries.rekam_medis.processed, + success: summaries.rekam_medis.success, + failed: summaries.rekam_medis.failed, + }, + pemberian_tindakan: { + lastRunAt: timestamp, + processed: summaries.pemberian_tindakan.processed, + success: summaries.pemberian_tindakan.success, + failed: summaries.pemberian_tindakan.failed, + }, + }, + }); - // return { - // summaries, - // checkpointFile: this.statePath, - // }; - // } + return { + summaries, + checkpointFile: this.statePath, + }; + } private async syncPemberianObat( state: BackfillState, diff --git a/backend/api/src/modules/proof/proof.service.ts b/backend/api/src/modules/proof/proof.service.ts index 7c1d07a..82cac78 100644 --- a/backend/api/src/modules/proof/proof.service.ts +++ b/backend/api/src/modules/proof/proof.service.ts @@ -96,20 +96,30 @@ export class ProofService { const payloadHash = sha256(JSON.stringify(payload)); - const response = { + // const response = { + // id: `PROOF_${payload.id_visit}`, + // event: 'proof_verification_logged', + // user_id: 'External', + // payload: payloadHash, + // }; + + const responseData = { id: `PROOF_${payload.id_visit}`, event: 'proof_verification_logged', user_id: 'External', payload: payloadHash, }; - // const response = await this.logService.storeLog({ - // id: `PROOF_${payload.id_visit}`, - // event: 'proof_verification_logged', - // user_id: 'External', - // payload: payloadHash, - // }); + const response = await this.logService.storeLog({ + id: `PROOF_${payload.id_visit}`, + event: 'proof_verification_logged', + user_id: 'External', + payload: payloadHash, + }); - return response; + return { + response, + responseData, + }; } } diff --git a/backend/api/src/modules/rekammedis/dto/create-rekammedis.dto.ts b/backend/api/src/modules/rekammedis/dto/create-rekammedis.dto.ts index a8fe3db..12d680a 100644 --- a/backend/api/src/modules/rekammedis/dto/create-rekammedis.dto.ts +++ b/backend/api/src/modules/rekammedis/dto/create-rekammedis.dto.ts @@ -112,4 +112,8 @@ export class CreateRekamMedisDto { @IsOptional() @IsString() tindak_lanjut?: string; + + @IsOptional() + @IsDateString({}, { message: 'Waktu visit harus berupa tanggal yang valid' }) + waktu_visit?: string; } diff --git a/backend/api/src/modules/rekammedis/rekammedis.controller.ts b/backend/api/src/modules/rekammedis/rekammedis.controller.ts index f67edab..c8c1581 100644 --- a/backend/api/src/modules/rekammedis/rekammedis.controller.ts +++ b/backend/api/src/modules/rekammedis/rekammedis.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Delete, Get, Header, HttpCode, @@ -98,4 +99,14 @@ export class RekamMedisController { ) { return this.rekammedisService.updateRekamMedis(id_visit, dto, user); } + + @Delete('/:id_visit') + @Header('Content-Type', 'application/json') + @UseGuards(AuthGuard) + async deleteRekamMedis( + @Param('id_visit') id_visit: string, + @CurrentUser() user: ActiveUserPayload, + ) { + return this.rekammedisService.deleteRekamMedisByIdVisit(id_visit, user); + } } diff --git a/backend/api/src/modules/rekammedis/rekammedis.module.ts b/backend/api/src/modules/rekammedis/rekammedis.module.ts index fad9405..52c8793 100644 --- a/backend/api/src/modules/rekammedis/rekammedis.module.ts +++ b/backend/api/src/modules/rekammedis/rekammedis.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { RekamMedisController } from './rekammedis.controller'; import { RekammedisService } from './rekammedis.service'; import { PrismaModule } from '../prisma/prisma.module'; -import { JwtModule } from '@nestjs/jwt'; import { LogModule } from '../log/log.module'; @Module({ diff --git a/backend/api/src/modules/rekammedis/rekammedis.service.ts b/backend/api/src/modules/rekammedis/rekammedis.service.ts index a95275d..a1196a0 100644 --- a/backend/api/src/modules/rekammedis/rekammedis.service.ts +++ b/backend/api/src/modules/rekammedis/rekammedis.service.ts @@ -6,7 +6,6 @@ import { LogService } from '../log/log.service'; import { sha256 } from '@api/common/crypto/hash'; import { ActiveUserPayload } from '../auth/decorator/current-user.decorator'; import { PayloadRekamMedisDto } from './dto/payload-rekammedis.dto'; -// import { CreateLogDto } from '../log/dto/create-log.dto'; @Injectable() export class RekammedisService { @@ -251,7 +250,10 @@ export class RekammedisService { }); } - async createRekamMedis(data: CreateRekamMedisDto, user: ActiveUserPayload) { + async createRekamMedisToDBAndBlockchain( + data: CreateRekamMedisDto, + userId: number, + ) { const latestId = await this.prisma.rekam_medis.findFirst({ orderBy: { waktu_visit: 'desc' }, }); @@ -303,7 +305,7 @@ export class RekammedisService { const data = { id: `REKAM_${newId}`, event: 'rekam_medis_created', - user_id: user.sub, + user_id: userId.toString(), payload: payloadHash, }; @@ -322,6 +324,30 @@ export class RekammedisService { } } + async createRekamMedis(data: CreateRekamMedisDto, user: ActiveUserPayload) { + const rekamMedis = { + ...data, + waktu_visit: new Date(), + }; + + try { + const response = await this.prisma.validation_queue.create({ + data: { + table_name: 'rekam_medis', + action: 'CREATE', + dataPayload: JSON.parse(JSON.stringify(rekamMedis)), + user_id_request: user.sub, + status: 'PENDING', + }, + }); + + return response; + } catch (error) { + console.error('Error creating validation queue:', error); + throw error; + } + } + async getRekamMedisLogById(id_visit: string) { const idLog = `REKAM_${id_visit}`; const rawLogs = await this.log.getLogById(idLog); @@ -354,10 +380,10 @@ export class RekammedisService { }; } - async updateRekamMedis( + async updateRekamMedisToDBAndBlockchain( id_visit: string, data: CreateRekamMedisDto, - user: ActiveUserPayload, + user_id_request: number, ) { const rekamMedis = await this.prisma.rekam_medis.update({ where: { id_visit }, @@ -382,7 +408,7 @@ export class RekammedisService { const logDto = { id: `REKAM_${id_visit}`, event: 'rekam_medis_updated', - user_id: user.sub, + user_id: user_id_request.toString(), payload: payloadHash, }; @@ -394,6 +420,30 @@ export class RekammedisService { }; } + async updateRekamMedis( + id_visit: string, + data: CreateRekamMedisDto, + user: ActiveUserPayload, + ) { + try { + const response = await this.prisma.validation_queue.create({ + data: { + table_name: 'rekam_medis', + action: 'UPDATE', + record_id: id_visit, + dataPayload: JSON.parse(JSON.stringify({ ...data })), + user_id_request: user.sub, + status: 'PENDING', + }, + }); + + return response; + } catch (error) { + console.error('Error updating validation queue:', error); + throw error; + } + } + async getAgeByIdVisit(id_visit: string) { let age: number | null = null; try { @@ -413,6 +463,91 @@ export class RekammedisService { return age; } + async deleteRekamMedisByIdVisit(id_visit: string, user: ActiveUserPayload) { + const data = await this.getRekamMedisById(id_visit); + if (!data) { + throw new Error(`Rekam Medis with id_visit ${id_visit} not found`); + } + try { + const response = await this.prisma.validation_queue.create({ + data: { + table_name: 'rekam_medis', + action: 'DELETE', + record_id: id_visit, + dataPayload: data, + user_id_request: user.sub, + status: 'PENDING', + }, + }); + return response; + } catch (error) { + console.error('Error deleting validation queue:', error); + throw error; + } + } + + async deleteRekamMedisFromDB(id_visit: string, user: ActiveUserPayload) { + try { + const deletedRekamMedis = await this.prisma.rekam_medis.delete({ + where: { id_visit }, + }); + return deletedRekamMedis; + } catch (error) { + console.error('Error deleting Rekam Medis:', error); + throw error; + } + } + + async getLast7DaysCount() { + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6); + sevenDaysAgo.setHours(0, 0, 0, 0); + + const count = await this.prisma.rekam_medis.count({ + where: { + waktu_visit: { + gte: sevenDaysAgo, + }, + }, + }); + const dailyCounts = await this.prisma.rekam_medis.groupBy({ + by: ['waktu_visit'], + where: { + waktu_visit: { + gte: sevenDaysAgo, + }, + }, + _count: { + id_visit: true, + }, + }); + + const dailyCountsMap = dailyCounts.reduce( + (acc, item) => { + const date = new Date(item.waktu_visit).toISOString().split('T')[0]; + acc[date] = (acc[date] || 0) + item._count.id_visit; + return acc; + }, + {} as Record, + ); + + const last7Days = Array.from({ length: 7 }, (_, i) => { + const date = new Date(); + date.setDate(date.getDate() - (6 - i)); + return date.toISOString().split('T')[0]; + }); + + const countsByDay = last7Days.map((date) => ({ + date, + count: dailyCountsMap[date] || 0, + })); + + return { + total: count, + byDay: countsByDay, + }; + } + async countRekamMedis() { return this.prisma.rekam_medis.count(); } diff --git a/backend/api/src/modules/tindakandokter/tindakandokter.controller.ts b/backend/api/src/modules/tindakandokter/tindakandokter.controller.ts index eff4049..bd6c2e0 100644 --- a/backend/api/src/modules/tindakandokter/tindakandokter.controller.ts +++ b/backend/api/src/modules/tindakandokter/tindakandokter.controller.ts @@ -51,6 +51,7 @@ export class TindakanDokterController { @Body() data: CreateTindakanDokterDto, @CurrentUser() user: ActiveUserPayload, ) { + // console.log('APASIH'); return await this.tindakanDokterService.createTindakanDokter(data, user); } diff --git a/backend/api/src/modules/tindakandokter/tindakandokter.module.ts b/backend/api/src/modules/tindakandokter/tindakandokter.module.ts index 20afd19..f99eaa0 100644 --- a/backend/api/src/modules/tindakandokter/tindakandokter.module.ts +++ b/backend/api/src/modules/tindakandokter/tindakandokter.module.ts @@ -3,6 +3,7 @@ import { TindakanDokterController } from './tindakandokter.controller'; import { TindakanDokterService } from './tindakandokter.service'; import { PrismaModule } from '../prisma/prisma.module'; import { LogModule } from '../log/log.module'; +import { ValidationModule } from '../validation/validation.module'; @Module({ imports: [PrismaModule, LogModule], diff --git a/backend/api/src/modules/tindakandokter/tindakandokter.service.ts b/backend/api/src/modules/tindakandokter/tindakandokter.service.ts index b03bc8f..26379df 100644 --- a/backend/api/src/modules/tindakandokter/tindakandokter.service.ts +++ b/backend/api/src/modules/tindakandokter/tindakandokter.service.ts @@ -120,35 +120,61 @@ export class TindakanDokterService { ); } - const createdTindakan = await this.prisma.pemberian_tindakan.create({ + const response = await this.prisma.validation_queue.create({ data: { - id_visit: dto.id_visit, - tindakan: dto.tindakan, - kategori_tindakan: dto.kategori_tindakan ?? null, - kelompok_tindakan: dto.kelompok_tindakan ?? null, + table_name: 'pemberian_tindakan', + action: 'CREATE', + dataPayload: { + id_visit: dto.id_visit, + tindakan: dto.tindakan, + kategori_tindakan: dto.kategori_tindakan ?? null, + kelompok_tindakan: dto.kelompok_tindakan ?? null, + }, + user_id_request: user.sub, + status: 'PENDING', }, }); - const hashingPayload = this.createHashingPayload({ - id_visit: createdTindakan.id_visit, - tindakan: createdTindakan.tindakan, - kategori_tindakan: createdTindakan.kategori_tindakan ?? null, - kelompok_tindakan: createdTindakan.kelompok_tindakan ?? null, - }); + // const createValidate = await this.validationService.storeQueueValidation( + // { + // table_name: 'pemberian_tindakan', + // record_id: null, + // dataPayload: { + // id_visit: dto.id_visit, + // tindakan: dto.tindakan, + // kategori_tindakan: dto.kategori_tindakan ?? null, + // kelompok_tindakan: dto.kelompok_tindakan ?? null, + // }, + // }, + // user, + // ); - const logPayloadHash = hashingPayload; + // const createdTindakan = await this.prisma.pemberian_tindakan.create({ + // data: { + // id_visit: dto.id_visit, + // tindakan: dto.tindakan, + // kategori_tindakan: dto.kategori_tindakan ?? null, + // kelompok_tindakan: dto.kelompok_tindakan ?? null, + // }, + // }); - const logResult = await this.logService.storeLog({ - id: `TINDAKAN_${createdTindakan.id}`, - event: 'tindakan_dokter_created', - user_id: user.sub, - payload: logPayloadHash, - }); + // const hashingPayload = this.createHashingPayload({ + // id_visit: createdTindakan.id_visit, + // tindakan: createdTindakan.tindakan, + // kategori_tindakan: createdTindakan.kategori_tindakan ?? null, + // kelompok_tindakan: createdTindakan.kelompok_tindakan ?? null, + // }); - return { - ...createdTindakan, - log: logResult, - }; + // const logPayloadHash = hashingPayload; + + // const logResult = await this.logService.storeLog({ + // id: `TINDAKAN_${createdTindakan.id}`, + // event: 'tindakan_dokter_created', + // user_id: user.sub, + // payload: logPayloadHash, + // }); + + return response; } async getTindakanDokterById(id: number) { @@ -217,29 +243,51 @@ export class TindakanDokterService { : {}), }; - const updatedTindakan = await this.prisma.pemberian_tindakan.update({ - where: { id: tindakanId }, - data: updateData, + const validationQueue = await this.prisma.validation_queue.create({ + data: { + table_name: 'pemberian_tindakan', + action: 'UPDATE', + dataPayload: { + ...(dto.id_visit !== undefined ? { id_visit: dto.id_visit } : {}), + ...(dto.tindakan !== undefined ? { tindakan: dto.tindakan } : {}), + ...(dto.kategori_tindakan !== undefined + ? { kategori_tindakan: dto.kategori_tindakan ?? null } + : {}), + ...(dto.kelompok_tindakan !== undefined + ? { kelompok_tindakan: dto.kelompok_tindakan ?? null } + : {}), + }, + record_id: tindakanId.toString(), + user_id_request: user.sub, + status: 'PENDING', + }, }); - const hashingPayload = this.createHashingPayload({ - id_visit: updatedTindakan.id_visit, - tindakan: updatedTindakan.tindakan, - kategori_tindakan: updatedTindakan.kategori_tindakan ?? null, - kelompok_tindakan: updatedTindakan.kelompok_tindakan ?? null, - }); + return validationQueue; - const logResult = await this.logService.storeLog({ - id: `TINDAKAN_${tindakanId}`, - event: 'tindakan_dokter_updated', - user_id: user.sub, - payload: hashingPayload, - }); + // const updatedTindakan = await this.prisma.pemberian_tindakan.update({ + // where: { id: tindakanId }, + // data: updateData, + // }); - return { - ...updatedTindakan, - log: logResult, - }; + // const hashingPayload = this.createHashingPayload({ + // id_visit: updatedTindakan.id_visit, + // tindakan: updatedTindakan.tindakan, + // kategori_tindakan: updatedTindakan.kategori_tindakan ?? null, + // kelompok_tindakan: updatedTindakan.kelompok_tindakan ?? null, + // }); + + // const logResult = await this.logService.storeLog({ + // id: `TINDAKAN_${tindakanId}`, + // event: 'tindakan_dokter_updated', + // user_id: user.sub, + // payload: hashingPayload, + // }); + + // return { + // ...updatedTindakan, + // log: logResult, + // }; } async getTindakanLogById(id: string) { diff --git a/backend/api/src/modules/validation/dto/store-validation-queue.dto.ts b/backend/api/src/modules/validation/dto/store-validation-queue.dto.ts new file mode 100644 index 0000000..e34c942 --- /dev/null +++ b/backend/api/src/modules/validation/dto/store-validation-queue.dto.ts @@ -0,0 +1,49 @@ +import { + IsEnum, + IsNotEmpty, + IsObject, + IsOptional, + IsString, + MaxLength, + IsNumber, +} from 'class-validator'; + +const TABLE_NAME_VALUES = [ + 'pemberian_obat', + 'pemberian_tindakan', + 'rekam_medis', +] as const; + +const VALIDATION_ACTION_VALUES = ['CREATE', 'UPDATE', 'DELETE'] as const; + +const VALIDATION_STATUS_VALUES = ['PENDING', 'APPROVED', 'REJECTED'] as const; + +export type TableName = (typeof TABLE_NAME_VALUES)[number]; +export type ValidationAction = (typeof VALIDATION_ACTION_VALUES)[number]; +export type ValidationStatus = (typeof VALIDATION_STATUS_VALUES)[number]; + +export class StoreValidationQueueDto { + @IsNotEmpty({ message: 'Nama tabel wajib diisi' }) + @IsEnum(TABLE_NAME_VALUES, { message: 'Nama tabel tidak valid' }) + table_name: TableName; + + @IsString({ message: 'ID record harus berupa string' }) + @MaxLength(50, { message: 'ID record maksimal 50 karakter' }) + record_id: string | null; + + @IsNotEmpty({ message: 'Aksi wajib diisi' }) + @IsEnum(VALIDATION_ACTION_VALUES, { message: 'Aksi tidak valid' }) + action: ValidationAction; + + @IsNotEmpty({ message: 'Payload data wajib diisi' }) + @IsObject({ message: 'Payload data harus berupa objek' }) + dataPayload: Record; + + @IsNotEmpty({ message: 'User pemohon wajib diisi' }) + @IsNumber({}, { message: 'User pemohon harus berupa angka' }) + user_id_request: number; + + @IsOptional() + @IsEnum(VALIDATION_STATUS_VALUES, { message: 'Status tidak valid' }) + status?: ValidationStatus; +} diff --git a/backend/api/src/modules/validation/validation.controller.spec.ts b/backend/api/src/modules/validation/validation.controller.spec.ts new file mode 100644 index 0000000..37f8d1e --- /dev/null +++ b/backend/api/src/modules/validation/validation.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ValidationController } from './validation.controller'; + +describe('ValidationController', () => { + let controller: ValidationController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ValidationController], + }).compile(); + + controller = module.get(ValidationController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/api/src/modules/validation/validation.controller.ts b/backend/api/src/modules/validation/validation.controller.ts new file mode 100644 index 0000000..0ee5fad --- /dev/null +++ b/backend/api/src/modules/validation/validation.controller.ts @@ -0,0 +1,62 @@ +import { Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '../auth/guard/auth.guard'; +import { ValidationService } from './validation.service'; +import { CurrentUser } from '../auth/decorator/current-user.decorator'; +import type { ActiveUserPayload } from '../auth/decorator/current-user.decorator'; + +@Controller('/validation') +export class ValidationController { + constructor(private readonly validationService: ValidationService) {} + + @Get('/') + @UseGuards(AuthGuard) + async getValidationStatus() { + return this.validationService.getAllValidationsQueue(); + } + + @Get('/:id') + @UseGuards(AuthGuard) + async getValidationById(@Param('id') id: number) { + return this.validationService.getValidationQueue(id); + } + + @Post('/:id/approve') + @UseGuards(AuthGuard) + async approveValidation( + @Param('id') id: number, + @CurrentUser() user: ActiveUserPayload, + ) { + return this.validationService.approveValidation(id, user); + } + + @Post('/:id/reject') + @UseGuards(AuthGuard) + async rejectValidation( + @Param('id') id: number, + @CurrentUser() user: ActiveUserPayload, + ) { + return this.validationService.rejectValidation(id, user); + } + + // @Post('/') + // @UseGuards(AuthGuard) + // async storeValidationQueue( + // @Body() dataPayload: StoreValidationQueueDto, + // @CurrentUser() user: ActiveUserPayload, + // ) { + // return this.validationService.storeQueueValidation(dataPayload, user); + // } + + // @Post('/process/:id') + // @UseGuards(AuthGuard) + // async processValidationQueue( + // // @Body('status') status: StoreValidationQueueDto['status'], + // // @CurrentUser() user: ActiveUserPayload, + // // @Body('id') id: string, + // @Body() data: StoreValidationQueueDto, + // @CurrentUser() user: ActiveUserPayload, + // ) { + // const { status, record_id: id } = data; + // return this.validationService.processValidationQueue(data, user); + // } +} diff --git a/backend/api/src/modules/validation/validation.module.ts b/backend/api/src/modules/validation/validation.module.ts new file mode 100644 index 0000000..cdb3d24 --- /dev/null +++ b/backend/api/src/modules/validation/validation.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { ValidationController } from './validation.controller'; +import { ValidationService } from './validation.service'; +import { PrismaModule } from '../prisma/prisma.module'; +import { LogModule } from '../log/log.module'; +import { RekamMedisModule } from '../rekammedis/rekammedis.module'; + +@Module({ + imports: [PrismaModule, LogModule, RekamMedisModule], + controllers: [ValidationController], + providers: [ValidationService], + exports: [ValidationService], +}) +export class ValidationModule {} diff --git a/backend/api/src/modules/validation/validation.service.spec.ts b/backend/api/src/modules/validation/validation.service.spec.ts new file mode 100644 index 0000000..6b79f0b --- /dev/null +++ b/backend/api/src/modules/validation/validation.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ValidationService } from './validation.service'; + +describe('ValidationService', () => { + let service: ValidationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ValidationService], + }).compile(); + + service = module.get(ValidationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/api/src/modules/validation/validation.service.ts b/backend/api/src/modules/validation/validation.service.ts new file mode 100644 index 0000000..c1e1c85 --- /dev/null +++ b/backend/api/src/modules/validation/validation.service.ts @@ -0,0 +1,168 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { ActiveUserPayload } from '../auth/decorator/current-user.decorator'; +import { RekammedisService } from '../rekammedis/rekammedis.service'; +import { CreateRekamMedisDto } from '../rekammedis/dto/create-rekammedis.dto'; + +@Injectable() +export class ValidationService { + constructor( + private prisma: PrismaService, + private rekamMedisService: RekammedisService, + ) {} + + private handlers: Record< + string, + { + approveCreate?: (queue: any) => Promise; + approveUpdate?: (queue: any) => Promise; + approveDelete?: (queue: any) => Promise; + } + > = { + rekam_medis: { + approveCreate: async (queue: any) => { + const payload = queue.dataPayload as Partial; + return this.rekamMedisService.createRekamMedisToDBAndBlockchain( + payload as CreateRekamMedisDto, + Number(queue.user_id_request), + ); + }, + approveUpdate: async (queue: any) => { + const payload = queue.dataPayload as Partial; + return this.rekamMedisService.updateRekamMedisToDBAndBlockchain( + queue.record_id, + payload as CreateRekamMedisDto, + Number(queue.user_id_request), + ); + }, + approveDelete: async (queue: any) => { + return this.rekamMedisService.deleteRekamMedisFromDB(queue.record_id, { + sub: queue.user_id_request.toString(), + } as ActiveUserPayload); + }, + }, + pemberian_tindakan: {}, + pemberian_obat: {}, + }; + + async getAllValidationsQueue() { + const result = await this.prisma.validation_queue.findMany({ + where: { status: 'PENDING' }, + }); + const totalCount = await this.prisma.validation_queue.count({ + where: { status: 'PENDING' }, + }); + return { data: result, totalCount }; + } + + async getAllValidationQueueDashboard() { + const result = await this.prisma.validation_queue.findMany({ + where: { status: 'PENDING' }, + take: 5, + orderBy: { created_at: 'desc' }, + }); + const totalCount = await this.prisma.validation_queue.count({ + where: { status: 'PENDING' }, + }); + return { data: result, totalCount }; + } + + async getValidationQueueById(id: number) { + return await this.prisma.validation_queue.findUnique({ + where: { id }, + }); + } + + async getValidationQueue(id: number) { + const result = await this.getValidationQueueById(id); + if (!result) return null; + if (result.action !== 'UPDATE') return result; + const previousLog = await this.fetchPreviousLog( + result.table_name, + result.record_id, + ); + return { previousLog, ...result }; + } + + private async fetchPreviousLog(tableName: string, recordId: string) { + if (!recordId) return null; + switch (tableName) { + case 'pemberian_tindakan': + return this.prisma.pemberian_tindakan.findUnique({ + where: { id: Number(recordId) }, + }); + case 'rekam_medis': + return this.prisma.rekam_medis.findUnique({ + where: { id_visit: recordId }, + }); + case 'pemberian_obat': + return this.prisma.pemberian_obat.findUnique({ + where: { id: Number(recordId) }, + }); + default: + return null; + } + } + + private async approveWithHandler(queue: any) { + const handler = this.handlers[queue.table_name]; + if (!handler) throw new Error('Unsupported table'); + switch (queue.action) { + case 'CREATE': + if (!handler.approveCreate) + throw new Error('Create not implemented for table'); + return handler.approveCreate(queue); + case 'UPDATE': + if (!handler.approveUpdate) + throw new Error('Update not implemented for table'); + return handler.approveUpdate(queue); + case 'DELETE': + if (!handler.approveDelete) + throw new Error('Delete not implemented for table'); + return handler.approveDelete(queue); + default: + throw new Error('Unknown action'); + } + } + + async approveValidation(id: number, user: ActiveUserPayload) { + const validationQueue = await this.getValidationQueueById(id); + if (!validationQueue) { + throw new Error('Validation queue not found'); + } + if (!validationQueue.dataPayload) { + throw new Error('Data payload is missing'); + } + try { + const approvalResult = await this.approveWithHandler(validationQueue); + const updated = await this.prisma.validation_queue.update({ + where: { id: validationQueue.id }, + data: { + status: 'APPROVED', + user_id_process: Number(user.sub), + processed_at: new Date(), + }, + }); + return { ...updated, approvalResult }; + } catch (error) { + console.error('Error approving validation:', (error as Error).message); + throw error; + } + } + + async rejectValidation(id: number, user: ActiveUserPayload) { + const validationQueue = await this.getValidationQueueById(id); + if (!validationQueue) { + throw new Error('Validation queue not found'); + } + const updated = await this.prisma.validation_queue.update({ + where: { id: validationQueue.id }, + data: { + status: 'REJECTED', + user_id_process: Number(user.sub), + processed_at: new Date(), + }, + }); + return updated; + } +} diff --git a/backend/blockchain/network/channel-artifacts/mychannel.block b/backend/blockchain/network/channel-artifacts/mychannel.block index 4c0208cf14145d5ff3e17918f8eb0b42e2c662c7..a16629d394e0f47574b438184f173258419816f5 100644 GIT binary patch delta 529 zcmX?9aHPPBOG!#0_?KV2Bwtl_P-f1)Sl*`reV(f8yyciyI_)!6U!K2cvCzZ)dk;Y2Fn0Bv|M}2G`_p z4o0EA@X7L0N}Efi9&%20kTRJZrm}Lgs_Jw0$qJIvlOsZ;m~4pOv%m6 zE8g7BoFy_jT;ky558`Q)B_#DHTNtmI+$X8Rs5Mz$uL-J;XYys;V30m(m_DKFf?VB# zTn|AmWP-SGvxdGsvsC=E%stvSPA!@^%c^omhEw}%<~4IQFT7+BTC%y(XaXbiWOlC2 zX8MNA^*&~IG(0;_7W*b_@0vZ?WR7TOOn=cV-DN`V2Chts4383()%HGQ{T1TS5xym9 zuk@wzM`f$y^|F7}&a%4S)Axf(fn&*&s^te2W=Fo$^xUPdEBndqjT56^@055qb$?5Z zj`L81p5I-ut5B~Swtdf{^y?)DVv$jXyug=b7QebNhzjytJLHUa) z&x_YoCjES!yk|pGRpOB{&QGs-tRC1yO}Z^KCzYk|f9Ips-+5@{S1BX(E`w`w zI0vIpU-)Jb@hp+a4mzomYqbweUaF%%*-|%qvbk|8hZdV-N^WLe@#J=9rODmA#+z3# zKjNIck9Xzdbvja$GsU-RjB0#;QQ0+@)d0 z2wfHA>JsF72y!D6#EqLZ^zE6Y?#5Mrod4?VvcMgIH$FfA_w+`S$Alg8O^byVZ*DZ2 zz{os_ooln1z9Dmc8$Zi$E3zLGS%7ZgJElXG4 z`Ik^1ur%oZOYV=cWq+JFwYOS7<2UuzU{X+M>9-4CX3!GAX{ao3G2hGTzM*!!U)Q-< zp2MxTO3X7M=A_EXr{DKco#QLCZSAED={n_ibt;>xzjVdb$r}XC<8e60q>z%Le(m?& zRqvzTr#-cP$Dwv7UME|=r#vD18kg(aM+a^Sxf{4LDKcC)4=!5Cwt8Pu!W^xhH@6~x zPE_QVYJcG>fBD4sDOaRDm=we$nex>*m0ue@)Yx&S;ev&(rm#DEOZ(O1zplFH%{nP6 J#l^({g8-~z(Zv7& diff --git a/frontend/hospital-log/package-lock.json b/frontend/hospital-log/package-lock.json index 1fde160..3a71278 100644 --- a/frontend/hospital-log/package-lock.json +++ b/frontend/hospital-log/package-lock.json @@ -11,11 +11,13 @@ "@tailwindcss/vite": "^4.1.16", "@vee-validate/zod": "^4.15.1", "cally": "^0.8.0", + "chart.js": "^4.5.1", "daisyui": "^5.3.10", "nouislider": "^15.8.1", "socket.io-client": "^4.8.1", "vee-validate": "^4.15.1", "vue": "^3.5.22", + "vue-chartjs": "^5.3.3", "vue-router": "4", "zod": "^4.1.12" }, @@ -112,6 +114,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@oxc-project/runtime": { "version": "0.92.0", "license": "MIT", @@ -774,6 +782,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -1441,6 +1461,16 @@ } } }, + "node_modules/vue-chartjs": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.3.tgz", + "integrity": "sha512-jqxtL8KZ6YJ5NTv6XzrzLS7osyegOi28UGNZW0h9OkDL7Sh1396ht4Dorh04aKrl2LiSalQ84WtqiG0RIJb0tA==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "vue": "^3.0.0-0 || ^2.7.0" + } + }, "node_modules/vue-router": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz", diff --git a/frontend/hospital-log/package.json b/frontend/hospital-log/package.json index 30fe499..68856c3 100644 --- a/frontend/hospital-log/package.json +++ b/frontend/hospital-log/package.json @@ -12,11 +12,13 @@ "@tailwindcss/vite": "^4.1.16", "@vee-validate/zod": "^4.15.1", "cally": "^0.8.0", + "chart.js": "^4.5.1", "daisyui": "^5.3.10", "nouislider": "^15.8.1", "socket.io-client": "^4.8.1", "vee-validate": "^4.15.1", "vue": "^3.5.22", + "vue-chartjs": "^5.3.3", "vue-router": "4", "zod": "^4.1.12" }, diff --git a/frontend/hospital-log/src/components/dashboard/DataTable.vue b/frontend/hospital-log/src/components/dashboard/DataTable.vue index 8a9d802..a40eb5d 100644 --- a/frontend/hospital-log/src/components/dashboard/DataTable.vue +++ b/frontend/hospital-log/src/components/dashboard/DataTable.vue @@ -26,6 +26,8 @@ const deleteDialogRef = ref | null>(null); const pendingDeleteItem = ref(null); const hasStatusColumn = () => props.columns.some((col) => col.key === "status"); +const hasUserIdProcessColumn = () => + props.columns.some((col) => col.key === "user_id_process"); const formatCellValue = (item: T, columnKey: keyof T) => { const value = item[columnKey]; @@ -132,6 +134,15 @@ const handleDeleteCancel = () => { > {{ formatCellValue(item, column.key) }} + + + Review + +
@@ -213,6 +224,8 @@ const handleDeleteCancel = () => { :message=" pendingDeleteItem?.id ? `Apakah Anda yakin ingin menghapus item dengan ID ${pendingDeleteItem.id}?` + : pendingDeleteItem?.id_visit + ? `Apakah Anda yakin ingin menghapus item dengan ID Visit ${pendingDeleteItem?.id_visit}?` : 'Apakah Anda yakin ingin menghapus item ini?' " confirm-text="Hapus" diff --git a/frontend/hospital-log/src/components/dashboard/Sidebar.vue b/frontend/hospital-log/src/components/dashboard/Sidebar.vue index 9ef6e7b..f550c40 100644 --- a/frontend/hospital-log/src/components/dashboard/Sidebar.vue +++ b/frontend/hospital-log/src/components/dashboard/Sidebar.vue @@ -233,6 +233,38 @@ const isActive = (routeName: string) => { + +
  • + +
  • +