hospital-log/backend/api/src/modules/rekammedis/rekammedis.service.ts

615 lines
16 KiB
TypeScript
Raw Normal View History

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Prisma, rekam_medis } from '@dist/generated/prisma';
import { CreateRekamMedisDto } from './dto/create-rekammedis.dto';
2025-11-06 07:10:04 +00:00
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';
@Injectable()
export class RekammedisService {
private readonly KNOWN_BLOOD_TYPES = ['A', 'B', 'AB', 'O'];
private readonly KNOWN_TINDAK_LANJUT = [
'Dipulangkan untuk Kontrol',
'Dirawat',
'Dirujuk ke RS',
'Konsul Ke Poli Lain',
'Konsultasi Dokter Spesialis',
'Kontrol',
'Kontrol Ulang',
'Masuk Rawat Inap',
'Meninggal Dunia Sebelum Dirawat',
'Meninggal Dunia Setelah Dirawat',
'Pulang',
'Rencana Operasi',
'Rujuk Balik',
'Selesai Pelayanan IGD',
'Selesai Pelayanan Rawat Jalan',
];
2025-11-06 07:10:04 +00:00
constructor(
private prisma: PrismaService,
private log: LogService,
) {}
createHashingPayload(currentData: PayloadRekamMedisDto): string {
return sha256(JSON.stringify(currentData));
}
determineStatus(rawFabricLog: any, index: number, arrLength: number): any {
const flatLog = {
...rawFabricLog.value,
txId: rawFabricLog.txId,
timestamp: rawFabricLog.value.timestamp,
};
console.log('Processed flat log:', flatLog);
if (
index === arrLength - 1 &&
rawFabricLog.value.event === 'rekam_medis_created'
) {
flatLog.status = 'ORIGINAL';
} else {
flatLog.status = 'UPDATED';
}
return flatLog;
}
async getAllRekamMedis(params: {
take?: number;
skip?: number;
page?: number;
orderBy?: any;
no_rm?: string;
order?: 'asc' | 'desc';
id_visit?: string;
nama_pasien?: string;
tanggal_start?: string;
tanggal_end?: string;
umur_min?: string;
umur_max?: string;
jenis_kelamin?: string;
gol_darah?: string;
kode_diagnosa?: string;
tindak_lanjut?: string;
}) {
const {
skip,
page,
orderBy,
order,
no_rm,
id_visit,
nama_pasien,
tanggal_start,
tanggal_end,
umur_min,
umur_max,
kode_diagnosa,
} = params;
const golDarahArray = params.gol_darah?.split(',') || [];
const tindakLanjutArray = params.tindak_lanjut?.split(',') || [];
2025-11-06 07:10:04 +00:00
const jkCharacter =
params.jenis_kelamin == 'perempuan'
? 'P'
: params.jenis_kelamin == 'laki-laki'
? 'L'
: '';
const take = params.take ? parseInt(params.take.toString()) : 10;
const skipValue = skip
? parseInt(skip.toString())
: page
? (parseInt(page.toString()) - 1) * take
: 0;
const buildMultiSelectFilter = (
fieldName: string,
selectedValues: string[],
knownValues: string[],
unknownLabel: string,
includesDash: boolean = false,
) => {
if (selectedValues.length === 0) {
return undefined;
}
const hasKnownValues = selectedValues.some((val) =>
knownValues.includes(val),
);
const hasUnknown = selectedValues.includes(unknownLabel);
const totalOptions = knownValues.length + 1;
if (selectedValues.length === totalOptions) {
return undefined;
}
if (hasUnknown && !hasKnownValues) {
const conditions: any[] = [{ [fieldName]: { equals: null } }];
if (includesDash) {
conditions.push({ [fieldName]: { equals: '-' } });
conditions.push({ [fieldName]: { notIn: knownValues } });
}
return conditions.length > 1 ? { OR: conditions } : conditions[0];
}
if (hasKnownValues && hasUnknown) {
const knownSelected = selectedValues.filter(
(val) => val !== unknownLabel,
);
const conditions: any[] = [
{ [fieldName]: { in: knownSelected } },
{ [fieldName]: { equals: null } },
];
if (includesDash) {
conditions.push({ [fieldName]: { equals: '-' } });
conditions.push({ [fieldName]: { notIn: knownValues } });
}
return { OR: conditions };
}
return { [fieldName]: { in: selectedValues } };
};
const golDarahFilter = buildMultiSelectFilter(
'gol_darah',
golDarahArray,
this.KNOWN_BLOOD_TYPES,
'Tidak Tahu',
true,
);
const tindakLanjutFilter = buildMultiSelectFilter(
'tindak_lanjut',
tindakLanjutArray,
this.KNOWN_TINDAK_LANJUT,
'Belum Ada Keterangan',
false,
);
const whereClause = {
no_rm: no_rm ? { startsWith: no_rm } : undefined,
id_visit: id_visit ? { contains: id_visit } : undefined,
nama_pasien: nama_pasien ? { contains: nama_pasien } : undefined,
waktu_visit:
tanggal_start && tanggal_end
? {
gte: new Date(tanggal_start),
lte: new Date(tanggal_end),
}
: undefined,
umur:
umur_min && umur_max
? {
gte: parseInt(umur_min, 10),
lte: parseInt(umur_max, 10),
}
: undefined,
2025-11-06 07:10:04 +00:00
jenis_kelamin: jkCharacter ? { equals: jkCharacter } : undefined,
kode_diagnosa: kode_diagnosa ? { contains: kode_diagnosa } : undefined,
2025-11-28 03:34:50 +00:00
OR: [
{ deleted_status: null },
{ deleted_status: 'DELETE_VALIDATION' },
{ deleted_status: { not: 'DELETED' } },
],
...golDarahFilter,
...tindakLanjutFilter,
};
const results = await this.prisma.rekam_medis.findMany({
skip: skipValue,
take: take,
where: whereClause,
orderBy: orderBy
2025-11-06 07:10:04 +00:00
? { [orderBy]: order || 'desc' }
: { waktu_visit: order ? order : 'asc' },
});
const count = await this.prisma.rekam_medis.count({
where: whereClause,
});
const umurMin = await this.prisma.rekam_medis.findMany({
distinct: ['umur'],
orderBy: {
umur: 'asc',
},
select: {
umur: true,
},
});
const umurMax = await this.prisma.rekam_medis.findMany({
distinct: ['umur'],
orderBy: {
umur: 'desc',
},
select: {
umur: true,
},
});
const rangeUmur = {
min: umurMin.length > 0 ? umurMin[0].umur : null,
max: umurMax.length > 0 ? umurMax[0].umur : null,
};
return {
...results,
totalCount: count,
rangeUmur: rangeUmur,
};
}
2025-11-06 07:10:04 +00:00
async getRekamMedisById(id_visit: string) {
return this.prisma.rekam_medis.findUnique({
where: { id_visit },
});
}
async createRekamMedisToDBAndBlockchain(
data: CreateRekamMedisDto,
userId: number,
) {
const latestId = await this.prisma.rekam_medis.findFirst({
orderBy: { waktu_visit: 'desc' },
});
let newId = '';
let xCounter = 0;
let rekamMedis: Prisma.rekam_medisCreateInput;
for (let i = (latestId?.id_visit?.length ?? 0) - 1; i >= 0; i--) {
if (latestId?.id_visit[i] === 'X') {
xCounter++;
} else {
newId = latestId?.id_visit?.substring(0, i + 1) || '';
break;
}
}
if (xCounter < 1) {
newId = (parseInt(latestId?.id_visit || '0', 10) + 1).toString();
} else {
newId = (parseInt(newId || '0', 10) + 1).toString();
}
rekamMedis = {
...data,
id_visit: newId,
waktu_visit: new Date(),
};
2025-11-06 07:10:04 +00:00
const logData = {
event: 'rekam_medis_created',
payload: {
dokter_id: 123,
visit_id: newId,
anamnese: data.anamnese,
jenis_kasus: data.jenis_kasus,
tindak_lanjut: data.tindak_lanjut,
},
};
try {
const newRekamMedis = await this.prisma.$transaction(async (tx) => {
const createdRekamMedis = await tx.rekam_medis.create({
data: rekamMedis,
});
2025-11-06 07:10:04 +00:00
const logPayload = JSON.stringify(logData.payload);
const payloadHash = sha256(logPayload);
const data = {
id: `REKAM_${newId}`,
event: 'rekam_medis_created',
user_id: userId.toString(),
2025-11-06 07:10:04 +00:00
payload: payloadHash,
};
const createdLog = await this.log.storeLog(data);
return {
...createdRekamMedis,
log: createdLog,
};
});
return newRekamMedis;
} catch (error) {
console.error('Error creating Rekam Medis:', error);
throw error;
}
}
2025-11-06 07:10:04 +00:00
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;
}
}
2025-11-06 07:10:04 +00:00
async getRekamMedisLogById(id_visit: string) {
const idLog = `REKAM_${id_visit}`;
const rawLogs = await this.log.getLogById(idLog);
const currentData = await this.getRekamMedisById(id_visit);
if (!currentData) {
throw new Error(`Rekam Medis with id_visit ${id_visit} not found`);
}
const currentDataHash = this.createHashingPayload({
dokter_id: 123,
visit_id: currentData.id_visit,
anamnese: currentData.anamnese ?? '',
jenis_kasus: currentData.jenis_kasus ?? '',
tindak_lanjut: currentData.tindak_lanjut ?? '',
});
const latestPayload = rawLogs[0].value.payload;
const isTampered = currentDataHash !== latestPayload;
const chronologicalLogs = [...rawLogs];
const processedLogs = chronologicalLogs.map((log, index) => {
return this.determineStatus(log, index, chronologicalLogs.length);
});
return {
logs: processedLogs,
isTampered: isTampered,
currentDataHash: currentDataHash,
};
}
async updateRekamMedisToDBAndBlockchain(
2025-11-06 07:10:04 +00:00
id_visit: string,
data: CreateRekamMedisDto,
user_id_request: number,
2025-11-06 07:10:04 +00:00
) {
const rekamMedis = await this.prisma.rekam_medis.update({
where: { id_visit },
data: {
...data,
},
});
const logData = {
event: 'rekam_medis_updated',
payload: {
dokter_id: 123,
visit_id: id_visit,
anamnese: data.anamnese,
jenis_kasus: data.jenis_kasus,
tindak_lanjut: data.tindak_lanjut,
},
};
const logPayload = JSON.stringify(logData.payload);
const payloadHash = sha256(logPayload);
const logDto = {
id: `REKAM_${id_visit}`,
event: 'rekam_medis_updated',
user_id: user_id_request.toString(),
2025-11-06 07:10:04 +00:00
payload: payloadHash,
};
const createdLog = await this.log.storeLog(logDto);
return {
...rekamMedis,
log: createdLog,
};
}
2025-11-06 10:07:08 +00:00
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) {
2025-11-13 07:06:16 +00:00
let age: number | null = null;
try {
const result = await this.prisma.rekam_medis.findUnique({
select: {
umur: true,
},
where: {
id_visit: id_visit,
},
});
age = result?.umur ?? null;
} catch (error) {
console.error('Error getting age by id_visit:', error);
throw error;
}
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 {
2025-11-28 03:34:50 +00:00
const response = await this.prisma.$transaction(async (tx) => {
const createdQueue = await tx.validation_queue.create({
data: {
table_name: 'rekam_medis',
action: 'DELETE',
record_id: id_visit,
dataPayload: data,
user_id_request: user.sub,
status: 'PENDING',
},
});
const updatedRekamMedis = await tx.rekam_medis.update({
where: { id_visit },
data: {
deleted_status: 'DELETE_VALIDATION',
},
});
return {
...createdQueue,
rekam_medis: updatedRekamMedis,
};
});
return response;
} catch (error) {
console.error('Error deleting validation queue:', error);
throw error;
}
}
2025-11-28 03:34:50 +00:00
async deleteRekamMedisFromDBAndBlockchain(id_visit: string, userId: number) {
const existing = await this.getRekamMedisById(id_visit);
if (!existing) {
throw new Error(`Rekam Medis with id_visit ${id_visit} not found`);
}
try {
2025-11-28 03:34:50 +00:00
const deletedRekamMedis = await this.prisma.$transaction(async (tx) => {
const deleted = await tx.rekam_medis.update({
data: {
deleted_status: 'DELETED',
},
where: { id_visit },
});
const logPayload = {
dokter_id: 123,
visit_id: id_visit,
anamnese: deleted.anamnese,
jenis_kasus: deleted.jenis_kasus,
tindak_lanjut: deleted.tindak_lanjut,
};
const logPayloadString = JSON.stringify(logPayload);
const payloadHash = sha256(logPayloadString);
const logDto = {
id: `REKAM_${id_visit}`,
event: 'rekam_medis_deleted',
user_id: userId.toString(),
payload: payloadHash,
};
await this.log.storeLog(logDto);
return deleted;
});
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,
},
OR: [
{ deleted_status: null },
{ deleted_status: 'DELETE_VALIDATION' },
{ deleted_status: { not: 'DELETED' } },
],
},
_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<string, number>,
);
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,
};
}
2025-11-06 10:07:08 +00:00
async countRekamMedis() {
return this.prisma.rekam_medis.count({
where: {
OR: [
{ deleted_status: null },
{ deleted_status: 'DELETE_VALIDATION' },
{ deleted_status: { not: 'DELETED' } },
],
},
});
2025-11-06 10:07:08 +00:00
}
}