feat: Done Rekam Medis Page (Filter, Pagination, Sorting)
This commit is contained in:
parent
3e85da0098
commit
3778b555a8
|
|
@ -27,6 +27,16 @@ export class RekamMedisController {
|
|||
@Query('orderBy') orderBy: string,
|
||||
@Query('no_rm') no_rm: string,
|
||||
@Query('order') order: 'asc' | 'desc',
|
||||
@Query('id_visit') id_visit: string,
|
||||
@Query('nama_pasien') nama_pasien: string,
|
||||
@Query('tanggal_start') tanggal_start: string,
|
||||
@Query('tanggal_end') tanggal_end: string,
|
||||
@Query('umur_min') umur_min: string,
|
||||
@Query('umur_max') umur_max: string,
|
||||
@Query('jenis_kelamin') jenis_kelamin: string,
|
||||
@Query('gol_darah') gol_darah: string,
|
||||
@Query('kode_diagnosa') kode_diagnosa: string,
|
||||
@Query('tindak_lanjut') tindak_lanjut: string,
|
||||
) {
|
||||
return this.rekammedisService.getAllRekamMedis({
|
||||
take,
|
||||
|
|
@ -35,6 +45,16 @@ export class RekamMedisController {
|
|||
orderBy,
|
||||
no_rm,
|
||||
order,
|
||||
id_visit,
|
||||
nama_pasien,
|
||||
tanggal_start,
|
||||
tanggal_end,
|
||||
umur_min,
|
||||
umur_max,
|
||||
jenis_kelamin,
|
||||
gol_darah,
|
||||
kode_diagnosa,
|
||||
tindak_lanjut,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,26 @@ import { CreateLogDto } from '../log/dto/create-log.dto';
|
|||
|
||||
@Injectable()
|
||||
export class RekammedisService {
|
||||
// Define known values as constants to avoid hardcoding everywhere
|
||||
private readonly KNOWN_BLOOD_TYPES = ['A', 'B', 'AB', 'O'];
|
||||
private readonly KNOWN_TINDAK_LANJUT = [
|
||||
'Dipulangkan untuk Kontrol',
|
||||
'Dirawat',
|
||||
'Dirujuk ke RS',
|
||||
'Konsul Ke Poli Lain',
|
||||
'Konsultasi Dokter Spesialis',
|
||||
'Kontrol',
|
||||
'Kontrol Ulang',
|
||||
'Masuk Rawat Inap',
|
||||
'Meninggal Dunia Sebelum Dirawat',
|
||||
'Meninggal Dunia Setelah Dirawat',
|
||||
'Pulang',
|
||||
'Rencana Operasi',
|
||||
'Rujuk Balik',
|
||||
'Selesai Pelayanan IGD',
|
||||
'Selesai Pelayanan Rawat Jalan',
|
||||
];
|
||||
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getAllRekamMedis(params: {
|
||||
|
|
@ -15,8 +35,36 @@ export class RekammedisService {
|
|||
orderBy?: any;
|
||||
no_rm?: string;
|
||||
order?: 'asc' | 'desc';
|
||||
id_visit?: string;
|
||||
nama_pasien?: string;
|
||||
tanggal_start?: string;
|
||||
tanggal_end?: string;
|
||||
umur_min?: string;
|
||||
umur_max?: string;
|
||||
jenis_kelamin?: string;
|
||||
gol_darah?: string;
|
||||
kode_diagnosa?: string;
|
||||
tindak_lanjut?: string;
|
||||
}) {
|
||||
const { skip, page, orderBy, order, no_rm } = params;
|
||||
const {
|
||||
skip,
|
||||
page,
|
||||
orderBy,
|
||||
order,
|
||||
no_rm,
|
||||
id_visit,
|
||||
nama_pasien,
|
||||
tanggal_start,
|
||||
tanggal_end,
|
||||
umur_min,
|
||||
umur_max,
|
||||
jenis_kelamin,
|
||||
kode_diagnosa,
|
||||
} = params;
|
||||
|
||||
const golDarahArray = params.gol_darah?.split(',') || [];
|
||||
const tindakLanjutArray = params.tindak_lanjut?.split(',') || [];
|
||||
console.log('Params Received:', params);
|
||||
const take = params.take ? parseInt(params.take.toString()) : 10;
|
||||
const skipValue = skip
|
||||
? parseInt(skip.toString())
|
||||
|
|
@ -24,27 +72,140 @@ export class RekammedisService {
|
|||
? (parseInt(page.toString()) - 1) * take
|
||||
: 0;
|
||||
|
||||
const buildMultiSelectFilter = (
|
||||
fieldName: string,
|
||||
selectedValues: string[],
|
||||
knownValues: string[],
|
||||
unknownLabel: string,
|
||||
includesDash: boolean = false,
|
||||
) => {
|
||||
if (selectedValues.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hasKnownValues = selectedValues.some((val) =>
|
||||
knownValues.includes(val),
|
||||
);
|
||||
const hasUnknown = selectedValues.includes(unknownLabel);
|
||||
const totalOptions = knownValues.length + 1;
|
||||
|
||||
if (selectedValues.length === totalOptions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (hasUnknown && !hasKnownValues) {
|
||||
const conditions: any[] = [{ [fieldName]: { equals: null } }];
|
||||
|
||||
if (includesDash) {
|
||||
conditions.push({ [fieldName]: { equals: '-' } });
|
||||
conditions.push({ [fieldName]: { notIn: knownValues } });
|
||||
}
|
||||
|
||||
return conditions.length > 1 ? { OR: conditions } : conditions[0];
|
||||
}
|
||||
|
||||
if (hasKnownValues && hasUnknown) {
|
||||
const knownSelected = selectedValues.filter(
|
||||
(val) => val !== unknownLabel,
|
||||
);
|
||||
const conditions: any[] = [
|
||||
{ [fieldName]: { in: knownSelected } },
|
||||
{ [fieldName]: { equals: null } },
|
||||
];
|
||||
|
||||
if (includesDash) {
|
||||
conditions.push({ [fieldName]: { equals: '-' } });
|
||||
conditions.push({ [fieldName]: { notIn: knownValues } });
|
||||
}
|
||||
|
||||
return { OR: conditions };
|
||||
}
|
||||
|
||||
return { [fieldName]: { in: selectedValues } };
|
||||
};
|
||||
|
||||
const golDarahFilter = buildMultiSelectFilter(
|
||||
'gol_darah',
|
||||
golDarahArray,
|
||||
this.KNOWN_BLOOD_TYPES,
|
||||
'Tidak Tahu',
|
||||
true,
|
||||
);
|
||||
|
||||
const tindakLanjutFilter = buildMultiSelectFilter(
|
||||
'tindak_lanjut',
|
||||
tindakLanjutArray,
|
||||
this.KNOWN_TINDAK_LANJUT,
|
||||
'Belum Ada Keterangan',
|
||||
false,
|
||||
);
|
||||
|
||||
const whereClause = {
|
||||
no_rm: no_rm ? { startsWith: no_rm } : undefined,
|
||||
id_visit: id_visit ? { contains: id_visit } : undefined,
|
||||
nama_pasien: nama_pasien ? { contains: nama_pasien } : undefined,
|
||||
waktu_visit:
|
||||
tanggal_start && tanggal_end
|
||||
? {
|
||||
gte: new Date(tanggal_start),
|
||||
lte: new Date(tanggal_end),
|
||||
}
|
||||
: undefined,
|
||||
umur:
|
||||
umur_min && umur_max
|
||||
? {
|
||||
gte: parseInt(umur_min, 10),
|
||||
lte: parseInt(umur_max, 10),
|
||||
}
|
||||
: undefined,
|
||||
jenis_kelamin: jenis_kelamin ? { equals: jenis_kelamin } : undefined,
|
||||
kode_diagnosa: kode_diagnosa ? { contains: kode_diagnosa } : undefined,
|
||||
...golDarahFilter,
|
||||
...tindakLanjutFilter,
|
||||
};
|
||||
|
||||
const results = await this.prisma.rekam_medis.findMany({
|
||||
skip: skipValue,
|
||||
take: take,
|
||||
where: {
|
||||
no_rm: no_rm ? no_rm : undefined,
|
||||
},
|
||||
where: whereClause,
|
||||
orderBy: orderBy
|
||||
? { [orderBy]: order || 'asc' }
|
||||
: { waktu_visit: order ? order : 'asc' },
|
||||
});
|
||||
|
||||
const count = await this.prisma.rekam_medis.count({
|
||||
where: {
|
||||
no_rm: no_rm ? { contains: no_rm } : undefined,
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
const umurMin = await this.prisma.rekam_medis.findMany({
|
||||
distinct: ['umur'],
|
||||
orderBy: {
|
||||
umur: 'asc',
|
||||
},
|
||||
select: {
|
||||
umur: true,
|
||||
},
|
||||
});
|
||||
|
||||
// console.log('Fetched Rekam Medis:', count);
|
||||
const umurMax = await this.prisma.rekam_medis.findMany({
|
||||
distinct: ['umur'],
|
||||
orderBy: {
|
||||
umur: 'desc',
|
||||
},
|
||||
select: {
|
||||
umur: true,
|
||||
},
|
||||
});
|
||||
|
||||
const rangeUmur = {
|
||||
min: umurMin.length > 0 ? umurMin[0].umur : null,
|
||||
max: umurMax.length > 0 ? umurMax[0].umur : null,
|
||||
};
|
||||
|
||||
return {
|
||||
...results,
|
||||
totalCount: count,
|
||||
rangeUmur: rangeUmur,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
23
frontend/hospital-log/package-lock.json
generated
23
frontend/hospital-log/package-lock.json
generated
|
|
@ -10,7 +10,9 @@
|
|||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"cally": "^0.8.0",
|
||||
"daisyui": "^5.3.10",
|
||||
"nouislider": "^15.8.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "4",
|
||||
|
|
@ -638,6 +640,12 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/atomico": {
|
||||
"version": "1.79.2",
|
||||
"resolved": "https://registry.npmjs.org/atomico/-/atomico-1.79.2.tgz",
|
||||
"integrity": "sha512-mshhLRMeIltNYbnQnqgnrvJ/uDa8XDfTQcjw3ymOygQqwHIQ4Sp0LcNYMCbACkV3DtV+eDXb9szwU4qMUuGwYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.21",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||
|
|
@ -729,6 +737,15 @@
|
|||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/cally": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cally/-/cally-0.8.0.tgz",
|
||||
"integrity": "sha512-jvQ2QMrsZM/ZPG/LWTkJEUPrp/ew1uS2KjKA/E6ru7mVvTMY2JgSagci9IghLmuamFh1pDajrxXAX4Qgo4FHbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"atomico": "^1.76.1"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001751",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
|
||||
|
|
@ -1005,6 +1022,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nouislider": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.8.1.tgz",
|
||||
"integrity": "sha512-93TweAi8kqntHJSPiSWQ1o/uZ29VWOmal9YKb6KKGGlCkugaNfAupT7o1qTHqdJvNQ7S0su5rO6qRFCjP8fxtw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@
|
|||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"cally": "^0.8.0",
|
||||
"daisyui": "^5.3.10",
|
||||
"nouislider": "^15.8.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "4",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: string[];
|
||||
label?: string;
|
||||
options: Array<{ label: string; value: string }>;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: string[]): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const handleChange = (value: string, checked: boolean) => {
|
||||
const currentValue = [...(props.modelValue || [])];
|
||||
|
||||
if (checked && !currentValue.includes(value)) {
|
||||
currentValue.push(value);
|
||||
} else if (!checked) {
|
||||
const index = currentValue.indexOf(value);
|
||||
if (index > -1) {
|
||||
currentValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
emit("update:modelValue", currentValue);
|
||||
};
|
||||
|
||||
const isChecked = (value: string) => {
|
||||
return props.modelValue?.includes(value) || false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<label v-if="label" class="text-sm font-medium text-dark mb-2 block">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
class="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:name="name"
|
||||
:value="option.value"
|
||||
:checked="isChecked(option.value)"
|
||||
@change="(e) => handleChange(option.value, (e.target as HTMLInputElement).checked)"
|
||||
class="checkbox checkbox-sm checkbox-primary"
|
||||
/>
|
||||
<span class="text-sm text-dark">{{ option.label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Ensure DaisyUI checkbox styling is applied */
|
||||
:deep(.checkbox) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
// Import Cally web component
|
||||
import "cally";
|
||||
|
||||
// Declare custom element for TypeScript
|
||||
declare global {
|
||||
namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
"calendar-range": any;
|
||||
"calendar-month": any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: { start: string; end: string };
|
||||
label?: string;
|
||||
max?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: { start: string; end: string }): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const calendarRef = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (calendarRef.value) {
|
||||
calendarRef.value.addEventListener("change", (e: Event) => {
|
||||
const target = e.target as any;
|
||||
|
||||
const start = target.startValue || props.modelValue.start;
|
||||
const end = target.endValue || props.modelValue.end;
|
||||
|
||||
emit("update:modelValue", { start, end });
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<label v-if="label" class="text-sm font-medium text-dark mb-2 block">
|
||||
{{ label }}
|
||||
</label>
|
||||
<calendar-range
|
||||
ref="calendarRef"
|
||||
:value="
|
||||
modelValue.start && modelValue.end
|
||||
? `${modelValue.start} ${modelValue.end}`
|
||||
: ''
|
||||
"
|
||||
:max="max"
|
||||
class="calendar-custom"
|
||||
>
|
||||
<!-- Navigation icons -->
|
||||
<svg
|
||||
aria-label="Previous"
|
||||
class="fill-current size-4"
|
||||
slot="previous"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M15.75 19.5 8.25 12l7.5-7.5"></path>
|
||||
</svg>
|
||||
<svg
|
||||
aria-label="Next"
|
||||
class="fill-current size-4"
|
||||
slot="next"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="m8.25 4.5 7.5 7.5-7.5 7.5"></path>
|
||||
</svg>
|
||||
<calendar-month></calendar-month>
|
||||
</calendar-range>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Calendar button styling */
|
||||
.calendar-custom::part(button) {
|
||||
border: 1px solid #ed7979;
|
||||
background-color: #ed7979;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.calendar-custom::part(button):hover {
|
||||
border: 1px solid #b34242;
|
||||
background-color: #b34242;
|
||||
}
|
||||
|
||||
.calendar-custom::part(button):focus-visible {
|
||||
outline: 2px solid #1a2a4f;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Dialog styling */
|
||||
.calendar-custom::part(dialog) {
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Calendar base */
|
||||
.calendar-custom::part(calendar) {
|
||||
--color-accent: #1a2a4f;
|
||||
--color-text-on-accent: #fff2ef;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* Second calendar (end date) */
|
||||
.calendar-custom::part(calendar):nth-of-type(2) {
|
||||
--color-accent: #ed7979;
|
||||
}
|
||||
|
||||
/* Month container */
|
||||
.calendar-custom::part(month) {
|
||||
background: white;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.calendar-custom::part(header) {
|
||||
padding: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #1a2a4f;
|
||||
}
|
||||
|
||||
/* Navigation buttons */
|
||||
.calendar-custom::part(prev-button),
|
||||
.calendar-custom::part(next-button) {
|
||||
color: #1a2a4f;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.calendar-custom::part(prev-button):hover,
|
||||
.calendar-custom::part(next-button):hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Day cells */
|
||||
.calendar-custom::part(day) {
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.calendar-custom::part(day):hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
/* Today styling */
|
||||
.calendar-custom::part(today) {
|
||||
background-color: #ed7979;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Selected date */
|
||||
.calendar-custom::part(selected) {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-text-on-accent);
|
||||
}
|
||||
|
||||
/* Range styling for date ranges */
|
||||
.calendar-custom::part(range-inner) {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-text-on-accent);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.calendar-custom::part(range-start) {
|
||||
border-start-start-radius: 8px;
|
||||
border-end-start-radius: 8px;
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
.calendar-custom::part(range-end) {
|
||||
border-start-end-radius: 8px;
|
||||
border-end-end-radius: 8px;
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
||||
.calendar-custom::part(range-start range-end) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Disabled dates */
|
||||
.calendar-custom::part(disabled) {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Weekday headers */
|
||||
.calendar-custom::part(weekday) {
|
||||
color: var(--color-accent);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import noUiSlider from "nouislider";
|
||||
import "nouislider/dist/nouislider.css";
|
||||
|
||||
interface Props {
|
||||
modelValue: [number, number];
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
label?: string;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: [number, number]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
unit: "",
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
const sliderRef = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (sliderRef.value) {
|
||||
const slider = noUiSlider.create(sliderRef.value, {
|
||||
start: props.modelValue,
|
||||
connect: true,
|
||||
step: props.step,
|
||||
range: {
|
||||
min: props.min,
|
||||
max: props.max,
|
||||
},
|
||||
tooltips: [
|
||||
{ to: (value: number) => Math.round(value).toString() },
|
||||
{ to: (value: number) => Math.round(value).toString() },
|
||||
],
|
||||
});
|
||||
|
||||
slider.on("update", (values: (string | number)[]) => {
|
||||
const range: [number, number] = [
|
||||
Math.round(Number(values[0])),
|
||||
Math.round(Number(values[1])),
|
||||
];
|
||||
emit("update:modelValue", range);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (sliderRef.value && (sliderRef.value as any).noUiSlider) {
|
||||
(sliderRef.value as any).noUiSlider.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full px-2">
|
||||
<label v-if="label" class="text-sm font-medium text-dark mb-2 block">
|
||||
{{ label }}: {{ modelValue[0] }} - {{ modelValue[1] }} {{ unit }}
|
||||
</label>
|
||||
<div ref="sliderRef" class="mb-4"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* noUiSlider customization */
|
||||
:deep(.noUi-target) {
|
||||
background: #e5e7eb;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.noUi-connect) {
|
||||
background: #1a2a4f;
|
||||
}
|
||||
|
||||
:deep(.noUi-handle) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #1a2a4f;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
:deep(.noUi-handle):hover {
|
||||
background: #ed7979;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
:deep(.noUi-handle):before,
|
||||
:deep(.noUi-handle):after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.noUi-tooltip) {
|
||||
background: #1a2a4f;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.noUi-horizontal .noUi-handle) {
|
||||
top: -6px;
|
||||
right: -10px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
label?: string;
|
||||
options: Array<{ label: string; value: string }>;
|
||||
placeholder?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: string): void;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const handleChange = (event: Event) => {
|
||||
const target = event.target as HTMLSelectElement;
|
||||
emit("update:modelValue", target.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-control w-full">
|
||||
<label v-if="label" class="label">
|
||||
<span class="label-text text-dark text-sm font-medium">{{ label }}</span>
|
||||
</label>
|
||||
<select
|
||||
:value="modelValue"
|
||||
@change="handleChange"
|
||||
:name="name"
|
||||
class="select select-bordered w-full bg-white border-gray-300"
|
||||
>
|
||||
<option v-if="placeholder" value="">{{ placeholder }}</option>
|
||||
<option
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: string): void;
|
||||
}
|
||||
|
||||
defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
emit("update:modelValue", value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="form-control w-full">
|
||||
<label v-if="label" class="label">
|
||||
<span class="label-text text-dark text-sm font-medium">{{ label }}</span>
|
||||
</label>
|
||||
<input
|
||||
:value="modelValue"
|
||||
@input="handleInput"
|
||||
type="text"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
class="input input-bordered w-full bg-white border-gray-300"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export { default as TextInput } from "./TextInput.vue";
|
||||
export { default as DateRangePicker } from "./DateRangePicker.vue";
|
||||
export { default as RangeSlider } from "./RangeSlider.vue";
|
||||
export { default as SelectInput } from "./SelectInput.vue";
|
||||
export { default as CheckboxGroup } from "./CheckboxGroup.vue";
|
||||
11
frontend/hospital-log/src/constants/interfaces.ts
Normal file
11
frontend/hospital-log/src/constants/interfaces.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
interface RekamMedis {
|
||||
id_visit: string;
|
||||
no_rm: string;
|
||||
nama_pasien: string;
|
||||
waktu_visit: Date;
|
||||
umur: number;
|
||||
jenis_kelamin: string;
|
||||
gol_darah: string;
|
||||
kode_diagnosa: string;
|
||||
tindak_lanjut: string;
|
||||
}
|
||||
|
|
@ -1,9 +1,39 @@
|
|||
import type { RekamMedis } from "./interfaces";
|
||||
|
||||
export const ITEMS_PER_PAGE_OPTIONS = [5, 10, 25, 50, 100] as const;
|
||||
|
||||
export const DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
export const DEBOUNCE_DELAY = 500; // milliseconds
|
||||
|
||||
export const FILTER = {
|
||||
TINDAK_LANJUT: {
|
||||
dipulangkan: "Dipulangkan untuk Kontrol",
|
||||
dirawat: "Dirawat",
|
||||
dirujuk: "Dirujuk ke RS",
|
||||
konsul_poli_lain: "Konsul Ke Poli Lain",
|
||||
konsul_spesialis: "Konsultasi Dokter Spesialis",
|
||||
kontrol: "Kontrol",
|
||||
kontrol_ulang: "Kontrol Ulang",
|
||||
rawat_inap: "Masuk Rawat Inap",
|
||||
meninggal_sebelum_dirawat: "Meninggal Dunia Sebelum Dirawat",
|
||||
meninggal_setelah_dirawat: "Meninggal Dunia Setelah Dirawat",
|
||||
pulang: "Pulang",
|
||||
rencana_operasi: "Rencana Operasi",
|
||||
rujuk_balik: "Rujuk Balik",
|
||||
selesai_igd: "Selesai Pelayanan IGD",
|
||||
selesai_rawat_jalan: "Selesai Pelayanan Rawat Jalan",
|
||||
belum_ada_keterangan: "Belum Ada Keterangan",
|
||||
},
|
||||
GOLONGAN_DARAH: {
|
||||
A: "A",
|
||||
B: "B",
|
||||
AB: "AB",
|
||||
O: "O",
|
||||
"Tidak Tahu": "Tidak Tahu",
|
||||
},
|
||||
};
|
||||
|
||||
export const SORT_OPTIONS = {
|
||||
OBAT: {
|
||||
id: "ID",
|
||||
|
|
@ -17,3 +47,51 @@ export const SORT_OPTIONS = {
|
|||
umur: "Umur",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const REKAM_MEDIS_TABLE_COLUMNS = [
|
||||
{
|
||||
key: "id_visit" as keyof RekamMedis,
|
||||
label: "ID Visit",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "no_rm" as keyof RekamMedis,
|
||||
label: "No RM",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "nama_pasien" as keyof RekamMedis,
|
||||
label: "Nama Pasien",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "waktu_visit" as keyof RekamMedis,
|
||||
label: "Waktu Visit",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "umur" as keyof RekamMedis,
|
||||
label: "Umur",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "jenis_kelamin" as keyof RekamMedis,
|
||||
label: "Jenis Kelamin",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "gol_darah" as keyof RekamMedis,
|
||||
label: "Golongan Darah",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "kode_diagnosa" as keyof RekamMedis,
|
||||
label: "Kode Diagnosa",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "tindak_lanjut" as keyof RekamMedis,
|
||||
label: "Tindak Lanjut",
|
||||
class: "text-dark",
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -16,23 +16,21 @@ import {
|
|||
DEBOUNCE_DELAY,
|
||||
ITEMS_PER_PAGE_OPTIONS,
|
||||
SORT_OPTIONS,
|
||||
FILTER,
|
||||
REKAM_MEDIS_TABLE_COLUMNS,
|
||||
} from "../../constants/pagination";
|
||||
|
||||
interface RekamMedis {
|
||||
id_visit: string;
|
||||
no_rm: string;
|
||||
nama_pasien: string;
|
||||
waktu_visit: Date;
|
||||
umur: number;
|
||||
jenis_kelamin: string;
|
||||
gol_darah: string;
|
||||
kode_diagnosa: string;
|
||||
tindak_lanjut: string;
|
||||
}
|
||||
import "cally";
|
||||
import noUiSlider from "nouislider";
|
||||
import "nouislider/dist/nouislider.css";
|
||||
import type { RekamMedis } from "../../constants/interfaces";
|
||||
|
||||
interface ApiResponse {
|
||||
data: RekamMedis[];
|
||||
totalCount: number;
|
||||
rangeUmur: {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
}
|
||||
|
||||
const data = ref<RekamMedis[]>([]);
|
||||
|
|
@ -47,54 +45,29 @@ const pagination = usePagination({
|
|||
initialPage: Number(route.query.page) || 1,
|
||||
initialPageSize: Number(route.query.pageSize) || DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
const today: string = new Date().toISOString().split("T")[0] || "";
|
||||
const ageSliderRef = ref<HTMLElement | null>(null);
|
||||
const ageRange = ref<[number, number]>([0, 100]);
|
||||
|
||||
const tableColumns = [
|
||||
{
|
||||
key: "id_visit" as keyof RekamMedis,
|
||||
label: "ID Visit",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "no_rm" as keyof RekamMedis,
|
||||
label: "No RM",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "nama_pasien" as keyof RekamMedis,
|
||||
label: "Nama Pasien",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "waktu_visit" as keyof RekamMedis,
|
||||
label: "Waktu Visit",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "umur" as keyof RekamMedis,
|
||||
label: "Umur",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "jenis_kelamin" as keyof RekamMedis,
|
||||
label: "Jenis Kelamin",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "gol_darah" as keyof RekamMedis,
|
||||
label: "Golongan Darah",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "kode_diagnosa" as keyof RekamMedis,
|
||||
label: "Kode Diagnosa",
|
||||
class: "text-dark",
|
||||
},
|
||||
{
|
||||
key: "tindak_lanjut" as keyof RekamMedis,
|
||||
label: "Tindak Lanjut",
|
||||
class: "text-dark",
|
||||
},
|
||||
];
|
||||
const filter = ref<{
|
||||
id_visit: string | null;
|
||||
nama_pasien: string | null;
|
||||
rentang_tanggal: { start: Date | null; end: Date | null };
|
||||
rentang_umur: [number, number];
|
||||
jenis_kelamin: string | null;
|
||||
kode_diagnosa: string;
|
||||
gol_darah: string[];
|
||||
tindak_lanjut: string[];
|
||||
}>({
|
||||
id_visit: null,
|
||||
nama_pasien: null,
|
||||
rentang_tanggal: { start: null, end: null },
|
||||
rentang_umur: [0, 100],
|
||||
jenis_kelamin: "initial",
|
||||
kode_diagnosa: "",
|
||||
gol_darah: [],
|
||||
tindak_lanjut: [],
|
||||
});
|
||||
|
||||
const updateQueryParams = () => {
|
||||
const query: Record<string, string> = {
|
||||
|
|
@ -110,18 +83,126 @@ const updateQueryParams = () => {
|
|||
query.sortBy = sortBy.value;
|
||||
}
|
||||
|
||||
if (filter.value.id_visit) {
|
||||
query.id_visit = filter.value.id_visit;
|
||||
}
|
||||
|
||||
if (filter.value.nama_pasien) {
|
||||
query.nama_pasien = filter.value.nama_pasien;
|
||||
}
|
||||
|
||||
if (filter.value.rentang_tanggal.end && filter.value.rentang_tanggal.start) {
|
||||
query.tanggal_start = filter.value.rentang_tanggal.start.toString();
|
||||
query.tanggal_end = filter.value.rentang_tanggal.end.toString();
|
||||
}
|
||||
|
||||
console.log(filter.value.rentang_umur[0], filter.value.rentang_umur[1]);
|
||||
console.log(ageRange.value[0], ageRange.value[1]);
|
||||
if (
|
||||
filter.value.rentang_umur[0] !== ageRange.value[0] &&
|
||||
filter.value.rentang_umur[1] !== ageRange.value[1]
|
||||
) {
|
||||
query.umur_min = filter.value.rentang_umur[0].toString();
|
||||
query.umur_max = filter.value.rentang_umur[1].toString();
|
||||
}
|
||||
|
||||
if (filter.value.jenis_kelamin && filter.value.jenis_kelamin !== "initial") {
|
||||
query.jenis_kelamin = filter.value.jenis_kelamin;
|
||||
}
|
||||
|
||||
if (filter.value.gol_darah.length > 0) {
|
||||
query.gol_darah = filter.value.gol_darah.join(",");
|
||||
}
|
||||
|
||||
if (filter.value.kode_diagnosa) {
|
||||
query.kode_diagnosa = filter.value.kode_diagnosa;
|
||||
}
|
||||
|
||||
if (filter.value.tindak_lanjut.length > 0) {
|
||||
query.tindak_lanjut = filter.value.tindak_lanjut.join(",");
|
||||
}
|
||||
|
||||
router.replace({ query });
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
const handleCalendarChange = (date: any) => {
|
||||
filter.value.rentang_tanggal.start = date.target.value.split("/")[0];
|
||||
filter.value.rentang_tanggal.end = date.target.value.split("/")[1];
|
||||
|
||||
console.log("Range Date:", filter.value.rentang_tanggal);
|
||||
};
|
||||
|
||||
const handleResetGolonganDarah = () => {
|
||||
filter.value.gol_darah = [];
|
||||
};
|
||||
|
||||
const handleResetTindakLanjut = () => {
|
||||
filter.value.tindak_lanjut = [];
|
||||
};
|
||||
|
||||
const handleResetFilter = () => {
|
||||
if (ageSliderRef.value && (ageSliderRef.value as any).noUiSlider) {
|
||||
(ageSliderRef.value as any).noUiSlider.set([
|
||||
ageRange.value[0],
|
||||
ageRange.value[1],
|
||||
]);
|
||||
}
|
||||
|
||||
filter.value = {
|
||||
id_visit: null,
|
||||
nama_pasien: null,
|
||||
rentang_tanggal: { start: null, end: null },
|
||||
rentang_umur: [ageRange.value[0], ageRange.value[1]],
|
||||
jenis_kelamin: "initial",
|
||||
kode_diagnosa: "",
|
||||
gol_darah: [],
|
||||
tindak_lanjut: [],
|
||||
};
|
||||
};
|
||||
|
||||
const handleApplyFilter = () => {
|
||||
pagination.reset();
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const fetchData = async (isFirst?: boolean) => {
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
take: pagination.pageSize.value.toString(),
|
||||
page: pagination.page.value.toString(),
|
||||
orderBy: sortBy.value,
|
||||
...(searchRekamMedis.value && { obat: searchRekamMedis.value }),
|
||||
...(searchRekamMedis.value && { no_rm: searchRekamMedis.value }),
|
||||
...(filter.value.id_visit && { id_visit: filter.value.id_visit }),
|
||||
...(filter.value.nama_pasien && {
|
||||
nama_pasien: filter.value.nama_pasien,
|
||||
}),
|
||||
...(filter.value.rentang_tanggal.start &&
|
||||
filter.value.rentang_tanggal.end && {
|
||||
tanggal_start: filter.value.rentang_tanggal.start.toString(),
|
||||
tanggal_end: filter.value.rentang_tanggal.end.toString(),
|
||||
}),
|
||||
...(filter.value.rentang_umur &&
|
||||
!isFirst && {
|
||||
umur_min: filter.value.rentang_umur[0].toString(),
|
||||
umur_max: filter.value.rentang_umur[1].toString(),
|
||||
}),
|
||||
...(filter.value.jenis_kelamin &&
|
||||
filter.value.jenis_kelamin !== "initial" && {
|
||||
jenis_kelamin: filter.value.jenis_kelamin,
|
||||
}),
|
||||
...(filter.value.kode_diagnosa && {
|
||||
kode_diagnosa: filter.value.kode_diagnosa,
|
||||
}),
|
||||
...(filter.value.gol_darah.length > 0 && {
|
||||
gol_darah: filter.value.gol_darah.join(","),
|
||||
}),
|
||||
...(filter.value.tindak_lanjut.length > 0 && {
|
||||
tindak_lanjut: filter.value.tindak_lanjut.join(","),
|
||||
}),
|
||||
});
|
||||
|
||||
console.log("Fetching with params:", queryParams.toString());
|
||||
|
||||
const result = await api.get<ApiResponse>(
|
||||
`/rekammedis?${queryParams.toString()}`
|
||||
);
|
||||
|
|
@ -129,9 +210,12 @@ const fetchData = async () => {
|
|||
if ("data" in result && Array.isArray(result.data)) {
|
||||
data.value = result.data;
|
||||
pagination.totalCount.value = result.totalCount;
|
||||
ageRange.value = [result.rangeUmur.min, result.rangeUmur.max];
|
||||
} else {
|
||||
const apiResponse = result as any;
|
||||
pagination.totalCount.value = apiResponse.totalCount;
|
||||
ageRange.value = [apiResponse.rangeUmur.min, apiResponse.rangeUmur.max];
|
||||
console.log("API Response:", ageRange.value);
|
||||
|
||||
const dataArray: RekamMedis[] = [];
|
||||
Object.keys(apiResponse).forEach((key) => {
|
||||
|
|
@ -143,12 +227,14 @@ const fetchData = async () => {
|
|||
}
|
||||
});
|
||||
// console.log("Fetched data array:", dataArray);
|
||||
data.value = dataArray;
|
||||
}
|
||||
|
||||
updateQueryParams();
|
||||
data.value = dataArray;
|
||||
if (!isFirst) {
|
||||
updateQueryParams();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching obat data:", error);
|
||||
console.error("Error fetching rekam medis data:", error);
|
||||
data.value = [];
|
||||
}
|
||||
};
|
||||
|
|
@ -162,6 +248,8 @@ const handleSearch = () => {
|
|||
|
||||
const handleSortChange = (newSortBy: string) => {
|
||||
sortBy.value = newSortBy;
|
||||
const element = document.activeElement;
|
||||
if (element && element instanceof HTMLElement) element.blur();
|
||||
pagination.reset();
|
||||
fetchData();
|
||||
};
|
||||
|
|
@ -212,8 +300,35 @@ onMounted(async () => {
|
|||
sortBy.value = route.query.sortBy as string;
|
||||
}
|
||||
|
||||
await fetchData();
|
||||
await fetchData(true);
|
||||
filter.value.rentang_umur = [ageRange.value[0], ageRange.value[1]];
|
||||
document.title = "RekamMedis - Hospital Log";
|
||||
|
||||
// Initialize noUiSlider
|
||||
if (ageSliderRef.value) {
|
||||
const slider = noUiSlider.create(ageSliderRef.value, {
|
||||
start: [filter.value.rentang_umur[0], filter.value.rentang_umur[1]],
|
||||
connect: true,
|
||||
step: 1,
|
||||
range: {
|
||||
min: filter.value.rentang_umur[0],
|
||||
max: filter.value.rentang_umur[1],
|
||||
},
|
||||
animate: true,
|
||||
animationDuration: 300,
|
||||
tooltips: [
|
||||
{ to: (value: number) => Math.round(value).toString() },
|
||||
{ to: (value: number) => Math.round(value).toString() },
|
||||
],
|
||||
});
|
||||
|
||||
slider.on("update", (values: (string | number)[]) => {
|
||||
filter.value.rentang_umur = [
|
||||
Math.round(Number(values[0])),
|
||||
Math.round(Number(values[1])),
|
||||
];
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -222,8 +337,207 @@ onMounted(async () => {
|
|||
<div class="flex h-full p-2">
|
||||
<Sidebar>
|
||||
<PageHeader title="Rekam Medis" subtitle="Manajemen Rekam Medis" />
|
||||
<div
|
||||
class="collapse collapse-arrow bg-white border-white border shadow-sm mb-2"
|
||||
>
|
||||
<input type="checkbox" />
|
||||
<div
|
||||
class="collapse-title font-semibold after:start-5 after:end-auto pe-4 ps-12"
|
||||
>
|
||||
Filter
|
||||
</div>
|
||||
<div class="collapse-content text-sm flex flex-col">
|
||||
<div class="flex gap-x-4 items-end">
|
||||
<div>
|
||||
<label for="id_visit" class="font-bold">ID Visit</label>
|
||||
<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"
|
||||
type="text"
|
||||
name="id_visit"
|
||||
id="id_visit"
|
||||
placeholder="Masukkan ID Visit"
|
||||
v-model="filter.id_visit"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="nama_pasien" class="font-bold">Nama Pasien</label>
|
||||
<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"
|
||||
type="text"
|
||||
name="nama_pasien"
|
||||
id="nama_pasien"
|
||||
placeholder="Masukkan Nama Pasien"
|
||||
v-model="filter.nama_pasien"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-md">
|
||||
<div class="w-6/12">
|
||||
<label for="range_tanggal" class="font-bold"
|
||||
>Rentang Tanggal</label
|
||||
>
|
||||
<button
|
||||
popoverTarget="cally-popover1"
|
||||
:class="[
|
||||
'btn border w-full mt-1 text-light bg-dark border-dark',
|
||||
filter.rentang_tanggal.start && filter.rentang_tanggal.end
|
||||
? 'inset-shadow-sm inset-shadow-black/70'
|
||||
: '',
|
||||
]"
|
||||
id="cally1"
|
||||
style="anchor-name: --cally1"
|
||||
>
|
||||
{{
|
||||
filter.rentang_tanggal.start && filter.rentang_tanggal.end
|
||||
? `${new Date(
|
||||
filter.rentang_tanggal.start
|
||||
).toLocaleDateString("id-ID")} - ${new Date(
|
||||
filter.rentang_tanggal.end
|
||||
).toLocaleDateString("id-ID")}`
|
||||
: "Pilih rentang tanggal"
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
popover
|
||||
id="cally-popover1"
|
||||
class="dropdown bg-light rounded-box shadow-xl"
|
||||
style="position-anchor: --cally1"
|
||||
>
|
||||
<calendar-range
|
||||
:max="today"
|
||||
locale="id-ID"
|
||||
class="cally"
|
||||
@change="handleCalendarChange"
|
||||
>
|
||||
<svg
|
||||
aria-label="Previous"
|
||||
class="fill-current size-4"
|
||||
slot="previous"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M15.75 19.5 8.25 12l7.5-7.5"></path>
|
||||
</svg>
|
||||
<svg
|
||||
aria-label="Next"
|
||||
class="fill-current size-4"
|
||||
slot="next"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="m8.25 4.5 7.5 7.5-7.5 7.5"></path>
|
||||
</svg>
|
||||
<calendar-month></calendar-month>
|
||||
</calendar-range>
|
||||
</div>
|
||||
<div class="w-full px-2 h-full">
|
||||
<label class="font-bold block mb-4"
|
||||
>Rentang Umur: {{ filter.rentang_umur[0] }} -
|
||||
{{ filter.rentang_umur[1] }} tahun</label
|
||||
>
|
||||
<div ref="ageSliderRef" class="mb-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-x-4 items-end mt-4">
|
||||
<div class="h-full">
|
||||
<label for="jenis_kelamin" class="font-bold"
|
||||
>Jenis Kelamin</label
|
||||
>
|
||||
<select
|
||||
v-model="filter.jenis_kelamin"
|
||||
class="select bg-white border border-gray-300 mt-1"
|
||||
>
|
||||
<option disabled selected value="initial">
|
||||
Pilih Jenis Kelamin
|
||||
</option>
|
||||
<option value="laki-laki">Laki-laki</option>
|
||||
<option value="perempuan">Perempuan</option>
|
||||
<option value="semua">Semua</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="h-full">
|
||||
<label for="golongan_darah" class="font-bold"
|
||||
>Golongan Darah</label
|
||||
>
|
||||
<form
|
||||
class="mt-1 flex flex-wrap gap-1 justify-center items-center"
|
||||
>
|
||||
<input
|
||||
v-for="(value, index) in FILTER.GOLONGAN_DARAH"
|
||||
:key="index"
|
||||
v-model="filter.gol_darah"
|
||||
class="btn btn-sm bg-light text-dark checked:bg-dark checked:border-dark checked:inset-shadow-sm checked:inset-shadow-black/70 checked:text-light"
|
||||
type="checkbox"
|
||||
name="golongan_darah"
|
||||
:aria-label="value"
|
||||
:value="value"
|
||||
/>
|
||||
<input
|
||||
@click="handleResetGolonganDarah"
|
||||
id="reset-golongan-darah"
|
||||
class="btn btn-sm bg-dark"
|
||||
type="reset"
|
||||
value="×"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<label for="kode_diagnosa" class="font-bold"
|
||||
>Kode Diagnosa</label
|
||||
>
|
||||
<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"
|
||||
type="text"
|
||||
name="kode_diagnosa"
|
||||
id="kode_diagnosa"
|
||||
placeholder="Masukkan Kode Diagnosa"
|
||||
v-model="filter.kode_diagnosa"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset class="fieldset mt-4">
|
||||
<label for="tindak_lanjut" class="font-bold text-sm"
|
||||
>Tindak Lanjut</label
|
||||
>
|
||||
<form class="flex flex-wrap gap-2 items-center">
|
||||
<input
|
||||
v-for="(value, index) in FILTER.TINDAK_LANJUT"
|
||||
:key="index"
|
||||
v-model="filter.tindak_lanjut"
|
||||
class="btn btn-xs bg-light text-dark checked:bg-dark checked:border-dark checked:inset-shadow-sm checked:inset-shadow-black/70 checked:text-light"
|
||||
type="checkbox"
|
||||
name="tindak_lanjut"
|
||||
:aria-label="value"
|
||||
:value="value"
|
||||
/>
|
||||
<input
|
||||
@click="handleResetTindakLanjut"
|
||||
id="reset-tindak-lanjut"
|
||||
class="btn btn-xs bg-dark btn-square"
|
||||
type="reset"
|
||||
value="×"
|
||||
/>
|
||||
</form>
|
||||
</fieldset>
|
||||
<div class="divider divider-neutral"></div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="handleResetFilter"
|
||||
class="btn btn-sm btn-outline btn-dark mr-2 hover:bg-dark hover:text-light active:inset-shadow-sm active:inset-shadow-black/50"
|
||||
>
|
||||
Reset Filter
|
||||
</button>
|
||||
<button
|
||||
@click="handleApplyFilter"
|
||||
class="btn btn-sm bg-dark hover:bg-light hover:text-dark active:inset-shadow-sm active:inset-shadow-black/50"
|
||||
>
|
||||
Terapkan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl shadow-sm">
|
||||
<div class="flex items-center px-4 py-4 justify-between gap-4">
|
||||
<SortDropdown
|
||||
v-model="sortBy"
|
||||
|
|
@ -242,7 +556,7 @@ onMounted(async () => {
|
|||
<!-- Data Table -->
|
||||
<DataTable
|
||||
:data="data"
|
||||
:columns="tableColumns"
|
||||
:columns="REKAM_MEDIS_TABLE_COLUMNS"
|
||||
:is-loading="api.isLoading.value"
|
||||
empty-message="Tidak ada data rekam medis"
|
||||
@details="handleDetails"
|
||||
|
|
@ -274,4 +588,152 @@ onMounted(async () => {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
#cally1 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* noUiSlider customization */
|
||||
:deep(.noUi-target) {
|
||||
background: #e5e7eb;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.noUi-connect) {
|
||||
background: #1a2a4f;
|
||||
}
|
||||
|
||||
:deep(.noUi-handle) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #1a2a4f;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
:deep(.noUi-handle):hover {
|
||||
background: #ed7979;
|
||||
transform: scale(1.1);
|
||||
.noUi-tooltip {
|
||||
display: block;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.noUi-handle):before,
|
||||
:deep(.noUi-handle):after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.noUi-tooltip) {
|
||||
background: #1a2a4f;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.noUi-active .noUi-tooltip) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.noUi-horizontal .noUi-handle) {
|
||||
top: -6px;
|
||||
right: -10px;
|
||||
}
|
||||
|
||||
calendar-range {
|
||||
&::part(button) {
|
||||
border: 1px solid #ed7979;
|
||||
background-color: #ed7979;
|
||||
border-radius: 8px;
|
||||
color: #1a2a4f;
|
||||
}
|
||||
|
||||
&::part(button):hover {
|
||||
transition: all 0.15s;
|
||||
border: 1px solid #b34242;
|
||||
background-color: #b34242;
|
||||
}
|
||||
|
||||
&::part(button):focus-visible {
|
||||
outline: 2px solid #1a2a4f;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
calendar-month {
|
||||
--color-accent: #1a2a4f;
|
||||
--color-text-on-accent: #fff2ef;
|
||||
--color-bg-today: #ed7979;
|
||||
--color-text-today: #1a2a4f;
|
||||
|
||||
/* Day buttons */
|
||||
&::part(button) {
|
||||
border-radius: 8px;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
&::part(button):hover {
|
||||
transition: all 0.15s;
|
||||
border: 1px solid #b34242;
|
||||
background-color: #b34242;
|
||||
}
|
||||
|
||||
/* Today button */
|
||||
&::part(today) {
|
||||
background-color: var(--color-bg-today);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Selected date */
|
||||
&::part(selected) {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-text-on-accent);
|
||||
}
|
||||
|
||||
/* Range styling for date ranges */
|
||||
&::part(range-inner) {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-text-on-accent);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&::part(range-start) {
|
||||
border-start-start-radius: 8px;
|
||||
border-end-start-radius: 8px;
|
||||
border-start-end-radius: 0;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
&::part(range-end) {
|
||||
border-start-end-radius: 8px;
|
||||
border-end-end-radius: 8px;
|
||||
border-start-start-radius: 0;
|
||||
border-end-start-radius: 0;
|
||||
}
|
||||
|
||||
&::part(range-start range-end) {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&::part(disabled) {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&::part(weekday) {
|
||||
color: var(--color-accent);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,16 @@ import { fileURLToPath, URL } from "node:url";
|
|||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), tailwindcss()],
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag.startsWith("calendar-"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user