feat: change UI language into english from indonesia

This commit is contained in:
yosaphatprs 2025-12-04 11:10:10 +07:00
parent 7633bd25e3
commit 520099ca8b
41 changed files with 627 additions and 649 deletions

View File

@ -204,7 +204,7 @@ describe('AuditController', () => {
const result = controller.createAuditTrail(); const result = controller.createAuditTrail();
expect(result).toEqual({ expect(result).toEqual({
message: 'Proses audit trail dijalankan', message: 'Audit trail process started',
status: 'STARTED', status: 'STARTED',
}); });
expect(mockAuditService.storeAuditTrail).toHaveBeenCalled(); expect(mockAuditService.storeAuditTrail).toHaveBeenCalled();

View File

@ -41,6 +41,6 @@ export class AuditController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
createAuditTrail() { createAuditTrail() {
this.auditService.storeAuditTrail(); this.auditService.storeAuditTrail();
return { message: 'Proses audit trail dijalankan', status: 'STARTED' }; return { message: 'Audit trail process started', status: 'STARTED' };
} }
} }

View File

@ -178,7 +178,7 @@ describe('AuthController', () => {
const result = controller.logout(mockResponse as any); const result = controller.logout(mockResponse as any);
expect(result).toEqual({ message: 'Logout berhasil' }); expect(result).toEqual({ message: 'Logout successful' });
expect(mockResponse.clearCookie).toHaveBeenCalledWith('access_token', { expect(mockResponse.clearCookie).toHaveBeenCalledWith('access_token', {
httpOnly: true, httpOnly: true,
secure: false, secure: false,
@ -195,7 +195,7 @@ describe('AuthController', () => {
const result = controller.logout(mockResponse as any); const result = controller.logout(mockResponse as any);
expect(result).toEqual({ message: 'Logout berhasil' }); expect(result).toEqual({ message: 'Logout successful' });
expect(mockResponse.clearCookie).toHaveBeenCalledWith('access_token', { expect(mockResponse.clearCookie).toHaveBeenCalledWith('access_token', {
httpOnly: true, httpOnly: true,
secure: true, secure: true,

View File

@ -63,6 +63,6 @@ export class AuthController {
sameSite: 'strict', sameSite: 'strict',
}); });
return { message: 'Logout berhasil' }; return { message: 'Logout successful' };
} }
} }

View File

@ -73,7 +73,7 @@ export class AuthService {
}); });
if (!user || !(await bcrypt.compare(password, user.password_hash))) { if (!user || !(await bcrypt.compare(password, user.password_hash))) {
throw new UnauthorizedException('Username atau password salah'); throw new UnauthorizedException('Wrong username or password');
} }
const csrfToken = crypto.randomBytes(32).toString('hex'); const csrfToken = crypto.randomBytes(32).toString('hex');

View File

@ -7,13 +7,13 @@ export enum UserRole {
} }
export class AuthDto { export class AuthDto {
@IsNotEmpty({ message: 'Username wajib diisi' }) @IsNotEmpty({ message: 'Username is required' })
@IsString({ message: 'Username harus berupa string' }) @IsString({ message: 'Username must be a string' })
@Length(1, 100, { message: 'Username maksimal 100 karakter' }) @Length(1, 100, { message: 'Username must be at most 100 characters' })
username: string; username: string;
@IsNotEmpty({ message: 'Password wajib diisi' }) @IsNotEmpty({ message: 'Password is required' })
@IsString({ message: 'Password harus berupa string' }) @IsString({ message: 'Password must be a string' })
@Length(6, undefined, { message: 'Password minimal 6 karakter' }) @Length(6, undefined, { message: 'Password must be at least 6 characters' })
password: string; password: string;
} }

View File

@ -9,26 +9,26 @@ import { Expose, Transform } from 'class-transformer';
import { UserRole } from './auth.dto'; import { UserRole } from './auth.dto';
export class CreateUserDto { export class CreateUserDto {
@IsNotEmpty({ message: 'Nama lengkap wajib diisi' }) @IsNotEmpty({ message: 'Full name is required' })
@IsString({ message: 'Nama lengkap harus berupa string' }) @IsString({ message: 'Full name must be a string' })
@Length(1, 255, { message: 'Nama lengkap maksimal 255 karakter' }) @Length(1, 255, { message: 'Full name must be at most 255 characters' })
nama_lengkap: string; nama_lengkap: string;
@IsNotEmpty({ message: 'Username wajib diisi' }) @IsNotEmpty({ message: 'Username is required' })
@IsString({ message: 'Username harus berupa string' }) @IsString({ message: 'Username must be a string' })
@Length(1, 100, { message: 'Username maksimal 100 karakter' }) @Length(1, 100, { message: 'Username must be at most 100 characters' })
username: string; username: string;
@IsNotEmpty({ message: 'Password wajib diisi' }) @IsNotEmpty({ message: 'Password is required' })
@IsString({ message: 'Password harus berupa string' }) @IsString({ message: 'Password must be a string' })
@Length(6, 100, { @Length(6, 100, {
message: 'Password minimal 6 karakter dan maksimal 100 karakter', message: 'Password must be between 6 and 100 characters',
}) })
password: string; password: string;
@IsOptional() @IsOptional()
@IsString({ message: 'Role harus berupa string' }) @IsString({ message: 'Role must be a string' })
@IsEnum(UserRole, { message: 'Role harus "admin" atau "user"' }) @IsEnum(UserRole, { message: 'Role must be "admin" or "user"' })
role?: UserRole; role?: UserRole;
} }

View File

@ -184,7 +184,7 @@ describe('FabricService', () => {
await expect( await expect(
service.storeLog('log-1', 'CREATE', 'user-1', '{}'), service.storeLog('log-1', 'CREATE', 'user-1', '{}'),
).rejects.toThrow('Gagal menyimpan log ke blockchain'); ).rejects.toThrow('Failed to store log to blockchain');
}); });
it('should not validate empty id (NO VALIDATION)', async () => { it('should not validate empty id (NO VALIDATION)', async () => {
@ -273,7 +273,7 @@ describe('FabricService', () => {
); );
await expect(service.getLogById('non-existent')).rejects.toThrow( await expect(service.getLogById('non-existent')).rejects.toThrow(
'Gagal mengambil log dari blockchain', 'Failed to retrieve log from blockchain',
); );
}); });
@ -325,7 +325,7 @@ describe('FabricService', () => {
); );
await expect(service.getAllLogs()).rejects.toThrow( await expect(service.getAllLogs()).rejects.toThrow(
'Gagal mengambil semua log dari blockchain', 'Failed to retrieve all logs from blockchain',
); );
}); });
}); });
@ -370,7 +370,7 @@ describe('FabricService', () => {
); );
await expect(service.getLogsWithPagination(10, '')).rejects.toThrow( await expect(service.getLogsWithPagination(10, '')).rejects.toThrow(
'Gagal mengambil log dengan paginasi dari blockchain', 'Failed to retrieve logs with pagination from blockchain',
); );
}); });

View File

