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('orderBy') orderBy: string,
|
||||||
@Query('no_rm') no_rm: string,
|
@Query('no_rm') no_rm: string,
|
||||||
@Query('order') order: 'asc' | 'desc',
|
@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({
|
return this.rekammedisService.getAllRekamMedis({
|
||||||
take,
|
take,
|
||||||
|
|
@ -35,6 +45,16 @@ export class RekamMedisController {
|
||||||
orderBy,
|
orderBy,
|
||||||
no_rm,
|
no_rm,
|
||||||
order,
|
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()
|
@Injectable()
|
||||||
export class RekammedisService {
|
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) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
async getAllRekamMedis(params: {
|
async getAllRekamMedis(params: {
|
||||||
|
|
@ -15,8 +35,36 @@ export class RekammedisService {
|
||||||
orderBy?: any;
|
orderBy?: any;
|
||||||
no_rm?: string;
|
no_rm?: string;
|
||||||
order?: 'asc' | 'desc';
|
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 take = params.take ? parseInt(params.take.toString()) : 10;
|
||||||
const skipValue = skip
|
const skipValue = skip
|
||||||
? parseInt(skip.toString())
|
? parseInt(skip.toString())
|
||||||
|
|
@ -24,27 +72,140 @@ export class RekammedisService {
|
||||||
? (parseInt(page.toString()) - 1) * take
|
? (parseInt(page.toString()) - 1) * take
|
||||||
: 0;
|
: 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({
|
const results = await this.prisma.rekam_medis.findMany({
|
||||||
skip: skipValue,
|
skip: skipValue,
|
||||||
take: take,
|
take: take,
|
||||||
where: {
|
where: whereClause,
|
||||||
no_rm: no_rm ? no_rm : undefined,
|
|
||||||
},
|
|
||||||
orderBy: orderBy
|
orderBy: orderBy
|
||||||
? { [orderBy]: order || 'asc' }
|
? { [orderBy]: order || 'asc' }
|
||||||
: { waktu_visit: order ? order : 'asc' },
|
: { waktu_visit: order ? order : 'asc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const count = await this.prisma.rekam_medis.count({
|
const count = await this.prisma.rekam_medis.count({
|
||||||
where: {
|
where: whereClause,
|
||||||
no_rm: no_rm ? { contains: no_rm } : undefined,
|
});
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
...results,
|
...results,
|
||||||
totalCount: count,
|
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": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
|
"cally": "^0.8.0",
|
||||||
"daisyui": "^5.3.10",
|
"daisyui": "^5.3.10",
|
||||||
|
"nouislider": "^15.8.1",
|
||||||
"vee-validate": "^4.15.1",
|
"vee-validate": "^4.15.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "4",
|
"vue-router": "4",
|
||||||
|
|
@ -638,6 +640,12 @@
|
||||||
"node": ">=14"
|
"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": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.21",
|
"version": "10.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
"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": "^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": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001751",
|
"version": "1.0.30001751",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
|
||||||
|
|
@ -1005,6 +1022,12 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/path-browserify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
|
"cally": "^0.8.0",
|
||||||
"daisyui": "^5.3.10",
|
"daisyui": "^5.3.10",
|
||||||
|
"nouislider": "^15.8.1",
|
||||||
"vee-validate": "^4.15.1",
|
"vee-validate": "^4.15.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "4",
|
"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 ITEMS_PER_PAGE_OPTIONS = [5, 10, 25, 50, 100] as const;
|
||||||
|
|
||||||
export const DEFAULT_PAGE_SIZE = 10;
|
export const DEFAULT_PAGE_SIZE = 10;
|
||||||
|
|
||||||
export const DEBOUNCE_DELAY = 500; // milliseconds
|
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 = {
|
export const SORT_OPTIONS = {
|
||||||
OBAT: {
|
OBAT: {
|
||||||
id: "ID",
|
id: "ID",
|
||||||
|
|
@ -17,3 +47,51 @@ export const SORT_OPTIONS = {
|
||||||
umur: "Umur",
|
umur: "Umur",
|
||||||
},
|
},
|
||||||
} as const;
|
} 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,
|
DEBOUNCE_DELAY,
|
||||||
ITEMS_PER_PAGE_OPTIONS,
|
ITEMS_PER_PAGE_OPTIONS,
|
||||||
SORT_OPTIONS,
|
SORT_OPTIONS,
|
||||||
|
FILTER,
|
||||||
|
REKAM_MEDIS_TABLE_COLUMNS,
|
||||||
} from "../../constants/pagination";
|
} from "../../constants/pagination";
|
||||||
|
import "cally";
|
||||||
interface RekamMedis {
|
import noUiSlider from "nouislider";
|
||||||
id_visit: string;
|
import "nouislider/dist/nouislider.css";
|
||||||
no_rm: string;
|
import type { RekamMedis } from "../../constants/interfaces";
|
||||||
nama_pasien: string;
|
|
||||||
waktu_visit: Date;
|
|
||||||
umur: number;
|
|
||||||
jenis_kelamin: string;
|
|
||||||
gol_darah: string;
|
|
||||||
kode_diagnosa: string;
|
|
||||||
tindak_lanjut: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiResponse {
|
interface ApiResponse {
|
||||||
data: RekamMedis[];
|
data: RekamMedis[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
|
rangeUmur: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = ref<RekamMedis[]>([]);
|
const data = ref<RekamMedis[]>([]);
|
||||||
|
|
@ -47,54 +45,29 @@ const pagination = usePagination({
|
||||||
initialPage: Number(route.query.page) || 1,
|
initialPage: Number(route.query.page) || 1,
|
||||||
initialPageSize: Number(route.query.pageSize) || DEFAULT_PAGE_SIZE,
|
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 = [
|
const filter = ref<{
|
||||||
{
|
id_visit: string | null;
|
||||||
key: "id_visit" as keyof RekamMedis,
|
nama_pasien: string | null;
|
||||||
label: "ID Visit",
|
rentang_tanggal: { start: Date | null; end: Date | null };
|
||||||
class: "text-dark",
|
rentang_umur: [number, number];
|
||||||
},
|
jenis_kelamin: string | null;
|
||||||
{
|
kode_diagnosa: string;
|
||||||
key: "no_rm" as keyof RekamMedis,
|
gol_darah: string[];
|
||||||
label: "No RM",
|
tindak_lanjut: string[];
|
||||||
class: "text-dark",
|
}>({
|
||||||
},
|
id_visit: null,
|
||||||
{
|
nama_pasien: null,
|
||||||
key: "nama_pasien" as keyof RekamMedis,
|
rentang_tanggal: { start: null, end: null },
|
||||||
label: "Nama Pasien",
|
rentang_umur: [0, 100],
|
||||||
class: "text-dark",
|
jenis_kelamin: "initial",
|
||||||
},
|
kode_diagnosa: "",
|
||||||
{
|
gol_darah: [],
|
||||||
key: "waktu_visit" as keyof RekamMedis,
|
tindak_lanjut: [],
|
||||||
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 updateQueryParams = () => {
|
const updateQueryParams = () => {
|
||||||
const query: Record<string, string> = {
|
const query: Record<string, string> = {
|
||||||
|
|
@ -110,18 +83,126 @@ const updateQueryParams = () => {
|
||||||
query.sortBy = sortBy.value;
|
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 });
|
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 {
|
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,
|
||||||
...(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>(
|
const result = await api.get<ApiResponse>(
|
||||||
`/rekammedis?${queryParams.toString()}`
|
`/rekammedis?${queryParams.toString()}`
|
||||||
);
|
);
|
||||||
|
|
@ -129,9 +210,12 @@ const fetchData = async () => {
|
||||||
if ("data" in result && Array.isArray(result.data)) {
|
if ("data" in result && Array.isArray(result.data)) {
|
||||||
data.value = result.data;
|
data.value = result.data;
|
||||||
pagination.totalCount.value = result.totalCount;
|
pagination.totalCount.value = result.totalCount;
|
||||||
|
ageRange.value = [result.rangeUmur.min, result.rangeUmur.max];
|
||||||
} else {
|
} else {
|
||||||
const apiResponse = result as any;
|
const apiResponse = result as any;
|
||||||
pagination.totalCount.value = apiResponse.totalCount;
|
pagination.totalCount.value = apiResponse.totalCount;
|
||||||
|
ageRange.value = [apiResponse.rangeUmur.min, apiResponse.rangeUmur.max];
|
||||||
|
console.log("API Response:", ageRange.value);
|
||||||
|
|
||||||
const dataArray: RekamMedis[] = [];
|
const dataArray: RekamMedis[] = [];
|
||||||
Object.keys(apiResponse).forEach((key) => {
|
Object.keys(apiResponse).forEach((key) => {
|
||||||
|
|
@ -143,12 +227,14 @@ const fetchData = async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// console.log("Fetched data array:", dataArray);
|
// console.log("Fetched data array:", dataArray);
|
||||||
data.value = dataArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateQueryParams();
|
data.value = dataArray;
|
||||||
|
if (!isFirst) {
|
||||||
|
updateQueryParams();
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching obat data:", error);
|
console.error("Error fetching rekam medis data:", error);
|
||||||
data.value = [];
|
data.value = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -162,6 +248,8 @@ const handleSearch = () => {
|
||||||
|
|
||||||
const handleSortChange = (newSortBy: string) => {
|
const handleSortChange = (newSortBy: string) => {
|
||||||
sortBy.value = newSortBy;
|
sortBy.value = newSortBy;
|
||||||
|
const element = document.activeElement;
|
||||||
|
if (element && element instanceof HTMLElement) element.blur();
|
||||||
pagination.reset();
|
pagination.reset();
|
||||||
fetchData();
|
fetchData();
|
||||||
};
|
};
|
||||||
|
|
@ -212,8 +300,35 @@ onMounted(async () => {
|
||||||
sortBy.value = route.query.sortBy as string;
|
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";
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
@ -222,8 +337,207 @@ onMounted(async () => {
|
||||||
<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="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">
|
<div class="flex items-center px-4 py-4 justify-between gap-4">
|
||||||
<SortDropdown
|
<SortDropdown
|
||||||
v-model="sortBy"
|
v-model="sortBy"
|
||||||
|
|
@ -242,7 +556,7 @@ onMounted(async () => {
|
||||||
<!-- Data Table -->
|
<!-- Data Table -->
|
||||||
<DataTable
|
<DataTable
|
||||||
:data="data"
|
:data="data"
|
||||||
:columns="tableColumns"
|
:columns="REKAM_MEDIS_TABLE_COLUMNS"
|
||||||
:is-loading="api.isLoading.value"
|
:is-loading="api.isLoading.value"
|
||||||
empty-message="Tidak ada data rekam medis"
|
empty-message="Tidak ada data rekam medis"
|
||||||
@details="handleDetails"
|
@details="handleDetails"
|
||||||
|
|
@ -274,4 +588,152 @@ onMounted(async () => {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), tailwindcss()],
|
plugins: [
|
||||||
|
vue({
|
||||||
|
template: {
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement: (tag) => tag.startsWith("calendar-"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user