2025-10-27 06:41:51 +00:00
|
|
|
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';
|
|
|
|
|
// import { CreateLogDto } from '../log/dto/create-log.dto';
|
2025-10-27 06:41:51 +00:00
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class RekammedisService {
|
2025-10-31 09:43:24 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2025-10-27 06:41:51 +00:00
|
|
|
|
|
|
|
|
async getAllRekamMedis(params: {
|
|
|
|
|
take?: number;
|
|
|
|
|
skip?: number;
|
|
|
|
|
page?: number;
|
|
|
|
|
orderBy?: any;
|
|
|
|
|
no_rm?: string;
|
|
|
|
|
order?: 'asc' | 'desc';
|
2025-10-31 09:43:24 +00:00
|
|
|
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;
|
2025-10-30 05:15:06 +00:00
|
|
|
}) {
|
2025-10-31 09:43:24 +00:00
|
|
|
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'
|
|
|
|
|
: '';
|
|
|
|
|
|
2025-10-27 06:41:51 +00:00
|
|
|
const take = params.take ? parseInt(params.take.toString()) : 10;
|
|
|
|
|
const skipValue = skip
|
|
|
|
|
? parseInt(skip.toString())
|
|
|
|
|
: page
|
|
|
|
|
? (parseInt(page.toString()) - 1) * take
|
|
|
|
|
: 0;
|
|
|
|
|
|
2025-10-31 09:43:24 +00:00
|
|
|
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,
|
2025-10-31 09:43:24 +00:00
|
|
|
kode_diagnosa: kode_diagnosa ? { contains: kode_diagnosa } : undefined,
|
|
|
|
|
...golDarahFilter,
|
|
|
|
|
...tindakLanjutFilter,
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-27 06:41:51 +00:00
|
|
|
const results = await this.prisma.rekam_medis.findMany({
|
|
|
|
|
skip: skipValue,
|
|
|
|
|
take: take,
|
2025-10-31 09:43:24 +00:00
|
|
|
where: whereClause,
|
2025-10-27 06:41:51 +00:00
|
|
|
orderBy: orderBy
|
2025-11-06 07:10:04 +00:00
|
|
|
? { [orderBy]: order || 'desc' }
|
2025-10-27 06:41:51 +00:00
|
|
|
: { waktu_visit: order ? order : 'asc' },
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-30 05:15:06 +00:00
|
|
|
const count = await this.prisma.rekam_medis.count({
|
2025-10-31 09:43:24 +00:00
|
|
|
where: whereClause,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const umurMin = await this.prisma.rekam_medis.findMany({
|
|
|
|
|
distinct: ['umur'],
|
|
|
|
|
orderBy: {
|
|
|
|
|
umur: 'asc',
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
umur: true,
|
2025-10-30 05:15:06 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-31 09:43:24 +00:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-30 05:15:06 +00:00
|
|
|
return {
|
|
|
|
|
...results,
|
|
|
|
|
totalCount: count,
|
2025-10-31 09:43:24 +00:00
|
|
|
rangeUmur: rangeUmur,
|
2025-10-30 05:15:06 +00:00
|
|
|
};
|
2025-10-27 06:41:51 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-06 07:10:04 +00:00
|
|
|
async getRekamMedisById(id_visit: string) {
|
|
|
|
|
return this.prisma.rekam_medis.findUnique({
|
|
|
|
|
where: { id_visit },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createRekamMedis(data: CreateRekamMedisDto, user: ActiveUserPayload) {
|
2025-10-27 06:41:51 +00:00
|
|
|
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 = {
|
2025-10-27 06:41:51 +00:00
|
|
|
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: user.sub,
|
|
|
|
|
payload: payloadHash,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createdLog = await this.log.storeLog(data);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...createdRekamMedis,
|
|
|
|
|
log: createdLog,
|
|
|
|
|
};
|
2025-10-27 06:41:51 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return newRekamMedis;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error creating Rekam Medis:', 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 updateRekamMedis(
|
|
|
|
|
id_visit: string,
|
|
|
|
|
data: CreateRekamMedisDto,
|
|
|
|
|
user: ActiveUserPayload,
|
|
|
|
|
) {
|
|
|
|
|
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.sub,
|
|
|
|
|
payload: payloadHash,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createdLog = await this.log.storeLog(logDto);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...rekamMedis,
|
|
|
|
|
log: createdLog,
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-11-06 10:07:08 +00:00
|
|
|
|
|
|
|
|
async countRekamMedis() {
|
|
|
|
|
return this.prisma.rekam_medis.count();
|
|
|
|
|
}
|
2025-10-27 06:41:51 +00:00
|
|
|
}
|