@ -65,7 +65,7 @@ export class FabricService implements OnModuleInit, OnApplicationShutdown {
const message = error instanceof Error ? error.message : 'Unknown error'; const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Failed to store log: ${message}`); this.logger.error(`Failed to store log: ${message}`);
throw new InternalServerErrorException( throw new InternalServerErrorException(
'Gagal menyimpan log ke blockchain', 'Failed to store log to blockchain',
); );
} }
} }
@ -78,7 +78,7 @@ export class FabricService implements OnModuleInit, OnApplicationShutdown {
const message = error instanceof Error ? error.message : 'Unknown error'; const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Failed to get log by ID: ${message}`); this.logger.error(`Failed to get log by ID: ${message}`);
throw new InternalServerErrorException( throw new InternalServerErrorException(
'Gagal mengambil log dari blockchain', 'Failed to retrieve log from blockchain',
); );
} }
} }
@ -91,7 +91,7 @@ export class FabricService implements OnModuleInit, OnApplicationShutdown {
const message = error instanceof Error ? error.message : 'Unknown error'; const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Failed to get all logs: ${message}`); this.logger.error(`Failed to get all logs: ${message}`);
throw new InternalServerErrorException( throw new InternalServerErrorException(
'Gagal mengambil semua log dari blockchain', 'Failed to retrieve all logs from blockchain',
); );
} }
} }
@ -106,7 +106,7 @@ export class FabricService implements OnModuleInit, OnApplicationShutdown {
const message = error instanceof Error ? error.message : 'Unknown error'; const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`Failed to get logs with pagination: ${message}`); this.logger.error(`Failed to get logs with pagination: ${message}`);
throw new InternalServerErrorException( throw new InternalServerErrorException(
'Gagal mengambil log dengan paginasi dari blockchain', 'Failed to retrieve logs with pagination from blockchain',
); );
} }
} }

View File

@ -1,12 +1,12 @@
import { IsString, IsNotEmpty, Length, IsEnum } from 'class-validator'; import { IsString, IsNotEmpty, Length, IsEnum } from 'class-validator';
export class StoreLogDto { export class StoreLogDto {
@IsNotEmpty({ message: 'ID wajib diisi' }) @IsNotEmpty({ message: 'ID is required' })
@IsString({ message: 'ID harus berupa string' }) @IsString({ message: 'ID must be a string' })
id: string; id: string;
@IsNotEmpty({ message: 'Event wajib diisi' }) @IsNotEmpty({ message: 'Event is required' })
@IsString({ message: 'Event harus berupa string' }) @IsString({ message: 'Event must be a string' })
@IsEnum( @IsEnum(
[ [
'tindakan_dokter_created', 'tindakan_dokter_created',
@ -20,17 +20,17 @@ export class StoreLogDto {
'rekam_medis_deleted', 'rekam_medis_deleted',
], ],
{ {
message: 'Event tidak valid', message: 'Invalid event',
}, },
) )
@Length(1, 100, { message: 'Event maksimal 100 karakter' }) @Length(1, 100, { message: 'Event must be at most 100 characters' })
event: string; event: string;
@IsNotEmpty({ message: 'User ID wajib diisi' }) @IsNotEmpty({ message: 'User ID is required' })
@IsString({ message: 'User ID harus berupa string' }) @IsString({ message: 'User ID must be a string' })
user_id: string; user_id: string;
@IsNotEmpty({ message: 'Payload wajib diisi' }) @IsNotEmpty({ message: 'Payload is required' })
@IsString({ message: 'Payload harus berupa string' }) @IsString({ message: 'Payload must be a string' })
payload: string; payload: string;
} }

View File

@ -134,7 +134,7 @@ export class ObatService {
async createObat(dto: CreateObatDto, user: ActiveUserPayload) { async createObat(dto: CreateObatDto, user: ActiveUserPayload) {
if (!(await this.isIdVisitExists(dto.id_visit))) { if (!(await this.isIdVisitExists(dto.id_visit))) {
throw new BadRequestException(`ID Visit ${dto.id_visit} tidak ditemukan`); throw new BadRequestException(`Visit ID ${dto.id_visit} not found`);
} }
try { try {
@ -157,7 +157,7 @@ export class ObatService {
async createObatToDBAndBlockchain(dto: CreateObatDto, userId: number) { async createObatToDBAndBlockchain(dto: CreateObatDto, userId: number) {
if (!(await this.isIdVisitExists(dto.id_visit))) { if (!(await this.isIdVisitExists(dto.id_visit))) {
throw new BadRequestException(`Visit with id ${dto.id_visit} not found`); throw new BadRequestException(`Visit id ${dto.id_visit} not found`);
} }
try { try {
@ -200,11 +200,11 @@ export class ObatService {
const obatId = Number(id); const obatId = Number(id);
if (isNaN(obatId)) { if (isNaN(obatId)) {
throw new BadRequestException('ID obat tidak valid'); throw new BadRequestException('ID medicine not valid');
} }
if (!(await this.getObatById(obatId))) { if (!(await this.getObatById(obatId))) {
throw new BadRequestException(`Obat with id ${obatId} not found`); throw new BadRequestException(`Medicine with id ${obatId} not found`);
} }
try { try {
@ -244,15 +244,13 @@ export class ObatService {
const obatId = Number(id); const obatId = Number(id);
if (isNaN(obatId)) { if (isNaN(obatId)) {
throw new BadRequestException('ID obat tidak valid'); throw new BadRequestException('Medicine ID not valid');
} }
const existingObat = await this.getObatById(obatId); const existingObat = await this.getObatById(obatId);
if (!existingObat) { if (!existingObat) {
throw new BadRequestException( throw new BadRequestException(`Medicine with ID ${obatId} not found`);
`Pemberian obat dengan ID ${obatId} tidak ditemukan`,
);
} }
const hasUpdates = const hasUpdates =
@ -261,7 +259,7 @@ export class ObatService {
dto.aturan_pakai !== existingObat.aturan_pakai; dto.aturan_pakai !== existingObat.aturan_pakai;
if (!hasUpdates) { if (!hasUpdates) {
throw new BadRequestException('Tidak ada perubahan data obat'); throw new BadRequestException('No changes in medicine data detected');
} }
try { try {
@ -325,12 +323,12 @@ export class ObatService {
const obatId = Number(id); const obatId = Number(id);
if (isNaN(obatId)) { if (isNaN(obatId)) {
throw new BadRequestException('ID obat tidak valid'); throw new BadRequestException('Medicine ID not valid');
} }
const existingObat = await this.getObatById(obatId); const existingObat = await this.getObatById(obatId);
if (!existingObat) { if (!existingObat) {
throw new BadRequestException(`Obat dengan ID ${obatId} tidak ditemukan`); throw new BadRequestException(`Medicine with ID ${obatId} not found`);
} }
try { try {

View File

@ -14,27 +14,29 @@ import {
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
export class CreateRekamMedisDto { export class CreateRekamMedisDto {
@IsNotEmpty({ message: 'Nomor rekam medis (no_rm) wajib diisi' }) @IsNotEmpty({ message: 'Medical record number (no_rm) is required' })
@IsString() @IsString()
@Length(1, 20, { message: 'Nomor rekam medis maksimal 20 karakter' }) @Length(1, 20, {
message: 'Medical record number must be at most 20 characters',
})
no_rm: string; no_rm: string;
@IsNotEmpty({ message: 'Nama pasien wajib diisi' }) @IsNotEmpty({ message: 'Patient name is required' })
@IsString() @IsString()
@Length(1, 100, { message: 'Nama pasien maksimal 100 karakter' }) @Length(1, 100, { message: 'Patient name must be at most 100 characters' })
nama_pasien: string; nama_pasien: string;
@IsOptional() @IsOptional()
@IsInt({ message: 'Umur harus berupa angka bulat' }) @IsInt({ message: 'Age must be an integer' })
@Min(0, { message: 'Umur tidak boleh negatif' }) @Min(0, { message: 'Age cannot be negative' })
@Max(150, { message: 'Umur tidak valid' }) @Max(150, { message: 'Age is not valid' })
@Transform(({ value }) => (value ? parseInt(value) : null)) @Transform(({ value }) => (value ? parseInt(value) : null))
umur?: number; umur?: number;
@IsOptional() @IsOptional()
@IsString() @IsString()
@IsIn(['L', 'P', 'l', 'p'], { @IsIn(['L', 'P', 'l', 'p'], {
message: 'Jenis kelamin harus "L" (Laki-laki) atau "P" (Perempuan)', message: 'Gender must be "L" (Male) or "P" (Female)',
}) })
@Transform(({ value }) => value?.toUpperCase()) @Transform(({ value }) => value?.toUpperCase())
jenis_kelamin?: string; jenis_kelamin?: string;
@ -42,7 +44,7 @@ export class CreateRekamMedisDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
@IsIn(['A', 'B', 'AB', 'O', '-'], { @IsIn(['A', 'B', 'AB', 'O', '-'], {
message: 'Golongan darah harus A, B, AB, O, atau -', message: 'Blood type must be A, B, AB, O, or -',
}) })
@Length(1, 2) @Length(1, 2)
gol_darah?: string; gol_darah?: string;
@ -70,37 +72,37 @@ export class CreateRekamMedisDto {
anamnese?: string; anamnese?: string;
@IsOptional() @IsOptional()
@IsInt({ message: 'Tekanan darah sistolik harus berupa angka bulat' }) @IsInt({ message: 'Systolic blood pressure must be an integer' })
@Transform(({ value }) => (value ? parseInt(value) : null)) @Transform(({ value }) => (value ? parseInt(value) : null))
sistolik?: number; sistolik?: number;
@IsOptional() @IsOptional()
@IsInt({ message: 'Tekanan darah diastolik harus berupa angka bulat' }) @IsInt({ message: 'Diastolic blood pressure must be an integer' })
@Transform(({ value }) => (value ? parseInt(value) : null)) @Transform(({ value }) => (value ? parseInt(value) : null))
diastolik?: number; diastolik?: number;
@IsOptional() @IsOptional()
@IsInt({ message: 'Nadi harus berupa angka bulat' }) @IsInt({ message: 'Pulse must be an integer' })
@Transform(({ value }) => (value ? parseInt(value) : null)) @Transform(({ value }) => (value ? parseInt(value) : null))
nadi?: number; nadi?: number;
@IsOptional() @IsOptional()
@IsNumber({}, { message: 'Suhu harus berupa angka' }) @IsNumber({}, { message: 'Temperature must be a number' })
@Transform(({ value }) => (value ? parseFloat(value) : null)) @Transform(({ value }) => (value ? parseFloat(value) : null))
suhu?: number; suhu?: number;
@IsOptional() @IsOptional()
@IsInt({ message: 'Pernapasan harus berupa angka bulat' }) @IsInt({ message: 'Respiration must be an integer' })
@Transform(({ value }) => (value ? parseInt(value) : null)) @Transform(({ value }) => (value ? parseInt(value) : null))
nafas?: number; nafas?: number;
@IsOptional() @IsOptional()
@IsNumber({}, { message: 'Tinggi badan harus berupa angka' }) @IsNumber({}, { message: 'Height must be a number' })
@Transform(({ value }) => (value ? parseFloat(value) : null)) @Transform(({ value }) => (value ? parseFloat(value) : null))
tinggi_badan?: number; tinggi_badan?: number;
@IsOptional() @IsOptional()
@IsNumber({}, { message: 'Berat badan harus berupa angka' }) @IsNumber({}, { message: 'Weight must be a number' })
@Transform(({ value }) => (value ? parseFloat(value) : null)) @Transform(({ value }) => (value ? parseFloat(value) : null))
berat_badan?: number; berat_badan?: number;
@ -114,6 +116,6 @@ export class CreateRekamMedisDto {
tindak_lanjut?: string; tindak_lanjut?: string;
@IsOptional() @IsOptional()
@IsDateString({}, { message: 'Waktu visit harus berupa tanggal yang valid' }) @IsDateString({}, { message: 'Visit time must be a valid date' })
waktu_visit?: string; waktu_visit?: string;
} }

View File

@ -1,18 +1,18 @@
import { IsEnum, IsNumber, IsString } from 'class-validator'; import { IsEnum, IsNumber, IsString } from 'class-validator';
export class PayloadRekamMedisDto { export class PayloadRekamMedisDto {
@IsNumber({}, { message: 'ID dokter harus berupa angka' }) @IsNumber({}, { message: 'Doctor ID must be a number' })
dokter_id: number; dokter_id: number;
@IsString({ message: 'ID kunjungan harus berupa string' }) @IsString({ message: 'Visit ID must be a string' })
visit_id: string; visit_id: string;
@IsEnum({}, { message: 'Anamnese harus berupa enum' }) @IsEnum({}, { message: 'Anamnese must be an enum' })
anamnese: string; anamnese: string;
@IsEnum({}, { message: 'Jenis kasus harus berupa enum' }) @IsEnum({}, { message: 'Case type must be an enum' })
jenis_kasus: string; jenis_kasus: string;
@IsEnum({}, { message: 'Tindak lanjut harus berupa enum' }) @IsEnum({}, { message: 'Follow-up must be an enum' })
tindak_lanjut: string; tindak_lanjut: string;
} }

View File

@ -43,6 +43,7 @@ export class TindakanDokterController {
skip, skip,
page, page,
orderBy: orderBy ? { [orderBy]: order || 'asc' } : undefined, orderBy: orderBy ? { [orderBy]: order || 'asc' } : undefined,
order,
}); });
} }

View File

@ -125,7 +125,7 @@ export class TindakanDokterService {
}); });
if (!visitExists) { if (!visitExists) {
throw new BadRequestException(`ID Visit ${dto.id_visit} tidak ditemukan`); throw new BadRequestException(`Visit ID ${dto.id_visit} not found`);
} }
const response = await this.prisma.validation_queue.create({ const response = await this.prisma.validation_queue.create({
@ -172,7 +172,7 @@ export class TindakanDokterService {
}); });
return newTindakan; return newTindakan;
} catch (error) { } catch (error) {
console.error('Error creating Rekam Medis:', error); console.error('Error creating Doctor Action:', error);
throw error; throw error;
} }
} }
@ -181,7 +181,7 @@ export class TindakanDokterService {
const tindakanId = Number(id); const tindakanId = Number(id);
if (Number.isNaN(tindakanId)) { if (Number.isNaN(tindakanId)) {
throw new BadRequestException('ID tindakan tidak valid'); throw new BadRequestException('Invalid action ID');
} }
return this.prisma.pemberian_tindakan.findUnique({ return this.prisma.pemberian_tindakan.findUnique({
@ -197,15 +197,13 @@ export class TindakanDokterService {
const tindakanId = Number(id); const tindakanId = Number(id);
if (Number.isNaN(tindakanId)) { if (Number.isNaN(tindakanId)) {
throw new BadRequestException('ID tindakan tidak valid'); throw new BadRequestException('Invalid doctor action ID');
} }
const existing = await this.getTindakanDokterById(tindakanId); const existing = await this.getTindakanDokterById(tindakanId);
if (!existing) { if (!existing) {
throw new BadRequestException( throw new BadRequestException(`Doctor Action with ID ${id} not found`);
`Tindakan dokter dengan ID ${id} tidak ditemukan`,
);
} }
const hasUpdates = const hasUpdates =
@ -215,7 +213,7 @@ export class TindakanDokterService {
dto.kelompok_tindakan !== existing.kelompok_tindakan; dto.kelompok_tindakan !== existing.kelompok_tindakan;
if (!hasUpdates) { if (!hasUpdates) {
throw new BadRequestException('Tidak ada data tindakan yang diubah'); throw new BadRequestException("Doctor action data hasn't been changed");
} }
if (dto.id_visit) { if (dto.id_visit) {
@ -224,9 +222,7 @@ export class TindakanDokterService {
}); });
if (!visitExists) { if (!visitExists) {
throw new BadRequestException( throw new BadRequestException(`Visit ID ${dto.id_visit} not found`);
`ID Visit ${dto.id_visit} tidak ditemukan`,
);
} }
} }
@ -276,7 +272,7 @@ export class TindakanDokterService {
}); });
return updatedTindakan; return updatedTindakan;
} catch (error) { } catch (error) {
console.error('Error updating Tindakan Dokter:', error); console.error('Error updating Doctor Action:', error);
throw error; throw error;
} }
} }
@ -285,7 +281,7 @@ export class TindakanDokterService {
const tindakanId = parseInt(id, 10); const tindakanId = parseInt(id, 10);
if (Number.isNaN(tindakanId)) { if (Number.isNaN(tindakanId)) {
throw new BadRequestException('ID tindakan tidak valid'); throw new BadRequestException('Invalid action ID');
} }
const currentData = await this.prisma.pemberian_tindakan.findUnique({ const currentData = await this.prisma.pemberian_tindakan.findUnique({
@ -293,9 +289,7 @@ export class TindakanDokterService {
}); });
if (!currentData) { if (!currentData) {
throw new BadRequestException( throw new BadRequestException(`Doctor action with ID ${id} not found`);
`Tindakan dokter dengan ID ${id} tidak ditemukan`,
);
} }
const idLog = `TINDAKAN_${id}`; const idLog = `TINDAKAN_${id}`;
@ -330,15 +324,13 @@ export class TindakanDokterService {
const tindakanId = Number(id); const tindakanId = Number(id);
if (Number.isNaN(tindakanId)) { if (Number.isNaN(tindakanId)) {
throw new BadRequestException('ID tindakan tidak valid'); throw new BadRequestException('Invalid action ID');
} }
const existingTindakan = await this.getTindakanDokterById(tindakanId); const existingTindakan = await this.getTindakanDokterById(tindakanId);
if (!existingTindakan) { if (!existingTindakan) {
throw new BadRequestException( throw new BadRequestException(`Doctor action with ID ${id} not found`);
`Tindakan dokter dengan ID ${id} tidak ditemukan`,
);
} }
try { try {
@ -368,7 +360,7 @@ export class TindakanDokterService {
return validationQueue; return validationQueue;
} catch (error) { } catch (error) {
console.error('Error deleting Tindakan Dokter:', error); console.error('Error deleting Doctor Action:', error);
throw error; throw error;
} }
} }
@ -376,14 +368,14 @@ export class TindakanDokterService {
async deleteTindakanDokterFromDBAndBlockchain(id: number, userId: number) { async deleteTindakanDokterFromDBAndBlockchain(id: number, userId: number) {
const tindakanId = Number(id); const tindakanId = Number(id);
if (Number.isNaN(tindakanId)) { if (Number.isNaN(tindakanId)) {
throw new BadRequestException('ID tindakan tidak valid'); throw new BadRequestException('Invalid action ID');
} }
const existingTindakan = await this.getTindakanDokterById(tindakanId); const existingTindakan = await this.getTindakanDokterById(tindakanId);
if (!existingTindakan) { if (!existingTindakan) {
throw new BadRequestException( throw new BadRequestException(
`Tindakan dokter dengan ID ${tindakanId} tidak ditemukan`, `Doctor action with ID ${tindakanId} not found`,
); );
} }
@ -410,7 +402,7 @@ export class TindakanDokterService {
}); });
return deletedTindakan; return deletedTindakan;
} catch (error) { } catch (error) {
console.error('Error deleting Tindakan Dokter:', error); console.error('Error deleting Doctor Action:', error);
throw error; throw error;
} }
} }

View File

@ -81,7 +81,7 @@ const handleDeleteCancel = () => {
> >
{{ column.label }} {{ column.label }}
</th> </th>
<th v-if="isAksi" class="text-dark">Aksi</th> <th v-if="isAksi" class="text-dark">Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -112,7 +112,7 @@ const handleDeleteCancel = () => {
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/> />
</svg> </svg>
<p>{{ emptyMessage || "Tidak ada data" }}</p> <p>{{ emptyMessage || "No data available" }}</p>
</div> </div>
</td> </td>
</tr> </tr>
@ -196,7 +196,7 @@ const handleDeleteCancel = () => {
? 'tooltip tooltip-right flex items-center justify-center' ? 'tooltip tooltip-right flex items-center justify-center'
: '', : '',
]" ]"
data-tip="Data ini sedang dalam proses validasi untuk dihapus" data-tip="This data is currently undergoing validation for deletion"
> >
<span <span
:class="[ :class="[
@ -235,7 +235,7 @@ const handleDeleteCancel = () => {
? 'tooltip tooltip-right flex items-center' ? 'tooltip tooltip-right flex items-center'
: '', : '',
]" ]"
data-tip="Data ini sedang dalam proses validasi untuk dihapus" data-tip="This data is currently undergoing validation for deletion"
> >
<span <span
:class="[ :class="[
@ -346,16 +346,16 @@ const handleDeleteCancel = () => {
<DialogConfirm <DialogConfirm
ref="deleteDialogRef" ref="deleteDialogRef"
title="Hapus Data" title="Delete Data"
:message=" :message="
pendingDeleteItem?.id pendingDeleteItem?.id
? `Apakah Anda yakin ingin menghapus item dengan ID ${pendingDeleteItem.id}?` ? `Are you sure you want to delete item with ID ${pendingDeleteItem.id}?`
: pendingDeleteItem?.id_visit : pendingDeleteItem?.id_visit
? `Apakah Anda yakin ingin menghapus item dengan ID Visit ${pendingDeleteItem?.id_visit}?` ? `Are you sure you want to delete item with Visit ID ${pendingDeleteItem?.id_visit}?`
: 'Apakah Anda yakin ingin menghapus item ini?' : 'Are you sure you want to delete this item?'
" "
confirm-text="Hapus" confirm-text="Delete"
cancel-text="Batal" cancel-text="Cancel"
@confirm="handleDeleteConfirm" @confirm="handleDeleteConfirm"
@cancel="handleDeleteCancel" @cancel="handleDeleteCancel"
/> />

View File

@ -12,7 +12,7 @@ const dateTime = ref(new Date());
let clockInterval: number; let clockInterval: number;
const formattedDate = computed(() => const formattedDate = computed(() =>
dateTime.value.toLocaleDateString("id-ID", { dateTime.value.toLocaleDateString("en-US", {
day: "2-digit", day: "2-digit",
month: "long", month: "long",
year: "numeric", year: "numeric",

View File

@ -39,15 +39,14 @@ const handlePageChange = (page: number) => {
<div> <div>
<!-- Pagination Info --> <!-- Pagination Info -->
<div class="text-sm text-gray-600 px-4 py-4"> <div class="text-sm text-gray-600 px-4 py-4">
Menampilkan data {{ startIndex }} - {{ endIndex }} dari Showing data {{ startIndex }} - {{ endIndex }} from {{ totalCount }} items
{{ totalCount }} data
</div> </div>
<!-- Pagination Controls --> <!-- Pagination Controls -->
<div class="flex justify-between items-center px-4 pb-4"> <div class="flex justify-between items-center px-4 pb-4">
<!-- Items per page selector --> <!-- Items per page selector -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xs text-gray-600">Data per halaman:</span> <span class="text-xs text-gray-600">Items per page:</span>
<div class="dropdown dropdown-top"> <div class="dropdown dropdown-top">
<div <div
tabindex="0" tabindex="0"

View File

@ -144,7 +144,7 @@ const isActive = (routeName: string) => {
? 'bg-dark text-white hover:bg-dark' ? 'bg-dark text-white hover:bg-dark'
: '', : '',
]" ]"
data-tip="Rekam Medis" data-tip="Medical Records"
@click="navigateTo('rekam-medis')" @click="navigateTo('rekam-medis')"
> >
<svg <svg
@ -167,7 +167,7 @@ const isActive = (routeName: string) => {
</svg> </svg>
<span <span
class="is-drawer-close:hidden is-drawer-open:opacity-100 transition-opacity is-drawer-open:duration-300 is-drawer-open:delay-300" class="is-drawer-close:hidden is-drawer-open:opacity-100 transition-opacity is-drawer-open:duration-300 is-drawer-open:delay-300"
>Rekam<span class="opacity-0">_</span>Medis</span >Medical<span class="opacity-0">_</span>Records</span
> >
</button> </button>
</li> </li>
@ -181,7 +181,7 @@ const isActive = (routeName: string) => {
? 'bg-dark text-white hover:bg-dark' ? 'bg-dark text-white hover:bg-dark'
: '', : '',
]" ]"
data-tip="Pemberian Tindakan" data-tip="Doctor Actions"
@click="navigateTo('pemberian-tindakan')" @click="navigateTo('pemberian-tindakan')"
> >
<svg <svg
@ -204,7 +204,7 @@ const isActive = (routeName: string) => {
</svg> </svg>
<span <span
class="is-drawer-close:hidden is-drawer-open:opacity-100 transition-opacity is-drawer-open:duration-300 is-drawer-open:delay-300" class="is-drawer-close:hidden is-drawer-open:opacity-100 transition-opacity is-drawer-open:duration-300 is-drawer-open:delay-300"
>Tindakan</span >Actions</span
> >
</button> </button>
</li> </li>
@ -216,7 +216,7 @@ const isActive = (routeName: string) => {
'mb-1 is-drawer-close:tooltip is-drawer-close:tooltip-right active:bg-dark', 'mb-1 is-drawer-close:tooltip is-drawer-close:tooltip-right active:bg-dark',
isActive('obat') ? 'bg-dark text-white hover:bg-dark' : '', isActive('obat') ? 'bg-dark text-white hover:bg-dark' : '',
]" ]"
data-tip="Obat" data-tip="Medicine Administration"
@click="navigateTo('obat')" @click="navigateTo('obat')"
> >
<svg <svg
@ -236,7 +236,7 @@ const isActive = (routeName: string) => {
</svg> </svg>
<span <span
class="is-drawer-close:hidden is-drawer-open:opacity-100 transition-opacity is-drawer-open:duration-300 is-drawer-open:delay-300" class="is-drawer-close:hidden is-drawer-open:opacity-100 transition-opacity is-drawer-open:duration-300 is-drawer-open:delay-300"
>Obat</span >Medicine</span
> >
</button> </button>
</li> </li>
@ -248,7 +248,7 @@ const isActive = (routeName: string) => {
'mb-1 is-drawer-close:tooltip is-drawer-close:tooltip-right active:bg-dark', 'mb-1 is-drawer-close:tooltip is-drawer-close:tooltip-right active:bg-dark',
isActive('validasi') ? 'bg-dark text-white hover:bg-dark' : '', isActive('validasi') ? 'bg-dark text-white hover:bg-dark' : '',
]" ]"
data-tip="Validasi" data-tip="Validation"
@click="navigateTo('validasi')" @click="navigateTo('validasi')"
> >
<svg <svg
@ -268,7 +268,7 @@ const isActive = (routeName: string) => {
</svg> </svg>
<span <span
class="is-drawer-close:hidden is-drawer-open:opacity-100 transition-opacity is-drawer-open:duration-300 is-drawer-open:delay-300" class="is-drawer-close:hidden is-drawer-open:opacity-100 transition-opacity is-drawer-open:duration-300 is-drawer-open:delay-300"
>Validasi</span >Validation</span
> >
</button> </button>
</li> </li>
@ -404,10 +404,10 @@ const isActive = (routeName: string) => {
<!-- Logout Confirmation Dialog --> <!-- Logout Confirmation Dialog -->
<DialogConfirm <DialogConfirm
ref="logoutDialog" ref="logoutDialog"
title="Keluar" title="Logout"
message="Apakah Anda yakin ingin keluar dari aplikasi?" message="Are you sure you want to logout from the application?"
confirm-text="Ya, Keluar" confirm-text="Yes, Logout"
cancel-text="Batal" cancel-text="Cancel"
@confirm="handleLogoutConfirm" @confirm="handleLogoutConfirm"
/> />
</div> </div>

View File

@ -37,7 +37,7 @@ export const FILTER = {
B: "B", B: "B",
AB: "AB", AB: "AB",
O: "O", O: "O",
"Tidak Tahu": "Tidak Tahu", Unknown: "Unknown",
}, },
KATEGORI_TINDAKAN: { KATEGORI_TINDAKAN: {
radiologi: "Radiologi", radiologi: "Radiologi",
@ -63,34 +63,34 @@ export const FILTER = {
laboratorium: "LABORATORIUM", laboratorium: "LABORATORIUM",
}, },
AUDIT_TYPE: { AUDIT_TYPE: {
all: "Semua Tipe", all: "All Types",
rekam_medis: "Rekam Medis", rekam_medis: "Medical Records",
tindakan: "Tindakan", tindakan: "Actions",
obat: "Obat", obat: "Medicine",
proof: "Proof", proof: "Proof",
}, },
AUDIT_TAMPERED: { AUDIT_TAMPERED: {
all: "Semua Data", all: "All Data",
tampered: "Termanipulasi", tampered: "Tampered",
clean: "Tidak Termanipulasi", clean: "Not Tampered",
}, },
}; };
export const SORT_OPTIONS = { export const SORT_OPTIONS = {
REKAM_MEDIS: { REKAM_MEDIS: {
waktu_visit: "ID Visit", waktu_visit: "Visit ID",
no_rm: "Nomor Rekam Medis", no_rm: "Medical Record Number",
umur: "Umur", umur: "Age",
}, },
TINDAKAN: { TINDAKAN: {
id: "ID", id: "ID",
id_visit: "ID Visit", id_visit: "Visit ID",
}, },
OBAT: { OBAT: {
id: "ID", id: "ID",
id_visit: "ID Visit", id_visit: "Visit ID",
jumlah_obat: "Jumlah Obat", jumlah_obat: "Medicine Amount",
obat: "Obat", obat: "Medicine Name",
}, },
USERS: { USERS: {
id: "ID", id: "ID",
@ -98,9 +98,9 @@ export const SORT_OPTIONS = {
role: "Role", role: "Role",
}, },
VALIDATION: { VALIDATION: {
id: "ID Validasi", id: "Validation ID",
created_at: "Waktu Dibuat", created_at: "Created At",
processed_at: "Waktu Diproses", processed_at: "Processed At",
}, },
AUDIT_TRAIL: { AUDIT_TRAIL: {
last_sync: "Last Sync", last_sync: "Last Sync",
@ -111,47 +111,47 @@ export const SORT_OPTIONS = {
export const REKAM_MEDIS_TABLE_COLUMNS = [ export const REKAM_MEDIS_TABLE_COLUMNS = [
{ {
key: "id_visit" as keyof RekamMedis, key: "id_visit" as keyof RekamMedis,
label: "ID Visit", label: "Visit ID",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "no_rm" as keyof RekamMedis, key: "no_rm" as keyof RekamMedis,
label: "No RM", label: "Medical Record Number",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "nama_pasien" as keyof RekamMedis, key: "nama_pasien" as keyof RekamMedis,
label: "Nama Pasien", label: "Patient Name",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "waktu_visit" as keyof RekamMedis, key: "waktu_visit" as keyof RekamMedis,
label: "Waktu Visit", label: "Visit Time",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "umur" as keyof RekamMedis, key: "umur" as keyof RekamMedis,
label: "Umur", label: "Age",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "jenis_kelamin" as keyof RekamMedis, key: "jenis_kelamin" as keyof RekamMedis,
label: "Jenis Kelamin", label: "Gender",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "gol_darah" as keyof RekamMedis, key: "gol_darah" as keyof RekamMedis,
label: "Golongan Darah", label: "Blood Type",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "kode_diagnosa" as keyof RekamMedis, key: "kode_diagnosa" as keyof RekamMedis,
label: "Kode Diagnosa", label: "Diagnosis Code",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "tindak_lanjut" as keyof RekamMedis, key: "tindak_lanjut" as keyof RekamMedis,
label: "Tindak Lanjut", label: "Follow-up Action",
class: "text-dark", class: "text-dark",
}, },
]; ];
@ -164,22 +164,22 @@ export const TINDAKAN_TABLE_COLUMNS = [
}, },
{ {
key: "id_visit" as keyof TindakanDokter, key: "id_visit" as keyof TindakanDokter,
label: "ID Visit", label: "Visit ID",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "tindakan" as keyof TindakanDokter, key: "tindakan" as keyof TindakanDokter,
label: "Tindakan", label: "Action",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "kategori_tindakan" as keyof TindakanDokter, key: "kategori_tindakan" as keyof TindakanDokter,
label: "Kategori Tindakan", label: "Action Category",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "kelompok_tindakan" as keyof TindakanDokter, key: "kelompok_tindakan" as keyof TindakanDokter,
label: "Kelompok Tindakan", label: "Action Group",
class: "text-dark", class: "text-dark",
}, },
]; ];
@ -192,7 +192,7 @@ export const USERS_TABLE_COLUMNS = [
}, },
{ {
key: "name" as keyof Users, key: "name" as keyof Users,
label: "Nama Lengkap", label: "Full Name",
class: "text-dark", class: "text-dark",
}, },
{ {
@ -221,7 +221,7 @@ export const USERS_TABLE_COLUMNS = [
export const LOG_TABLE_COLUMNS = [ export const LOG_TABLE_COLUMNS = [
{ {
key: "txId" as keyof BlockchainLog, key: "txId" as keyof BlockchainLog,
label: "ID Transaksi", label: "Transaction ID",
class: "text-dark", class: "text-dark",
}, },
{ {
@ -259,27 +259,27 @@ export const VALIDATION_TABLE_COLUMNS = [
}, },
{ {
key: "table_name" as keyof ValidationLog, key: "table_name" as keyof ValidationLog,
label: "Kelompok Data", label: "Data Group",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "record_id" as keyof ValidationLog, key: "record_id" as keyof ValidationLog,
label: "ID Record", label: "Record ID",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "action" as keyof ValidationLog, key: "action" as keyof ValidationLog,
label: "Aksi", label: "Action",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "user_id_request" as keyof ValidationLog, key: "user_id_request" as keyof ValidationLog,
label: "User ID Request", label: "Requesting User ID",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "user_id_process" as keyof ValidationLog, key: "user_id_process" as keyof ValidationLog,
label: "User ID Proses", label: "Processing User ID",
class: "text-dark", class: "text-dark",
}, },
{ {
@ -289,12 +289,12 @@ export const VALIDATION_TABLE_COLUMNS = [
}, },
{ {
key: "created_at" as keyof ValidationLog, key: "created_at" as keyof ValidationLog,
label: "Waktu Dibuat", label: "Created At",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "processed_at" as keyof ValidationLog, key: "processed_at" as keyof ValidationLog,
label: "Waktu Diproses", label: "Processed At",
class: "text-dark", class: "text-dark",
}, },
]; ];

View File

@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
const JENIS_KELAMIN_OPTIONS = ["laki-laki", "perempuan"] as const; const JENIS_KELAMIN_OPTIONS = ["laki-laki", "perempuan"] as const;
const GOLONGAN_DARAH_OPTIONS = ["A", "B", "AB", "O", "Tidak Tahu"] as const; const GOLONGAN_DARAH_OPTIONS = ["A", "B", "AB", "O", "Unknown"] as const;
const TINDAK_LANJUT_OPTIONS = [ const TINDAK_LANJUT_OPTIONS = [
"Dipulangkan untuk Kontrol", "Dipulangkan untuk Kontrol",
"Dirawat", "Dirawat",
@ -82,66 +82,69 @@ const numericString = (message: string, options?: NumericFieldOptions) =>
export const rekamMedisFormSchema = z export const rekamMedisFormSchema = z
.object({ .object({
no_rm: trimmedString("No Rekam Medis wajib diisi", { no_rm: trimmedString("Medical record number is required", {
max: { max: {
value: 20, value: 20,
message: "Nomor rekam medis maksimal 20 karakter", message: "Medical record number must be at most 20 characters",
}, },
}), }),
nama_pasien: trimmedString("Nama pasien wajib diisi", { nama_pasien: trimmedString("Patient name is required", {
max: { max: {
value: 100, value: 100,
message: "Nama pasien maksimal 100 karakter", message: "Patient name must be at most 100 characters",
}, },
}), }),
umur: numericString("Umur wajib diisi dan harus valid", { umur: numericString("Age is required and must be valid", {
min: 0, min: 0,
max: 150, max: 150,
}), }),
jenis_kelamin: z jenis_kelamin: z
.string() .string()
.trim() .trim()
.refine((value) => isJenisKelaminOption(value), "Pilih jenis kelamin"), .refine((value) => isJenisKelaminOption(value), "Please select gender"),
gol_darah: z gol_darah: z
.array(z.string().trim()) .array(z.string().trim())
.min(1, "Pilih golongan darah") .min(1, "Please select blood type")
.refine( .refine(
(values) => values.every((value) => isGolonganDarahOption(value)), (values) => values.every((value) => isGolonganDarahOption(value)),
"Golongan darah tidak valid" "Invalid blood type"
), ),
pekerjaan: trimmedString("Pekerjaan wajib diisi"), pekerjaan: trimmedString("Occupation is required"),
anamnese: trimmedString("Anamnese wajib diisi"), anamnese: trimmedString("Anamnese is required"),
kode_diagnosa: trimmedString("Kode diagnosa wajib diisi"), kode_diagnosa: trimmedString("Diagnosis code is required"),
diagnosa: trimmedString("Diagnosa wajib diisi"), diagnosa: trimmedString("Diagnosis is required"),
diastolik: numericString("Tekanan diastolik harus diisi dan valid", { diastolik: numericString(
"Diastolic pressure is required and must be valid",
{
min: 0,
}
),
sistolik: numericString("Systolic pressure is required and must be valid", {
min: 0, min: 0,
}), }),
sistolik: numericString("Tekanan sistolik harus diisi dan valid", { nadi: numericString("Pulse is required and must be valid", {
min: 0, min: 0,
}), }),
nadi: numericString("Nadi harus diisi dan valid", { nafas: numericString("Respiration is required and must be valid", {
min: 0, min: 0,
}), }),
nafas: numericString("Nafas harus diisi dan valid", { suhu: numericString("Temperature is required and must be valid", {
min: 0,
}),
suhu: numericString("Suhu harus diisi dan valid", {
min: 25, min: 25,
max: 45, max: 45,
}), }),
tinggi_badan: numericString("Tinggi badan harus diisi dan valid", { tinggi_badan: numericString("Height is required and must be valid", {
min: 0, min: 0,
}), }),
berat_badan: numericString("Berat badan harus diisi dan valid", { berat_badan: numericString("Weight is required and must be valid", {
min: 0, min: 0,
}), }),
jenis_kasus: trimmedString("Jenis kasus wajib diisi"), jenis_kasus: trimmedString("Case type is required"),
tindak_lanjut: z tindak_lanjut: z
.string() .string()
.trim() .trim()
.refine( .refine(
(value) => isTindakLanjutOption(value), (value) => isTindakLanjutOption(value),
"Pastikan Tindak Lanjut Valid" "Please select a valid follow-up option"
), ),
}) })
.passthrough(); .passthrough();

View File

@ -54,29 +54,29 @@ const isKelompokTindakanOption = (value: string): value is KelompokTindakan =>
const kategoriTindakanSchema = z const kategoriTindakanSchema = z
.string() .string()
.trim() .trim()
.min(1, "Kategori tindakan wajib diisi") .min(1, "Action category is required")
.refine(isKategoriTindakanOption, "Pastikan kategori tindakan valid") .refine(isKategoriTindakanOption, "Please select a valid action category")
.transform((value) => value as KategoriTindakan); .transform((value) => value as KategoriTindakan);
const kelompokTindakanSchema = z const kelompokTindakanSchema = z
.string() .string()
.trim() .trim()
.min(1, "Kelompok tindakan wajib diisi") .min(1, "Action group is required")
.refine(isKelompokTindakanOption, "Pastikan kelompok tindakan valid") .refine(isKelompokTindakanOption, "Please select a valid action group")
.transform((value) => value as KelompokTindakan); .transform((value) => value as KelompokTindakan);
export const tindakanFormSchema = z export const tindakanFormSchema = z
.object({ .object({
id_visit: trimmedString("ID Visit wajib diisi", { id_visit: trimmedString("Visit ID is required", {
max: { max: {
value: 50, value: 50,
message: "ID Visit maksimal 50 karakter", message: "Visit ID must be at most 50 characters",
}, },
}), }),
tindakan: trimmedString("Nama tindakan wajib diisi", { tindakan: trimmedString("Action name is required", {
max: { max: {
value: 150, value: 150,
message: "Nama tindakan maksimal 150 karakter", message: "Action name must be at most 150 characters",
}, },
}), }),
kategori_tindakan: kategoriTindakanSchema, kategori_tindakan: kategoriTindakanSchema,

View File

@ -3,9 +3,9 @@
class="h-screen w-screen flex justify-center flex-col items-center bg-white text-dark text-lg" class="h-screen w-screen flex justify-center flex-col items-center bg-white text-dark text-lg"
> >
<h1 class="font-bold">404 - Page Not Found</h1> <h1 class="font-bold">404 - Page Not Found</h1>
<p>Halaman tidak ditemukan.</p> <p>Page not found.</p>
<router-link to="/dashboard" <router-link to="/dashboard"
>Kembali ke <span class="font-bold">Dashboard</span></router-link >Back to <span class="font-bold">Dashboard</span></router-link
> >
</div> </div>
</template> </template>

View File

@ -8,12 +8,12 @@ import { useRouter } from "vue-router";
const validationSchema = toTypedSchema( const validationSchema = toTypedSchema(
zod.object({ zod.object({
username: zod username: zod
.string({ message: "Input tidak valid" }) .string({ message: "Invalid input" })
.min(1, { message: "Masukkan username" }), .min(1, { message: "Please enter username" }),
password: zod password: zod
.string({ message: "Input tidak valid" }) .string({ message: "Invalid input" })
.min(1, { message: "Masukkan password" }) .min(1, { message: "Please enter password" })
.min(6, { message: "Password minimal 6 karakter" }), .min(6, { message: "Password must be at least 6 characters" }),
}) })
); );
@ -55,7 +55,7 @@ const onSubmit = handleSubmit(async (values: any) => {
if (error && Array.isArray(error.message)) { if (error && Array.isArray(error.message)) {
loginError.value = error.message[0]; loginError.value = error.message[0];
} else { } else {
loginError.value = error.message || "Terjadi kesalahan saat login."; loginError.value = error.message || "An error occurred during login.";
} }
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -63,7 +63,7 @@ const onSubmit = handleSubmit(async (values: any) => {
}); });
const baseButtonClass = const baseButtonClass =
"btn btn-primary hover:btn-primary-content text-white font-bold border-primary py-4 px-4 rounded-md focus:outline-none focus:shadow-outline w-full"; "btn bg-dark text-white font-bold py-4 px-4 rounded-md focus:outline-none focus:shadow-outline w-full";
const buttonClass = computed(() => { const buttonClass = computed(() => {
return [ return [
@ -96,7 +96,7 @@ const buttonClass = computed(() => {
class="shadow appearance-none border rounded w-full py-2 px-3 mb-1 text-dark leading-tight focus:outline-none focus:shadow-outline" class="shadow appearance-none border rounded w-full py-2 px-3 mb-1 text-dark leading-tight focus:outline-none focus:shadow-outline"
id="username" id="username"
type="text" type="text"
placeholder="Masukkan username" placeholder="Enter username"
/> />
<span class="text-red-800">{{ usernameError }}</span> <span class="text-red-800">{{ usernameError }}</span>
</div> </div>
@ -109,7 +109,7 @@ const buttonClass = computed(() => {
class="shadow appearance-none border rounded w-full py-2 px-3 text-dark mb-1 leading-tight focus:outline-none focus:shadow-outline" class="shadow appearance-none border rounded w-full py-2 px-3 text-dark mb-1 leading-tight focus:outline-none focus:shadow-outline"
id="password" id="password"
type="password" type="password"
placeholder="Masukkan password" placeholder="Enter password"
/> />
<span class="text-red-800">{{ passwordError }}</span> <span class="text-red-800">{{ passwordError }}</span>
</div> </div>
@ -119,7 +119,7 @@ const buttonClass = computed(() => {
v-if="isLoading" v-if="isLoading"
class="loading loading-dots loading-sm" class="loading loading-dots loading-sm"
></span> ></span>
<span v-else>Masuk</span> <span v-else>Sign In</span>
</button> </button>
</div> </div>
</form> </form>

View File

@ -86,7 +86,7 @@ const rekamMedisCountLineChartData = computed(() => {
labels: labels, labels: labels,
datasets: [ datasets: [
{ {
label: "Jumlah Rekam Medis Baru", label: "New Medical Records Count",
backgroundColor: "#1a2a4f", backgroundColor: "#1a2a4f",
borderColor: "#1a2a4f", borderColor: "#1a2a4f",
borderWidth: 2, borderWidth: 2,
@ -123,7 +123,7 @@ const rekamMedisLineChartOptions = ref({
callbacks: { callbacks: {
label: function (context: any) { label: function (context: any) {
return ( return (
context.dataset.label + ": " + context.parsed.y + " rekam medis" context.dataset.label + ": " + context.parsed.y + " medical records"
); );
}, },
}, },
@ -159,10 +159,10 @@ const rekamMedisLineChartOptions = ref({
}); });
const auditDataPerKelompokData = computed(() => ({ const auditDataPerKelompokData = computed(() => ({
labels: ["Rekam Medis", "Pemberian Tindakan", "Obat"], labels: ["Medical Records", "Doctor Actions", "Medicine"],
datasets: [ datasets: [
{ {
label: "Jumlah Data", label: "Data Count",
backgroundColor: "#1a2a4f", backgroundColor: "#1a2a4f",
data: stats.tamperedDataPerKelompok.value, data: stats.tamperedDataPerKelompok.value,
}, },
@ -200,11 +200,11 @@ const auditBarOption = {
const normalizeTableName = (tableName: string) => { const normalizeTableName = (tableName: string) => {
switch (tableName) { switch (tableName) {
case "rekam_medis": case "rekam_medis":
return "Rekam Medis"; return "Medical Records";
case "pemberian_tindakan": case "pemberian_tindakan":
return "Pemberian Tindakan"; return "Doctor Actions";
case "obat": case "pemberian_obat":
return "Obat"; return "Medicine Administration";
default: default:
return tableName; return tableName;
} }
@ -262,31 +262,33 @@ onMounted(() => {
<div class="bg-light w-full text-dark"> <div class="bg-light w-full text-dark">
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader title="Dashboard" subtitle="Detail Dashboard" /> <PageHeader title="Dashboard" subtitle="Dashboard Details" />
<div> <div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4"> <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mt-4">
<div <div
class="bg-white p-4 rounded-lg shadow-md flex flex-col items-center" class="bg-white p-4 rounded-lg shadow-md flex flex-col items-center"
> >
<h2 class="text-lg font-semibold mb-2">Total Rekam Medis</h2> <h2 class="text-lg font-semibold mb-2">Total Medical Records</h2>
<p class="text-3xl font-bold">{{ stats.countRekamMedis }}</p> <p class="text-3xl font-bold">{{ stats.countRekamMedis }}</p>
</div> </div>
<div <div
class="bg-white p-4 rounded-lg shadow-md flex flex-col items-center" class="bg-white p-4 rounded-lg shadow-md flex flex-col items-center"
> >
<h2 class="text-lg font-semibold mb-2">Total Tindakan Dokter</h2> <h2 class="text-lg font-semibold mb-2">Total Doctor Actions</h2>
<p class="text-3xl font-bold">{{ stats.countTindakanDokter }}</p> <p class="text-3xl font-bold">{{ stats.countTindakanDokter }}</p>
</div> </div>
<div <div
class="bg-white p-4 rounded-lg shadow-md flex flex-col items-center" class="bg-white p-4 rounded-lg shadow-md flex flex-col items-center"
> >
<h2 class="text-lg font-semibold mb-2">Total Obat</h2> <h2 class="text-lg font-semibold mb-2">
Total Medicine Administration
</h2>
<p class="text-3xl font-bold">{{ stats.countObat }}</p> <p class="text-3xl font-bold">{{ stats.countObat }}</p>
</div> </div>
<div <div
class="bg-white p-4 rounded-lg shadow-md flex flex-col items-center" class="bg-white p-4 rounded-lg shadow-md flex flex-col items-center"
> >
<h2 class="text-lg font-semibold mb-2">Total Data Audit</h2> <h2 class="text-lg font-semibold mb-2">Total Audit Data</h2>
<p class="text-3xl font-bold"> <p class="text-3xl font-bold">
{{ stats.auditNonTampered.value + stats.auditTampered.value }} {{ stats.auditNonTampered.value + stats.auditTampered.value }}
</p> </p>
@ -297,7 +299,7 @@ onMounted(() => {
class="flex flex-col flex-2 mt-4 bg-white p-4 rounded-lg shadow-md min-w-0" class="flex flex-col flex-2 mt-4 bg-white p-4 rounded-lg shadow-md min-w-0"
> >
<h5 class="flex-1 text-lg font-semibold text-center"> <h5 class="flex-1 text-lg font-semibold text-center">
Jumlah Rekam Medis Baru 7 Hari Terakhir New Medical Records in Last 7 Days
</h5> </h5>
<div class="flex-5 w-full"> <div class="flex-5 w-full">
<Line <Line
@ -310,13 +312,13 @@ onMounted(() => {
to="/rekam-medis" to="/rekam-medis"
class="text-sm hover:opacity-75 transition-all" class="text-sm hover:opacity-75 transition-all"
> >
Lihat Data Rekam Medis >> View Medical Records >>
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
<div class="flex-1 mt-4 bg-white p-4 rounded-lg shadow-md min-w-0"> <div class="flex-1 mt-4 bg-white p-4 rounded-lg shadow-md min-w-0">
<h5 class="text-lg font-semibold mb-2 text-center"> <h5 class="text-lg font-semibold mb-2 text-center">
Data Audit Trail Audit Trail Summary
</h5> </h5>
<div class="w-full"> <div class="w-full">
<Doughnut <Doughnut
@ -339,7 +341,7 @@ onMounted(() => {
to="/audit-trail" to="/audit-trail"
class="text-sm hover:opacity-75 transition-all" class="text-sm hover:opacity-75 transition-all"
> >
Lihat Detail Audit Trail >> View Audit Trail Details >>
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
@ -347,17 +349,17 @@ onMounted(() => {
<div class="flex gap-x-4"> <div class="flex gap-x-4">
<div class="mt-4 flex-2 bg-white p-4 rounded-lg shadow-md h-fit"> <div class="mt-4 flex-2 bg-white p-4 rounded-lg shadow-md h-fit">
<h5 class="text-lg font-semibold text-center"> <h5 class="text-lg font-semibold text-center">
Data yang perlu divalidasi (<span Data pending validation (<span class="font-bold text-red-400">{{
class="font-bold text-red-400" validasiData.totalCount
>{{ validasiData.totalCount }}</span }}</span
>) >)
</h5> </h5>
<table class="w-full mt-4"> <table class="w-full mt-4">
<thead> <thead>
<tr class="text-left"> <tr class="text-left">
<th class="pb-2">Kelompok Data</th> <th class="pb-2">Data Group</th>
<th class="pb-2">Tipe Aksi</th> <th class="pb-2">Action Type</th>
<th class="pb-2">ID User</th> <th class="pb-2">User ID</th>
</tr> </tr>
</thead> </thead>
<tbody class=""> <tbody class="">
@ -383,13 +385,13 @@ onMounted(() => {
to="/validasi" to="/validasi"
class="text-sm hover:opacity-75 transition-all" class="text-sm hover:opacity-75 transition-all"
> >
Lihat Detail Validasi Data >> View Validation Details >>
</RouterLink> </RouterLink>
</div> </div>
</div> </div>
<div class="mt-4 flex-1 bg-white p-4 rounded-lg shadow-md"> <div class="mt-4 flex-1 bg-white p-4 rounded-lg shadow-md">
<h5 class="text-lg font-semibold text-center"> <h5 class="text-lg font-semibold text-center">
Tampered data per kelompok data Tampered data per data group
</h5> </h5>
<div class="h-64 w-full"> <div class="h-64 w-full">
<Bar <Bar

View File

@ -139,11 +139,11 @@ const deriveType = (entry: Partial<AuditLogEntry>): AuditLogType => {
}; };
const typeLabelMap: Record<AuditLogType, string> = { const typeLabelMap: Record<AuditLogType, string> = {
rekam_medis: "Rekam Medis", rekam_medis: "Medical Records",
tindakan: "Tindakan", tindakan: "Actions",
obat: "Obat", obat: "Medicine",
proof: "Proof", proof: "Proof",
unknown: "Tidak Diketahui", unknown: "Unknown",
}; };
const normalizeEntry = (entry: any): AuditLogEntry => { const normalizeEntry = (entry: any): AuditLogEntry => {
@ -343,17 +343,17 @@ onMounted(async () => {
}); });
socket.on("connect", () => { socket.on("connect", () => {
console.log("Berhasil terhubung ke WebSocket:", socket.id); console.log("Successfully connected to WebSocket:", socket.id);
}); });
socket.on("audit.progress", (data) => { socket.on("audit.progress", (data) => {
console.log("Menerima progres:", data); console.log("Receiving progress:", data);
status.value = "RUNNING"; status.value = "RUNNING";
progres.value = data.progress_count; progres.value = data.progress_count;
}); });
socket.on("audit.complete", (data) => { socket.on("audit.complete", (data) => {
console.log("Menerima selesai:", data); console.log("Receiving completion:", data);
fetchData(); fetchData();
status.value = "COMPLETED"; status.value = "COMPLETED";
}); });
@ -379,7 +379,7 @@ onBeforeUnmount(() => {
<div class="bg-light w-full text-dark"> <div class="bg-light w-full text-dark">
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader title="Audit Trail" subtitle="Riwayat Log Blockchain" /> <PageHeader title="Audit Trail" subtitle="Blockchain Log History" />
<div <div
class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2" class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2"
@ -394,37 +394,37 @@ onBeforeUnmount(() => {
<div class="flex gap-x-4"> <div class="flex gap-x-4">
<div class="flex gap-x-4 items-end"> <div class="flex gap-x-4 items-end">
<div class="h-full"> <div class="h-full">
<label for="jenis_kelamin" class="font-bold">Tipe Data</label> <label for="jenis_kelamin" class="font-bold">Data Type</label>
<select <select
v-model="filters.type" v-model="filters.type"
class="select bg-white border border-gray-300 mt-1" class="select bg-white border border-gray-300 mt-1"
> >
<option disabled selected value="initial"> <option disabled selected value="initial">
Pilih Tipe Data Select Data Type
</option> </option>
<option value="rekam_medis">Rekam Medis</option> <option value="rekam_medis">Medical Records</option>
<option value="tindakan">Tindakan</option> <option value="tindakan">Doctors Actions</option>
<option value="obat">Obat</option> <option value="obat">Medicine Administration</option>
<option value="proof">Proof</option> <option value="proof">Proof</option>
<option value="all">Semua Tipe</option> <option value="all">All Types</option>
</select> </select>
</div> </div>
</div> </div>
<div class="flex gap-x-4 items-end"> <div class="flex gap-x-4 items-end">
<div class="h-full"> <div class="h-full">
<label for="jenis_kelamin" class="font-bold" <label for="jenis_kelamin" class="font-bold"
>Status Manipulasi</label >Tampering Status</label
> >
<select <select
v-model="filters.tampered" v-model="filters.tampered"
class="select bg-white border border-gray-300 mt-1" class="select bg-white border border-gray-300 mt-1"
> >
<option disabled selected value="initial"> <option disabled selected value="initial">
Pilih Status Manipulasi Select Tampering Status
</option> </option>
<option value="tampered">Tampered</option> <option value="tampered">Tampered</option>
<option value="non_tampered">Non-tampered</option> <option value="non_tampered">Non-tampered</option>
<option value="all">Semua</option> <option value="all">All</option>
</select> </select>
</div> </div>
</div> </div>
@ -448,14 +448,14 @@ onBeforeUnmount(() => {
<div class="flex flex-col w-full gap-2 md:flex-row md:items-center"> <div class="flex flex-col w-full gap-2 md:flex-row md:items-center">
<SearchInput <SearchInput
v-model="searchId" v-model="searchId"
placeholder="Cari berdasarkan ID Log" placeholder="Search by Log ID (e.g., REKAM_12345)"
@search="handleSearch" @search="handleSearch"
/> />
<div class="flex items-center gap-2 md:ml-4"> <div class="flex items-center gap-2 md:ml-4">
<SortDropdown <SortDropdown
v-model="sortBy" v-model="sortBy"
:options="SORT_OPTIONS.AUDIT_TRAIL" :options="SORT_OPTIONS.AUDIT_TRAIL"
label="Urut berdasarkan:" label="Sort by:"
@change="handleSortChange" @change="handleSortChange"
/> />
<button <button
@ -481,11 +481,11 @@ onBeforeUnmount(() => {
</div> </div>
<DialogConfirm <DialogConfirm
ref="auditDialog" ref="auditDialog"
title="Konfirmasi" title="Confirmation"
message="Apakah anda yakin ingin menjalankan audit? (Proses ini mungkin message="Are you sure you want to run the audit? (This process may
memakan waktu lama)" take a long time)"
confirm-text="Iya, Jalankan" confirm-text="Yes, Run"
cancel-text="Batal" cancel-text="Cancel"
@confirm="runAuditTrail" @confirm="runAuditTrail"
/> />
<ButtonDark <ButtonDark
@ -494,7 +494,7 @@ onBeforeUnmount(() => {
status === 'STARTING' || status === 'STARTING' ||
status === 'RUNNING' status === 'RUNNING'
" "
text="Lakukan Audit" text="Run Audit"
@click="showAuditModal" @click="showAuditModal"
/> />
</div> </div>
@ -509,11 +509,9 @@ onBeforeUnmount(() => {
<div class="h-48 flex justify-center items-center flex-col gap-4"> <div class="h-48 flex justify-center items-center flex-col gap-4">
<span class="loading loading-ring loading-xl"></span> <span class="loading loading-ring loading-xl"></span>
<div class="text-center"> <div class="text-center">
<p class="font-semibold mb-2"> <p class="font-semibold mb-2">Audit process is running...</p>
Proses audit sedang berjalan...
</p>
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600">
{{ progres }} data telah diperiksa {{ progres }} data has been checked
</p> </p>
</div> </div>
</div> </div>
@ -523,7 +521,7 @@ onBeforeUnmount(() => {
:data="logs" :data="logs"
:columns="AUDIT_TABLE_COLUMNS" :columns="AUDIT_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Tidak ada log audit" empty-message="No audit logs available"
:is-aksi="false" :is-aksi="false"
/> />
<PaginationControls <PaginationControls

View File

@ -103,23 +103,23 @@ const validate = (): CreateObatFormErrors => {
const validationErrors: CreateObatFormErrors = {}; const validationErrors: CreateObatFormErrors = {};
if (!data.value.id_visit.trim()) { if (!data.value.id_visit.trim()) {
validationErrors.id_visit = "ID Visit wajib diisi"; validationErrors.id_visit = "ID Visit is required";
} }
if (!data.value.obat.trim()) { if (!data.value.obat.trim()) {
validationErrors.obat = "Nama obat wajib diisi"; validationErrors.obat = "Medicine name is required";
} }
const jumlahRaw = data.value.jumlah_obat.trim(); const jumlahRaw = data.value.jumlah_obat.trim();
if (!jumlahRaw) { if (!jumlahRaw) {
validationErrors.jumlah_obat = "Jumlah obat wajib diisi"; validationErrors.jumlah_obat = "Medicine quantity is required";
} else { } else {
const jumlahNumber = Number(jumlahRaw); const jumlahNumber = Number(jumlahRaw);
if (!Number.isInteger(jumlahNumber) || jumlahNumber <= 0) { if (!Number.isInteger(jumlahNumber) || jumlahNumber <= 0) {
validationErrors.jumlah_obat = validationErrors.jumlah_obat =
"Jumlah obat harus berupa bilangan bulat positif"; "Medicine quantity must be a positive integer";
} }
} }
@ -164,7 +164,7 @@ const handleSubmit = async () => {
} catch (error) { } catch (error) {
console.error("Failed to create obat:", error); console.error("Failed to create obat:", error);
submitError.value = submitError.value =
(error as ApiError)?.message || "Gagal menyimpan data obat."; (error as ApiError)?.message || "Failed to save medicine data.";
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -194,7 +194,7 @@ watch(
); );
onMounted(() => { onMounted(() => {
document.title = "Tambah Pemberian Obat - Hospital Log"; document.title = "Add Medicine Administration - Hospital Log";
fetchVisitOptions(""); fetchVisitOptions("");
}); });
</script> </script>
@ -203,19 +203,22 @@ onMounted(() => {
<div class="bg-light w-full text-dark"> <div class="bg-light w-full text-dark">
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader title="Pemberian Obat" subtitle="Tambah Pemberian Obat" /> <PageHeader
title="Medicine Administration"
subtitle="Add Medicine Administration"
/>
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
<div class="flex flex-col px-4 py-4 justify-between gap-4"> <div class="flex flex-col px-4 py-4 justify-between gap-4">
<div class="breadcrumbs text-sm"> <div class="breadcrumbs text-sm">
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/obat">Pemberian Obat</RouterLink> <RouterLink to="/obat">Medicine Administration</RouterLink>
</li> </li>
<li>Tambah Pemberian Obat</li> <li>Add Medicine Administration</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Tambah Pemberian Obat</h5> <h5 class="font-bold">Add Medicine Administration</h5>
<div v-if="isSuccess" role="alert" class="alert alert-success"> <div v-if="isSuccess" role="alert" class="alert alert-success">
<svg <svg
@ -231,7 +234,7 @@ onMounted(() => {
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Berhasil menambahkan data obat!</span> <span>Successfully added medicine data!</span>
</div> </div>
<div v-if="submitError" role="alert" class="alert alert-error"> <div v-if="submitError" role="alert" class="alert alert-error">
@ -256,7 +259,7 @@ onMounted(() => {
v-if="isLoading" v-if="isLoading"
> >
<span class="loading loading-ring loading-xl"></span> <span class="loading loading-ring loading-xl"></span>
<h5>Mohon tunggu, sedang menyimpan data obat...</h5> <h5>Please wait, saving medicine data...</h5>
</div> </div>
<form v-else @submit.prevent="handleSubmit" class="text-sm"> <form v-else @submit.prevent="handleSubmit" class="text-sm">
@ -264,7 +267,7 @@ onMounted(() => {
class="mt-2" class="mt-2"
label="ID Visit" label="ID Visit"
type="text" type="text"
placeholder="Masukkan ID visit" placeholder="Enter visit ID"
v-model="data.id_visit" v-model="data.id_visit"
:error="errors.id_visit || null" :error="errors.id_visit || null"
list="visit-options" list="visit-options"
@ -280,25 +283,25 @@ onMounted(() => {
</datalist> </datalist>
<FieldInput <FieldInput
class="mt-4" class="mt-4"
label="Obat" label="Medicine"
type="text" type="text"
placeholder="Masukkan nama obat" placeholder="Enter medicine name"
v-model="data.obat" v-model="data.obat"
:error="errors.obat || null" :error="errors.obat || null"
/> />
<FieldInput <FieldInput
class="mt-4" class="mt-4"
label="Jumlah Obat" label="Medicine Quantity"
type="text" type="text"
placeholder="Masukkan jumlah obat" placeholder="Enter medicine quantity"
v-model="data.jumlah_obat" v-model="data.jumlah_obat"
:error="errors.jumlah_obat || null" :error="errors.jumlah_obat || null"
/> />
<FieldInput <FieldInput
class="mt-4" class="mt-4"
label="Aturan Pakai" label="Usage Instructions"
type="text" type="text"
placeholder="Masukkan aturan pakai" placeholder="Enter usage instructions"
v-model="data.aturan_pakai" v-model="data.aturan_pakai"
:error="errors.aturan_pakai || null" :error="errors.aturan_pakai || null"
/> />
@ -308,7 +311,7 @@ onMounted(() => {
> >
<ButtonDark <ButtonDark
type="submit" type="submit"
:text="isLoading ? 'Menyimpan...' : 'Simpan Obat'" :text="isLoading ? 'Saving...' : 'Save Medicine'"
:isLoading="isLoading" :isLoading="isLoading"
/> />
</div> </div>

View File

@ -127,7 +127,7 @@ const fetchLogData = async () => {
onMounted(async () => { onMounted(async () => {
await fetchData(); await fetchData();
await fetchLogData(); await fetchLogData();
document.title = "Rekam Medis Detail - Hospital Log"; document.title = "Medicine Administration Detail - Hospital Log";
}); });
</script> </script>
@ -136,8 +136,8 @@ onMounted(async () => {
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Pemberian Obat" title="Medicine Administration"
:subtitle="`Detail Pemberian Obat. ID ${route.params.id}`" :subtitle="`Medicine Administration Detail. ID ${route.params.id}`"
/> />
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
@ -145,21 +145,21 @@ onMounted(async () => {
<div class="breadcrumbs text-sm"> <div class="breadcrumbs text-sm">
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/obat">Pemberian Obat</RouterLink> <RouterLink to="/obat">Medicine Administration</RouterLink>
</li> </li>
<li>Detail Pemberian Obat {{ route.params.id }}</li> <li>Medicine Administration Detail {{ route.params.id }}</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Detail Pemberian Obat</h5> <h5 class="font-bold">Medicine Administration Detail</h5>
<div class="text-sm"> <div class="text-sm">
<p>ID: {{ data?.id }}</p> <p>ID: {{ data?.id }}</p>
<p>ID Visit: {{ data?.id_visit }}</p> <p>ID Visit: {{ data?.id_visit }}</p>
<p>Obat: {{ data?.obat }}</p> <p>Medicine: {{ data?.obat }}</p>
<p>Jumlah Obat: {{ data?.jumlah_obat }}</p> <p>Medicine Quantity: {{ data?.jumlah_obat }}</p>
<p>Aturan Pakai: {{ data?.aturan_pakai }}</p> <p>Usage Instructions: {{ data?.aturan_pakai }}</p>
</div> </div>
<hr /> <hr />
<h5 class="font-bold">Log Perubahan</h5> <h5 class="font-bold">Change Log</h5>
<div role="alert" class="alert alert-error" v-if="isTampered"> <div role="alert" class="alert alert-error" v-if="isTampered">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -174,13 +174,13 @@ onMounted(async () => {
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Peringatan! Manipulasi Data Terdeteksi.</span> <span>Warning! Data Manipulation Detected.</span>
</div> </div>
<DataTable <DataTable
:data="dataLog" :data="dataLog"
:columns="LOG_TABLE_COLUMNS" :columns="LOG_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Belum terdapat log perubahan" empty-message="No change logs available yet"
:is-aksi="false" :is-aksi="false"
/> />
</div> </div>

View File

@ -51,15 +51,15 @@ const sortOrder = ref<"asc" | "desc">(
const tableColumns = [ const tableColumns = [
{ key: "id" as keyof ObatData, label: "#", class: "text-dark" }, { key: "id" as keyof ObatData, label: "#", class: "text-dark" },
{ key: "id_visit" as keyof ObatData, label: "ID Visit", class: "text-dark" }, { key: "id_visit" as keyof ObatData, label: "ID Visit", class: "text-dark" },
{ key: "obat" as keyof ObatData, label: "Obat", class: "text-dark" }, { key: "obat" as keyof ObatData, label: "Medicine", class: "text-dark" },
{ {
key: "jumlah_obat" as keyof ObatData, key: "jumlah_obat" as keyof ObatData,
label: "Jumlah Obat", label: "Medicine Quantity",
class: "text-dark", class: "text-dark",
}, },
{ {
key: "aturan_pakai" as keyof ObatData, key: "aturan_pakai" as keyof ObatData,
label: "Aturan Pakai", label: "Usage Instructions",
class: "text-dark", class: "text-dark",
}, },
]; ];
@ -207,7 +207,10 @@ onMounted(async () => {
<div class="bg-light w-full text-dark"> <div class="bg-light w-full text-dark">
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader title="Obat" subtitle="Manajemen Obat" /> <PageHeader
title="Medicine Administration"
subtitle="Medicine Management"
/>
<div class="bg-white rounded-xl shadow-md"> <div class="bg-white rounded-xl shadow-md">
<div <div
class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 px-4 pt-4 pb-2" class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 px-4 pt-4 pb-2"
@ -215,14 +218,14 @@ onMounted(async () => {
<div class="flex flex-col w-full gap-2 md:flex-row md:items-center"> <div class="flex flex-col w-full gap-2 md:flex-row md:items-center">
<SearchInput <SearchInput
v-model="searchObat" v-model="searchObat"
placeholder="Cari berdasarkan Obat" placeholder="Search by Medicine Administration ID"
@search="handleSearch" @search="handleSearch"
/> />
<div class="flex items-center gap-2 md:ml-4"> <div class="flex items-center gap-2 md:ml-4">
<SortDropdown <SortDropdown
v-model="sortBy" v-model="sortBy"
:options="SORT_OPTIONS.OBAT" :options="SORT_OPTIONS.OBAT"
label="Urut berdasarkan:" label="Sort by:"
@change="handleSortChange" @change="handleSortChange"
/> />
<button <button
@ -250,7 +253,7 @@ onMounted(async () => {
to="/pemberian-obat-add" to="/pemberian-obat-add"
class="btn bg-dark btn-sm self-start md:self-auto" class="btn bg-dark btn-sm self-start md:self-auto"
> >
Tambah Obat Add Medicine
</RouterLink> </RouterLink>
</div> </div>
@ -272,7 +275,7 @@ onMounted(async () => {
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Data obat berhasil dikirim untuk validasi penghapusan</span> <span>Medicine data successfully sent for deletion validation</span>
</div> </div>
<!-- Data Table --> <!-- Data Table -->
@ -280,7 +283,7 @@ onMounted(async () => {
:data="data" :data="data"
:columns="tableColumns" :columns="tableColumns"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Tidak ada data obat" empty-message="No medicine data available"
@details="handleDetails" @details="handleDetails"
@update="handleUpdate" @update="handleUpdate"
@delete="handleDelete" @delete="handleDelete"

View File

@ -110,7 +110,7 @@ const handleSubmit = async () => {
isLoading.value = true; isLoading.value = true;
if (id.value === data.value.id) { if (id.value === data.value.id) {
if (id.value !== parseInt(route.params.id as string)) { if (id.value !== parseInt(route.params.id as string)) {
alert("ID tidak valid. Data akan dimuat ulang."); alert("Invalid ID. Data will be reloaded.");
await fetchData(); await fetchData();
return; return;
} }
@ -181,7 +181,7 @@ const fetchLogData = async () => {
onMounted(async () => { onMounted(async () => {
await fetchData(); await fetchData();
await fetchLogData(); await fetchLogData();
document.title = "Rekam Medis Detail - Hospital Log"; document.title = "Medicine Administration Detail - Hospital Log";
}); });
</script> </script>
@ -190,8 +190,8 @@ onMounted(async () => {
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Pemberian Obat" title="Medicine Administration"
:subtitle="`Update Pemberian Obat. ID ${route.params.id}`" :subtitle="`Update Medicine Administration. ID ${route.params.id}`"
/> />
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
@ -199,12 +199,12 @@ onMounted(async () => {
<div class="breadcrumbs text-sm"> <div class="breadcrumbs text-sm">
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/obat">Pemberian Obat</RouterLink> <RouterLink to="/obat">Medicine Administration</RouterLink>
</li> </li>
<li>Update Pemberian Obat {{ route.params.id }}</li> <li>Update Medicine Administration {{ route.params.id }}</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Update Pemberian Obat</h5> <h5 class="font-bold">Update Medicine Administration</h5>
<div v-if="isSuccess" role="alert" class="alert alert-success"> <div v-if="isSuccess" role="alert" class="alert alert-success">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -219,7 +219,7 @@ onMounted(async () => {
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Berhasil memperbarui data!</span> <span>Successfully updated data!</span>
</div> </div>
<div> <div>
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
@ -239,40 +239,40 @@ onMounted(async () => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
label="Obat" label="Medicine"
type="text" type="text"
v-model="data.obat" v-model="data.obat"
:is-disabled="false" :is-disabled="false"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
label="Jumlah Obat" label="Medicine Quantity"
type="text" type="text"
v-model="data.jumlah_obat" v-model="data.jumlah_obat"
:is-disabled="false" :is-disabled="false"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
label="Aturan Pakai" label="Usage Instructions"
type="text" type="text"
placeholder="Belum ada aturan pakai." placeholder="No usage instructions yet."
v-model="data.aturan_pakai" v-model="data.aturan_pakai"
:is-disabled="false" :is-disabled="false"
/> />
<div class="flex justify-end"> <div class="flex justify-end">
<ButtonDark <ButtonDark
type="submit" type="submit"
:text="isLoading ? 'Menyimpan...' : 'Simpan Perubahan'" :text="isLoading ? 'Saving...' : 'Save Changes'"
class="mt-4" class="mt-4"
:isLoading="isLoading" :isLoading="isLoading"
> >
Simpan Perubahan Save Changes
</ButtonDark> </ButtonDark>
</div> </div>
</form> </form>
</div> </div>
<hr /> <hr />
<h5 class="font-bold">Log Perubahan</h5> <h5 class="font-bold">Change Log</h5>
<div role="alert" class="alert alert-error" v-if="isTampered"> <div role="alert" class="alert alert-error" v-if="isTampered">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -287,13 +287,13 @@ onMounted(async () => {
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Peringatan! Manipulasi Data Terdeteksi.</span> <span>Warning! Data tampering detected.</span>
</div> </div>
<DataTable <DataTable
:data="dataLog" :data="dataLog"
:columns="LOG_TABLE_COLUMNS" :columns="LOG_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Belum terdapat log perubahan" empty-message="No change logs yet"
:is-aksi="false" :is-aksi="false"
/> />
</div> </div>

View File

@ -128,7 +128,7 @@ const handleSubmit = async () => {
} catch (error) { } catch (error) {
console.error("Failed to create tindakan:", error); console.error("Failed to create tindakan:", error);
submitError.value = submitError.value =
(error as ApiError)?.message || "Gagal menyimpan data tindakan."; (error as ApiError)?.message || "Failed to save action data.";
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
@ -158,7 +158,7 @@ watch(
); );
onMounted(() => { onMounted(() => {
document.title = "Tambah Pemberian Tindakan - Hospital Log"; document.title = "Add Doctor Action - Hospital Log";
fetchVisitOptions(""); fetchVisitOptions("");
}); });
</script> </script>
@ -167,10 +167,7 @@ onMounted(() => {
<div class="bg-light w-full text-dark"> <div class="bg-light w-full text-dark">
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader title="Doctor Action" subtitle="Add Doctor Action" />
title="Pemberian Tindakan"
subtitle="Tambah Pemberian Tindakan"
/>
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
<div class="flex flex-col px-4 py-4 justify-between gap-4"> <div class="flex flex-col px-4 py-4 justify-between gap-4">
@ -178,13 +175,13 @@ onMounted(() => {
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/pemberian-tindakan" <RouterLink to="/pemberian-tindakan"
>Pemberian Tindakan</RouterLink >Doctor Action</RouterLink
> >
</li> </li>
<li>Tambah Pemberian Tindakan</li> <li>Add Doctor Action</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Tambah Pemberian Tindakan</h5> <h5 class="font-bold">Add Doctor Action</h5>
<div v-if="isSuccess" role="alert" class="alert alert-success"> <div v-if="isSuccess" role="alert" class="alert alert-success">
<svg <svg
@ -200,7 +197,7 @@ onMounted(() => {
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Berhasil menambahkan data tindakan!</span> <span>Successfully added action data!</span>
</div> </div>
<div v-if="submitError" role="alert" class="alert alert-error"> <div v-if="submitError" role="alert" class="alert alert-error">
@ -225,15 +222,15 @@ onMounted(() => {
v-if="isLoading" v-if="isLoading"
> >
<span class="loading loading-ring loading-xl"></span> <span class="loading loading-ring loading-xl"></span>
<h5>Mohon tunggu, sedang menyimpan data tindakan...</h5> <h5>Please wait, saving action data...</h5>
</div> </div>
<form v-else @submit.prevent="handleSubmit" class="text-sm"> <form v-else @submit.prevent="handleSubmit" class="text-sm">
<FieldInput <FieldInput
class="mt-2" class="mt-2"
label="ID Visit" label="Visit ID"
type="text" type="text"
placeholder="Masukkan ID visit" placeholder="Enter visit ID"
v-model="data.id_visit" v-model="data.id_visit"
:error="errors.id_visit || null" :error="errors.id_visit || null"
list="visit-options" list="visit-options"
@ -249,16 +246,16 @@ onMounted(() => {
</datalist> </datalist>
<FieldInput <FieldInput
class="mt-4" class="mt-4"
label="Tindakan" label="Doctor Action"
type="text" type="text"
placeholder="Masukkan nama tindakan" placeholder="Enter action name"
v-model="data.tindakan" v-model="data.tindakan"
:error="errors.tindakan || null" :error="errors.tindakan || null"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Kategori Tindakan" placeholder="Enter doctor action category"
label="Kategori Tindakan" label="Doctor Action Category"
list="kategori_tindakan" list="kategori_tindakan"
v-model="data.kategori_tindakan" v-model="data.kategori_tindakan"
:error="errors.kategori_tindakan || null" :error="errors.kategori_tindakan || null"
@ -274,8 +271,8 @@ onMounted(() => {
</datalist> </datalist>
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Kelompok Tindakan" placeholder="Enter doctor action group"
label="Kelompok Tindakan" label="Doctor Action Group"
list="kelompok_tindakan" list="kelompok_tindakan"
v-model="data.kelompok_tindakan" v-model="data.kelompok_tindakan"
:error="errors.kelompok_tindakan || null" :error="errors.kelompok_tindakan || null"
@ -294,7 +291,7 @@ onMounted(() => {
> >
<ButtonDark <ButtonDark
type="submit" type="submit"
:text="isLoading ? 'Menyimpan...' : 'Simpan Tindakan'" :text="isLoading ? 'Saving...' : 'Save Action'"
:isLoading="isLoading" :isLoading="isLoading"
/> />
</div> </div>

View File

@ -128,7 +128,7 @@ const fetchLogData = async () => {
onMounted(async () => { onMounted(async () => {
await fetchData(); await fetchData();
await fetchLogData(); await fetchLogData();
document.title = `Detail Pemberian Tindakan - ID ${route.params.id}`; document.title = `Doctor Action Details - ID ${route.params.id}`;
}); });
</script> </script>
@ -137,8 +137,8 @@ onMounted(async () => {
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Pemberian Tindakan" title="Doctor Actions"
:subtitle="`Detail Pemberian Tindakan ${route.params.id}`" :subtitle="`Doctor Action Details ${route.params.id}`"
/> />
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
@ -147,28 +147,28 @@ onMounted(async () => {
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/pemberian-tindakan"> <RouterLink to="/pemberian-tindakan">
Pemberian Tindakan Doctor Actions
</RouterLink> </RouterLink>
</li> </li>
<li>Detail Pemberian Tindakan {{ route.params.id }}</li> <li>Doctor Action Details {{ route.params.id }}</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Detail Pemberian Tindakan</h5> <h5 class="font-bold">Doctor Action Details</h5>
<div class="text-sm"> <div class="text-sm">
<p>ID: {{ tindakan?.id }}</p> <p>ID: {{ tindakan?.id }}</p>
<p>ID Visit: {{ tindakan?.id_visit }}</p> <p>Visit ID: {{ tindakan?.id_visit }}</p>
<p>Tindakan: {{ tindakan?.tindakan }}</p> <p>Doctor Action: {{ tindakan?.tindakan }}</p>
<p> <p>
Kategori Tindakan: Doctor Action Category:
{{ tindakan?.kategori_tindakan || "-" }} {{ tindakan?.kategori_tindakan || "-" }}
</p> </p>
<p> <p>
Kelompok Tindakan: Doctor Action Group:
{{ tindakan?.kelompok_tindakan || "-" }} {{ tindakan?.kelompok_tindakan || "-" }}
</p> </p>
</div> </div>
<hr /> <hr />
<h5 class="font-bold">Log Perubahan</h5> <h5 class="font-bold">Change Log</h5>
<div role="alert" class="alert alert-error" v-if="isTampered"> <div role="alert" class="alert alert-error" v-if="isTampered">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -183,14 +183,14 @@ onMounted(async () => {
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Peringatan! Manipulasi Data Terdeteksi.</span> <span>Warning! Data Manipulation Detected.</span>
</div> </div>
<DataTable <DataTable
:data="dataLog" :data="dataLog"
:columns="LOG_TABLE_COLUMNS" :columns="LOG_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Belum terdapat log perubahan" empty-message="No change logs available yet"
:is-aksi="false" :is-aksi="false"
/> />
</div> </div>

View File

@ -275,7 +275,7 @@ const handleSubmit = async () => {
} catch (error) { } catch (error) {
console.error("Failed to update tindakan:", error); console.error("Failed to update tindakan:", error);
submitError.value = submitError.value =
(error as ApiError)?.message || "Gagal memperbarui data tindakan."; (error as ApiError)?.message || "Failed to update action data.";
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
} }
@ -312,7 +312,7 @@ onMounted(async () => {
fetchLogData(), fetchLogData(),
fetchVisitOptions(""), fetchVisitOptions(""),
]); ]);
document.title = `Edit Pemberian Tindakan - ID ${routeId.value}`; document.title = `Edit Doctor Action - ID ${routeId.value}`;
}); });
watch( watch(
@ -342,8 +342,8 @@ watch(
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Pemberian Tindakan" title="Doctor Actions"
:subtitle="`Update Pemberian Tindakan ${routeId}`" :subtitle="`Update Doctor Action ${routeId}`"
/> />
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
@ -352,13 +352,13 @@ watch(
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/pemberian-tindakan"> <RouterLink to="/pemberian-tindakan">
Pemberian Tindakan Doctor Actions
</RouterLink> </RouterLink>
</li> </li>
<li>Update Pemberian Tindakan {{ routeId }}</li> <li>Update Doctor Action {{ routeId }}</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Update Pemberian Tindakan</h5> <h5 class="font-bold">Update Doctor Action</h5>
<div v-if="isSuccess" role="alert" class="alert alert-success"> <div v-if="isSuccess" role="alert" class="alert alert-success">
<svg <svg
@ -374,7 +374,7 @@ watch(
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Berhasil memperbarui data!</span> <span>Successfully updated data!</span>
</div> </div>
<div v-if="submitError" role="alert" class="alert alert-error"> <div v-if="submitError" role="alert" class="alert alert-error">
@ -399,7 +399,7 @@ watch(
v-if="isInitialLoading" v-if="isInitialLoading"
> >
<span class="loading loading-ring loading-xl"></span> <span class="loading loading-ring loading-xl"></span>
<h5>Memuat data tindakan...</h5> <h5>Loading action data...</h5>
</div> </div>
<form v-else @submit.prevent="handleSubmit" class="text-sm"> <form v-else @submit.prevent="handleSubmit" class="text-sm">
@ -412,9 +412,9 @@ watch(
/> />
<FieldInput <FieldInput
class="mt-4" class="mt-4"
label="ID Visit" label="Visit ID"
type="text" type="text"
placeholder="Masukkan ID visit" placeholder="Enter visit ID"
v-model="data.id_visit" v-model="data.id_visit"
:error="errors.id_visit || null" :error="errors.id_visit || null"
list="visit-options" list="visit-options"
@ -430,16 +430,16 @@ watch(
</datalist> </datalist>
<FieldInput <FieldInput
class="mt-4" class="mt-4"
label="Tindakan" label="Doctor Action"
type="text" type="text"
placeholder="Masukkan nama tindakan" placeholder="Enter doctor action name"
v-model="data.tindakan" v-model="data.tindakan"
:error="errors.tindakan || null" :error="errors.tindakan || null"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Kategori Tindakan" placeholder="Enter doctor action category"
label="Kategori Tindakan" label="Doctor Action Category"
list="kategori_tindakan" list="kategori_tindakan"
v-model="data.kategori_tindakan" v-model="data.kategori_tindakan"
:error="errors.kategori_tindakan || null" :error="errors.kategori_tindakan || null"
@ -455,8 +455,8 @@ watch(
</datalist> </datalist>
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Kelompok Tindakan" placeholder="Enter doctor action group"
label="Kelompok Tindakan" label="Doctor Action Group"
list="kelompok_tindakan" list="kelompok_tindakan"
v-model="data.kelompok_tindakan" v-model="data.kelompok_tindakan"
:error="errors.kelompok_tindakan || null" :error="errors.kelompok_tindakan || null"
@ -475,14 +475,14 @@ watch(
> >
<ButtonDark <ButtonDark
type="submit" type="submit"
:text="isSubmitting ? 'Menyimpan...' : 'Simpan Perubahan'" :text="isSubmitting ? 'Saving...' : 'Save Changes'"
:isLoading="isSubmitting" :isLoading="isSubmitting"
/> />
</div> </div>
</form> </form>
<hr /> <hr />
<h5 class="font-bold">Log Perubahan</h5> <h5 class="font-bold">Change Log</h5>
<div role="alert" class="alert alert-error" v-if="isTampered"> <div role="alert" class="alert alert-error" v-if="isTampered">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -497,14 +497,14 @@ watch(
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Peringatan! Manipulasi Data Terdeteksi.</span> <span>Warning! Data Manipulation Detected.</span>
</div> </div>
<DataTable <DataTable
:data="dataLog" :data="dataLog"
:columns="LOG_TABLE_COLUMNS" :columns="LOG_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Belum terdapat log perubahan" empty-message="No change logs available yet"
:is-aksi="false" :is-aksi="false"
/> />
</div> </div>

View File

@ -72,25 +72,12 @@ const updateQueryParams = () => {
}; };
const fetchData = async () => { const fetchData = async () => {
console.log("Fetch Data Params :", {
take: pagination.pageSize.value.toString(),
page: pagination.page.value.toString(),
orderBy: sortBy.value,
order: sortOrder.value,
...(searchIdVisit.value && { id_visit: searchIdVisit.value }),
...(filter.value.tindakan && { tindakan: filter.value.tindakan }),
...(filter.value.kategori.length > 0
? { kategori: filter.value.kategori.join(",") }
: {}),
...(filter.value.kelompok.length > 0
? { kelompok: filter.value.kelompok.join(",") }
: {}),
});
try { try {
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
take: pagination.pageSize.value.toString(), take: pagination.pageSize.value.toString(),
page: pagination.page.value.toString(), page: pagination.page.value.toString(),
orderBy: sortBy.value, orderBy: sortBy.value,
order: sortOrder.value,
...(searchIdVisit.value && { id_visit: searchIdVisit.value }), ...(searchIdVisit.value && { id_visit: searchIdVisit.value }),
...(filter.value.tindakan && { tindakan: filter.value.tindakan }), ...(filter.value.tindakan && { tindakan: filter.value.tindakan }),
...(filter.value.kategori.length > 0 ...(filter.value.kategori.length > 0
@ -166,7 +153,10 @@ const handleSortChange = (newSortBy: string) => {
}; };
const toggleSortOrder = () => { const toggleSortOrder = () => {
console.log(sortOrder.value);
sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc"; sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc";
pagination.reset();
fetchData();
}; };
const handlePageSizeChange = (newSize: number) => { const handlePageSizeChange = (newSize: number) => {
@ -191,7 +181,7 @@ const handleDelete = async (item: TindakanDokter) => {
await fetchData(); await fetchData();
} catch (error) { } catch (error) {
console.error("Error deleting tindakan:", error); console.error("Error deleting tindakan:", error);
alert("Gagal menghapus data tindakan"); alert("Failed to delete action data");
} }
}; };
@ -226,7 +216,7 @@ onMounted(async () => {
} }
await fetchData(); await fetchData();
document.title = "Tindakan Dokter - Hospital Log"; document.title = "Doctors Actions - Hospital Log";
}); });
</script> </script>
@ -235,8 +225,8 @@ onMounted(async () => {
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Pemberian Tindakan" title="Doctors Actions"
subtitle="Manajemen Pemberian Tindakan" subtitle="Doctors Actions Management"
/> />
<div <div
class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2" class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2"
@ -250,19 +240,19 @@ onMounted(async () => {
<div class="collapse-content text-sm flex flex-col"> <div class="collapse-content text-sm flex flex-col">
<div class="flex justify-between gap-4"> <div class="flex justify-between gap-4">
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<label for="tindakan" class="font-bold">Tindakan</label> <label for="tindakan" class="font-bold">Action</label>
<input <input
class="mt-1 border border-gray-200 bg-gray-50 px-3 py-2 rounded-md shadow-sm focus:border-gray-300 focus:bg-gray-100 focus:outline-0 inset-shadow-gray-300 focus:inset-shadow-xs/80" class="mt-1 border border-gray-200 bg-gray-50 px-3 py-2 rounded-md shadow-sm focus:border-gray-300 focus:bg-gray-100 focus:outline-0 inset-shadow-gray-300 focus:inset-shadow-xs/80"
type="text" type="text"
name="tindakan" name="tindakan"
id="tindakan" id="tindakan"
placeholder="Masukkan Tindakan" placeholder="Enter Action"
v-model="filter.tindakan" v-model="filter.tindakan"
/> />
</div> </div>
<div class="h-full flex-1"> <div class="h-full flex-1">
<label for="kelompok_tindakan" class="font-bold" <label for="kelompok_tindakan" class="font-bold"
>Kelompok tindakan</label >Action Group</label
> >
<form class="mt-2 flex flex-wrap gap-1 items-center"> <form class="mt-2 flex flex-wrap gap-1 items-center">
<input <input
@ -287,7 +277,7 @@ onMounted(async () => {
</div> </div>
<div class="h-full mt-4"> <div class="h-full mt-4">
<label for="kategori_tindakan" class="font-bold" <label for="kategori_tindakan" class="font-bold"
>Kategori Tindakan</label >Action Category</label
> >
<form class="mt-1 flex flex-wrap gap-1"> <form class="mt-1 flex flex-wrap gap-1">
<input <input
@ -322,7 +312,7 @@ onMounted(async () => {
@click="handleApplyFilter" @click="handleApplyFilter"
class="btn btn-sm bg-dark hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50" class="btn btn-sm bg-dark hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50"
> >
Terapkan Apply
</button> </button>
</div> </div>
</div> </div>
@ -334,14 +324,14 @@ onMounted(async () => {
<div class="flex flex-col w-full gap-2 md:flex-row md:items-center"> <div class="flex flex-col w-full gap-2 md:flex-row md:items-center">
<SearchInput <SearchInput
v-model="searchIdVisit" v-model="searchIdVisit"
placeholder="Cari berdasarkan ID Visit" placeholder="Search by Visit ID"
@search="handleSearch" @search="handleSearch"
/> />
<div class="flex items-center gap-2 md:ml-4"> <div class="flex items-center gap-2 md:ml-4">
<SortDropdown <SortDropdown
v-model="sortBy" v-model="sortBy"
:options="SORT_OPTIONS.TINDAKAN" :options="SORT_OPTIONS.TINDAKAN"
label="Urut berdasarkan:" label="Sort by:"
@change="handleSortChange" @change="handleSortChange"
/> />
<button <button
@ -369,7 +359,7 @@ onMounted(async () => {
to="/pemberian-tindakan/add" to="/pemberian-tindakan/add"
class="btn btn-sm bg-dark text-light hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50 self-start md:self-auto" class="btn btn-sm bg-dark text-light hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50 self-start md:self-auto"
> >
Tambah Pemberian Tindakan Add Doctors Action
</RouterLink> </RouterLink>
</div> </div>
@ -391,9 +381,7 @@ onMounted(async () => {
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span <span>Action data successfully sent for deletion validation</span>
>Data tindakan berhasil dikirim untuk validasi penghapusan</span
>
</div> </div>
<!-- Data Table --> <!-- Data Table -->
@ -401,7 +389,7 @@ onMounted(async () => {
:data="data" :data="data"
:columns="TINDAKAN_TABLE_COLUMNS" :columns="TINDAKAN_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Tidak ada data tindakan" empty-message="No action data available"
@details="handleDetails" @details="handleDetails"
@update="handleUpdate" @update="handleUpdate"
@delete="handleDelete" @delete="handleDelete"

View File

@ -3,7 +3,6 @@ import Sidebar from "../../../components/dashboard/Sidebar.vue";
import Footer from "../../../components/dashboard/Footer.vue"; import Footer from "../../../components/dashboard/Footer.vue";
import PageHeader from "../../../components/dashboard/PageHeader.vue"; import PageHeader from "../../../components/dashboard/PageHeader.vue";
import { onMounted, ref, watch } from "vue"; import { onMounted, ref, watch } from "vue";
import { useRoute } from "vue-router";
import FieldInput from "../../../components/dashboard/FieldInput.vue"; import FieldInput from "../../../components/dashboard/FieldInput.vue";
import { FILTER } from "../../../constants/pagination"; import { FILTER } from "../../../constants/pagination";
import { import {
@ -12,7 +11,6 @@ import {
} from "../../../validation/rekamMedis"; } from "../../../validation/rekamMedis";
import { useApi } from "../../../composables/useApi"; import { useApi } from "../../../composables/useApi";
const route = useRoute();
const api = useApi(); const api = useApi();
const data = ref<{ const data = ref<{
@ -233,7 +231,7 @@ watch(
); );
onMounted(() => { onMounted(() => {
document.title = "Tambah Rekam Medis - Hospital Log"; document.title = "Add Medical Record - Hospital Log";
}); });
</script> </script>
@ -242,8 +240,8 @@ onMounted(() => {
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Rekam Medis" title="Medical Record"
:subtitle="`Detail Rekam Medis ${route.params.id}`" :subtitle="`Create New Medical Record`"
/> />
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
@ -251,12 +249,12 @@ onMounted(() => {
<div class="breadcrumbs text-sm"> <div class="breadcrumbs text-sm">
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/rekam-medis">Rekam Medis</RouterLink> <RouterLink to="/rekam-medis">Medical Record</RouterLink>
</li> </li>
<li>Tambah Rekam Medis</li> <li>Add Medical Record</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Tambah Rekam Medis</h5> <h5 class="font-bold">Add Medical Record</h5>
<div v-if="isSuccess" role="alert" class="alert alert-success"> <div v-if="isSuccess" role="alert" class="alert alert-success">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -271,14 +269,14 @@ onMounted(() => {
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Berhasil menambahkan rekam medis!</span> <span>Successfully added medical record!</span>
</div> </div>
<div <div
class="flex h-screen flex-col justify-center gap-y-2 items-center" class="flex h-screen flex-col justify-center gap-y-2 items-center"
v-if="isLoading" v-if="isLoading"
> >
<span class="loading loading-ring loading-xl"></span> <span class="loading loading-ring loading-xl"></span>
<h5>Mohon tunggu, sedang menambahkan rekam medis...</h5> <h5>Please wait, adding medical record...</h5>
</div> </div>
<form <form
@submit.prevent="handleSubmit" @submit.prevent="handleSubmit"
@ -294,28 +292,28 @@ onMounted(() => {
<div <div
class="collapse-title font-semibold text-base text-dark" class="collapse-title font-semibold text-base text-dark"
> >
Identitas Pasien Patient Identity
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<FieldInput <FieldInput
placeholder="Masukkan No. Rekam Medis" placeholder="Enter Medical Record Number"
label="No Rekam Medis" label="Medical Record Number"
v-model="data.no_rm" v-model="data.no_rm"
:error="errors.no_rm" :error="errors.no_rm"
:readonly="false" :readonly="false"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Nama Pasien" placeholder="Enter Patient Name"
label="Nama Pasien" label="Patient Name"
v-model="data.nama_pasien" v-model="data.nama_pasien"
:error="errors.nama_pasien" :error="errors.nama_pasien"
:readonly="false" :readonly="false"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Umur" placeholder="Enter Age"
label="Umur" label="Age"
type="text" type="text"
v-model="data.umur" v-model="data.umur"
:error="errors.umur" :error="errors.umur"
@ -323,7 +321,7 @@ onMounted(() => {
/> />
<div class="flex flex-col mt-2"> <div class="flex flex-col mt-2">
<label for="jenis_kelamin" class="font-bold" <label for="jenis_kelamin" class="font-bold"
>Jenis Kelamin</label >Gender</label
> >
<select <select
v-model="data.jenis_kelamin" v-model="data.jenis_kelamin"
@ -336,10 +334,10 @@ onMounted(() => {
]" ]"
> >
<option disabled selected value="initial"> <option disabled selected value="initial">
Pilih Jenis Kelamin Select Gender
</option> </option>
<option value="laki-laki">Laki-laki</option> <option value="laki-laki">Male</option>
<option value="perempuan">Perempuan</option> <option value="perempuan">Female</option>
</select> </select>
<p <p
v-if="errors.jenis_kelamin" v-if="errors.jenis_kelamin"
@ -350,7 +348,7 @@ onMounted(() => {
</div> </div>
<div class="flex flex-col mt-2"> <div class="flex flex-col mt-2">
<label for="golongan_darah" class="font-bold" <label for="golongan_darah" class="font-bold"
>Golongan Darah</label >Blood Type</label
> >
<div <div
class="filter mt-1 flex flex-wrap justify-start items-center" class="filter mt-1 flex flex-wrap justify-start items-center"
@ -382,16 +380,16 @@ onMounted(() => {
</div> </div>
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Pekerjaan" placeholder="Enter Occupation (e.g., Teacher, Engineer, Student)"
label="Pekerjaan" label="Occupation"
v-model="data.pekerjaan" v-model="data.pekerjaan"
:error="errors.pekerjaan" :error="errors.pekerjaan"
:readonly="false" :readonly="false"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Suku" placeholder="Enter Ethnicity"
label="Suku" label="Ethnicity"
v-model="data.suku" v-model="data.suku"
:error="errors.suku" :error="errors.suku"
:readonly="false" :readonly="false"
@ -409,21 +407,21 @@ onMounted(() => {
<div <div
class="collapse-title font-semibold text-base text-dark" class="collapse-title font-semibold text-base text-dark"
> >
Tanda-Tanda Vital Vital Signs
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<FieldInput <FieldInput
placeholder="Masukkan Nadi (bpm)" placeholder="Enter Pulse (bpm)"
label="Nadi (bpm)" label="Pulse (bpm)"
type="text" type="text"
v-model="data.nadi" v-model="data.nadi"
:error="errors.nadi" :error="errors.nadi"
:readonly="false" :readonly="false"
/> />
<FieldInput <FieldInput
placeholder="Masukkan Nafas (per menit)" placeholder="Enter Respiration (per minute)"
label="Nafas (per menit)" label="Respiration (per minute)"
type="text" type="text"
v-model="data.nafas" v-model="data.nafas"
:error="errors.nafas" :error="errors.nafas"
@ -431,8 +429,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Sistolik (mmHg)" placeholder="Enter Systolic Blood Pressure (mmHg)"
label="Tekanan Darah Sistolik (mmHg)" label="Systolic Blood Pressure (mmHg)"
type="text" type="text"
v-model="data.sistolik" v-model="data.sistolik"
:error="errors.sistolik" :error="errors.sistolik"
@ -440,8 +438,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Diastolik (mmHg)" placeholder="Enter Diastolic Blood Pressure (mmHg)"
label="Tekanan Darah Diastolik (mmHg)" label="Diastolic Blood Pressure (mmHg)"
type="text" type="text"
v-model="data.diastolik" v-model="data.diastolik"
:error="errors.diastolik" :error="errors.diastolik"
@ -449,8 +447,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Suhu (°C)" placeholder="Enter Body Temperature (°C)"
label="Suhu Tubuh (°C)" label="Body Temperature (°C)"
type="text" type="text"
v-model="data.suhu" v-model="data.suhu"
:error="errors.suhu" :error="errors.suhu"
@ -458,8 +456,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Tinggi Badan (cm)" placeholder="Enter Height (cm)"
label="Tinggi Badan (cm)" label="Height (cm)"
type="text" type="text"
v-model="data.tinggi_badan" v-model="data.tinggi_badan"
:error="errors.tinggi_badan" :error="errors.tinggi_badan"
@ -467,8 +465,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Berat Badan (kg)" placeholder="Enter Weight (kg)"
label="Berat Badan (kg)" label="Weight (kg)"
type="text" type="text"
v-model="data.berat_badan" v-model="data.berat_badan"
:error="errors.berat_badan" :error="errors.berat_badan"
@ -486,7 +484,7 @@ onMounted(() => {
> >
<input type="checkbox" checked /> <input type="checkbox" checked />
<div class="collapse-title font-semibold text-base text-dark"> <div class="collapse-title font-semibold text-base text-dark">
Informasi Medis Medical Information
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<div class="flex flex-col"> <div class="flex flex-col">
@ -494,7 +492,7 @@ onMounted(() => {
<textarea <textarea
id="anamnese" id="anamnese"
v-model="data.anamnese" v-model="data.anamnese"
placeholder="Masukkan Anamnese" placeholder="Enter Anamnese"
:class="[ :class="[
'textarea bg-white mt-1 min-h-[100px] border', 'textarea bg-white mt-1 min-h-[100px] border',
errors.anamnese errors.anamnese
@ -509,18 +507,18 @@ onMounted(() => {
</div> </div>
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Kode Diagnosa" placeholder="Enter Diagnosis Code"
label="Kode Diagnosa" label="Diagnosis Code"
v-model="data.kode_diagnosa" v-model="data.kode_diagnosa"
:error="errors.kode_diagnosa" :error="errors.kode_diagnosa"
:readonly="false" :readonly="false"
/> />
<div class="flex flex-col mt-2"> <div class="flex flex-col mt-2">
<label for="diagnosa" class="font-bold">Diagnosa</label> <label for="diagnosa" class="font-bold">Diagnosis</label>
<textarea <textarea
id="diagnosa" id="diagnosa"
v-model="data.diagnosa" v-model="data.diagnosa"
placeholder="Masukkan Diagnosa" placeholder="Enter Diagnosis"
:class="[ :class="[
'textarea bg-white mt-1 min-h-[100px] border', 'textarea bg-white mt-1 min-h-[100px] border',
errors.diagnosa errors.diagnosa
@ -535,8 +533,8 @@ onMounted(() => {
</div> </div>
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Jenis Kasus" placeholder="Enter Case Type"
label="Jenis Kasus" label="Case Type"
v-model="data.jenis_kasus" v-model="data.jenis_kasus"
:error="errors.jenis_kasus" :error="errors.jenis_kasus"
:readonly="false" :readonly="false"
@ -551,12 +549,12 @@ onMounted(() => {
> >
<input type="checkbox" checked /> <input type="checkbox" checked />
<div class="collapse-title font-semibold text-base text-dark"> <div class="collapse-title font-semibold text-base text-dark">
Tindak Lanjut Follow-up
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<FieldInput <FieldInput
placeholder="Masukkan Tindak Lanjut" placeholder="Enter Follow-up"
label="Tindak Lanjut" label="Follow-up"
list="tindak_lanjut" list="tindak_lanjut"
v-model="data.tindak_lanjut" v-model="data.tindak_lanjut"
:error="errors.tindak_lanjut" :error="errors.tindak_lanjut"
@ -588,7 +586,7 @@ onMounted(() => {
isLoading ? 'loading loading-spinner loading-xs' : '' isLoading ? 'loading loading-spinner loading-xs' : ''
" "
></span> ></span>
{{ isLoading ? "Menyimpan..." : "Simpan Rekam Medis" }} {{ isLoading ? "Saving..." : "Save Medical Record" }}
</button> </button>
</div> </div>
</form> </form>

View File

@ -98,7 +98,7 @@ const fetchLogData = async () => {
onMounted(async () => { onMounted(async () => {
await fetchData(); await fetchData();
await fetchLogData(); await fetchLogData();
document.title = "Rekam Medis Detail - Hospital Log"; document.title = "Medical Record Detail - Hospital Log";
}); });
</script> </script>
@ -107,8 +107,8 @@ onMounted(async () => {
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Rekam Medis" title="Medical Records"
:subtitle="`Detail Rekam Medis ${route.params.id}`" :subtitle="`Medical Record Detail ${route.params.id}`"
/> />
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
@ -116,64 +116,60 @@ onMounted(async () => {
<div class="breadcrumbs text-sm"> <div class="breadcrumbs text-sm">
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/rekam-medis">Rekam Medis</RouterLink> <RouterLink to="/rekam-medis">Medical Records</RouterLink>
</li> </li>
<li>Detail Rekam Medis {{ route.params.id }}</li> <li>Medical Record {{ route.params.id }} Detail</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Detail Rekam Medis</h5> <h5 class="font-bold">Medical Record Detail</h5>
<div class="text-sm"> <div class="text-sm">
<p>ID Visit: {{ data?.id_visit }}</p> <p>Visit ID: {{ data?.id_visit }}</p>
<p>Waktu Visit: {{ data?.waktu_visit }}</p> <p>Visit Time: {{ data?.waktu_visit }}</p>
<p>Nomor Rekam Medis: {{ data?.no_rm }}</p> <p>Medical Record Number: {{ data?.no_rm }}</p>
<p>Nama Pasien: {{ data?.nama_pasien }}</p> <p>Patient Name: {{ data?.nama_pasien }}</p>
<p>Umur: {{ data?.umur }} tahun</p> <p>Age: {{ data?.umur }} years</p>
<p> <p>
Jenis Kelamin: Gender:
{{ data?.jenis_kelamin == "L" ? "Laki-laki" : "Perempuan" }} {{ data?.jenis_kelamin == "L" ? "Male" : "Female" }}
</p> </p>
<p> <p>
Golongan Darah: Blood Type:
{{ {{ data?.gol_darah !== "-" ? data?.gol_darah : "Unknown" }}
data?.gol_darah !== "-" ? data?.gol_darah : "Belum Diketahui"
}}
</p> </p>
<p> <p>
Pekerjaan: Occupation:
{{ data?.pekerjaan ? data?.pekerjaan : "-" }} {{ data?.pekerjaan ? data?.pekerjaan : "-" }}
</p> </p>
<p> <p>
Suku: Ethnicity:
{{ data?.suku ? data?.suku : "-" }} {{ data?.suku ? data?.suku : "-" }}
</p> </p>
<p>Anamnese: {{ data?.anamnese ? data?.anamnese : "-" }}</p> <p>Anamnesis: {{ data?.anamnese ? data?.anamnese : "-" }}</p>
<p> <p>
Kode Diagnosa: Diagnosis Code:
{{ data?.kode_diagnosa ? data?.kode_diagnosa : "-" }} {{ data?.kode_diagnosa ? data?.kode_diagnosa : "-" }}
</p> </p>
<p>Diagnosa: {{ data?.diagnosa ? data?.diagnosa : "-" }}</p> <p>Diagnosis: {{ data?.diagnosa ? data?.diagnosa : "-" }}</p>
<p>Diastolik: {{ data?.diastolik ? data?.diastolik : "-" }}</p> <p>Diastolic: {{ data?.diastolik ? data?.diastolik : "-" }}</p>
<p>Sistolik: {{ data?.sistolik ? data?.sistolik : "-" }}</p> <p>Systolic: {{ data?.sistolik ? data?.sistolik : "-" }}</p>
<p>Nadi: {{ data?.nadi ? data?.nadi : "-" }}</p> <p>Pulse: {{ data?.nadi ? data?.nadi : "-" }}</p>
<p>Nafas: {{ data?.nafas ? data?.nafas : "-" }}</p> <p>Respiration: {{ data?.nafas ? data?.nafas : "-" }}</p>
<p>Suhu: {{ data?.suhu ? data?.suhu : "-" }}</p> <p>Temperature: {{ data?.suhu ? data?.suhu : "-" }}</p>
<p> <p>
Tinggi Badan: Height:
{{ data?.tinggi_badan ? data?.tinggi_badan : "-" }} {{ data?.tinggi_badan ? data?.tinggi_badan : "-" }}
</p> </p>
<p>Weight: {{ data?.berat_badan ? data?.berat_badan : "-" }}</p>
<p> <p>
Berat Badan: {{ data?.berat_badan ? data?.berat_badan : "-" }} Case Type: {{ data?.jenis_kasus ? data?.jenis_kasus : "-" }}
</p> </p>
<p> <p>
Jenis Kasus: {{ data?.jenis_kasus ? data?.jenis_kasus : "-" }} Follow-up Action:
</p>
<p>
Tindak Lanjut:
{{ data?.tindak_lanjut ? data?.tindak_lanjut : "-" }} {{ data?.tindak_lanjut ? data?.tindak_lanjut : "-" }}
</p> </p>
</div> </div>
<hr /> <hr />
<h5 class="font-bold">Log Perubahan</h5> <h5 class="font-bold">Change Log</h5>
<div role="alert" class="alert alert-error" v-if="isTampered"> <div role="alert" class="alert alert-error" v-if="isTampered">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -188,14 +184,14 @@ onMounted(async () => {
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Peringatan! Manipulasi Data Terdeteksi.</span> <span>Warning! Data tampering detected.</span>
</div> </div>
<DataTable <DataTable
:data="dataLog" :data="dataLog"
:columns="LOG_TABLE_COLUMNS" :columns="LOG_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Belum terdapat log perubahan" empty-message="No change logs yet"
:is-aksi="false" :is-aksi="false"
/> />
</div> </div>

View File

@ -283,7 +283,7 @@ const handleDelete = async (item: RekamMedis) => {
await fetchData(); await fetchData();
} catch (error) { } catch (error) {
console.error("Error deleting rekam medis:", error); console.error("Error deleting rekam medis:", error);
alert("Gagal menghapus data rekam medis"); alert("Failed to delete medical record");
} }
}; };
@ -359,7 +359,10 @@ onBeforeUnmount(() => {
<div class="bg-light w-full text-dark"> <div class="bg-light w-full text-dark">
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader title="Rekam Medis" subtitle="Manajemen Rekam Medis" /> <PageHeader
title="Medical Records"
subtitle="Medical Records Management"
/>
<div <div
class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2" class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2"
> >
@ -372,7 +375,7 @@ onBeforeUnmount(() => {
<div class="collapse-content text-sm flex flex-col"> <div class="collapse-content text-sm flex flex-col">
<div class="flex gap-x-4 items-end"> <div class="flex gap-x-4 items-end">
<div> <div>
<label for="id_visit" class="font-bold">ID Visit</label> <label for="id_visit" class="font-bold">Visit ID</label>
<input <input
class="mt-1 border border-gray-200 bg-gray-50 px-3 py-2 rounded-md shadow-sm focus:border-gray-300 focus:bg-gray-100 focus:outline-0 inset-shadow-gray-300 focus:inset-shadow-xs/80" class="mt-1 border border-gray-200 bg-gray-50 px-3 py-2 rounded-md shadow-sm focus:border-gray-300 focus:bg-gray-100 focus:outline-0 inset-shadow-gray-300 focus:inset-shadow-xs/80"
type="text" type="text"
@ -383,21 +386,19 @@ onBeforeUnmount(() => {
/> />
</div> </div>
<div> <div>
<label for="nama_pasien" class="font-bold">Nama Pasien</label> <label for="nama_pasien" class="font-bold">Patient Name</label>
<input <input
class="mt-1 border border-gray-200 bg-gray-50 px-3 py-2 rounded-md shadow-sm focus:border-gray-300 focus:bg-gray-100 focus:outline-0 inset-shadow-gray-300 focus:inset-shadow-xs/80" class="mt-1 border border-gray-200 bg-gray-50 px-3 py-2 rounded-md shadow-sm focus:border-gray-300 focus:bg-gray-100 focus:outline-0 inset-shadow-gray-300 focus:inset-shadow-xs/80"
type="text" type="text"
name="nama_pasien" name="nama_pasien"
id="nama_pasien" id="nama_pasien"
placeholder="Masukkan Nama Pasien" placeholder="Enter Patient Name"
v-model="filter.nama_pasien" v-model="filter.nama_pasien"
/> />
</div> </div>
<div class="w-6/12"> <div class="w-6/12">
<label for="range_tanggal" class="font-bold" <label for="range_tanggal" class="font-bold">Date Range</label>
>Rentang Tanggal</label
>
<button <button
popoverTarget="cally-popover1" popoverTarget="cally-popover1"
:class="[ :class="[
@ -413,10 +414,10 @@ onBeforeUnmount(() => {
filter.rentang_tanggal.start && filter.rentang_tanggal.end filter.rentang_tanggal.start && filter.rentang_tanggal.end
? `${new Date( ? `${new Date(
filter.rentang_tanggal.start filter.rentang_tanggal.start
).toLocaleDateString("id-ID")} - ${new Date( ).toLocaleDateString("en-US")} - ${new Date(
filter.rentang_tanggal.end filter.rentang_tanggal.end
).toLocaleDateString("id-ID")}` ).toLocaleDateString("en-US")}`
: "Pilih rentang tanggal" : "Select date range"
}} }}
</button> </button>
</div> </div>
@ -455,33 +456,29 @@ onBeforeUnmount(() => {
</div> </div>
<div class="w-full px-2 h-full"> <div class="w-full px-2 h-full">
<label class="font-bold block mb-4" <label class="font-bold block mb-4"
>Rentang Umur: {{ filter.rentang_umur[0] }} - >Age Range: {{ filter.rentang_umur[0] }} -
{{ filter.rentang_umur[1] }} tahun</label {{ filter.rentang_umur[1] }} years</label
> >
<div ref="ageSliderRef" class="mb-4"></div> <div ref="ageSliderRef" class="mb-4"></div>
</div> </div>
</div> </div>
<div class="flex gap-x-4 items-end mt-4"> <div class="flex gap-x-4 items-end mt-4">
<div class="h-full"> <div class="h-full">
<label for="jenis_kelamin" class="font-bold" <label for="jenis_kelamin" class="font-bold">Gender</label>
>Jenis Kelamin</label
>
<select <select
v-model="filter.jenis_kelamin" v-model="filter.jenis_kelamin"
class="select bg-white border border-gray-300 mt-1" class="select bg-white border border-gray-300 mt-1"
> >
<option disabled selected value="initial"> <option disabled selected value="initial">
Pilih Jenis Kelamin Select Gender
</option> </option>
<option value="laki-laki">Laki-laki</option> <option value="laki-laki">Male</option>
<option value="perempuan">Perempuan</option> <option value="perempuan">Female</option>
<option value="semua">Semua</option> <option value="semua">All</option>
</select> </select>
</div> </div>
<div class="h-full"> <div class="h-full">
<label for="golongan_darah" class="font-bold" <label for="golongan_darah" class="font-bold">Blood Type</label>
>Golongan Darah</label
>
<form <form
class="mt-1 flex flex-wrap gap-1 justify-center items-center" class="mt-1 flex flex-wrap gap-1 justify-center items-center"
> >
@ -507,21 +504,21 @@ onBeforeUnmount(() => {
<div class="flex flex-col"> <div class="flex flex-col">
<label for="kode_diagnosa" class="font-bold" <label for="kode_diagnosa" class="font-bold"
>Kode Diagnosa</label >Diagnosis Code</label
> >
<input <input
class="mt-1 border border-gray-200 bg-gray-50 px-3 py-2 rounded-md shadow-sm focus:border-gray-300 focus:bg-gray-100 focus:outline-0 inset-shadow-gray-300 focus:inset-shadow-xs/80" class="mt-1 border border-gray-200 bg-gray-50 px-3 py-2 rounded-md shadow-sm focus:border-gray-300 focus:bg-gray-100 focus:outline-0 inset-shadow-gray-300 focus:inset-shadow-xs/80"
type="text" type="text"
name="kode_diagnosa" name="kode_diagnosa"
id="kode_diagnosa" id="kode_diagnosa"
placeholder="Masukkan Kode Diagnosa" placeholder="Enter Diagnosis Code"
v-model="filter.kode_diagnosa" v-model="filter.kode_diagnosa"
/> />
</div> </div>
</div> </div>
<fieldset class="fieldset mt-4"> <fieldset class="fieldset mt-4">
<label for="tindak_lanjut" class="font-bold text-sm" <label for="tindak_lanjut" class="font-bold text-sm"
>Tindak Lanjut</label >Follow-up Action</label
> >
<form class="flex flex-wrap gap-2 items-center"> <form class="flex flex-wrap gap-2 items-center">
<input <input
@ -555,7 +552,7 @@ onBeforeUnmount(() => {
@click="handleApplyFilter" @click="handleApplyFilter"
class="btn btn-sm bg-dark hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50" class="btn btn-sm bg-dark hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50"
> >
Terapkan Apply Filter
</button> </button>
</div> </div>
</div> </div>
@ -567,14 +564,14 @@ onBeforeUnmount(() => {
<div class="flex flex-col w-full gap-2 md:flex-row md:items-center"> <div class="flex flex-col w-full gap-2 md:flex-row md:items-center">
<SearchInput <SearchInput
v-model="searchRekamMedis" v-model="searchRekamMedis"
placeholder="Cari berdasarkan Nomor Rekam Medis" placeholder="Search by Medical Record Number"
@search="handleSearch" @search="handleSearch"
/> />
<div class="flex items-center gap-2 md:ml-4"> <div class="flex items-center gap-2 md:ml-4">
<SortDropdown <SortDropdown
v-model="sortBy" v-model="sortBy"
:options="SORT_OPTIONS.REKAM_MEDIS" :options="SORT_OPTIONS.REKAM_MEDIS"
label="Urut berdasarkan:" label="Sort by:"
@change="handleSortChange" @change="handleSortChange"
/> />
<button <button
@ -602,7 +599,7 @@ onBeforeUnmount(() => {
to="/rekam-medis/tambah" to="/rekam-medis/tambah"
class="btn btn-sm bg-dark text-light hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50 self-start md:self-auto" class="btn btn-sm bg-dark text-light hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50 self-start md:self-auto"
> >
Tambah Rekam Medis Add Medical Record
</RouterLink> </RouterLink>
</div> </div>
@ -625,8 +622,7 @@ onBeforeUnmount(() => {
/> />
</svg> </svg>
<span <span
>Data rekam medis berhasil dikirim untuk validasi >Medical record successfully sent for deletion validation</span
penghapusan</span
> >
</div> </div>
@ -635,7 +631,7 @@ onBeforeUnmount(() => {
:data="data" :data="data"
:columns="REKAM_MEDIS_TABLE_COLUMNS" :columns="REKAM_MEDIS_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Tidak ada data rekam medis" empty-message="No medical records available"
@details="handleDetails" @details="handleDetails"
@update="handleUpdate" @update="handleUpdate"
@delete="handleDelete" @delete="handleDelete"

View File

@ -206,7 +206,7 @@ const loadRekamMedis = async () => {
errors.value = {}; errors.value = {};
} catch (error) { } catch (error) {
console.error("Failed to fetch rekam medis:", error); console.error("Failed to fetch rekam medis:", error);
fetchError.value = "Gagal memuat data rekam medis."; fetchError.value = "Failed to load medical record.";
} finally { } finally {
isFetching.value = false; isFetching.value = false;
} }
@ -347,7 +347,7 @@ watch(
); );
onMounted(() => { onMounted(() => {
document.title = "Edit Rekam Medis - Hospital Log"; document.title = "Edit Medical Record - Hospital Log";
loadRekamMedis(); loadRekamMedis();
}); });
</script> </script>
@ -357,8 +357,8 @@ onMounted(() => {
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Rekam Medis" title="Medical Record"
:subtitle="`Edit Rekam Medis ${route.params.id}`" :subtitle="`Edit Medical Record ${route.params.id}`"
/> />
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
@ -366,19 +366,19 @@ onMounted(() => {
<div class="breadcrumbs text-sm"> <div class="breadcrumbs text-sm">
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/rekam-medis">Rekam Medis</RouterLink> <RouterLink to="/rekam-medis">Medical Records</RouterLink>
</li> </li>
<li>Edit Rekam Medis</li> <li>Edit Medical Record</li>
</ul> </ul>
</div> </div>
<h5 class="font-bold">Edit Rekam Medis</h5> <h5 class="font-bold">Edit Medical Record</h5>
<div <div
class="flex h-screen flex-col justify-center gap-y-2 items-center" class="flex h-screen flex-col justify-center gap-y-2 items-center"
v-if="isFetching" v-if="isFetching"
> >
<span class="loading loading-ring loading-xl"></span> <span class="loading loading-ring loading-xl"></span>
<h5>Mohon tunggu, sedang memuat rekam medis...</h5> <h5>Please wait, loading medical record...</h5>
</div> </div>
<div v-else> <div v-else>
@ -400,7 +400,7 @@ onMounted(() => {
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
<span>Berhasil memperbarui rekam medis!</span> <span>Successfully updated medical record!</span>
</div> </div>
<div <div
v-if="fetchError" v-if="fetchError"
@ -410,7 +410,7 @@ onMounted(() => {
<RouterLink <RouterLink
class="btn btn-sm bg-dark text-light" class="btn btn-sm bg-dark text-light"
to="/rekam-medis" to="/rekam-medis"
>Kembali ke Rekam Medis</RouterLink >Back to Medical Records</RouterLink
> >
</div> </div>
@ -419,7 +419,7 @@ onMounted(() => {
v-else-if="isSubmitting" v-else-if="isSubmitting"
> >
<span class="loading loading-ring loading-xl"></span> <span class="loading loading-ring loading-xl"></span>
<h5>Mohon tunggu, sedang memperbarui rekam medis...</h5> <h5>Please wait, updating medical record...</h5>
</div> </div>
<form v-else @submit.prevent="handleSubmit" class="text-sm"> <form v-else @submit.prevent="handleSubmit" class="text-sm">
@ -432,28 +432,28 @@ onMounted(() => {
<div <div
class="collapse-title font-semibold text-base text-dark" class="collapse-title font-semibold text-base text-dark"
> >
Identitas Pasien Patient Identity
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<FieldInput <FieldInput
placeholder="Masukkan No. Rekam Medis" placeholder="Enter Medical Record Number"
label="No Rekam Medis" label="Medical Record Number"
v-model="data.no_rm" v-model="data.no_rm"
:error="errors.no_rm" :error="errors.no_rm"
:readonly="false" :readonly="false"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Nama Pasien" placeholder="Enter Patient Name"
label="Nama Pasien" label="Patient Name"
v-model="data.nama_pasien" v-model="data.nama_pasien"
:error="errors.nama_pasien" :error="errors.nama_pasien"
:readonly="false" :readonly="false"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Umur" placeholder="Enter Age"
label="Umur" label="Age"
type="text" type="text"
v-model="data.umur" v-model="data.umur"
:error="errors.umur" :error="errors.umur"
@ -461,7 +461,7 @@ onMounted(() => {
/> />
<div class="flex flex-col mt-2"> <div class="flex flex-col mt-2">
<label for="jenis_kelamin" class="font-bold" <label for="jenis_kelamin" class="font-bold"
>Jenis Kelamin</label >Gender</label
> >
<select <select
v-model="data.jenis_kelamin" v-model="data.jenis_kelamin"
@ -474,10 +474,10 @@ onMounted(() => {
]" ]"
> >
<option disabled value="initial"> <option disabled value="initial">
Pilih Jenis Kelamin Select Gender
</option> </option>
<option value="laki-laki">Laki-laki</option> <option value="laki-laki">Male</option>
<option value="perempuan">Perempuan</option> <option value="perempuan">Female</option>
</select> </select>
<p <p
v-if="errors.jenis_kelamin" v-if="errors.jenis_kelamin"
@ -488,7 +488,7 @@ onMounted(() => {
</div> </div>
<div class="flex flex-col mt-2"> <div class="flex flex-col mt-2">
<label for="golongan_darah" class="font-bold" <label for="golongan_darah" class="font-bold"
>Golongan Darah</label >Blood Type</label
> >
<div <div
class="filter mt-1 flex flex-wrap justify-start items-center" class="filter mt-1 flex flex-wrap justify-start items-center"
@ -520,16 +520,16 @@ onMounted(() => {
</div> </div>
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Pekerjaan" placeholder="Enter Occupation"
label="Pekerjaan" label="Occupation"
v-model="data.pekerjaan" v-model="data.pekerjaan"
:error="errors.pekerjaan" :error="errors.pekerjaan"
:readonly="false" :readonly="false"
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Suku" placeholder="Enter Ethnicity"
label="Suku" label="Ethnicity"
v-model="data.suku" v-model="data.suku"
:error="errors.suku" :error="errors.suku"
:readonly="false" :readonly="false"
@ -546,21 +546,21 @@ onMounted(() => {
<div <div
class="collapse-title font-semibold text-base text-dark" class="collapse-title font-semibold text-base text-dark"
> >
Tanda-Tanda Vital Vital Signs
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-x-4">
<FieldInput <FieldInput
placeholder="Masukkan Nadi (bpm)" placeholder="Enter Pulse (bpm)"
label="Nadi (bpm)" label="Pulse (bpm)"
type="text" type="text"
v-model="data.nadi" v-model="data.nadi"
:error="errors.nadi" :error="errors.nadi"
:readonly="false" :readonly="false"
/> />
<FieldInput <FieldInput
placeholder="Masukkan Nafas (per menit)" placeholder="Enter Respiration (per minute)"
label="Nafas (per menit)" label="Respiration (per minute)"
type="text" type="text"
v-model="data.nafas" v-model="data.nafas"
:error="errors.nafas" :error="errors.nafas"
@ -568,8 +568,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Sistolik (mmHg)" placeholder="Enter Systolic (mmHg)"
label="Tekanan Darah Sistolik (mmHg)" label="Systolic Blood Pressure (mmHg)"
type="text" type="text"
v-model="data.sistolik" v-model="data.sistolik"
:error="errors.sistolik" :error="errors.sistolik"
@ -577,8 +577,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Diastolik (mmHg)" placeholder="Enter Diastolic (mmHg)"
label="Tekanan Darah Diastolik (mmHg)" label="Diastolic Blood Pressure (mmHg)"
type="text" type="text"
v-model="data.diastolik" v-model="data.diastolik"
:error="errors.diastolik" :error="errors.diastolik"
@ -586,8 +586,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Suhu (°C)" placeholder="Enter Temperature (°C)"
label="Suhu Tubuh (°C)" label="Body Temperature (°C)"
type="text" type="text"
v-model="data.suhu" v-model="data.suhu"
:error="errors.suhu" :error="errors.suhu"
@ -595,8 +595,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Tinggi Badan (cm)" placeholder="Enter Height (cm)"
label="Tinggi Badan (cm)" label="Height (cm)"
type="text" type="text"
v-model="data.tinggi_badan" v-model="data.tinggi_badan"
:error="errors.tinggi_badan" :error="errors.tinggi_badan"
@ -604,8 +604,8 @@ onMounted(() => {
/> />
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Berat Badan (kg)" placeholder="Enter Body Weight (kg)"
label="Berat Badan (kg)" label="Body Weight (kg)"
type="text" type="text"
v-model="data.berat_badan" v-model="data.berat_badan"
:error="errors.berat_badan" :error="errors.berat_badan"
@ -622,7 +622,7 @@ onMounted(() => {
> >
<input type="checkbox" checked /> <input type="checkbox" checked />
<div class="collapse-title font-semibold text-base text-dark"> <div class="collapse-title font-semibold text-base text-dark">
Informasi Medis Medical Information
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<div class="flex flex-col"> <div class="flex flex-col">
@ -630,7 +630,7 @@ onMounted(() => {
<textarea <textarea
id="anamnese" id="anamnese"
v-model="data.anamnese" v-model="data.anamnese"
placeholder="Masukkan Anamnese" placeholder="Enter Anamnese"
:class="[ :class="[
'textarea bg-white mt-1 min-h-[100px] border', 'textarea bg-white mt-1 min-h-[100px] border',
errors.anamnese errors.anamnese
@ -645,18 +645,18 @@ onMounted(() => {
</div> </div>
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Kode Diagnosa" placeholder="Enter Diagnosis Code"
label="Kode Diagnosa" label="Diagnosis Code"
v-model="data.kode_diagnosa" v-model="data.kode_diagnosa"
:error="errors.kode_diagnosa" :error="errors.kode_diagnosa"
:readonly="false" :readonly="false"
/> />
<div class="flex flex-col mt-2"> <div class="flex flex-col mt-2">
<label for="diagnosa" class="font-bold">Diagnosa</label> <label for="diagnosa" class="font-bold">Diagnosis</label>
<textarea <textarea
id="diagnosa" id="diagnosa"
v-model="data.diagnosa" v-model="data.diagnosa"
placeholder="Masukkan Diagnosa" placeholder="Enter Diagnosis"
:class="[ :class="[
'textarea bg-white mt-1 min-h-[100px] border', 'textarea bg-white mt-1 min-h-[100px] border',
errors.diagnosa errors.diagnosa
@ -671,8 +671,8 @@ onMounted(() => {
</div> </div>
<FieldInput <FieldInput
class="mt-2" class="mt-2"
placeholder="Masukkan Jenis Kasus" placeholder="Enter Case Type"
label="Jenis Kasus" label="Case Type"
v-model="data.jenis_kasus" v-model="data.jenis_kasus"
:error="errors.jenis_kasus" :error="errors.jenis_kasus"
:readonly="false" :readonly="false"
@ -685,12 +685,12 @@ onMounted(() => {
> >
<input type="checkbox" checked /> <input type="checkbox" checked />
<div class="collapse-title font-semibold text-base text-dark"> <div class="collapse-title font-semibold text-base text-dark">
Tindak Lanjut Follow Up
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<FieldInput <FieldInput
placeholder="Masukkan Tindak Lanjut" placeholder="Enter Follow Up"
label="Tindak Lanjut" label="Follow Up"
list="tindak_lanjut" list="tindak_lanjut"
v-model="data.tindak_lanjut" v-model="data.tindak_lanjut"
:error="errors.tindak_lanjut" :error="errors.tindak_lanjut"
@ -714,7 +714,7 @@ onMounted(() => {
class="btn btn-sm bg-dark text-light" class="btn btn-sm bg-dark text-light"
:disabled="isSubmitting" :disabled="isSubmitting"
> >
Simpan Perubahan Save Changes
</button> </button>
</div> </div>
</form> </form>

View File

@ -130,15 +130,13 @@ const handleUpdate = (item: Users) => {
}; };
const handleDelete = async (item: Users) => { const handleDelete = async (item: Users) => {
if ( if (confirm(`Are you sure you want to delete user "${item.username}"?`)) {
confirm(`Apakah Anda yakin ingin menghapus pengguna "${item.username}"?`)
) {
try { try {
await api.delete(`/obat/${item.id}`); await api.delete(`/obat/${item.id}`);
await fetchData(); await fetchData();
} catch (error) { } catch (error) {
console.error("Error deleting obat:", error); console.error("Error deleting obat:", error);
alert("Gagal menghapus data obat"); alert("Failed to delete user data");
} }
} }
}; };
@ -182,7 +180,7 @@ onMounted(async () => {
<div class="bg-light w-full text-dark"> <div class="bg-light w-full text-dark">
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader title="Users" subtitle="Manajemen Pengguna" /> <PageHeader title="Users" subtitle="User Management" />
<div class="bg-white rounded-xl shadow-md"> <div class="bg-white rounded-xl shadow-md">
<div <div
@ -191,14 +189,14 @@ onMounted(async () => {
<div class="flex flex-col w-full gap-2 md:flex-row md:items-center"> <div class="flex flex-col w-full gap-2 md:flex-row md:items-center">
<SearchInput <SearchInput
v-model="searchUsername" v-model="searchUsername"
placeholder="Cari berdasarkan username" placeholder="Search by username"
@search="handleSearch" @search="handleSearch"
/> />
<div class="flex items-center gap-2 md:ml-4"> <div class="flex items-center gap-2 md:ml-4">
<SortDropdown <SortDropdown
v-model="sortBy" v-model="sortBy"
:options="SORT_OPTIONS.USERS" :options="SORT_OPTIONS.USERS"
label="Urut berdasarkan:" label="Sort by:"
@change="handleSortChange" @change="handleSortChange"
/> />
<button <button
@ -226,7 +224,7 @@ onMounted(async () => {
to="/users/add" to="/users/add"
class="btn btn-sm bg-dark text-light hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50 self-start md:self-auto" class="btn btn-sm bg-dark text-light hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50 self-start md:self-auto"
> >
Tambah Pengguna Add User
</RouterLink> </RouterLink>
</div> </div>
@ -235,7 +233,7 @@ onMounted(async () => {
:data="data" :data="data"
:columns="USERS_TABLE_COLUMNS" :columns="USERS_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Tidak ada data pengguna" empty-message="No user data"
@details="handleDetails" @details="handleDetails"
@update="handleUpdate" @update="handleUpdate"
@delete="handleDelete" @delete="handleDelete"

View File

@ -182,15 +182,15 @@ const showRejectDialog = () => {
}; };
const tableLabelMap: Record<string, string> = { const tableLabelMap: Record<string, string> = {
pemberian_obat: "Pemberian Obat", pemberian_obat: "Medicine Administration",
pemberian_tindakan: "Pemberian Tindakan", pemberian_tindakan: "Doctor Actions",
rekam_medis: "Rekam Medis", rekam_medis: "Medical Records",
}; };
const actionLabelMap: Record<string, string> = { const actionLabelMap: Record<string, string> = {
CREATE: "Tambah Data", CREATE: "Add Data",
UPDATE: "Ubah Data", UPDATE: "Edit Data",
DELETE: "Hapus Data", DELETE: "Delete Data",
}; };
const statusBadgeClass = computed(() => { const statusBadgeClass = computed(() => {
@ -235,22 +235,22 @@ const handleApprove = async () => {
{} {}
); );
console.log("Approve response:", response); console.log("Approve response:", response);
alert("Permintaan berhasil disetujui"); alert("Request successfully approved");
router.push({ name: "validasi" }); router.push({ name: "validasi" });
} catch (error) { } catch (error) {
console.error("Error approving validation:", error); console.error("Error approving validation:", error);
alert("Gagal menyetujui permintaan"); alert("Failed to approve request");
} }
}; };
const handleReject = async () => { const handleReject = async () => {
try { try {
await api.post(`/validation/${route.params.id}/reject`, {}); await api.post(`/validation/${route.params.id}/reject`, {});
alert("Permintaan berhasil ditolak"); alert("Request successfully rejected");
router.push({ name: "validasi" }); router.push({ name: "validasi" });
} catch (error) { } catch (error) {
console.error("Error rejecting validation:", error); console.error("Error rejecting validation:", error);
alert("Gagal menolak permintaan"); alert("Failed to reject request");
} }
}; };
@ -276,7 +276,7 @@ const formatTimestamp = (rawValue?: string) => {
onMounted(async () => { onMounted(async () => {
await fetchData(); await fetchData();
document.title = `Review Validasi - ID ${route.params.id}`; document.title = `Review Validation - ID ${route.params.id}`;
}); });
</script> </script>
@ -285,8 +285,8 @@ onMounted(async () => {
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader <PageHeader
title="Validasi" title="Validation"
:subtitle="`Review Permintaan Validasi ${route.params.id}`" :subtitle="`Review Validation Request ${route.params.id}`"
/> />
<div class="bg-white rounded-xl shadow-md text-dark"> <div class="bg-white rounded-xl shadow-md text-dark">
@ -294,49 +294,51 @@ onMounted(async () => {
<div class="breadcrumbs text-sm"> <div class="breadcrumbs text-sm">
<ul> <ul>
<li class="font-bold"> <li class="font-bold">
<RouterLink to="/validasi"> Validasi </RouterLink> <RouterLink to="/validasi"> Validation </RouterLink>
</li> </li>
<li>Review Permintaan {{ route.params.id }}</li> <li>Review Request {{ route.params.id }}</li>
</ul> </ul>
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<h5 class="font-bold">Detail Permintaan Validasi</h5> <h5 class="font-bold">Validation Request Details</h5>
<span class="badge" :class="statusBadgeClass"> <span class="badge" :class="statusBadgeClass">
{{ validation?.status }} {{ validation?.status }}
</span> </span>
</div> </div>
<div class="text-sm"> <div class="text-sm">
<p><strong>ID Permintaan:</strong> {{ validation?.id }}</p>
<p> <p>
<strong>Tabel:</strong> <strong>Validation Request ID:</strong> {{ validation?.id }}
</p>
<p>
<strong>Table:</strong>
{{ {{
tableLabelMap[validation?.table_name || ""] || tableLabelMap[validation?.table_name || ""] ||
validation?.table_name validation?.table_name
}} }}
</p> </p>
<p><strong>ID Record:</strong> {{ validation?.record_id }}</p> <p><strong>Record ID:</strong> {{ validation?.record_id }}</p>
<p> <p>
<strong>Aksi:</strong> <strong>Action:</strong>
{{ {{
actionLabelMap[validation?.action || ""] || validation?.action actionLabelMap[validation?.action || ""] || validation?.action
}} }}
</p> </p>
<p> <p>
<strong>Pemohon:</strong> User ID <strong>Requester:</strong> User ID
{{ validation?.user_id_request }} {{ validation?.user_id_request }}
</p> </p>
<p> <p>
<strong>Diajukan:</strong> <strong>Submitted:</strong>
{{ formatTimestamp(validation?.created_at) }} {{ formatTimestamp(validation?.created_at) }}
</p> </p>
<p v-if="validation?.processed_at"> <p v-if="validation?.processed_at">
<strong>Diproses:</strong> <strong>Processed:</strong>
{{ formatTimestamp(validation?.processed_at) }} {{ formatTimestamp(validation?.processed_at) }}
</p> </p>
<p v-if="validation?.user_id_process"> <p v-if="validation?.user_id_process">
<strong>Diproses oleh:</strong> User ID <strong>Processed by:</strong> User ID
{{ validation?.user_id_process }} {{ validation?.user_id_process }}
</p> </p>
</div> </div>
@ -364,7 +366,7 @@ onMounted(async () => {
<div <div
class="bg-white border border-dark rounded-lg p-4 text-sm overflow-x-auto flex-1" class="bg-white border border-dark rounded-lg p-4 text-sm overflow-x-auto flex-1"
> >
<h1 class="font-bold mb-2">Data Sebelum</h1> <h1 class="font-bold mb-2">Before Data</h1>
<pre <pre
class="whitespace-pre-wrap" class="whitespace-pre-wrap"
v-html="formatJsonWithHighlight(previousLog, 'old')" v-html="formatJsonWithHighlight(previousLog, 'old')"
@ -373,7 +375,7 @@ onMounted(async () => {
<div <div
class="bg-white border border-dark rounded-lg p-4 text-sm overflow-x-auto flex-1" class="bg-white border border-dark rounded-lg p-4 text-sm overflow-x-auto flex-1"
> >
<h1 class="font-bold mb-2">Data Sesudah</h1> <h1 class="font-bold mb-2">After Data</h1>
<pre <pre
class="whitespace-pre-wrap" class="whitespace-pre-wrap"
v-html="formatJsonWithHighlight(normalizedNewPayload, 'new')" v-html="formatJsonWithHighlight(normalizedNewPayload, 'new')"
@ -389,13 +391,13 @@ onMounted(async () => {
type="button" type="button"
@click="showApproveDialog" @click="showApproveDialog"
:disabled="api.isLoading.value" :disabled="api.isLoading.value"
text="Setujui" text="Approve"
/> />
<button <button
@click="showRejectDialog" @click="showRejectDialog"
class="bg-white btn btn-sm text-dark" class="bg-white btn btn-sm text-dark"
> >
Tolak Reject
</button> </button>
</div> </div>
</div> </div>
@ -403,19 +405,19 @@ onMounted(async () => {
<DialogConfirm <DialogConfirm
ref="approveDialog" ref="approveDialog"
title="Setujui Permintaan" title="Approve Request"
message="Apakah Anda yakin ingin menyetujui permintaan validasi ini?" message="Are you sure you want to approve this validation request?"
confirm-text="Ya, Setujui" confirm-text="Yes, Approve"
cancel-text="Batal" cancel-text="Cancel"
@confirm="handleApprove" @confirm="handleApprove"
/> />
<DialogConfirm <DialogConfirm
ref="rejectDialog" ref="rejectDialog"
title="Tolak Permintaan" title="Reject Request"
message="Apakah Anda yakin ingin menolak permintaan validasi ini?" message="Are you sure you want to reject this validation request?"
confirm-text="Ya, Tolak" confirm-text="Yes, Reject"
cancel-text="Batal" cancel-text="Cancel"
@confirm="handleReject" @confirm="handleReject"
/> />
</Sidebar> </Sidebar>

View File

@ -83,11 +83,11 @@ const normalizedData = (rawData: any[]): ValidationLog[] => {
const formattedTableName = (tableName: string): string => { const formattedTableName = (tableName: string): string => {
switch (tableName) { switch (tableName) {
case "rekam_medis": case "rekam_medis":
return "Rekam Medis"; return "Medical Records";
case "pemberian_tindakan": case "pemberian_tindakan":
return "Tindakan Dokter"; return "Doctor Actions";
case "pemberian_obat": case "pemberian_obat":
return "Obat"; return "Medicine";
default: default:
return tableName; return tableName;
} }
@ -115,7 +115,7 @@ const normalizedData = (rawData: any[]): ValidationLog[] => {
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
return dateString; return dateString;
} }
return date.toLocaleString("id-ID", { return date.toLocaleString("en-US", {
timeZone: "Asia/Jakarta", timeZone: "Asia/Jakarta",
day: "2-digit", day: "2-digit",
month: "2-digit", month: "2-digit",
@ -132,7 +132,7 @@ const normalizedData = (rawData: any[]): ValidationLog[] => {
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {
return dateString; return dateString;
} }
return date.toLocaleString("id-ID", { return date.toLocaleString("en-US", {
timeZone: "Asia/Jakarta", timeZone: "Asia/Jakarta",
day: "2-digit", day: "2-digit",
month: "2-digit", month: "2-digit",
@ -228,13 +228,13 @@ const handleUpdate = (item: ValidationLog) => {
}; };
const handleDelete = async (item: ValidationLog) => { const handleDelete = async (item: ValidationLog) => {
if (confirm(`Apakah Anda yakin ingin menghapus item "${item.id}"?`)) { if (confirm(`Are you sure you want to delete item "${item.id}"?`)) {
try { try {
await api.delete(`/validation/${item.id}`); await api.delete(`/validation/${item.id}`);
await fetchData(); await fetchData();
} catch (error) { } catch (error) {
console.error("Error deleting validation:", error); console.error("Error deleting validation:", error);
alert("Gagal menghapus data validasi"); alert("Failed to delete validation data");
} }
} }
}; };
@ -302,7 +302,7 @@ onMounted(async () => {
<div class="bg-light w-full text-dark"> <div class="bg-light w-full text-dark">
<div class="flex h-full p-2"> <div class="flex h-full p-2">
<Sidebar> <Sidebar>
<PageHeader title="Validasi" subtitle="Manajemen Validasi" /> <PageHeader title="Validation" subtitle="Validation Management" />
<div <div
class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2" class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2"
@ -318,57 +318,59 @@ onMounted(async () => {
<div class="flex gap-x-4 items-end"> <div class="flex gap-x-4 items-end">
<div class="h-full"> <div class="h-full">
<label for="jenis_kelamin" class="font-bold" <label for="jenis_kelamin" class="font-bold"
>Kelompok Data</label >Data Group</label
> >
<select <select
v-model="filters.kelompok_data" v-model="filters.kelompok_data"
class="select bg-white border border-gray-300 mt-1" class="select bg-white border border-gray-300 mt-1"
> >
<option disabled selected value="initial"> <option disabled selected value="initial">
Pilih Kelompok Data Select Data Group
</option> </option>
<option value="rekam_medis">Rekam Medis</option> <option value="rekam_medis">Medical Records</option>
<option value="pemberian_tindakan">Tindakan</option> <option value="pemberian_tindakan">Doctor Actions</option>
<option value="pemberian_obat">Obat</option> <option value="pemberian_obat">
<option value="all">Semua Tipe</option> Medicine Administration
</option>
<option value="all">All Types</option>
</select> </select>
</div> </div>
</div> </div>
<div class="flex gap-x-4 items-end"> <div class="flex gap-x-4 items-end">
<div class="h-full"> <div class="h-full">
<label for="jenis_kelamin" class="font-bold" <label for="jenis_kelamin" class="font-bold"
>Jenis Aksi</label >Action Type</label
> >
<select <select
v-model="filters.aksi" v-model="filters.aksi"
class="select bg-white border border-gray-300 mt-1" class="select bg-white border border-gray-300 mt-1"
> >
<option disabled selected value="initial"> <option disabled selected value="initial">
Pilih Jenis Aksi Select Action Type
</option> </option>
<option value="CREATE">Create</option> <option value="CREATE">Create</option>
<option value="UPDATE">Update</option> <option value="UPDATE">Update</option>
<option value="DELETE">Delete</option> <option value="DELETE">Delete</option>
<option value="all">Semua</option> <option value="all">All</option>
</select> </select>
</div> </div>
</div> </div>
<div class="flex gap-x-4 items-end"> <div class="flex gap-x-4 items-end">
<div class="h-full"> <div class="h-full">
<label for="jenis_kelamin" class="font-bold" <label for="jenis_kelamin" class="font-bold"
>Status Validasi</label >Validation Status</label
> >
<select <select
v-model="filters.status" v-model="filters.status"
class="select bg-white border border-gray-300 mt-1" class="select bg-white border border-gray-300 mt-1"
> >
<option disabled selected value="initial"> <option disabled selected value="initial">
Pilih Status Validasi Select Validation Status
</option> </option>
<option value="PENDING">Pending</option> <option value="PENDING">Pending</option>
<option value="APPROVED">Approved</option> <option value="APPROVED">Approved</option>
<option value="REJECTED">Rejected</option> <option value="REJECTED">Rejected</option>
<option value="all">Semua</option> <option value="all">All</option>
</select> </select>
</div> </div>
</div> </div>
@ -391,14 +393,14 @@ onMounted(async () => {
<div class="flex flex-col w-full gap-2 md:flex-row md:items-center"> <div class="flex flex-col w-full gap-2 md:flex-row md:items-center">
<SearchInput <SearchInput
v-model="searchId" v-model="searchId"
placeholder="Cari berdasarkan ID Record" placeholder="Search by Record ID"
@search="handleSearch" @search="handleSearch"
/> />
<div class="flex items-center gap-2 md:ml-4"> <div class="flex items-center gap-2 md:ml-4">
<SortDropdown <SortDropdown
v-model="sortBy" v-model="sortBy"
:options="SORT_OPTIONS.VALIDATION" :options="SORT_OPTIONS.VALIDATION"
label="Urut berdasarkan:" label="Sort by:"
@change="handleSortChange" @change="handleSortChange"
/> />
<button <button
@ -429,7 +431,7 @@ onMounted(async () => {
:data="data" :data="data"
:columns="VALIDATION_TABLE_COLUMNS" :columns="VALIDATION_TABLE_COLUMNS"
:is-loading="api.isLoading.value" :is-loading="api.isLoading.value"
empty-message="Tidak ada data validasi" empty-message="No validation data available"
@details="handleDetails" @details="handleDetails"
@update="handleUpdate" @update="handleUpdate"
@delete="handleDelete" @delete="handleDelete"