Template
remove website contents
This commit is contained in:
No files matched your search
@@ -493,7 +493,7 @@ const FooterMenu1: FooterType[] = [
|
||||
link: '/pages/pricing'
|
||||
},
|
||||
{
|
||||
menu: 'Account Settings',
|
||||
menu: 'Account 123',
|
||||
link: '/pages/account-settings'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Alamat Identitas -->
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
ALAMAT IDENTITAS
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<!-- Desa/Kelurahan -->
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localAlamatIdentitas.desa"
|
||||
label="Desa/Kelurahan"
|
||||
placeholder="Cari desa atau kelurahan..."
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Alamat Lengkap -->
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="localAlamatIdentitas.alamatLengkap"
|
||||
label="Alamat Lengkap"
|
||||
placeholder="Alamat"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
rows="3"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- RT -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatIdentitas.rt"
|
||||
label="RT"
|
||||
placeholder="RT"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="3"
|
||||
maxlength="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- RW -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatIdentitas.rw"
|
||||
label="RW"
|
||||
placeholder="RW"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="3"
|
||||
maxlength="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Kode Pos -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatIdentitas.kodePos"
|
||||
label="Kode Pos"
|
||||
placeholder="Kode Pos"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="5"
|
||||
maxlength="5"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Alamat Domisili -->
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
ALAMAT DOMISILI
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<!-- Checkbox Samakan dengan alamat identitas -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-checkbox
|
||||
v-model="samaDenganIdentitas"
|
||||
label="Samakan dengan alamat identitas"
|
||||
density="compact"
|
||||
@change="handleSamaDenganIdentitas"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="!samaDenganIdentitas">
|
||||
<!-- Desa/Kelurahan -->
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localAlamatDomisili.desa"
|
||||
label="Desa/Kelurahan"
|
||||
placeholder="Cari desa atau kelurahan..."
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Alamat Lengkap -->
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="localAlamatDomisili.alamatLengkap"
|
||||
label="Alamat Lengkap"
|
||||
placeholder="Alamat"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
rows="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- RT -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatDomisili.rt"
|
||||
label="RT"
|
||||
placeholder="RT"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="3"
|
||||
maxlength="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- RW -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatDomisili.rw"
|
||||
label="RW"
|
||||
placeholder="RW"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="3"
|
||||
maxlength="3"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Kode Pos -->
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
v-model="localAlamatDomisili.kodePos"
|
||||
label="Kode Pos"
|
||||
placeholder="Kode Pos"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
counter="5"
|
||||
maxlength="5"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
alamatIdentitas: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
alamatDomisili: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
samaDenganIdentitas: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:alamatIdentitas",
|
||||
"update:alamatDomisili",
|
||||
"update:samaDenganIdentitas"
|
||||
]);
|
||||
|
||||
const localAlamatIdentitas = ref({ ...props.alamatIdentitas });
|
||||
const localAlamatDomisili = ref({ ...props.alamatDomisili });
|
||||
const samaDenganIdentitas = ref(props.samaDenganIdentitas);
|
||||
|
||||
const rules = {
|
||||
required: (v) => !!v || "Field ini wajib diisi"
|
||||
};
|
||||
|
||||
const handleSamaDenganIdentitas = () => {
|
||||
if (samaDenganIdentitas.value) {
|
||||
localAlamatDomisili.value = { ...localAlamatIdentitas.value };
|
||||
} else {
|
||||
localAlamatDomisili.value = {
|
||||
desa: "",
|
||||
alamatLengkap: "",
|
||||
rt: "",
|
||||
rw: "",
|
||||
kodePos: ""
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes
|
||||
watch(
|
||||
localAlamatIdentitas,
|
||||
(newVal) => {
|
||||
emit("update:alamatIdentitas", newVal);
|
||||
if (samaDenganIdentitas.value) {
|
||||
localAlamatDomisili.value = { ...newVal };
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
localAlamatDomisili,
|
||||
(newVal) => {
|
||||
emit("update:alamatDomisili", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(samaDenganIdentitas, (newVal) => {
|
||||
emit("update:samaDenganIdentitas", newVal);
|
||||
});
|
||||
</script>
|
||||
@@ -1,516 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Section Header -->
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
DATA DIRI
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<!-- Checkbox Bayi Baru Lahir -->
|
||||
<v-row>
|
||||
<v-col cols="12" class="text-right py-0">
|
||||
<v-checkbox
|
||||
v-model="isBayiBaru"
|
||||
label="Bayi baru lahir"
|
||||
density="compact"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<!-- Photo Upload -->
|
||||
<v-col cols="12" md="2" class="text-center">
|
||||
<v-avatar size="100" color="grey-lighten-3">
|
||||
<v-icon size="50" color="grey">mdi-account</v-icon>
|
||||
</v-avatar>
|
||||
<v-btn
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="small"
|
||||
class="mt-2"
|
||||
@click="uploadPhoto"
|
||||
>
|
||||
Upload Foto
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<!-- Form Fields -->
|
||||
<v-col cols="12" md="10">
|
||||
<v-row>
|
||||
<!-- Nama Lengkap -->
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localData.namaLengkap"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Nama Lengkap"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
@input="debouncedParseNameToFhir"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
|
||||
<!-- FHIR Name Preview - Auto shows when typing -->
|
||||
<v-expand-transition>
|
||||
<v-alert
|
||||
v-if="fhirName && localData.namaLengkap && isTypingComplete"
|
||||
density="compact"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mt-2"
|
||||
closable
|
||||
@click:close="hideFhirPreview"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<span class="text-body-2">FHIR Name Structure</span>
|
||||
</template>
|
||||
|
||||
<div class="text-caption mt-2">
|
||||
<v-row dense>
|
||||
<v-col
|
||||
v-if="fhirName.prefix && fhirName.prefix.length > 0"
|
||||
cols="12"
|
||||
>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
class="mr-1"
|
||||
>
|
||||
Prefix
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{
|
||||
fhirName.prefix.join(", ")
|
||||
}}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="fhirName.given && fhirName.given.length > 0"
|
||||
cols="12"
|
||||
>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
class="mr-1"
|
||||
>
|
||||
Given
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{
|
||||
fhirName.given.join(" ")
|
||||
}}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="fhirName.family" cols="12">
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
class="mr-1"
|
||||
>
|
||||
Family
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{
|
||||
fhirName.family
|
||||
}}</span>
|
||||
</v-col>
|
||||
|
||||
<v-col
|
||||
v-if="fhirName.suffix && fhirName.suffix.length > 0"
|
||||
cols="12"
|
||||
>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
color="info"
|
||||
variant="outlined"
|
||||
class="mr-1"
|
||||
>
|
||||
Suffix
|
||||
</v-chip>
|
||||
<span class="font-weight-medium">{{
|
||||
fhirName.suffix.join(", ")
|
||||
}}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-expand-transition>
|
||||
|
||||
<!-- Optional: Inline chips preview -->
|
||||
<div
|
||||
v-if="
|
||||
fhirName &&
|
||||
localData.namaLengkap &&
|
||||
isTypingComplete &&
|
||||
!showDetailedPreview
|
||||
"
|
||||
class="mt-2"
|
||||
>
|
||||
<v-chip-group>
|
||||
<v-chip
|
||||
v-for="(prefix, index) in fhirName.prefix || []"
|
||||
:key="`prefix-${index}`"
|
||||
size="x-small"
|
||||
color="primary"
|
||||
>
|
||||
{{ prefix }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-for="(given, index) in fhirName.given || []"
|
||||
:key="`given-${index}`"
|
||||
size="x-small"
|
||||
color="success"
|
||||
>
|
||||
{{ given }}
|
||||
</v-chip>
|
||||
<v-chip v-if="fhirName.family" size="x-small" color="warning">
|
||||
{{ fhirName.family }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-for="(suffix, index) in fhirName.suffix || []"
|
||||
:key="`suffix-${index}`"
|
||||
size="x-small"
|
||||
color="info"
|
||||
>
|
||||
{{ suffix }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- Nomor Telepon Selular -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorTeleponSelular"
|
||||
label="Nomor Telepon Selular"
|
||||
placeholder="Nomor Telepon Selular"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required, rules.phone]"
|
||||
required
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
v-bind="props"
|
||||
class="mr-2"
|
||||
>
|
||||
+62
|
||||
<v-icon end>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item value="+62">+62</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Nomor Telepon Rumah -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorTeleponRumah"
|
||||
label="Nomor Telepon Rumah"
|
||||
placeholder="Nomor Telepon Rumah"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
v-bind="props"
|
||||
class="mr-2"
|
||||
>
|
||||
+62
|
||||
<v-icon end>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item value="+62">+62</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Email -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.email"
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.email]"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Jenis Kelamin -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.jenisKelamin"
|
||||
label="Jenis Kelamin"
|
||||
:items="jenisKelaminOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Tempat Lahir -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.tempatLahir"
|
||||
label="Tempat Lahir"
|
||||
placeholder="Tempat Lahir"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Tanggal Lahir -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.tanggalLahir"
|
||||
label="Tanggal Lahir"
|
||||
placeholder="Tanggal Lahir (DD/MM/YYYY)"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
>
|
||||
<template v-slot:append-inner>
|
||||
<v-icon>mdi-calendar</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Jenis Identitas -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.jenisIdentitas"
|
||||
label="Jenis Identitas"
|
||||
:items="jenisIdentitasOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Nomor Identitas -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorIdentitas"
|
||||
label="Nomor Identitas"
|
||||
placeholder="Nomor Identitas"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Nama Ibu Kandung -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.namaIbuKandung"
|
||||
label="Nama Ibu Kandung"
|
||||
placeholder="Nama Lengkap Ibu Kandung"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- No. Rekam Medis -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorRekamMedis"
|
||||
label="No. Rekam Medis Lama / Manual"
|
||||
placeholder="No. Rekam medis"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { debounce } from "lodash-es"; // or use VueUse's useDebounceFn
|
||||
import {
|
||||
parseFhirHumanName,
|
||||
validateFhirHumanName
|
||||
} from "~/utils/module/fhirNameParser";
|
||||
import type { FhirHumanName } from "~/types/fhir/humanName";
|
||||
|
||||
// Types
|
||||
interface DataDiriForm {
|
||||
photo: File | null;
|
||||
namaLengkap: string;
|
||||
nomorTeleponSelular: string;
|
||||
nomorTeleponRumah: string;
|
||||
email: string;
|
||||
jenisKelamin: string;
|
||||
tempatLahir: string;
|
||||
tanggalLahir: string;
|
||||
jenisIdentitas: string;
|
||||
nomorIdentitas: string;
|
||||
namaIbuKandung: string;
|
||||
nomorRekamMedis: string;
|
||||
fhirName?: FhirHumanName | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
formData: DataDiriForm;
|
||||
isBayiBaru: boolean;
|
||||
}
|
||||
|
||||
// Props & Emits
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:formData": [value: DataDiriForm];
|
||||
"update:isBayiBaru": [value: boolean];
|
||||
}>();
|
||||
|
||||
// Reactive data
|
||||
const localData = ref<DataDiriForm>({ ...props.formData });
|
||||
const isBayiBaru = ref(props.isBayiBaru);
|
||||
const fhirName = ref<FhirHumanName | null>(null);
|
||||
const isTypingComplete = ref(false);
|
||||
const showDetailedPreview = ref(true); // Toggle between detailed and chip view
|
||||
|
||||
// Rules
|
||||
const rules = {
|
||||
required: (v: string) => !!v || "Field ini wajib diisi",
|
||||
email: (v: string) => !v || /.+@.+\..+/.test(v) || "Format email tidak valid",
|
||||
phone: (v: string) =>
|
||||
!v || /^[0-9+\-() ]+$/.test(v) || "Format nomor telepon tidak valid"
|
||||
};
|
||||
|
||||
// Options
|
||||
interface SelectOption {
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const jenisKelaminOptions: SelectOption[] = [
|
||||
{ title: "Laki-laki", value: "L" },
|
||||
{ title: "Perempuan", value: "P" }
|
||||
];
|
||||
|
||||
const jenisIdentitasOptions: SelectOption[] = [
|
||||
{ title: "KTP", value: "KTP" },
|
||||
{ title: "SIM", value: "SIM" },
|
||||
{ title: "Paspor", value: "PASSPORT" }
|
||||
];
|
||||
|
||||
// Methods
|
||||
import { formatFhirName } from "~/utils/module/fhirNameParser";
|
||||
|
||||
const parseNameToFhir = (): void => {
|
||||
if (localData.value.namaLengkap && localData.value.namaLengkap.trim()) {
|
||||
fhirName.value = parseFhirHumanName(localData.value.namaLengkap);
|
||||
|
||||
if (fhirName.value) {
|
||||
const errors = validateFhirHumanName(fhirName.value);
|
||||
if (errors.length > 0) {
|
||||
console.warn("FHIR Name validation errors:", errors);
|
||||
}
|
||||
|
||||
// Store FHIR name in form data
|
||||
localData.value.fhirName = fhirName.value;
|
||||
|
||||
// Format the FHIR name back to a string and update namaLengkap
|
||||
const formattedName = formatFhirName(fhirName.value);
|
||||
if (formattedName !== localData.value.namaLengkap) {
|
||||
localData.value.namaLengkap = formattedName;
|
||||
}
|
||||
|
||||
// Show preview after parsing
|
||||
isTypingComplete.value = true;
|
||||
}
|
||||
} else {
|
||||
// Clear FHIR name if input is empty
|
||||
fhirName.value = null;
|
||||
localData.value.fhirName = null;
|
||||
isTypingComplete.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Debounced version of parseNameToFhir (wait 500ms after user stops typing)
|
||||
const debouncedParseNameToFhir = debounce(() => {
|
||||
parseNameToFhir();
|
||||
}, 500);
|
||||
|
||||
// Handle paste event
|
||||
const handlePaste = (event: ClipboardEvent): void => {
|
||||
// Reset typing complete flag
|
||||
isTypingComplete.value = false;
|
||||
|
||||
// Parse after a short delay to ensure v-model is updated
|
||||
setTimeout(() => {
|
||||
parseNameToFhir();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Hide FHIR preview
|
||||
const hideFhirPreview = (): void => {
|
||||
isTypingComplete.value = false;
|
||||
};
|
||||
|
||||
const uploadPhoto = (): void => {
|
||||
// Implementation for photo upload
|
||||
console.log("Upload photo clicked");
|
||||
};
|
||||
|
||||
// Watchers
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:formData", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(isBayiBaru, (newVal) => {
|
||||
emit("update:isBayiBaru", newVal);
|
||||
});
|
||||
|
||||
// Reset typing complete when name is cleared
|
||||
watch(
|
||||
() => localData.value.namaLengkap,
|
||||
(newVal) => {
|
||||
if (!newVal) {
|
||||
isTypingComplete.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Optional: Add smooth transitions */
|
||||
.v-chip-group {
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,100 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
KESEHATAN
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<!-- Golongan Darah -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.golonganDarah"
|
||||
label="Golongan Darah"
|
||||
:items="golonganDarahOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Status Perokok -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.statusPerokok"
|
||||
label="Status Perokok"
|
||||
:items="statusPerokokOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Jenis Penyakit -->
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="localData.jenisPenyakit"
|
||||
label="Jenis Penyakit"
|
||||
:items="jenisPenyakitOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
placeholder="Pilih Jenis Penyakit"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
|
||||
const localData = ref({ ...props.formData });
|
||||
|
||||
const golonganDarahOptions = [
|
||||
{ title: "A", value: "A" },
|
||||
{ title: "B", value: "B" },
|
||||
{ title: "AB", value: "AB" },
|
||||
{ title: "O", value: "O" }
|
||||
];
|
||||
|
||||
const statusPerokokOptions = [
|
||||
{ title: "Tidak Merokok", value: "tidak" },
|
||||
{ title: "Perokok Aktif", value: "aktif" },
|
||||
{ title: "Perokok Pasif", value: "pasif" },
|
||||
{ title: "Mantan Perokok", value: "mantan" }
|
||||
];
|
||||
|
||||
const jenisPenyakitOptions = [
|
||||
"Diabetes",
|
||||
"Hipertensi",
|
||||
"Jantung",
|
||||
"Asma",
|
||||
"TBC",
|
||||
"Hepatitis",
|
||||
"HIV/AIDS",
|
||||
"Kanker",
|
||||
"Ginjal",
|
||||
"Stroke"
|
||||
];
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:formData", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-subtitle class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2">
|
||||
PEMBAYARAN
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="localData.jenisPembayaran"
|
||||
label="Pembayaran"
|
||||
:items="pembayaranOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:formData'])
|
||||
|
||||
const localData = ref({ ...props.formData })
|
||||
|
||||
const rules = {
|
||||
required: v => !!v || 'Field ini wajib diisi'
|
||||
}
|
||||
|
||||
const pembayaranOptions = [
|
||||
{ title: 'BPJS', value: 'BPJS' },
|
||||
{ title: 'Umum', value: 'UMUM' },
|
||||
{ title: 'Asuransi', value: 'ASURANSI' },
|
||||
{ title: 'Perusahaan', value: 'PERUSAHAAN' }
|
||||
]
|
||||
|
||||
watch(localData, (newVal) => {
|
||||
emit('update:formData', newVal)
|
||||
}, { deep: true })
|
||||
</script>
|
||||
@@ -1,167 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
PENANGGUNG JAWAB / WALI PASIEN
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<!-- Nama Lengkap -->
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localData.namaLengkap"
|
||||
label="Nama Lengkap"
|
||||
placeholder="Nama Lengkap"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Jenis Kelamin -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.jenisKelamin"
|
||||
label="Jenis Kelamin"
|
||||
:items="jenisKelaminOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Hubungan dengan Pasien -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.hubunganDenganPasien"
|
||||
label="Hubungan dengan Pasien"
|
||||
:items="hubunganOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Nomor Telepon -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.nomorTelepon"
|
||||
label="Nomor Telepon Selular / Rumah"
|
||||
placeholder="Nomor Telepon"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-menu>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
v-bind="props"
|
||||
class="mr-2"
|
||||
>
|
||||
+62
|
||||
<v-icon end>mdi-menu-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item value="+62">+62</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Pekerjaan -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.pekerjaan"
|
||||
label="Pekerjaan"
|
||||
placeholder="Pekerjaan"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Alamat -->
|
||||
<v-col cols="12">
|
||||
<label class="text-subtitle-2 font-weight-medium">Alamat</label>
|
||||
<v-checkbox
|
||||
v-model="samaDenganPasien"
|
||||
label="Samakan dengan alamat pasien"
|
||||
density="compact"
|
||||
@change="handleSamakanAlamat"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" v-if="!samaDenganPasien">
|
||||
<v-textarea
|
||||
v-model="localData.alamat"
|
||||
placeholder="Alamat"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
rows="3"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
samaDenganPasien: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
alamatPasien: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData", "update:samaDenganPasien"]);
|
||||
|
||||
const localData = ref({ ...props.formData });
|
||||
const samaDenganPasien = ref(props.samaDenganPasien);
|
||||
|
||||
const jenisKelaminOptions = [
|
||||
{ title: "Laki-laki", value: "L" },
|
||||
{ title: "Perempuan", value: "P" }
|
||||
];
|
||||
|
||||
const hubunganOptions = [
|
||||
"Suami",
|
||||
"Istri",
|
||||
"Anak",
|
||||
"Orang Tua",
|
||||
"Saudara",
|
||||
"Kerabat",
|
||||
"Lainnya"
|
||||
];
|
||||
|
||||
const handleSamakanAlamat = () => {
|
||||
if (samaDenganPasien.value && props.alamatPasien) {
|
||||
localData.value.alamat = props.alamatPasien.alamatLengkap || "";
|
||||
} else {
|
||||
localData.value.alamat = "";
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:formData", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(samaDenganPasien, (newVal) => {
|
||||
emit("update:samaDenganPasien", newVal);
|
||||
});
|
||||
</script>
|
||||
@@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-subtitle
|
||||
class="bg-grey-lighten-4 text-subtitle-2 font-weight-medium py-2"
|
||||
>
|
||||
SOSIAL
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<!-- Agama -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.agama"
|
||||
label="Agama"
|
||||
:items="agamaOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Status Pernikahan -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.statusPernikahan"
|
||||
label="Status Pernikahan"
|
||||
:items="statusPernikahanOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Pendidikan Terakhir -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.pendidikanTerakhir"
|
||||
label="Pendidikan Terakhir"
|
||||
:items="pendidikanOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Pekerjaan -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.pekerjaan"
|
||||
label="Pekerjaan"
|
||||
:items="pekerjaanOptions"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Bahasa yang Dikuasai -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.bahasaDikuasai"
|
||||
label="Bahasa yang Dikuasai"
|
||||
placeholder="Bahasa yang Dikuasai"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<!-- Suku/Etnis -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.sukuEtnis"
|
||||
label="Suku/Etnis"
|
||||
placeholder="Suku/Etnis"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
formData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:formData"]);
|
||||
|
||||
const localData = ref({ ...props.formData });
|
||||
|
||||
const agamaOptions = [
|
||||
"Islam",
|
||||
"Kristen",
|
||||
"Katolik",
|
||||
"Hindu",
|
||||
"Buddha",
|
||||
"Konghucu",
|
||||
"Lainnya"
|
||||
];
|
||||
|
||||
const statusPernikahanOptions = [
|
||||
{ title: "Belum Menikah", value: "belum_menikah" },
|
||||
{ title: "Menikah", value: "menikah" },
|
||||
{ title: "Cerai Hidup", value: "cerai_hidup" },
|
||||
{ title: "Cerai Mati", value: "cerai_mati" }
|
||||
];
|
||||
|
||||
const pendidikanOptions = [
|
||||
"Tidak Sekolah",
|
||||
"SD",
|
||||
"SMP",
|
||||
"SMA/SMK",
|
||||
"D1",
|
||||
"D3",
|
||||
"D4/S1",
|
||||
"S2",
|
||||
"S3"
|
||||
];
|
||||
|
||||
const pekerjaanOptions = [
|
||||
"PNS",
|
||||
"TNI/Polri",
|
||||
"Pegawai Swasta",
|
||||
"Wiraswasta",
|
||||
"Petani",
|
||||
"Nelayan",
|
||||
"Buruh",
|
||||
"Ibu Rumah Tangga",
|
||||
"Pelajar/Mahasiswa",
|
||||
"Tidak Bekerja",
|
||||
"Lainnya"
|
||||
];
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:formData", newVal);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -1,227 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h6">NAKES & JADWAL</span>
|
||||
<v-btn variant="text" color="primary" size="small" @click="resetSection">
|
||||
Atur Ulang
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.paymentMethod"
|
||||
:items="paymentMethods"
|
||||
label="Pembiayaan"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Pembiayaan <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.referralNumber"
|
||||
label="Nomor Rujukan (Opsional)"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Nomor Rujukan"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="localData.polyclinicName"
|
||||
:items="polyclinics"
|
||||
label="Nama Poliklinik"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
:rules="[rules.required]"
|
||||
@update:modelValue="onPolyclinicChange"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select
|
||||
v-model="localData.doctorName"
|
||||
:items="doctors"
|
||||
label="Nama Nakes"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
:rules="[rules.required]"
|
||||
:disabled="!localData.polyclinicName"
|
||||
@update:modelValue="onDoctorChange"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.consultationDate"
|
||||
label="Tanggal Konsultasi"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
type="date"
|
||||
:rules="[rules.required]"
|
||||
:min="minDate"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Tanggal Konsultasi <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.consultationTime"
|
||||
:items="timeSlots"
|
||||
label="Jam Konsultasi"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih Jam Konsultasi"
|
||||
:rules="[rules.required]"
|
||||
:disabled="!localData.consultationDate || !localData.doctorName"
|
||||
append-inner-icon="mdi-clock-outline"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Jam Konsultasi <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="localData.doctorName && localData.consultationDate">
|
||||
<v-col cols="12">
|
||||
<v-alert type="info" variant="tonal" density="compact">
|
||||
Slot Nakes: {{ availableSlots }} slot tersedia
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
scheduleData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:scheduleData", "update"]);
|
||||
|
||||
const localData = ref({ ...props.scheduleData });
|
||||
|
||||
const paymentMethods = ["Pribadi", "BPJS", "Asuransi Swasta", "Perusahaan"];
|
||||
|
||||
const polyclinics = [
|
||||
"Poli Umum",
|
||||
"Poli Gigi",
|
||||
"Poli Anak",
|
||||
"Poli Kandungan",
|
||||
"Poli Penyakit Dalam",
|
||||
"Poli THT"
|
||||
];
|
||||
|
||||
const doctors = ref([]);
|
||||
const timeSlots = ref([]);
|
||||
const availableSlots = ref(0);
|
||||
|
||||
const minDate = computed(() => {
|
||||
const today = new Date();
|
||||
return today.toISOString().split("T")[0];
|
||||
});
|
||||
|
||||
const rules = {
|
||||
required: (value) => !!value || "Field ini wajib diisi"
|
||||
};
|
||||
|
||||
const onPolyclinicChange = (value) => {
|
||||
// Reset doctor and time when polyclinic changes
|
||||
localData.value.doctorName = "";
|
||||
localData.value.consultationTime = "";
|
||||
|
||||
// Simulate loading doctors based on polyclinic
|
||||
if (value) {
|
||||
doctors.value = [
|
||||
"dr. Ahmad Subhan, Sp.A",
|
||||
"dr. Siti Nurhaliza",
|
||||
"dr. Budi Santoso, Sp.PD",
|
||||
"dr. Maria Christina"
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const onDoctorChange = (value) => {
|
||||
// Reset time when doctor changes
|
||||
localData.value.consultationTime = "";
|
||||
|
||||
// Simulate loading time slots
|
||||
if (value && localData.value.consultationDate) {
|
||||
loadTimeSlots();
|
||||
}
|
||||
};
|
||||
|
||||
const loadTimeSlots = () => {
|
||||
// Simulate available time slots
|
||||
timeSlots.value = [
|
||||
{ title: "08:00 - 08:30", value: "08:00" },
|
||||
{ title: "08:30 - 09:00", value: "08:30" },
|
||||
{ title: "09:00 - 09:30", value: "09:00" },
|
||||
{ title: "09:30 - 10:00", value: "09:30" },
|
||||
{ title: "10:00 - 10:30", value: "10:00" },
|
||||
{ title: "10:30 - 11:00", value: "10:30" }
|
||||
];
|
||||
availableSlots.value = 6;
|
||||
};
|
||||
|
||||
const resetSection = () => {
|
||||
localData.value = {
|
||||
paymentMethod: "Pribadi",
|
||||
referralNumber: "",
|
||||
polyclinicName: "",
|
||||
doctorName: "",
|
||||
consultationDate: "",
|
||||
consultationTime: "",
|
||||
slotAvailable: true
|
||||
};
|
||||
doctors.value = [];
|
||||
timeSlots.value = [];
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:scheduleData", newVal);
|
||||
emit("update");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => localData.value.consultationDate,
|
||||
(newVal) => {
|
||||
if (newVal && localData.value.doctorName) {
|
||||
loadTimeSlots();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -1,249 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h6">DATA PASIEN</span>
|
||||
<v-btn variant="text" color="primary" size="small" @click="resetSection">
|
||||
Atur Ulang
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="localData.name"
|
||||
label="Nama Pasien"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Masukkan Nama Pasien"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.medicalRecordNumber"
|
||||
label="No. Rekam medis"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="MRN Pasien"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.category"
|
||||
:items="patientCategories"
|
||||
label="Kategori Pasien"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih Kategori Pasien"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.birthDate"
|
||||
label="Tanggal Lahir"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
type="date"
|
||||
placeholder="Tanggal Lahir (DD/MM/YYYY)"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Tanggal Lahir <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.gender"
|
||||
:items="genderOptions"
|
||||
label="Jenis Kelamin"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
:rules="[rules.required]"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Jenis Kelamin <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="localData.address"
|
||||
label="Alamat"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
rows="3"
|
||||
placeholder="Alamat"
|
||||
:rules="[rules.required]"
|
||||
counter="100"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Alamat <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="2">
|
||||
<v-select
|
||||
v-model="localData.phoneCode"
|
||||
:items="phoneCodes"
|
||||
label="Kode"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Kode <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="5">
|
||||
<v-text-field
|
||||
v-model="localData.phoneNumber"
|
||||
label="Nomor Telepon"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Nomor Telepon"
|
||||
:rules="[rules.required, rules.phone]"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Nomor Telepon <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="5">
|
||||
<v-text-field
|
||||
v-model="localData.email"
|
||||
label="Email"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Email"
|
||||
:rules="[rules.email]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.identityType"
|
||||
:items="identityTypes"
|
||||
label="Jenis Identitas"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
:rules="[rules.required]"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Jenis Identitas <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="localData.identityNumber"
|
||||
label="Nomor Identitas"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Nomor Identitas"
|
||||
:rules="[rules.required]"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Nomor Identitas <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
patientData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:patientData", "update"]);
|
||||
|
||||
const localData = ref({ ...props.patientData });
|
||||
|
||||
const patientCategories = ["Umum", "BPJS", "Asuransi Lain"];
|
||||
|
||||
const genderOptions = [
|
||||
{ title: "Laki-laki", value: "L" },
|
||||
{ title: "Perempuan", value: "P" }
|
||||
];
|
||||
|
||||
const phoneCodes = ["+62", "+65", "+60", "+1"];
|
||||
|
||||
const identityTypes = [
|
||||
{ title: "KTP", value: "ktp" },
|
||||
{ title: "SIM", value: "sim" },
|
||||
{ title: "Paspor", value: "paspor" },
|
||||
{ title: "Kartu Pelajar", value: "kartu_pelajar" }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
required: (value) => !!value || "Field ini wajib diisi",
|
||||
email: (value) => {
|
||||
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return !value || pattern.test(value) || "Email tidak valid";
|
||||
},
|
||||
phone: (value) => {
|
||||
const pattern = /^[0-9]{8,15}$/;
|
||||
return pattern.test(value) || "Nomor telepon tidak valid";
|
||||
}
|
||||
};
|
||||
|
||||
const resetSection = () => {
|
||||
localData.value = {
|
||||
name: "",
|
||||
medicalRecordNumber: "",
|
||||
category: "",
|
||||
birthDate: "",
|
||||
gender: "",
|
||||
address: "",
|
||||
phoneCode: "+62",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
identityType: "",
|
||||
identityNumber: ""
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:patientData", newVal);
|
||||
emit("update");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span class="text-h6">JENIS PENDAFTARAN</span>
|
||||
<v-btn variant="text" color="primary" size="small" @click="resetSection">
|
||||
Atur Ulang
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<div class="mb-2">
|
||||
<span class="text-subtitle-2">
|
||||
Jenis Kunjungan <span class="text-red">*</span>
|
||||
</span>
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="localData.visitType"
|
||||
:rules="[rules.required]"
|
||||
inline
|
||||
>
|
||||
<v-radio label="Kunjungan Sakit" value="sakit" />
|
||||
<v-radio label="Kunjungan Sehat" value="sehat" />
|
||||
</v-radio-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.treatmentType"
|
||||
:items="treatmentTypes"
|
||||
label="Jenis Perawatan"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
>
|
||||
<template v-slot:label>
|
||||
Jenis Perawatan <span class="text-red">*</span>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="localData.triage"
|
||||
:items="triageOptions"
|
||||
label="Triase"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
placeholder="Pilih"
|
||||
clearable
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
registrationData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:registrationData", "update"]);
|
||||
|
||||
const localData = ref({ ...props.registrationData });
|
||||
|
||||
const treatmentTypes = ["Rawat Jalan", "Rawat Inap", "IGD"];
|
||||
|
||||
const triageOptions = [
|
||||
{ title: "Merah (Emergency)", value: "merah" },
|
||||
{ title: "Kuning (Urgent)", value: "kuning" },
|
||||
{ title: "Hijau (Less Urgent)", value: "hijau" },
|
||||
{ title: "Hitam (Non Urgent)", value: "hitam" }
|
||||
];
|
||||
|
||||
const rules = {
|
||||
required: (value) => !!value || "Field ini wajib diisi"
|
||||
};
|
||||
|
||||
const resetSection = () => {
|
||||
localData.value = {
|
||||
visitType: "",
|
||||
treatmentType: "Rawat Jalan",
|
||||
triage: ""
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:registrationData", newVal);
|
||||
emit("update");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -1,159 +0,0 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<span class="text-h6">SKALA RISIKO JATUH (OPSIONAL)</span>
|
||||
<v-icon color="warning" size="small">mdi-alert-box</v-icon>
|
||||
</div>
|
||||
<v-btn variant="text" color="primary" size="small" @click="resetSection">
|
||||
Atur Ulang
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-expansion-panels v-model="panel" class="mt-2">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>
|
||||
<span class="font-weight-medium"
|
||||
>SKALA RISIKO JATUH - GET UP AND GO</span
|
||||
>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-card flat>
|
||||
<v-card-text>
|
||||
<!-- Walking Ability Question -->
|
||||
<div class="mb-6">
|
||||
<div class="mb-3">
|
||||
<p class="text-subtitle-1 font-weight-medium mb-1">
|
||||
Cara berjalan pasien:
|
||||
</p>
|
||||
<ul class="ml-4">
|
||||
<li>Jalan tidak seimbang, sempoyongan, atau limbung</li>
|
||||
<li>
|
||||
Menggunakan alat bantu (kruk, tripot, kursi roda, orang
|
||||
lain)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="localData.walkingAbility"
|
||||
inline
|
||||
@update:modelValue="calculateScore"
|
||||
>
|
||||
<v-radio label="Ya" :value="true" />
|
||||
<v-radio label="Tidak" :value="false" />
|
||||
</v-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- Sitting Support Question -->
|
||||
<div class="mb-6">
|
||||
<div class="mb-3">
|
||||
<p class="text-subtitle-1 font-weight-medium mb-1">
|
||||
Menopang saat akan duduk
|
||||
</p>
|
||||
<p class="text-body-2 text-grey-darken-1">
|
||||
Tampak memegang sandaran kursi atau meja / benda lain
|
||||
sebagai penopang saat akan duduk
|
||||
</p>
|
||||
</div>
|
||||
<v-radio-group
|
||||
v-model="localData.supportWhileSitting"
|
||||
inline
|
||||
@update:modelValue="calculateScore"
|
||||
>
|
||||
<v-radio label="Ya" :value="true" />
|
||||
<v-radio label="Tidak" :value="false" />
|
||||
</v-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- Score Display -->
|
||||
<v-divider class="my-4" />
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<span class="text-subtitle-1 font-weight-medium">Skor</span>
|
||||
<v-chip :color="scoreColor" variant="tonal" size="large">
|
||||
{{ localData.score }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- Risk Level Alert -->
|
||||
<v-alert
|
||||
v-if="riskLevel"
|
||||
:type="riskLevel.type"
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
class="mt-4"
|
||||
>
|
||||
{{ riskLevel.message }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
riskData: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:riskData", "update"]);
|
||||
|
||||
const localData = ref({ ...props.riskData });
|
||||
const panel = ref(0);
|
||||
|
||||
const calculateScore = () => {
|
||||
let score = 0;
|
||||
if (localData.value.walkingAbility === true) score += 1;
|
||||
if (localData.value.supportWhileSitting === true) score += 1;
|
||||
localData.value.score = score;
|
||||
};
|
||||
|
||||
const scoreColor = computed(() => {
|
||||
if (localData.value.score === 0) return "success";
|
||||
if (localData.value.score === 1) return "warning";
|
||||
return "error";
|
||||
});
|
||||
|
||||
const riskLevel = computed(() => {
|
||||
if (localData.value.score === 0) {
|
||||
return {
|
||||
type: "success",
|
||||
message: "Risiko Rendah: Tidak berisiko jatuh"
|
||||
};
|
||||
} else if (localData.value.score === 1) {
|
||||
return {
|
||||
type: "warning",
|
||||
message: "Risiko Sedang: Memerlukan pengawasan"
|
||||
};
|
||||
} else if (localData.value.score === 2) {
|
||||
return {
|
||||
type: "error",
|
||||
message: "Risiko Tinggi: Memerlukan bantuan dan pengawasan ketat"
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const resetSection = () => {
|
||||
localData.value = {
|
||||
walkingAbility: null,
|
||||
supportWhileSitting: null,
|
||||
score: 0
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
localData,
|
||||
(newVal) => {
|
||||
emit("update:riskData", newVal);
|
||||
emit("update");
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
@@ -1,73 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { Form } from 'vee-validate';
|
||||
|
||||
/*Social icons*/
|
||||
import google from '@/assets/images/svgs/google-icon.svg';
|
||||
import facebook from '@/assets/images/svgs/facebook-icon.svg';
|
||||
|
||||
const checkbox = ref(false);
|
||||
const valid = ref(false);
|
||||
const show1 = ref(false);
|
||||
const password = ref('admin123');
|
||||
const username = ref('[email protected]');
|
||||
const passwordRules = ref([
|
||||
(v: string) => !!v || 'Password is required',
|
||||
(v: string) => (v && v.length <= 10) || 'Password must be less than 10 characters'
|
||||
]);
|
||||
const emailRules = ref([(v: string) => !!v || 'E-mail is required', (v: string) => /.+@.+\..+/.test(v) || 'E-mail must be valid']);
|
||||
|
||||
function validate(values: any, { setErrors }: any) {
|
||||
const authStore = useAuthStore();
|
||||
return authStore.login(username.value, password.value).catch((error) => setErrors({ apiError: error }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row class="d-flex mb-3">
|
||||
<v-col cols="6" sm="6" class="pr-2">
|
||||
<v-btn variant="outlined" size="large" class="border text-subtitle-1 font-weight-semibold hover-link-primary" block>
|
||||
<img :src="facebook" width="20" class="mr-1" alt="facebook" />
|
||||
<span class="d-md-flex d-none mr-1">Sign in with</span> Facebook
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="6" class="pl-2">
|
||||
<v-btn variant="outlined" size="large" class="border text-subtitle-1 font-weight-semibold hover-link-primary" block>
|
||||
<img :src="google" height="16" class="mr-2" alt="google" />
|
||||
<span class="d-md-flex d-none mr-1">Sign in with</span> Google
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex align-center text-center mb-6">
|
||||
<div class="text-h6 w-100 px-5 font-weight-regular auth-divider position-relative">
|
||||
<span class="bg-surface px-5 py-3 position-relative textSecondary">Or sign in with email</span>
|
||||
</div>
|
||||
</div>
|
||||
<Form @submit="validate" v-slot="{ errors, isSubmitting }" class="mt-5">
|
||||
<v-label class="font-weight-semibold pb-2 ">Username</v-label>
|
||||
<VTextField v-model="username" :rules="emailRules" class="mb-8" required hide-details="auto"></VTextField>
|
||||
<div class="d-flex justify-space-between align-center pb-2">
|
||||
<v-label class="font-weight-semibold ">Password</v-label>
|
||||
<RouterLink to="/auth/forgot-password2" class="text-primary text-decoration-none font-weight-medium">Forgot Password ?</RouterLink>
|
||||
</div>
|
||||
|
||||
<VTextField v-model="password" :rules="passwordRules" required hide-details="auto" type="password" class="pwdInput"></VTextField>
|
||||
<div class="d-flex flex-wrap align-center my-3 ml-n2">
|
||||
<v-checkbox
|
||||
class="pe-2"
|
||||
v-model="checkbox"
|
||||
:rules="[(v: any) => !!v || 'You must agree to continue!']"
|
||||
required
|
||||
hide-details
|
||||
color="primary"
|
||||
>
|
||||
<template v-slot:label class="font-weight-medium">Keep me logged in</template>
|
||||
</v-checkbox>
|
||||
</div>
|
||||
<v-btn size="large" :loading="isSubmitting" color="darkgray" :disabled="valid" block type="submit" flat>Sign In</v-btn>
|
||||
<div v-if="errors.apiError" class="mt-2">
|
||||
<v-alert color="error">{{ errors.apiError }}</v-alert>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -1,61 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Logo from '@/layouts/full/logo/Logo.vue';
|
||||
/*Social icons*/
|
||||
import google from '@/assets/images/svgs/google-icon.svg';
|
||||
import facebook from '@/assets/images/svgs/facebook-icon.svg';
|
||||
|
||||
const checkbox = ref(false);
|
||||
const valid = ref(true);
|
||||
const show1 = ref(false);
|
||||
const password = ref('');
|
||||
const email = ref('');
|
||||
const passwordRules = ref([
|
||||
(v: string) => !!v || 'Password is required',
|
||||
(v: string) => (v && v.length <= 10) || 'Password must be less than 10 characters'
|
||||
]);
|
||||
const emailRules = ref([(v: string) => !!v || 'E-mail is required', (v: string) => /.+@.+\..+/.test(v) || 'E-mail must be valid']);
|
||||
const fname = ref('');
|
||||
const fnameRules = ref([
|
||||
(v: string) => !!v || 'Name is required',
|
||||
(v: string) => (v && v.length <= 10) || 'Name must be less than 10 characters'
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<v-row class="d-flex mb-6">
|
||||
<v-col cols="6" sm="6" class="pr-2">
|
||||
<v-btn variant="outlined" size="large" class="border text-subtitle-1 font-weight-semibold hover-link-primary" block>
|
||||
<img :src="facebook" width="20" class="mr-1" alt="facebook" />
|
||||
<span class="d-md-flex d-none mr-1">Sign in with</span> Facebook
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="6" class="pl-2">
|
||||
<v-btn variant="outlined" size="large" class="border text-subtitle-1 font-weight-semibold hover-link-primary" block>
|
||||
<img :src="google" height="16" class="mr-2" alt="google" />
|
||||
<span class="d-md-flex d-none mr-1">Sign in with</span> Google
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex align-center text-center mb-6">
|
||||
<div class="text-h6 w-100 px-5 font-weight-regular auth-divider position-relative">
|
||||
<span class="bg-surface px-5 py-3 position-relative">Or sign up with email</span>
|
||||
</div>
|
||||
</div>
|
||||
<v-form ref="form" v-model="valid" lazy-validation action="/pages/boxedlogin" class="mt-5">
|
||||
<v-label class="font-weight-medium pb-2">Name</v-label>
|
||||
<VTextField v-model="fname" :rules="fnameRules" required ></VTextField>
|
||||
<v-label class="font-weight-medium pb-2">Email Adddress</v-label>
|
||||
<VTextField v-model="email" :rules="emailRules" required ></VTextField>
|
||||
<v-label class="font-weight-medium pb-2">Password</v-label>
|
||||
<VTextField
|
||||
v-model="password"
|
||||
:counter="10"
|
||||
:rules="passwordRules"
|
||||
required
|
||||
variant="outlined"
|
||||
type="password"
|
||||
color="primary"
|
||||
></VTextField>
|
||||
<v-btn size="large" class="mt-2" color="darkgray" block submit flat>Sign Up</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
@@ -1,75 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Form } from 'vee-validate';
|
||||
|
||||
/*Social icons*/
|
||||
import google from '@/assets/images/svgs/google-icon.svg';
|
||||
import facebook from '@/assets/images/svgs/facebook-icon.svg';
|
||||
|
||||
const checkbox = ref(false);
|
||||
const valid = ref(false);
|
||||
const show1 = ref(false);
|
||||
const password = ref('admin123');
|
||||
const username = ref('[email protected]');
|
||||
const passwordRules = ref([
|
||||
(v: string) => !!v || 'Password is required',
|
||||
(v: string) => (v && v.length <= 10) || 'Password must be less than 10 characters'
|
||||
]);
|
||||
const emailRules = ref([(v: string) => !!v || 'E-mail is required', (v: string) => /.+@.+\..+/.test(v) || 'E-mail must be valid']);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row class="d-flex mb-3">
|
||||
<v-col cols="6" sm="6" class="pr-2">
|
||||
<v-btn variant="outlined" size="large" class="border text-subtitle-1 hover-link-primary" block>
|
||||
<img :src="google" height="16" class="mr-2" alt="google" />
|
||||
Google
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="6" class="pl-2">
|
||||
<v-btn variant="outlined" size="large" class="border text-subtitle-1 hover-link-primary" block>
|
||||
<img :src="facebook" width="20" class="mr-1" alt="facebook" />
|
||||
Facebook
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex align-center text-center mb-6">
|
||||
<div class="text-h6 w-100 px-5 font-weight-regular auth-divider position-relative">
|
||||
<span class="bg-surface px-5 py-3 position-relative">or sign in with</span>
|
||||
</div>
|
||||
</div>
|
||||
<Form class="mt-5">
|
||||
<v-label class="font-weight-semibold pb-2 ">Username</v-label>
|
||||
<VTextField
|
||||
v-model="username"
|
||||
:rules="emailRules"
|
||||
class="mb-8"
|
||||
required
|
||||
hide-details="auto"
|
||||
></VTextField>
|
||||
<v-label class="font-weight-semibold pb-2 ">Password</v-label>
|
||||
<VTextField
|
||||
v-model="password"
|
||||
:rules="passwordRules"
|
||||
required
|
||||
hide-details="auto"
|
||||
type="password"
|
||||
class="pwdInput"
|
||||
></VTextField>
|
||||
<div class="d-flex flex-wrap align-center my-3 ml-n2">
|
||||
<v-checkbox class="pe-2" v-model="checkbox" :rules="[(v:any) => !!v || 'You must agree to continue!']" required hide-details color="primary">
|
||||
<template v-slot:label class="font-weight-medium">Remeber this Device</template>
|
||||
</v-checkbox>
|
||||
<div class="ml-sm-auto">
|
||||
<RouterLink to="" class="text-primary text-decoration-none font-weight-medium"
|
||||
>Forgot Password ?</RouterLink
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn size="large" color="primary" :disabled="valid" block type="submit" flat>Sign In</v-btn>
|
||||
<div class="mt-2">
|
||||
<v-alert color="error"></v-alert>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
@@ -1,61 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Logo from '~/components/layout/full/logo/Logo.vue';
|
||||
/*Social icons*/
|
||||
import google from '~/assets/images/svgs/google-icon.svg';
|
||||
import facebook from '~/assets/images/svgs/facebook-icon.svg';
|
||||
|
||||
const checkbox = ref(false);
|
||||
const valid = ref(true);
|
||||
const show1 = ref(false);
|
||||
const password = ref('');
|
||||
const email = ref('');
|
||||
const passwordRules = ref([
|
||||
(v: string) => !!v || 'Password is required',
|
||||
(v: string) => (v && v.length <= 10) || 'Password must be less than 10 characters'
|
||||
]);
|
||||
const emailRules = ref([(v: string) => !!v || 'E-mail is required', (v: string) => /.+@.+\..+/.test(v) || 'E-mail must be valid']);
|
||||
const fname = ref('');
|
||||
const fnameRules = ref([
|
||||
(v: string) => !!v || 'Name is required',
|
||||
(v: string) => (v && v.length <= 10) || 'Name must be less than 10 characters'
|
||||
]);
|
||||
</script>
|
||||
<template>
|
||||
<v-row class="d-flex mb-6">
|
||||
<v-col cols="6" sm="6" class="pr-2">
|
||||
<v-btn variant="outlined" size="large" class="border text-subtitle-1 hover-link-primary" block>
|
||||
<img :src="google" height="16" class="mr-2" alt="google" />
|
||||
Google
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="6" class="pl-2">
|
||||
<v-btn variant="outlined" size="large" class="border text-subtitle-1 hover-link-primary" block>
|
||||
<img :src="facebook" width="20" class="mr-1" alt="facebook" />
|
||||
Facebook
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<div class="d-flex align-center text-center mb-6">
|
||||
<div class="text-h6 w-100 px-5 font-weight-regular auth-divider position-relative">
|
||||
<span class="bg-surface px-5 py-3 position-relative">or sign in with</span>
|
||||
</div>
|
||||
</div>
|
||||
<v-form ref="form" v-model="valid" lazy-validation action="/pages/boxedlogin" class="mt-5">
|
||||
<v-label class=" font-weight-medium pb-2">Name</v-label>
|
||||
<VTextField v-model="fname" :rules="fnameRules" required ></VTextField>
|
||||
<v-label class=" font-weight-medium pb-2">Email Adddress</v-label>
|
||||
<VTextField v-model="email" :rules="emailRules" required ></VTextField>
|
||||
<v-label class=" font-weight-medium pb-2">Password</v-label>
|
||||
<VTextField
|
||||
v-model="password"
|
||||
:counter="10"
|
||||
:rules="passwordRules"
|
||||
required
|
||||
variant="outlined"
|
||||
type="password"
|
||||
color="primary"
|
||||
></VTextField>
|
||||
<v-btn size="large" class="mt-2" color="primary" block submit flat>Sign Up</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Logo from '@/layouts/full/logo/Logo.vue';
|
||||
const valid = ref(true);
|
||||
const show1 = ref(false);
|
||||
const email = ref('');
|
||||
const emailRules = ref([(v: string) => !!v || 'E-mail is required', (v: string) => /.+@.+\..+/.test(v) || 'E-mail must be valid']);
|
||||
</script>
|
||||
<template>
|
||||
<v-form ref="form" v-model="valid" lazy-validation action="/dashboards/analytical" class="mt-6">
|
||||
<v-label class="font-weight-semibold pb-2">Email Address</v-label>
|
||||
<VTextField v-model="email" :rules="emailRules" required ></VTextField>
|
||||
<v-btn size="large" color="darkgray" to="/" block submit flat>Forgot Password</v-btn>
|
||||
</v-form>
|
||||
</template>
|
||||
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<div class="mt-6">
|
||||
<v-label class="text-subtitle-1 font-weight-semibold pb-2 text-lightText">Type your 6 digits security code</v-label>
|
||||
<div class="d-flex justify-space-between gap-3 mb-2 verification">
|
||||
<VTextField></VTextField>
|
||||
<VTextField></VTextField>
|
||||
<VTextField></VTextField>
|
||||
<VTextField></VTextField>
|
||||
<VTextField></VTextField>
|
||||
<VTextField></VTextField>
|
||||
</div>
|
||||
<v-btn color="darkgray" size="large" block flat>Verify My Account</v-btn>
|
||||
<h6 class="text-16 text-medium-emphasis d-flex align-center mt-6" >
|
||||
Didn't get the code?
|
||||
<RouterLink to="/" class="pl-0 text-primary text-16 opacity-1 pl-2 text-decoration-none"> Resend</RouterLink>
|
||||
</h6>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,186 +0,0 @@
|
||||
<template>
|
||||
<div class="menu-tree-item">
|
||||
<v-row
|
||||
no-gutters
|
||||
class="pa-2 border-b"
|
||||
:class="{
|
||||
'bg-grey-lighten-4': level > 0,
|
||||
'bg-red-lighten-5': !item.isActive
|
||||
}"
|
||||
align="center"
|
||||
>
|
||||
<!-- Menu Title with Icon -->
|
||||
<v-col cols="6" sm="4" class="d-flex align-center">
|
||||
<div :style="{ marginLeft: `${level * 20}px` }" class="d-flex align-center">
|
||||
<!-- Expand/Collapse Button -->
|
||||
<v-btn
|
||||
v-if="item.children && item.children.length > 0"
|
||||
icon
|
||||
size="x-small"
|
||||
variant="text"
|
||||
@click="toggleExpanded"
|
||||
class="me-1"
|
||||
>
|
||||
<v-icon>{{ isExpanded ? 'mdi-chevron-down' : 'mdi-chevron-right' }}</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<!-- Menu Icon -->
|
||||
<v-icon
|
||||
:icon="item.icon || 'mdi-circle-outline'"
|
||||
size="small"
|
||||
class="me-2"
|
||||
:color="item.isActive ? 'primary' : 'grey'"
|
||||
/>
|
||||
|
||||
<!-- Menu Title -->
|
||||
<span
|
||||
class="text-body-2 font-weight-medium"
|
||||
:class="{ 'text-grey': !item.isActive }"
|
||||
>
|
||||
{{ item.title }}
|
||||
</span>
|
||||
|
||||
<!-- Active Status Badge -->
|
||||
<v-chip
|
||||
v-if="!item.isActive"
|
||||
size="x-small"
|
||||
color="error"
|
||||
class="ml-2"
|
||||
>
|
||||
Inactive
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<!-- URL Link (Hidden on mobile) -->
|
||||
<v-col cols="4" sm="5" class="d-none d-sm-block">
|
||||
<span class="text-body-2 text-grey-darken-1">
|
||||
{{ item.url || '-' }}
|
||||
</span>
|
||||
</v-col>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<v-col cols="6" sm="3" class="text-right">
|
||||
<v-btn-group variant="text" density="compact">
|
||||
<!-- Toggle Active/Inactive -->
|
||||
<v-btn
|
||||
:icon="item.isActive ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
size="small"
|
||||
:color="item.isActive ? 'success' : 'warning'"
|
||||
@click="$emit('toggle', item.id)"
|
||||
>
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ item.isActive ? 'Active' : 'Inactive' }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<v-btn
|
||||
icon="mdi-pencil"
|
||||
size="small"
|
||||
color="primary"
|
||||
@click="$emit('edit', item)"
|
||||
>
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">Edit</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<v-btn
|
||||
icon="mdi-delete"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">Delete</v-tooltip>
|
||||
</v-btn>
|
||||
</v-btn-group>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Child Items -->
|
||||
<template v-if="item.children && isExpanded">
|
||||
<MenuTreeItem
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:item="child"
|
||||
:level="level + 1"
|
||||
@edit="$emit('edit', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
@toggle="$emit('toggle', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">Confirm Delete</v-card-title>
|
||||
<v-card-text>
|
||||
Are you sure you want to delete "{{ item.title }}"?
|
||||
<span v-if="item.children && item.children.length > 0" class="text-error">
|
||||
This will also delete all child items.
|
||||
</span>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" variant="text" @click="showDeleteDialog = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn color="error" variant="elevated" @click="handleDelete">
|
||||
Delete
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem } from '../../types/menu'
|
||||
|
||||
interface Props {
|
||||
item: MenuItem
|
||||
level: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
edit: [item: MenuItem]
|
||||
delete: [itemId: string]
|
||||
toggle: [itemId: string]
|
||||
}>()
|
||||
|
||||
const isExpanded = ref(props.level === 0) // Top level items expanded by default
|
||||
const showDeleteDialog = ref(false)
|
||||
|
||||
const toggleExpanded = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
const confirmDelete = () => {
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
showDeleteDialog.value = false
|
||||
// Emit delete event
|
||||
emit('delete', props.item.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-tree-item {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.menu-tree-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { BlogCardData } from '@/data/dashboard/dashboardData';
|
||||
</script>
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" lg="4" v-for="card in BlogCardData" :key="card.title">
|
||||
<v-card elevation="10" rounded="md" class="card-hover">
|
||||
<div>
|
||||
<v-img :src="card.coveravatar" height="250px" cover class="rounded-t-md align-end text-right">
|
||||
<v-card-item
|
||||
><v-chip class="bg-surface text-body-2 font-weight-medium" size="small" rounded="sm" v-text="card.read"></v-chip
|
||||
></v-card-item>
|
||||
</v-img>
|
||||
<v-avatar size="40" class="mt-n7 mx-6">
|
||||
<img :src="card.avatar" alt="icon" height="40" />
|
||||
</v-avatar>
|
||||
<v-card-item class="pt-4">
|
||||
<v-chip class="text-body-2 font-weight-medium bg-grey100" size="small" rounded="sm" v-text="card.category"></v-chip>
|
||||
<h5 class="text-h5 text-13 my-6 custom-text-primary">
|
||||
<NuxtLink class="text-decoration-none color-inherits custom-title" :to="card.link">{{ card.title }}</NuxtLink>
|
||||
</h5>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<v-avatar class="" size="18">
|
||||
<EyeIcon size="18" class="text-textPrimary" />
|
||||
</v-avatar>
|
||||
<span class="text-subtitle-1 ml-2 text-textSecondary" v-text="card.view"></span>
|
||||
<v-avatar class="ml-4" size="18">
|
||||
<Message2Icon size="18" class="text-textPrimary" />
|
||||
</v-avatar>
|
||||
<span class="text-subtitle-1 ml-2 text-textSecondary" v-text="card.comments"></span>
|
||||
</div>
|
||||
<div>
|
||||
<v-avatar size="10">
|
||||
<CircleIcon size="10" class="text-textPrimary" />
|
||||
</v-avatar>
|
||||
<span class="text-subtitle-2 ml-2 text-textSecondary" v-text="card.time"></span>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-item>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { DailyActivitiesData } from '@/data/dashboard/dashboardData';
|
||||
import { Icon } from '@iconify/vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card elevation="10">
|
||||
<v-card-item>
|
||||
<v-card-title class="text-h5">Daily activities</v-card-title>
|
||||
<div class="daily-activities mt-8 px-3">
|
||||
<div v-for="list in DailyActivitiesData" :key="list.title">
|
||||
<v-row class="d-flex mb-1">
|
||||
<v-col cols="4" lg="3" md="auto" sm="auto" class="px-0 pt-0 pb-0 d-flex align-start">
|
||||
<p class="text-body-1 text-textSecondary text-no-wrap">{{ list.title }}</p>
|
||||
</v-col>
|
||||
<v-col cols="1" sm="1" class="px-0 text-center pt-0 pb-0 mt-1">
|
||||
<Icon icon="tabler:circle-filled" size="13" :class="'text-' + list.textcolor" />
|
||||
<div v-if="list.line" class="line mx-auto bg-grey100"></div>
|
||||
</v-col>
|
||||
<v-col cols="7" sm="8" class="pt-0 pb-0">
|
||||
<h6 v-if="list.boldtext" class="text-body-1 text-textPrimary">{{ list.subtitle }}</h6>
|
||||
<p v-else class="text-body-1 text-textPrimary">{{ list.subtitle }}</p>
|
||||
<div class="mt-n1">
|
||||
<NuxtLink :to="list.url" class="text-body-1 text-primary text-decoration-none" v-if="list.link">{{
|
||||
list.link
|
||||
}}</NuxtLink>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-item>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
</script>
|
||||
<template>
|
||||
<v-card elevation="10">
|
||||
<v-card-item>
|
||||
<div class="d-flex ga-3 align-center">
|
||||
<v-avatar size="48" class="rounded-md bg-lightsecondary">
|
||||
<Icon icon="solar:football-outline" class="text-secondary" height="25" />
|
||||
</v-avatar>
|
||||
<h6 class="text-h6 heading">New Customers</h6>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center justify-space-between mb-3 mt-12">
|
||||
<h5 class="text-textPrimary text-subtitle-1 font-weight-medium">New Goals</h5>
|
||||
<div class="text-textPrimary text-subtitle-1 font-weight-medium">83%</div>
|
||||
</div>
|
||||
<v-progress-linear model-value="83" height="7" color="secondary" bg-color="lightsecondary" rounded></v-progress-linear>
|
||||
</v-card-item>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -1,115 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
const select = ref('Sept 2025');
|
||||
const items = ref(['Sept 2025', 'Oct 2025', 'Nov 2025']);
|
||||
/* Chart */
|
||||
const chartOptions = computed(() => {
|
||||
return {
|
||||
chart: {
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
type: 'bar',
|
||||
fontFamily: 'inherit',
|
||||
foreColor: '#adb0bb',
|
||||
height: 285,
|
||||
stacked: true,
|
||||
offsetX: -15
|
||||
},
|
||||
colors: ['rgba(var(--v-theme-primary))', 'rgba(var(--v-theme-error))'],
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
barHeight: '60%',
|
||||
columnWidth: '15%',
|
||||
borderRadius: [6],
|
||||
borderRadiusApplication: 'end',
|
||||
borderRadiusWhenStacked: 'all'
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
grid: {
|
||||
show: true,
|
||||
padding: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0
|
||||
},
|
||||
borderColor: 'rgba(0,0,0,0.05)',
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: true
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
lines: {
|
||||
show: true
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
min: -5,
|
||||
max: 5,
|
||||
tickAmount: 4
|
||||
},
|
||||
xaxis: {
|
||||
axisBorder: {
|
||||
show: false
|
||||
},
|
||||
axisTicks: {
|
||||
show: false
|
||||
},
|
||||
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'July', 'Aug', 'Sep'],
|
||||
labels: {
|
||||
style: { fontSize: '13px', colors: '#adb0bb', fontWeight: '400' }
|
||||
}
|
||||
},
|
||||
|
||||
tooltip: {
|
||||
theme: 'dark'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const Chart = {
|
||||
series: [
|
||||
{
|
||||
name: '2025',
|
||||
data: [1.2, 2.7, 1, 3.6, 2.1, 2.7, 2.2, 1.3, 2.5]
|
||||
},
|
||||
{
|
||||
name: '2023',
|
||||
data: [-2.8, -1.1, -2.5, -1.5, -2.3, -1.9, -1, -2.1, -1.3]
|
||||
}
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<v-card elevation="10">
|
||||
<v-card-item>
|
||||
<div class="d-md-flex justify-space-between mb-mb-0 mb-3">
|
||||
<v-card-title class="text-h5">Revenue Forecast</v-card-title>
|
||||
<div>
|
||||
<v-select
|
||||
v-model="select"
|
||||
:items="items"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="text-body-1"
|
||||
hide-details
|
||||
></v-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx-n1 mt-4 pt-2">
|
||||
<apexchart type="bar" height="285" class="rounded-bars" :options="chartOptions" :series="Chart.series"> </apexchart>
|
||||
</div>
|
||||
</v-card-item>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -1,54 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { RevenueProjectsData } from '@/data/dashboard/dashboardData';
|
||||
</script>
|
||||
<template>
|
||||
<v-card elevation="10" class="revenue-products">
|
||||
<v-card-item class="pb-4">
|
||||
<div class="d-flex ga-3 align-center justify-space-between">
|
||||
<v-card-title class="text-h5">Revenue by Product</v-card-title>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<v-table class="revenue-table">
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-body-1">Assigned</th>
|
||||
<th class="text-body-1">Progress</th>
|
||||
<th class="text-body-1">Priority</th>
|
||||
<th class="text-body-1">Budget</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in RevenueProjectsData" :key="item.leadname" class="month-item">
|
||||
<td>
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar size="48" rounded="md"> <img :src="item.img" :alt="item.img" width="48" /></v-avatar>
|
||||
<div class="mx-3">
|
||||
<h6 class="text-subtitle-1 text-no-wrap font-weight-medium">{{ item.leadname }}</h6>
|
||||
<span class="text-body-1 text-no-wrap text-textSecondary">{{ item.designation }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-no-wrap text-body-1 text-textSecondary">
|
||||
{{ item.projectname }}
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<v-chip rounded="sm" class="font-weight-semibold" :color="item.statuscolor" size="small" label>{{
|
||||
item.statustext
|
||||
}}</v-chip>
|
||||
</td>
|
||||
<td>
|
||||
<p class="text-body-1">{{ item.money }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-table>
|
||||
</div>
|
||||
</v-card-item>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
/* Chart */
|
||||
const chartOptions = computed(() => {
|
||||
return {
|
||||
chart: {
|
||||
type: 'line',
|
||||
fontFamily: 'inherit',
|
||||
foreColor: '#adb0bb',
|
||||
height: 60,
|
||||
sparkline: {
|
||||
enabled: true
|
||||
},
|
||||
group: 'sparklines'
|
||||
},
|
||||
colors:['rgba(var(--v-theme-error))'],
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 2
|
||||
},
|
||||
markers: {
|
||||
size: 0
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark',
|
||||
fixed: {
|
||||
enabled: true,
|
||||
position: 'right'
|
||||
},
|
||||
x: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const Chart = {
|
||||
series: [
|
||||
{
|
||||
name: 'Income',
|
||||
data: [30, 25, 35, 20, 30, 40]
|
||||
}
|
||||
]
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<v-card elevation="10">
|
||||
<v-card-item>
|
||||
<div class="d-flex ga-3 align-center">
|
||||
<v-avatar size="48" class="rounded-md bg-lighterror">
|
||||
<Icon icon="solar:box-linear" class="text-error" height="25" />
|
||||
</v-avatar>
|
||||
<h6 class="text-h6 heading">Total Income</h6>
|
||||
</div>
|
||||
<v-row class="mt-6">
|
||||
<v-col cols="6">
|
||||
<h3 class="text-h3 heading">$680</h3>
|
||||
<div class="text-success text-subtitle-2 font-weight-medium mt-2">+18%</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<apexchart type="line" height="60" :options="chartOptions" :series="Chart.series"> </apexchart>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-item>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -1,154 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { useCustomizerStore } from '~/store/customizer';
|
||||
import {
|
||||
CheckIcon,
|
||||
LayoutColumnsIcon,
|
||||
LayoutDistributeHorizontalIcon,
|
||||
LayoutDistributeVerticalIcon,
|
||||
LayoutNavbarIcon,
|
||||
LayoutSidebarLeftCollapseIcon,
|
||||
TextDirectionLtrIcon,
|
||||
TextDirectionRtlIcon
|
||||
} from 'vue-tabler-icons';
|
||||
|
||||
const theme = useTheme();
|
||||
const customizer = useCustomizerStore();
|
||||
|
||||
// template skin color options
|
||||
const themeColors = ref([
|
||||
{
|
||||
name: 'BLUE_THEME',
|
||||
bg: 'themeBlue'
|
||||
},
|
||||
{
|
||||
name: 'AQUA_THEME',
|
||||
bg: 'themeAqua'
|
||||
},
|
||||
{
|
||||
name: 'PURPLE_THEME',
|
||||
bg: 'themePurple'
|
||||
},
|
||||
{
|
||||
name: 'GREEN_THEME',
|
||||
bg: 'themeGreen'
|
||||
},
|
||||
{
|
||||
name: 'CYAN_THEME',
|
||||
bg: 'themeCyan'
|
||||
},
|
||||
{
|
||||
name: 'ORANGE_THEME',
|
||||
bg: 'themeOrange'
|
||||
}
|
||||
]);
|
||||
|
||||
// Dark Theme Colors
|
||||
const DarkthemeColors = ref([
|
||||
{ name: 'DARK_BLUE_THEME', bg: 'themeDarkBlue' },
|
||||
{ name: 'DARK_AQUA_THEME', bg: 'themeDarkAqua' },
|
||||
{ name: 'DARK_PURPLE_THEME', bg: 'themeDarkPurple' },
|
||||
{ name: 'DARK_GREEN_THEME', bg: 'themeDarkGreen' },
|
||||
{ name: 'DARK_CYAN_THEME', bg: 'themeDarkCyan' },
|
||||
{ name: 'DARK_ORANGE_THEME', bg: 'themeDarkOrange' }
|
||||
]);
|
||||
</script>
|
||||
|
||||
<!------------------------------------->
|
||||
<!-- Customizer -->
|
||||
<!------------------------------------->
|
||||
<template>
|
||||
<div class="pa-6">
|
||||
<h5 class="text-h5">Settings</h5>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
<perfect-scrollbar style="height: calc(100vh - 90px)">
|
||||
<div class="pa-6">
|
||||
<h6 class="text-h6 mb-2">Sidebar Layout</h6>
|
||||
<v-btn-toggle v-model="customizer.setHorizontalLayout" color="primary" class="my-2 btn-group-custom gap-3" rounded="0" group>
|
||||
<v-btn :value="false" variant="text" elevation="9" class="rounded-md">
|
||||
<LayoutColumnsIcon stroke-width="1.5" size="21" class="mr-2 icon" /> Vertical
|
||||
</v-btn>
|
||||
<v-btn :value="true" variant="text" elevation="9" class="rounded-md">
|
||||
<LayoutNavbarIcon stroke-width="1.5" size="21" class="mr-2 icon" /> Horizontal
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
|
||||
<!------Template Direction------>
|
||||
|
||||
<h6 class="text-h6 mt-8 mb-5">Template Color</h6>
|
||||
<v-item-group mandatory v-model="customizer.actTheme" class="ml-n2 v-row">
|
||||
<v-col cols="4" v-for="theme in themeColors" :key="theme.name" class="pa-2">
|
||||
<v-item v-slot="{ isSelected, toggle }" :value="theme.name">
|
||||
<v-sheet
|
||||
rounded="md"
|
||||
class="border cursor-pointer d-block text-center px-5 py-4 hover-btns"
|
||||
elevation="9"
|
||||
@click="toggle"
|
||||
>
|
||||
<v-avatar :class="theme.bg" size="25" variant="text">
|
||||
<CheckIcon color="white" size="18" v-if="isSelected" />
|
||||
</v-avatar>
|
||||
</v-sheet>
|
||||
</v-item>
|
||||
</v-col>
|
||||
</v-item-group>
|
||||
<h6 class="text-h6 mt-11 mb-5">Template Dark Color</h6>
|
||||
<v-item-group mandatory v-model="customizer.actTheme" class="ml-n2 v-row">
|
||||
<v-col cols="4" v-for="theme in DarkthemeColors" :key="theme.name" class="pa-2">
|
||||
<v-item v-slot="{ isSelected, toggle }" :value="theme.name">
|
||||
<v-sheet
|
||||
rounded="md"
|
||||
class="border cursor-pointer d-block text-center px-5 py-4 hover-btns"
|
||||
elevation="9"
|
||||
@click="toggle"
|
||||
>
|
||||
<v-avatar :class="theme.bg" size="25">
|
||||
<CheckIcon color="white" size="18" v-if="isSelected" />
|
||||
</v-avatar>
|
||||
</v-sheet>
|
||||
</v-item>
|
||||
</v-col>
|
||||
</v-item-group>
|
||||
<h6 class="text-h6 mt-11 mb-2">Container Option</h6>
|
||||
<v-btn-toggle v-model="customizer.boxed" color="primary" class="my-2 btn-group-custom gap-3" rounded="0" group>
|
||||
<v-btn :value="true" variant="text" elevation="9" class="rounded-md">
|
||||
<LayoutDistributeVerticalIcon stroke-width="1.5" size="21" class="mr-2 icon" />
|
||||
Boxed
|
||||
</v-btn>
|
||||
<v-btn :value="false" variant="text" elevation="9" class="rounded-md ">
|
||||
<LayoutDistributeHorizontalIcon stroke-width="1.5" size="21" class="mr-2 icon" />
|
||||
Full
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
<!---Horizontal demo hide this option --->
|
||||
<v-sheet v-if="customizer.setHorizontalLayout != true">
|
||||
<h6 class="text-h6 mt-11 mb-2">Sidebar Type</h6>
|
||||
<v-btn-toggle v-model="customizer.mini_sidebar" color="primary" class="my-2 btn-group-custom gap-3" rounded="0" group>
|
||||
<v-btn :value="false" variant="text" elevation="9" class="rounded-md">
|
||||
<LayoutSidebarIcon stroke-width="1.5" size="21" class="mr-2 icon" />
|
||||
Full
|
||||
</v-btn>
|
||||
<v-btn :value="true" variant="text" elevation="9" class="rounded-md">
|
||||
<LayoutSidebarLeftCollapseIcon stroke-width="1.5" size="21" class="mr-2 icon" />
|
||||
Collapse
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-sheet>
|
||||
<h6 class="text-h6 mt-11 mb-2">Card with</h6>
|
||||
<v-btn-toggle v-model="customizer.setBorderCard" color="primary" class="my-2 btn-group-custom gap-3" rounded="0" group>
|
||||
<v-btn :value="false" variant="text" elevation="9" class="rounded-md">
|
||||
<LayoutSidebarLeftCollapseIcon stroke-width="1.5" size="21" class="mr-2 icon" />
|
||||
Shadow
|
||||
</v-btn>
|
||||
<v-btn :value="true" variant="text" elevation="9" class="rounded-md">
|
||||
<LayoutSidebarIcon stroke-width="1.5" size="21" class="mr-2 icon" />
|
||||
Border
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
</perfect-scrollbar>
|
||||
</template>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -1,47 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { languageDD } from '@/_mockApis/headerData';
|
||||
import flag1 from '~/assets/images/flag/icon-flag-en.svg';
|
||||
import flag2 from '~/assets/images/flag/icon-flag-ro.svg';
|
||||
import flag3 from '~/assets/images/flag/icon-flag-zh.svg';
|
||||
import flag4 from '~/assets/images/flag/icon-flag-fr.svg';
|
||||
</script>
|
||||
<template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- language DD -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<v-menu open-on-hover open-on-click>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon class="custom-hover-primary" size="small" variant="text" color="primary" v-bind="props">
|
||||
<v-avatar size="20">
|
||||
<img v-if="$i18n.locale === 'en'" :src="flag1" :alt="$i18n.locale" width="22" height="22" class="obj-cover" />
|
||||
<img v-if="$i18n.locale === 'fr'" :src="flag4" :alt="$i18n.locale" width="22" height="22" class="obj-cover" />
|
||||
<img v-if="$i18n.locale === 'ro'" :src="flag2" :alt="$i18n.locale" width="22" height="22" class="obj-cover" />
|
||||
<img v-if="$i18n.locale === 'zh'" :src="flag3" :alt="$i18n.locale" width="22" height="22" class="obj-cover" />
|
||||
</v-avatar>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet rounded="md" width="200" elevation="10">
|
||||
<v-list class="theme-list">
|
||||
<v-list-item
|
||||
v-for="(item, index) in languageDD"
|
||||
:key="index"
|
||||
color="primary"
|
||||
:active="$i18n.locale == item.value"
|
||||
class="d-flex align-center"
|
||||
@click="() => ($i18n.locale = item.value)"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="22">
|
||||
<img :src="item.avatar" :alt="item.avatar" width="22" height="22" class="obj-cover" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="text-subtitle-1 font-weight-regular">
|
||||
{{ item.title }}
|
||||
<span class="text-disabled text-subtitle-1 pl-2">({{ item.subtext }})</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- mega menu DD -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<v-menu open-on-hover open-on-click >
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="hidden-sm-and-down custom-hover-primary" size="small" variant="text" icon color="primary" v-bind="props"> <Icon icon="solar:widget-3-line-duotone" height="20" /> </v-btn>
|
||||
</template>
|
||||
<v-sheet width="900" height="300" elevation="10" rounded="md" class="pa-0 overflow-hidden">
|
||||
<div>
|
||||
<v-row>
|
||||
<v-col cols="12" md="8" class="d-flex">
|
||||
<div class="pa-6">
|
||||
<LazyLayoutFullVerticalHeaderAppsLink />
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" class="pa-0">
|
||||
<img src="@/assets/images/backgrounds/mega-dd-bg.jpg" alt="matdash-img" height="320" class="w-100" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
s
|
||||
@@ -1,43 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {notifications} from '~/_mockApis/headerData';
|
||||
import { Icon } from '@iconify/vue';
|
||||
</script>
|
||||
<template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- notifications DD -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<v-menu open-on-hover open-on-click >
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon class="custom-hover-primary" size="small" variant="text" color="primary" v-bind="props">
|
||||
<Icon icon="solar:bell-bing-line-duotone" height="22" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet rounded="md" width="360" elevation="10">
|
||||
<div class="px-6 pt-6 pb-4">
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<h6 class="text-h5">Notifications</h6>
|
||||
<v-chip color="primary" variant="flat" size="small" class="text-white rounded-sm">5 New</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<perfect-scrollbar style="height: 350px">
|
||||
<v-list class="py-0 theme-list" lines="two">
|
||||
<v-list-item v-for="item in notifications" :key="item.title" :value="item" color="primary" class="py-3 px-6">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="45" :color=" 'light' + item.color" rounded="circle">
|
||||
<Icon :icon="'solar:' + item.avatar" height="20" :class="'text-' + item.color"/>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div class="d-flex justify-space-between">
|
||||
<h6 class="text-subtitle-1 heading mb-1">{{ item.title }}</h6>
|
||||
<span class="text-subtitle-2 textSecondary">{{ item.time }}</span>
|
||||
</div>
|
||||
<p class="text-subtitle-2 font-weight-regular textSecondary">{{ item.subtitle }}</p>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</perfect-scrollbar>
|
||||
<div class="py-4 px-6 text-center">
|
||||
<v-btn color="primary" variant="flat" size="large" block>See all Notifications</v-btn>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
@@ -1,227 +0,0 @@
|
||||
<!-- components/layout/ProfileDD.vue -->
|
||||
<script setup lang="ts">
|
||||
import { MailIcon } from "vue-tabler-icons";
|
||||
import { profileDD } from "~/_mockApis/headerData";
|
||||
import { PerfectScrollbar } from "vue3-perfect-scrollbar";
|
||||
import { useUserInfo } from "~/composables/useUserInfo";
|
||||
import { computed } from "vue";
|
||||
|
||||
const userInfo = useUserInfo();
|
||||
|
||||
// Enhanced logout with proper error handling
|
||||
const logout = async () => {
|
||||
try {
|
||||
// Use the updated logout method from useUserInfo that handles Keycloak logout and session clearing
|
||||
await userInfo.logout({
|
||||
reason: "idle",
|
||||
confirmDialog: false // Show confirmation dialog
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Logout from profile failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// **TAMBAHAN: Full logout function dengan konfirmasi**
|
||||
const fullLogout = async () => {
|
||||
try {
|
||||
// Tampilkan konfirmasi sebelum full logout
|
||||
const confirmed = confirm(
|
||||
"Apakah Anda yakin ingin keluar dari semua sesi? Ini akan menghapus semua data lokal dan sesi Keycloak."
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
console.log("Initiating full logout from ProfileDD...");
|
||||
|
||||
// Gunakan fullLogout dari useUserInfo composable
|
||||
await userInfo.fullLogout();
|
||||
} catch (error) {
|
||||
console.error("Full logout from profile failed:", error);
|
||||
|
||||
// Fallback jika fullLogout gagal
|
||||
try {
|
||||
console.log("Attempting fallback logout...");
|
||||
await userInfo.logout({
|
||||
reason: "manual",
|
||||
clearStorage: true
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
console.error("Fallback logout also failed:", fallbackError);
|
||||
|
||||
// Last resort - force redirect
|
||||
if (process.client) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.href = "/auth/login?reason=force";
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// **TAMBAHAN: Logout dengan konfirmasi untuk UX yang lebih baik**
|
||||
const logoutWithConfirmation = async () => {
|
||||
try {
|
||||
const confirmed = confirm("Apakah Anda yakin ingin keluar?");
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
await logout();
|
||||
} catch (error) {
|
||||
console.error("Logout with confirmation failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Get user display info from session
|
||||
const getUserDisplayInfo = () => {
|
||||
if (!userInfo.user.value && !userInfo.data.value?.user)
|
||||
return {
|
||||
name: "Guest User",
|
||||
email: "[email protected]",
|
||||
role: "Guest"
|
||||
};
|
||||
|
||||
const user = userInfo.user.value || userInfo.data.value?.user || {};
|
||||
return {
|
||||
name: user.name || user.given_name || user.preferred_username || "User",
|
||||
email: user.email || "No email",
|
||||
role: userInfo.userRoles.value[0] || "User"
|
||||
};
|
||||
};
|
||||
|
||||
const displayInfo = computed(() => getUserDisplayInfo());
|
||||
|
||||
// Computed properties for decodedToken and clientScopes
|
||||
// const decodedToken = computed(() => userInfo.decodedToken.value);
|
||||
// const clientScopes = computed(() => userInfo.clientScopes.value);
|
||||
|
||||
// **TAMBAHAN: Computed property untuk menampilkan status session**
|
||||
const sessionInfo = computed(() => {
|
||||
return {
|
||||
isAuthenticated: userInfo.isAuthenticated.value,
|
||||
sessionExpires: userInfo.sessionExpires.value,
|
||||
hasValidToken: !!userInfo.idToken.value
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-menu :close-on-content-click="false">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn class="custom-hover-primary" variant="text" v-bind="props" icon>
|
||||
<v-avatar size="35">
|
||||
<img
|
||||
src="~/assets/images/profile/user-1.jpg"
|
||||
width="35"
|
||||
alt="User Avatar"
|
||||
/>
|
||||
</v-avatar>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-sheet rounded="md" width="360" elevation="10">
|
||||
<div class="px-8 pt-6">
|
||||
<h6 class="text-h5 font-weight-medium">User Profile</h6>
|
||||
<div class="d-flex align-center mt-4 pb-6">
|
||||
<v-avatar size="80">
|
||||
<img src="~/assets/images/profile/user-1.jpg" width="80" />
|
||||
</v-avatar>
|
||||
<div class="ml-3">
|
||||
<h6 class="text-h6 mb-n1">{{ displayInfo.name }}</h6>
|
||||
<span class="text-subtitle-1 font-weight-regular textSecondary">
|
||||
{{ displayInfo.role }}
|
||||
</span>
|
||||
<div class="d-flex align-center mt-1">
|
||||
<MailIcon size="18" stroke-width="1.5" />
|
||||
<span
|
||||
class="text-subtitle-1 font-weight-regular textSecondary ml-2"
|
||||
>
|
||||
{{ displayInfo.email }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- **TAMBAHAN: Tampilkan status session** -->
|
||||
<div class="mt-2">
|
||||
<v-chip
|
||||
:color="sessionInfo.isAuthenticated ? 'success' : 'error'"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ sessionInfo.isAuthenticated ? "Active" : "Inactive" }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
</div>
|
||||
|
||||
<PerfectScrollbar style="height: calc(100vh - 240px); max-height: 240px">
|
||||
<v-list class="py-0 theme-list" lines="two">
|
||||
<v-list-item
|
||||
v-for="item in profileDD"
|
||||
:key="item.title"
|
||||
class="py-4 px-8 custom-text-primary"
|
||||
:to="item.href"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-avatar
|
||||
size="48"
|
||||
color="lightprimary"
|
||||
class="mr-3"
|
||||
rounded="md"
|
||||
>
|
||||
<img
|
||||
:src="item.avatar"
|
||||
width="24"
|
||||
height="24"
|
||||
:alt="item.avatar"
|
||||
/>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<div>
|
||||
<h6 class="text-subtitle-1 font-weight-bold mb-2 custom-title">
|
||||
{{ item.title }}
|
||||
</h6>
|
||||
</div>
|
||||
<p class="text-subtitle-1 font-weight-regular textSecondary">
|
||||
{{ item.subtitle }}
|
||||
</p>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</PerfectScrollbar>
|
||||
|
||||
<!-- **DIPERBAIKI: Logout buttons section dengan multiple options** -->
|
||||
<div class="pt-4 pb-6 px-8">
|
||||
<!-- Regular Logout Button -->
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
block
|
||||
@click="logoutWithConfirmation"
|
||||
prepend-icon="mdi-logout"
|
||||
class="mb-2"
|
||||
>
|
||||
Logout
|
||||
</v-btn>
|
||||
|
||||
<!-- **TAMBAHAN: Full Logout Button** -->
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="outlined"
|
||||
block
|
||||
@click="fullLogout"
|
||||
prepend-icon="mdi-logout-variant"
|
||||
class="mb-2"
|
||||
>
|
||||
Full Logout
|
||||
</v-btn>
|
||||
|
||||
<!-- **TAMBAHAN: Quick info text** -->
|
||||
<div class="text-center mt-2">
|
||||
<span class="text-caption textSecondary">
|
||||
Full logout akan menghapus semua sesi dan data lokal
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<v-menu open-on-hover open-on-click >
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon class="custom-hover-primary" size="small" variant="text" color="primary" v-bind="props">
|
||||
<Icon icon="solar:widget-line-duotone" height="22" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet rounded="md" width="360" elevation="10">
|
||||
<perfect-scrollbar style="height: 370px">
|
||||
<div class="pa-6">
|
||||
<LazyLayoutFullVerticalHeaderAppsLink />
|
||||
</div>
|
||||
</perfect-scrollbar>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<script setup>
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { searchSugg } from '~/_mockApis/headerData';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- search1 -->
|
||||
<!-- ------------------------------------------------>
|
||||
<v-menu :close-on-content-click="false">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon class="custom-hover-primary" size="small" variant="text" color="primary" v-bind="props">
|
||||
<Icon icon="solar:magnifer-linear" height="20" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet width="360" elevation="10" rounded="md">
|
||||
<div class="d-flex align-center justify-space-between pa-5">
|
||||
<v-text-field placeholder="Search" color="primary" density="compact" variant="outlined" hide-details></v-text-field>
|
||||
</div>
|
||||
<v-divider></v-divider>
|
||||
<h5 class="text-h5 mt-4 px-5 pb-4">Quick Page Links</h5>
|
||||
<perfect-scrollbar style="height: 380px">
|
||||
<v-list class="pt-0 pb-5" lines="two">
|
||||
<v-list-item
|
||||
:value="item"
|
||||
v-for="(item, index) in searchSugg"
|
||||
:key="index"
|
||||
:to="item.href"
|
||||
color="primary"
|
||||
class="px-5 py-2"
|
||||
>
|
||||
<h6 class="text-subtitle-1 heading mb-1">{{ item.title }}</h6>
|
||||
<p class="text-subtitle-2 textSecondary">{{ item.href }}</p>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</perfect-scrollbar>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</template>
|
||||
@@ -1,120 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { useCustomizerStore } from '~/store/customizer';
|
||||
// import { useEcomStore } from '@/stores/apps/eCommerce';
|
||||
// import LanguageDD from './LanguageDD.vue';
|
||||
import NotificationDD from './NotificationDD.vue';
|
||||
import ProfileDD from './ProfileDD.vue';
|
||||
import Searchbar from './Searchbar.vue';
|
||||
import RightMobileSidebar from './RightMobileSidebar.vue';
|
||||
import Navigations from './Navigations.vue';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import Logo from '../logo/Logo.vue';
|
||||
import ThemeToggler from './ThemeToggler.vue';
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { useCustomizerStore } from "~/store/customizer";
|
||||
|
||||
import { Icon } from "@iconify/vue";
|
||||
import Logo from "../logo/Logo.vue";
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const showSearch = ref(false);
|
||||
const priority = ref(customizer.setHorizontalLayout ? 0 : 0);
|
||||
function searchbox() {
|
||||
showSearch.value = !showSearch.value;
|
||||
showSearch.value = !showSearch.value;
|
||||
}
|
||||
watch(priority, (newPriority) => {
|
||||
priority.value = newPriority;
|
||||
priority.value = newPriority;
|
||||
});
|
||||
|
||||
// count items
|
||||
// const store = useEcomStore();
|
||||
// const getCart = computed(() => {
|
||||
// return store.cart;
|
||||
// });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar elevation="0" :priority="priority" height="70" id="top" class="main-head">
|
||||
<v-btn
|
||||
class="hidden-lg-and-up custom-hover-primary"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
icon
|
||||
@click.stop="customizer.SET_SIDEBAR_DRAWER"
|
||||
>
|
||||
<Icon icon="solar:hamburger-menu-line-duotone" height="22" />
|
||||
</v-btn>
|
||||
<v-app-bar
|
||||
elevation="0"
|
||||
:priority="priority"
|
||||
height="70"
|
||||
id="top"
|
||||
class="main-head"
|
||||
>
|
||||
<v-btn
|
||||
class="hidden-lg-and-up custom-hover-primary"
|
||||
size="small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
icon
|
||||
@click.stop="customizer.SET_SIDEBAR_DRAWER"
|
||||
>
|
||||
<Icon icon="solar:hamburger-menu-line-duotone" height="22" />
|
||||
</v-btn>
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Search part -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<Searchbar />
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Search part -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<Searchbar />
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Mega menu -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-sm-and-down">
|
||||
<Navigations />
|
||||
</div>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Mega menu -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-sm-and-down">
|
||||
<Navigations />
|
||||
</div>
|
||||
|
||||
<v-spacer class="hidden-sm-and-down" />
|
||||
<v-spacer class="hidden-sm-and-down" />
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Mobile Logo -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-md-and-up">
|
||||
<Logo />
|
||||
</div>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Mobile Logo -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-md-and-up">
|
||||
<Logo />
|
||||
</div>
|
||||
|
||||
|
||||
<ThemeToggler />
|
||||
<ThemeToggler />
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- translate -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-sm-and-down">
|
||||
<LanguageDD />
|
||||
</div>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- translate -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-sm-and-down">
|
||||
<LanguageDD />
|
||||
</div>
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- ShoppingCart -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- <v-btn icon class="custom-hover-primary hidden-sm-and-down" size="small" variant="text" color="primary" to="/ecommerce/checkout">
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- ShoppingCart -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- <v-btn icon class="custom-hover-primary hidden-sm-and-down" size="small" variant="text" color="primary" to="/ecommerce/checkout">
|
||||
<v-badge color="error" :content="getCart?.length">
|
||||
<Icon icon="solar:cart-large-2-outline" height="22" />
|
||||
</v-badge>
|
||||
</v-btn> -->
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Notification -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-sm-and-down">
|
||||
<NotificationDD />
|
||||
</div>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- Notification -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-sm-and-down">
|
||||
<NotificationDD />
|
||||
</div>
|
||||
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- User Profile -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-sm-and-down">
|
||||
<ProfileDD />
|
||||
</div>
|
||||
<!-- ---------------------------------------------- -->
|
||||
<!-- User Profile -->
|
||||
<!-- ---------------------------------------------- -->
|
||||
<div class="hidden-sm-and-down">
|
||||
<ProfileDD />
|
||||
</div>
|
||||
|
||||
<!----Mobile ----->
|
||||
<v-menu :close-on-content-click="true" class="mobile_popup">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn icon class="hidden-md-and-up custom-hover-primary" color="primary" variant="text" v-bind="props" size="small">
|
||||
<Icon icon="solar:menu-dots-bold-duotone" height="22" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet rounded="lg" elevation="10" class="mt-4 dropdown-box px-4 py-3">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<RightMobileSidebar />
|
||||
<!-- <LanguageDD /> -->
|
||||
<!-- <v-btn icon variant="text" class="mr-sm-3 mr-2 custom-hover-primary" to="/ecommerce/checkout" size="small">
|
||||
<!----Mobile ----->
|
||||
<v-menu :close-on-content-click="true" class="mobile_popup">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
class="hidden-md-and-up custom-hover-primary"
|
||||
color="primary"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
size="small"
|
||||
>
|
||||
<Icon icon="solar:menu-dots-bold-duotone" height="22" />
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-sheet rounded="lg" elevation="10" class="mt-4 dropdown-box px-4 py-3">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<RightMobileSidebar />
|
||||
<!-- <LanguageDD /> -->
|
||||
<!-- <v-btn icon variant="text" class="mr-sm-3 mr-2 custom-hover-primary" to="/ecommerce/checkout" size="small">
|
||||
<v-badge color="primary" :content="getCart?.length" offset-x="-4" offset-y="-6">
|
||||
<Icon icon="solar:cart-large-2-outline" height="22" />
|
||||
</v-badge>
|
||||
</v-btn> -->
|
||||
<NotificationDD />
|
||||
<ProfileDD />
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
<NotificationDD />
|
||||
<ProfileDD />
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
@@ -15,36 +15,6 @@ const MiniSideIcons: minisidebar[] = [
|
||||
tooltip:'Pages',
|
||||
id: 2,
|
||||
},
|
||||
{
|
||||
icon: 'palette-round-line-duotone',
|
||||
tooltip:'Forms',
|
||||
id: 3
|
||||
},
|
||||
{
|
||||
icon:'align-vertical-spacing-broken',
|
||||
tooltip:'Tables',
|
||||
id: 4
|
||||
},
|
||||
{
|
||||
icon:'chart-line-duotone',
|
||||
tooltip:'Charts',
|
||||
id: 5
|
||||
},
|
||||
{
|
||||
icon:'widget-6-line-duotone',
|
||||
tooltip:'UI Components',
|
||||
id: 6
|
||||
},
|
||||
{
|
||||
icon:'lock-keyhole-line-duotone',
|
||||
tooltip:'Authentication Pages',
|
||||
id: 7
|
||||
},
|
||||
{
|
||||
icon:'mirror-left-line-duotone',
|
||||
tooltip:'Others',
|
||||
id: 8
|
||||
}
|
||||
]
|
||||
|
||||
export default MiniSideIcons;
|
||||
@@ -21,689 +21,29 @@ const sidebarItem: menu[] = [
|
||||
id: 1,
|
||||
children: [
|
||||
{
|
||||
title: 'Dashboard1',
|
||||
title: 'Dashboard',
|
||||
icon: 'widget-add-line-duotone',
|
||||
to: '/dashboards/dashboard1'
|
||||
},
|
||||
{
|
||||
title: 'Dashboard2',
|
||||
icon: 'chart-line-duotone',
|
||||
to: '/dashboards/dashboard2'
|
||||
},
|
||||
{
|
||||
title: 'Dashboard3',
|
||||
icon: 'screencast-2-line-duotone',
|
||||
to: '/dashboards/dashboard3'
|
||||
},
|
||||
{
|
||||
title: 'Front Pages',
|
||||
icon: 'home-angle-linear',
|
||||
to: '/',
|
||||
children: [
|
||||
{
|
||||
title: 'Homepage',
|
||||
to: '/front-page/homepage'
|
||||
},
|
||||
{
|
||||
title: 'About Us',
|
||||
to: '/front-page/about-us'
|
||||
},
|
||||
{
|
||||
title: 'Blog',
|
||||
to: '/front-page/blog/posts'
|
||||
},
|
||||
{
|
||||
title: 'Blog Details',
|
||||
to: '/front-page/blog/early-black-friday-amazon-deals-cheap-tvs-headphones'
|
||||
},
|
||||
{
|
||||
title: 'Contact Us',
|
||||
to: '/front-page/contact-us'
|
||||
},
|
||||
{
|
||||
title: 'Portfolio',
|
||||
to: '/front-page/portfolio'
|
||||
},
|
||||
{
|
||||
title: 'Pricing',
|
||||
to: '/front-page/pricing'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
header: 'apps',
|
||||
id: 1,
|
||||
children: [
|
||||
{
|
||||
title: 'ECommerce',
|
||||
icon: 'cart-3-line-duotone',
|
||||
to: '/ecommerce/',
|
||||
children: [
|
||||
{
|
||||
title: 'Shop',
|
||||
to: '/ecommerce/products'
|
||||
},
|
||||
{
|
||||
title: 'Detail',
|
||||
to: '/ecommerce/product/detail/1'
|
||||
},
|
||||
{
|
||||
title: 'List',
|
||||
to: '/ecommerce/productlist'
|
||||
},
|
||||
{
|
||||
title: 'Checkout',
|
||||
to: '/ecommerce/checkout'
|
||||
},
|
||||
{
|
||||
title: 'Add Product',
|
||||
to: '/ecommerce/add-product'
|
||||
},
|
||||
{
|
||||
title: 'Edit Product',
|
||||
to: '/ecommerce/edit-product'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Blog',
|
||||
icon: 'widget-4-line-duotone',
|
||||
to: '/',
|
||||
children: [
|
||||
{
|
||||
title: 'Blog Posts',
|
||||
to: '/apps/blog/posts'
|
||||
},
|
||||
{
|
||||
title: 'Blog Details',
|
||||
to: '/apps/blog/early-black-friday-amazon-deals-cheap-tvs-headphones'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'User Profile',
|
||||
icon: 'shield-user-line-duotone',
|
||||
to: '/',
|
||||
children: [
|
||||
{
|
||||
title: 'Profile',
|
||||
to: '/apps/user/profile'
|
||||
},
|
||||
{
|
||||
title: 'Followers',
|
||||
to: '/apps/user/profile/followers'
|
||||
},
|
||||
{
|
||||
title: 'Friends',
|
||||
to: '/apps/user/profile/friends'
|
||||
},
|
||||
{
|
||||
title: 'Gallery',
|
||||
to: '/apps/user/profile/gallery'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Invoice',
|
||||
icon: 'bill-check-outline',
|
||||
to: '/',
|
||||
children: [
|
||||
{
|
||||
title: 'List',
|
||||
to: '/apps/invoice'
|
||||
},
|
||||
{
|
||||
title: 'Details',
|
||||
to: '/apps/invoice/details/102'
|
||||
},
|
||||
{
|
||||
title: 'Create',
|
||||
to: '/apps/invoice/create'
|
||||
},
|
||||
{
|
||||
title: 'Edit',
|
||||
to: '/apps/invoice/edit/102'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Calendar',
|
||||
icon: 'calendar-mark-line-duotone',
|
||||
to: '/apps/calendar'
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
icon: 'letter-linear',
|
||||
to: '/apps/email'
|
||||
},
|
||||
{
|
||||
title: 'Chats',
|
||||
icon: 'chat-round-line-line-duotone',
|
||||
to: '/apps/chats'
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
icon: 'document-text-line-duotone',
|
||||
to: '/apps/notes'
|
||||
},
|
||||
{
|
||||
title: 'Kanban',
|
||||
icon: 'airbuds-case-minimalistic-line-duotone',
|
||||
to: '/apps/kanban'
|
||||
},
|
||||
{
|
||||
title: 'Contact',
|
||||
icon: 'iphone-line-duotone',
|
||||
to: '/apps/contacts'
|
||||
},
|
||||
{
|
||||
title: 'Tickets',
|
||||
icon: 'ticker-star-outline',
|
||||
to: '/apps/tickets'
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
header: 'pages',
|
||||
id: 2,
|
||||
children: [
|
||||
|
||||
{
|
||||
title: 'Account Setting',
|
||||
title: 'Dashboard',
|
||||
icon: 'settings-minimalistic-line-duotone',
|
||||
to: '/pages/account-settings'
|
||||
},
|
||||
{
|
||||
title: 'Banners Widgets',
|
||||
icon: 'align-vertical-spacing-line-duotone',
|
||||
to: '/widgets/banners'
|
||||
},
|
||||
{
|
||||
title: 'Cards Widgets',
|
||||
icon: 'cardholder-line-duotone',
|
||||
to: '/widgets/cards'
|
||||
},
|
||||
{
|
||||
title: 'Charts Widgets',
|
||||
icon: 'chart-square-line-duotone',
|
||||
to: '/widgets/charts'
|
||||
},
|
||||
{
|
||||
title: 'FAQ',
|
||||
icon: 'question-circle-line-duotone',
|
||||
to: '/pages/faq'
|
||||
},
|
||||
{
|
||||
title: 'Gallery Lightbox',
|
||||
icon: 'gallery-bold-duotone',
|
||||
to: '/pages/gallery-lightbox',
|
||||
},
|
||||
{
|
||||
title: 'Landing Page',
|
||||
icon: 'passport-line-duotone',
|
||||
to: '/'
|
||||
},
|
||||
{
|
||||
title: 'Pricing',
|
||||
icon: 'dollar-line-duotone',
|
||||
to: '/pages/pricing'
|
||||
},
|
||||
{
|
||||
title: 'Search Results',
|
||||
icon: 'card-search-line-duotone',
|
||||
to: '/pages/search-results'
|
||||
},
|
||||
{
|
||||
title: 'Social Contacts',
|
||||
icon: 'chat-round-like-linear',
|
||||
to: '/pages/social-media-contacts'
|
||||
},
|
||||
{
|
||||
title: 'Treeview',
|
||||
icon: 'transmission-line-duotone',
|
||||
to: '/pages/treeview'
|
||||
},
|
||||
to: '/pages'
|
||||
}
|
||||
|
||||
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
header: 'icons',
|
||||
id: 2,
|
||||
children: [
|
||||
{
|
||||
title: 'Tabler Icons',
|
||||
icon: 'sticker-smile-circle-2-line-duotone',
|
||||
to: '/icons/tabler'
|
||||
},
|
||||
{
|
||||
title: 'Solar Icons',
|
||||
icon: 'sticker-smile-circle-2-line-duotone',
|
||||
to: '/icons/solar'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
header: 'Forms',
|
||||
id: 3,
|
||||
children: [
|
||||
{
|
||||
title: 'Form Elements',
|
||||
icon: 'text-selection-line-duotone',
|
||||
to: '/components/',
|
||||
children: [
|
||||
{
|
||||
title: 'Autocomplete',
|
||||
to: '/forms/form-elements/autocomplete'
|
||||
},
|
||||
{
|
||||
title: 'Combobox',
|
||||
to: '/forms/form-elements/combobox'
|
||||
},
|
||||
{
|
||||
title: 'Button',
|
||||
to: '/forms/form-elements/button'
|
||||
},
|
||||
{
|
||||
title: 'Checkbox',
|
||||
to: '/forms/form-elements/checkbox'
|
||||
},
|
||||
{
|
||||
title: 'Custom Inputs',
|
||||
to: '/forms/form-elements/custominputs'
|
||||
},
|
||||
{
|
||||
title: 'File Inputs',
|
||||
to: '/forms/form-elements/fileinputs'
|
||||
},
|
||||
{
|
||||
title: 'Radio',
|
||||
to: '/forms/form-elements/radio'
|
||||
},
|
||||
{
|
||||
title: 'Date Time',
|
||||
to: '/forms/form-elements/date-time'
|
||||
},
|
||||
{
|
||||
title: 'Select',
|
||||
to: '/forms/form-elements/select'
|
||||
},
|
||||
{
|
||||
title: 'Slider',
|
||||
to: '/forms/form-elements/slider'
|
||||
},
|
||||
{
|
||||
title: 'Switch',
|
||||
to: '/forms/form-elements/switch'
|
||||
},
|
||||
{
|
||||
title: 'Time Picker',
|
||||
to: '/forms/form-elements/time-picker'
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Stepper',
|
||||
to: '/forms/form-elements/stepper'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Form Layout',
|
||||
icon: 'layers-minimalistic-outline',
|
||||
to: '/forms/form-layouts'
|
||||
},
|
||||
{
|
||||
title: 'Form Horizontal',
|
||||
icon: 'password-minimalistic-input-line-duotone',
|
||||
to: '/forms/form-horizontal'
|
||||
},
|
||||
{
|
||||
title: 'Form Vertical',
|
||||
icon: 'slider-vertical-line-duotone',
|
||||
to: '/forms/form-vertical'
|
||||
},
|
||||
{
|
||||
title: 'Form Custom',
|
||||
icon: 'clapperboard-play-outline',
|
||||
to: '/forms/form-custom'
|
||||
},
|
||||
{
|
||||
title: 'Form Validation',
|
||||
icon: 'soundwave-square-line-duotone',
|
||||
to: '/forms/form-validation'
|
||||
},
|
||||
{
|
||||
title: 'Editor',
|
||||
icon: 'clapperboard-edit-line-duotone',
|
||||
to: '/forms/editor'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
header: 'tables',
|
||||
id: 4,
|
||||
children: [
|
||||
{
|
||||
title: 'Basic Table',
|
||||
icon: 'tablet-line-duotone',
|
||||
to: '/tables/basic'
|
||||
},
|
||||
{
|
||||
title: 'Dark Table',
|
||||
icon: 'bedside-table-4-outline',
|
||||
to: '/tables/dark'
|
||||
},
|
||||
{
|
||||
title: 'Density Table',
|
||||
icon: 'bedside-table-3-linear',
|
||||
to: '/tables/density'
|
||||
},
|
||||
{
|
||||
title: 'Fixed Header Table',
|
||||
icon: 'archive-up-minimlistic-broken',
|
||||
to: '/tables/fixed-header'
|
||||
},
|
||||
{
|
||||
title: 'Height Table',
|
||||
icon: 'archive-down-minimlistic-broken',
|
||||
to: '/tables/height'
|
||||
},
|
||||
{
|
||||
title: 'Editable Table',
|
||||
icon: 'document-add-linear',
|
||||
to: '/tables/editable'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
header: 'datatables',
|
||||
id: 4,
|
||||
children: [
|
||||
{
|
||||
title: 'Basic Table',
|
||||
icon: 'database-outline',
|
||||
to: '/tables/datatables/basic'
|
||||
},
|
||||
{
|
||||
title: 'Header Table',
|
||||
icon: 'folder-open-broken',
|
||||
to: '/tables/datatables/header'
|
||||
},
|
||||
{
|
||||
title: 'Selection Table',
|
||||
icon: 'chart-square-broken',
|
||||
to: '/tables/datatables/selection'
|
||||
},
|
||||
{
|
||||
title: 'Sorting Table',
|
||||
icon: 'card-send-line-duotone',
|
||||
to: '/tables/datatables/sorting'
|
||||
},
|
||||
{
|
||||
title: 'Pagination Table',
|
||||
icon: 'tag-horizontal-broken',
|
||||
to: '/tables/datatables/pagination'
|
||||
},
|
||||
{
|
||||
title: 'Filtering Table',
|
||||
icon: 'tuning-square-2-line-duotone',
|
||||
to: '/tables/datatables/filtering'
|
||||
},
|
||||
{
|
||||
title: 'Grouping Table',
|
||||
icon: 'tuning-square-2-line-duotone',
|
||||
to: '/tables/datatables/grouping'
|
||||
},
|
||||
{
|
||||
title: 'Table Slots',
|
||||
icon: 'closet-line-duotone',
|
||||
to: '/tables/datatables/slots'
|
||||
},
|
||||
{
|
||||
title: 'CRUD Table',
|
||||
icon: 'text-underline-cross-broken',
|
||||
to: '/tables/datatables/crudtable'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
header: 'Charts',
|
||||
id: 5,
|
||||
children: [
|
||||
{
|
||||
title: 'Line',
|
||||
icon: 'chat-square-2-outline',
|
||||
to: '/charts/line-chart'
|
||||
},
|
||||
{
|
||||
title: 'Gredient',
|
||||
icon: 'round-graph-linear',
|
||||
to: '/charts/gredient-chart'
|
||||
},
|
||||
{
|
||||
title: 'Area',
|
||||
icon: 'graph-up-linear',
|
||||
to: '/charts/area-chart'
|
||||
},
|
||||
{
|
||||
title: 'Candlestick',
|
||||
icon: 'chandelier-broken',
|
||||
to: '/charts/candlestick-chart'
|
||||
},
|
||||
{
|
||||
title: 'Column',
|
||||
icon: 'colour-tuneing-broken',
|
||||
to: '/charts/column-chart'
|
||||
},
|
||||
{
|
||||
title: 'Doughnut & Pie',
|
||||
icon: 'pie-chart-2-linear',
|
||||
to: '/charts/doughnut-pie-chart'
|
||||
},
|
||||
{
|
||||
title: 'Radialbar & Radar',
|
||||
icon: 'radar-2-outline',
|
||||
to: '/charts/radialbar-chart'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
header: 'UI',
|
||||
id: 6,
|
||||
children: [
|
||||
{
|
||||
title: 'Alert',
|
||||
icon: 'info-circle-linear',
|
||||
to: '/ui-components/alert'
|
||||
},
|
||||
{
|
||||
title: 'Accordion',
|
||||
icon: 'waterdrops-line-duotone',
|
||||
to: '/ui-components/accordion'
|
||||
},
|
||||
{
|
||||
title: 'Avatar',
|
||||
icon: 'expressionless-circle-outline',
|
||||
to: '/ui-components/avatar'
|
||||
},
|
||||
{
|
||||
title: 'Chip',
|
||||
icon: 'tag-horizontal-line-duotone',
|
||||
to: '/ui-components/chip'
|
||||
},
|
||||
{
|
||||
title: 'Dialog',
|
||||
icon: 'bolt-line-duotone',
|
||||
to: '/ui-components/dialogs'
|
||||
},
|
||||
{
|
||||
title: 'List',
|
||||
icon: 'checklist-bold-duotone',
|
||||
to: '/ui-components/list'
|
||||
},
|
||||
{
|
||||
title: 'Menus',
|
||||
icon: 'menu-dots-circle-outline',
|
||||
to: '/ui-components/menus'
|
||||
},
|
||||
{
|
||||
title: 'Rating',
|
||||
icon: 'shield-star-outline',
|
||||
to: '/ui-components/rating'
|
||||
},
|
||||
{
|
||||
title: 'Tabs',
|
||||
icon: 'box-minimalistic-line-duotone',
|
||||
to: '/ui-components/tabs'
|
||||
},
|
||||
{
|
||||
title: 'Tooltip',
|
||||
icon: 'transmission-square-outline',
|
||||
to: '/ui-components/tooltip'
|
||||
},
|
||||
{
|
||||
title: 'Typography',
|
||||
icon: 'text-circle-linear',
|
||||
to: '/ui-components/typography'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
header: 'Auth',
|
||||
id: 7,
|
||||
children: [
|
||||
{
|
||||
title: 'Error',
|
||||
icon: 'bug-minimalistic-line-duotone',
|
||||
to: '/auth/404'
|
||||
},
|
||||
{
|
||||
title: 'Side Login',
|
||||
icon: 'login-3-line-duotone',
|
||||
to: '/auth/login'
|
||||
},
|
||||
{
|
||||
title: 'Boxed Login',
|
||||
icon: 'login-3-line-duotone',
|
||||
to: '/auth/login2'
|
||||
},
|
||||
{
|
||||
title: 'Side Register',
|
||||
icon: 'user-plus-rounded-line-duotone',
|
||||
to: '/auth/register'
|
||||
},
|
||||
{
|
||||
title: 'Boxed Register',
|
||||
icon: 'user-plus-rounded-line-duotone',
|
||||
to: '/auth/register2'
|
||||
},
|
||||
{
|
||||
title: 'Side Forgot Pwd',
|
||||
icon: 'password-outline',
|
||||
to: '/auth/forgot-password'
|
||||
},
|
||||
{
|
||||
title: 'Boxed Forgot Pwd',
|
||||
icon: 'password-outline',
|
||||
to: '/auth/forgot-password2'
|
||||
},
|
||||
{
|
||||
title: 'Side Two Steps',
|
||||
icon: 'siderbar-line-duotone',
|
||||
to: '/auth/two-step'
|
||||
},
|
||||
{
|
||||
title: 'Boxed Two Steps',
|
||||
icon: 'siderbar-line-duotone',
|
||||
to: '/auth/two-step2'
|
||||
},
|
||||
{
|
||||
title: 'Maintenance',
|
||||
icon: 'settings-line-duotone',
|
||||
to: '/auth/maintenance'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
header: 'Multi Level',
|
||||
id: 8,
|
||||
children: [
|
||||
{
|
||||
title: 'Menu Level',
|
||||
icon: 'align-left-line-duotone',
|
||||
to: '#',
|
||||
id: 8,
|
||||
children: [
|
||||
{
|
||||
title: 'Level 1',
|
||||
to: '/level1'
|
||||
},
|
||||
{
|
||||
title: 'Level 1 ',
|
||||
to: '/2level',
|
||||
children: [
|
||||
{
|
||||
title: 'Level 2',
|
||||
to: '/barry'
|
||||
},
|
||||
{
|
||||
title: 'Level 2',
|
||||
to: '/2.2level',
|
||||
children: [
|
||||
{
|
||||
title: 'Level 3',
|
||||
to: '/barry'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
header: 'More Options',
|
||||
id: 8,
|
||||
children: [
|
||||
{
|
||||
title: 'Applications',
|
||||
icon: 'check-circle-bold',
|
||||
BgColor: 'primary'
|
||||
},
|
||||
{
|
||||
title: 'Form Options',
|
||||
icon: 'check-circle-bold',
|
||||
BgColor: 'secondary'
|
||||
},
|
||||
{
|
||||
title: 'Table Variations',
|
||||
icon: 'check-circle-bold',
|
||||
BgColor: 'error'
|
||||
},
|
||||
{
|
||||
title: 'Charts Selection',
|
||||
icon: 'check-circle-bold',
|
||||
BgColor: 'warning'
|
||||
},
|
||||
{
|
||||
title: 'Widgets',
|
||||
icon: 'check-circle-bold',
|
||||
BgColor: 'success'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export default sidebarItem;
|
||||
@@ -1,134 +0,0 @@
|
||||
import type { MenuItem } from '~/types/menu'
|
||||
|
||||
export const useMenuManagement = () => {
|
||||
const menuItems = ref<MenuItem[]>([])
|
||||
const menuOptions = ref<string[]>([])
|
||||
const references = ref<string[]>([])
|
||||
const selectedReference = ref('Referensi')
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Load menus from JSON file
|
||||
const loadMenus = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await $fetch<{ success: boolean; data: { menus: MenuItem[]; menuOptions: string[]; references: string[] } }>('/api/menus/list')
|
||||
const data = response.data
|
||||
menuItems.value = data.menus || []
|
||||
menuOptions.value = data.menuOptions || []
|
||||
references.value = data.references || []
|
||||
} catch (error) {
|
||||
console.error('Error loading menus:', error)
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Save single menu item
|
||||
const saveMenu = async (menuItem: MenuItem) => {
|
||||
try {
|
||||
const response = await $fetch<{ success: boolean; data: MenuItem }>('/api/menus/save', {
|
||||
method: 'POST',
|
||||
body: { menuItem, reference: selectedReference.value }
|
||||
})
|
||||
const data = response.data
|
||||
|
||||
// Update local state
|
||||
const index = menuItems.value.findIndex(item => item.id === menuItem.id)
|
||||
if (index >= 0) {
|
||||
menuItems.value[index] = data
|
||||
} else {
|
||||
menuItems.value.push(data)
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Error saving menu:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Delete menu item
|
||||
const deleteMenu = async (menuId: string) => {
|
||||
try {
|
||||
await $fetch('/api/menus/delete', {
|
||||
method: 'DELETE',
|
||||
body: { menuId, reference: selectedReference.value }
|
||||
})
|
||||
|
||||
// Remove from local state
|
||||
const removeFromItems = (items: MenuItem[]): MenuItem[] => {
|
||||
return items.filter(item => {
|
||||
if (item.id === menuId) return false
|
||||
if (item.children) {
|
||||
item.children = removeFromItems(item.children)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
menuItems.value = removeFromItems(menuItems.value)
|
||||
} catch (error) {
|
||||
console.error('Error deleting menu:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Update menu order
|
||||
const updateMenuOrder = async (newOrder: MenuItem[]) => {
|
||||
try {
|
||||
await $fetch('/api/menus/reorder', {
|
||||
method: 'POST',
|
||||
body: { menus: newOrder, reference: selectedReference.value }
|
||||
})
|
||||
|
||||
menuItems.value = newOrder
|
||||
} catch (error) {
|
||||
console.error('Error updating menu order:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Vue page from menu
|
||||
const generatePage = async (menuItem: MenuItem) => {
|
||||
try {
|
||||
const pageData = {
|
||||
name: menuItem.title.replace(/\s+/g, ''),
|
||||
path: menuItem.url,
|
||||
templateType: 'default',
|
||||
metadata: {
|
||||
title: menuItem.title,
|
||||
description: `Page for ${menuItem.title}`,
|
||||
keywords: [menuItem.title.toLowerCase()]
|
||||
},
|
||||
content: {
|
||||
title: menuItem.title,
|
||||
icon: menuItem.icon
|
||||
}
|
||||
}
|
||||
|
||||
await $fetch('/api/pages/generate', {
|
||||
method: 'POST',
|
||||
body: pageData
|
||||
})
|
||||
|
||||
return pageData
|
||||
} catch (error) {
|
||||
console.error('Error generating page:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menuItems: readonly(menuItems),
|
||||
menuOptions: readonly(menuOptions),
|
||||
references: readonly(references),
|
||||
selectedReference,
|
||||
isLoading: readonly(isLoading),
|
||||
loadMenus,
|
||||
saveMenu,
|
||||
deleteMenu,
|
||||
updateMenuOrder,
|
||||
generatePage
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
// composables/usePatientForm.ts
|
||||
import type { FhirPatient, FhirHumanName } from "~/types/fhir/humanName";
|
||||
|
||||
interface PatientFormData {
|
||||
dataDiri: {
|
||||
namaLengkap: string;
|
||||
jenisKelamin: string;
|
||||
tanggalLahir: string;
|
||||
nomorIdentitas: string;
|
||||
jenisIdentitas: string;
|
||||
fhirName?: FhirHumanName | null;
|
||||
};
|
||||
// ... other interfaces
|
||||
}
|
||||
|
||||
export const usePatientForm = () => {
|
||||
const convertToFhirPatient = (formData: PatientFormData): FhirPatient => {
|
||||
const fhirPatient: FhirPatient = {
|
||||
resourceType: "Patient",
|
||||
identifier: [],
|
||||
active: true,
|
||||
name: [],
|
||||
telecom: [],
|
||||
gender: undefined,
|
||||
birthDate: undefined,
|
||||
address: []
|
||||
};
|
||||
|
||||
// Add parsed FHIR name
|
||||
if (formData.dataDiri.fhirName) {
|
||||
fhirPatient.name!.push(formData.dataDiri.fhirName);
|
||||
}
|
||||
|
||||
// Gender mapping
|
||||
if (formData.dataDiri.jenisKelamin) {
|
||||
fhirPatient.gender =
|
||||
formData.dataDiri.jenisKelamin === "L" ? "male" : "female";
|
||||
}
|
||||
|
||||
// Birth date
|
||||
if (formData.dataDiri.tanggalLahir) {
|
||||
fhirPatient.birthDate = formData.dataDiri.tanggalLahir;
|
||||
}
|
||||
|
||||
// Identifiers
|
||||
if (formData.dataDiri.nomorIdentitas) {
|
||||
fhirPatient.identifier!.push({
|
||||
use: "official",
|
||||
type: {
|
||||
coding: [
|
||||
{
|
||||
system: "http://terminology.hl7.org/CodeSystem/v2-0203",
|
||||
code:
|
||||
formData.dataDiri.jenisIdentitas === "KTP" ? "NNESP" : "PPN",
|
||||
display: formData.dataDiri.jenisIdentitas
|
||||
}
|
||||
]
|
||||
},
|
||||
value: formData.dataDiri.nomorIdentitas
|
||||
});
|
||||
}
|
||||
|
||||
return fhirPatient;
|
||||
};
|
||||
|
||||
return {
|
||||
convertToFhirPatient
|
||||
};
|
||||
};
|
||||
interface PersonalInfo {
|
||||
nik?: string;
|
||||
fullName?: string;
|
||||
birthPlace?: string;
|
||||
birthDate?: string;
|
||||
gender?: string;
|
||||
}
|
||||
|
||||
interface ContactInfo {
|
||||
phone?: string;
|
||||
address?: string;
|
||||
province?: string;
|
||||
city?: string;
|
||||
}
|
||||
|
||||
// export const usePatientForm = () => {
|
||||
// const validatePersonalInfo = (data: PersonalInfo) => {
|
||||
// const errors: Record<string, string> = {};
|
||||
|
||||
// if (!data.nik) errors.nik = "NIK wajib diisi";
|
||||
// if (!data.fullName) errors.fullName = "Nama lengkap wajib diisi";
|
||||
// if (!data.birthPlace) errors.birthPlace = "Tempat lahir wajib diisi";
|
||||
// if (!data.birthDate) errors.birthDate = "Tanggal lahir wajib diisi";
|
||||
// if (!data.gender) errors.gender = "Jenis kelamin wajib diisi";
|
||||
|
||||
// return {
|
||||
// valid: Object.keys(errors).length === 0,
|
||||
// errors
|
||||
// };
|
||||
// };
|
||||
|
||||
// const validateContactInfo = (data: ContactInfo) => {
|
||||
// const errors: Record<string, string> = {};
|
||||
|
||||
// if (!data.phone) errors.phone = "Nomor telepon wajib diisi";
|
||||
// if (!data.address) errors.address = "Alamat wajib diisi";
|
||||
// if (!data.province) errors.province = "Provinsi wajib diisi";
|
||||
// if (!data.city) errors.city = "Kota wajib diisi";
|
||||
|
||||
// return {
|
||||
// valid: Object.keys(errors).length === 0,
|
||||
// errors
|
||||
// };
|
||||
// };
|
||||
|
||||
// const generateMRNumber = () => {
|
||||
// const date = new Date();
|
||||
// const year = date.getFullYear().toString().substr(-2);
|
||||
// const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
// const random = Math.floor(Math.random() * 10000)
|
||||
// .toString()
|
||||
// .padStart(4, "0");
|
||||
|
||||
// return `MR${year}${month}${random}`;
|
||||
// };
|
||||
|
||||
// return {
|
||||
// validatePersonalInfo,
|
||||
// validateContactInfo,
|
||||
// generateMRNumber
|
||||
// };
|
||||
// };
|
||||
@@ -1,606 +0,0 @@
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
import type { ExtendedUser } from "~/types/auth";
|
||||
import { computed, ref, watch, readonly } from "vue";
|
||||
|
||||
export interface UseUserInfoReturnType {
|
||||
// State
|
||||
isAuthenticated: ComputedRef<boolean>;
|
||||
isLoading: ComputedRef<boolean>;
|
||||
user: ComputedRef<ExtendedUser | null>;
|
||||
userRoles: ComputedRef<string[]>;
|
||||
clientRoles: ComputedRef<string[]>;
|
||||
accessToken: ComputedRef<string>;
|
||||
sessionExpires: ComputedRef<string | null>;
|
||||
isIdle: Ref<boolean>;
|
||||
|
||||
// Raw data for debugging
|
||||
data: Ref<any>;
|
||||
status: Ref<string>;
|
||||
|
||||
// Session data from [...].ts
|
||||
idToken: ComputedRef<string>;
|
||||
refreshToken: ComputedRef<string>;
|
||||
accessTokenPayload: ComputedRef<any>;
|
||||
expiresAt: ComputedRef<number | null>;
|
||||
sessionScope: ComputedRef<string>;
|
||||
jwtToken: ComputedRef<string>;
|
||||
|
||||
// Role checking functions
|
||||
hasRole: (role: string) => boolean;
|
||||
hasClientRole: (role: string) => boolean;
|
||||
hasAnyRole: (roles: string[]) => boolean;
|
||||
isAdmin: ComputedRef<boolean>;
|
||||
|
||||
// Session monitoring functions
|
||||
updateActivity: () => void;
|
||||
startSessionMonitoring: () => (() => void) | null;
|
||||
stopSessionMonitoring: () => void;
|
||||
|
||||
// Authentication actions
|
||||
login: (provider?: string) => Promise<void>;
|
||||
logout: (options?: any) => Promise<void>;
|
||||
fullLogout: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
forceRefreshSession: () => Promise<void>;
|
||||
isKeycloakSessionActive: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const useUserInfo = (): UseUserInfoReturnType => {
|
||||
const { data, status, signIn, signOut, getSession } = useAuth();
|
||||
|
||||
// Internal writable refs for manual setting
|
||||
const internalData = ref(data.value);
|
||||
const internalStatus = ref(status.value);
|
||||
|
||||
// Sync internal refs with useAuth data and status
|
||||
watch(
|
||||
() => data.value,
|
||||
(newVal) => {
|
||||
internalData.value = newVal;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => status.value,
|
||||
(newVal) => {
|
||||
internalStatus.value = newVal;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Session monitoring state
|
||||
const lastActivity = ref(Date.now());
|
||||
const isIdle = ref(false);
|
||||
const tabVisible = ref(true);
|
||||
|
||||
// Constants
|
||||
const IDLE_TIMEOUT = 15 * 60 * 1000; // 15 menit
|
||||
const SESSION_CHECK_INTERVAL = 5 * 60 * 1000; // 5 menit
|
||||
const TAB_INACTIVE_THRESHOLD = 10 * 60 * 1000; // 10 menit
|
||||
|
||||
// Basic computed properties
|
||||
const isAuthenticated = computed(
|
||||
() => internalStatus.value === "authenticated"
|
||||
);
|
||||
const isLoading = computed(() => internalStatus.value === "loading");
|
||||
|
||||
// User info from session
|
||||
const user: ComputedRef<ExtendedUser | null> = computed(() => {
|
||||
if (internalData.value?.user) {
|
||||
return internalData.value.user as ExtendedUser;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Session data dari [...].ts callbacks - INILAH DATA YANG AKAN DITAMPILKAN
|
||||
const idToken = computed(() => (internalData.value as any)?.id_token || "");
|
||||
|
||||
const accessToken = computed(
|
||||
() => (internalData.value as any)?.access_token || ""
|
||||
);
|
||||
|
||||
const refreshToken = computed(
|
||||
() => (internalData.value as any)?.refresh_token || ""
|
||||
);
|
||||
|
||||
const accessTokenPayload = computed(
|
||||
() => (internalData.value as any)?.access_token_payload || null
|
||||
);
|
||||
|
||||
const expiresAt = computed(
|
||||
() => (internalData.value as any)?.expires_at || null
|
||||
);
|
||||
|
||||
const sessionScope = computed(() => (internalData.value as any)?.scope || "");
|
||||
|
||||
const jwtToken = computed(() => (internalData.value as any)?.jwt || "");
|
||||
|
||||
// User roles dari session
|
||||
const userRoles = computed(() => {
|
||||
return (internalData.value?.user as any)?.roles || [];
|
||||
});
|
||||
|
||||
const clientRoles = computed(() => {
|
||||
return (internalData.value?.user as any)?.client_roles || [];
|
||||
});
|
||||
|
||||
// Session expires
|
||||
const sessionExpires = computed(() => data.value?.expires || null);
|
||||
|
||||
// Activity tracking functions
|
||||
const updateActivity = () => {
|
||||
if (process.client) {
|
||||
lastActivity.value = Date.now();
|
||||
isIdle.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkIdleStatus = () => {
|
||||
if (!process.client) return;
|
||||
const now = Date.now();
|
||||
const timeSinceLastActivity = now - lastActivity.value;
|
||||
if (
|
||||
timeSinceLastActivity >= IDLE_TIMEOUT &&
|
||||
tabVisible.value &&
|
||||
isAuthenticated.value
|
||||
) {
|
||||
isIdle.value = true;
|
||||
handleIdleTimeout();
|
||||
}
|
||||
};
|
||||
|
||||
const handleIdleTimeout = async () => {
|
||||
if (!process.client) return;
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (
|
||||
session &&
|
||||
session.expires &&
|
||||
new Date(session.expires) > new Date()
|
||||
) {
|
||||
await navigateTo("/auth/login?reason=idle&continue=true");
|
||||
} else {
|
||||
await logout({ reason: "expired" });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Idle timeout handling error:", error);
|
||||
await logout({ reason: "expired" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibilityChange = async () => {
|
||||
if (!process.client) return;
|
||||
tabVisible.value = !document.hidden;
|
||||
if (!document.hidden) {
|
||||
updateActivity();
|
||||
const inactiveTime = Date.now() - lastActivity.value;
|
||||
if (inactiveTime >= TAB_INACTIVE_THRESHOLD && isAuthenticated.value) {
|
||||
await handleTabReactivation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabReactivation = async () => {
|
||||
if (!process.client) return;
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (
|
||||
session &&
|
||||
session.expires &&
|
||||
new Date(session.expires) > new Date()
|
||||
) {
|
||||
await navigateTo("/auth/login?reason=tab_inactive&continue=true");
|
||||
} else {
|
||||
await navigateTo("/auth/login?reason=session_expired");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Tab reactivation error:", error);
|
||||
await navigateTo("/auth/login?reason=error");
|
||||
}
|
||||
};
|
||||
|
||||
const startSessionMonitoring = (): (() => void) | null => {
|
||||
if (!process.client) {
|
||||
console.warn("Session monitoring requires client-side environment");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAuthenticated.value) {
|
||||
console.warn("Session monitoring requires authenticated user");
|
||||
return null;
|
||||
}
|
||||
|
||||
const events = [
|
||||
"mousedown",
|
||||
"mousemove",
|
||||
"keydown",
|
||||
"scroll",
|
||||
"touchstart",
|
||||
"click"
|
||||
];
|
||||
const activityHandler = () => updateActivity();
|
||||
|
||||
events.forEach((event) => {
|
||||
document.addEventListener(event, activityHandler, { passive: true });
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
const idleInterval = setInterval(checkIdleStatus, 60 * 1000);
|
||||
|
||||
const sessionInterval = setInterval(async () => {
|
||||
if (isAuthenticated.value) {
|
||||
try {
|
||||
await getSession();
|
||||
} catch (error) {
|
||||
console.error("Session check failed:", error);
|
||||
clearInterval(sessionInterval);
|
||||
await logout({ reason: "expired" });
|
||||
}
|
||||
}
|
||||
}, SESSION_CHECK_INTERVAL);
|
||||
|
||||
return () => {
|
||||
events.forEach((event) => {
|
||||
document.removeEventListener(event, activityHandler);
|
||||
});
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
clearInterval(idleInterval);
|
||||
clearInterval(sessionInterval);
|
||||
};
|
||||
};
|
||||
|
||||
// **PERBAIKAN: Stop Session Monitoring Function**
|
||||
const stopSessionMonitoring = (): void => {
|
||||
// Implementation bisa ditambahkan jika diperlukan
|
||||
//console.log("Stopping session monitoring...");
|
||||
};
|
||||
|
||||
// Role checking functions
|
||||
const hasRole = (role: string): boolean => {
|
||||
return userRoles.value.includes(role);
|
||||
};
|
||||
|
||||
const hasClientRole = (role: string): boolean => {
|
||||
return clientRoles.value.includes(role);
|
||||
};
|
||||
|
||||
const hasAnyRole = (roles: string[]): boolean => {
|
||||
return roles.some((role) => userRoles.value.includes(role));
|
||||
};
|
||||
|
||||
const isAdmin = computed(
|
||||
() => hasRole("admin") || hasRole("super_admin") || hasRole("realm-admin")
|
||||
);
|
||||
|
||||
// Login function
|
||||
const login = async (provider = "keycloak") => {
|
||||
try {
|
||||
const currentUrl = window.location.pathname;
|
||||
const callbackUrl = currentUrl !== "/auth/login" ? currentUrl : "/";
|
||||
|
||||
const result = await signIn(provider, {
|
||||
callbackUrl,
|
||||
redirect: true
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
// **PERBAIKAN: Update activity setelah login berhasil**
|
||||
updateActivity();
|
||||
await getSession();
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Enhanced Logout function dengan reason support
|
||||
const logout = async (options?: {
|
||||
redirect?: string;
|
||||
clearStorage?: boolean;
|
||||
confirmDialog?: boolean;
|
||||
reason?: "manual" | "expired" | "admin" | "idle";
|
||||
}) => {
|
||||
const defaultOptions = {
|
||||
redirect: "/auth/login",
|
||||
clearStorage: true,
|
||||
confirmDialog: false,
|
||||
reason: "idle" as const,
|
||||
...options
|
||||
};
|
||||
|
||||
try {
|
||||
//console.log("Logout called with options:", defaultOptions);
|
||||
|
||||
// Menghilangkan pemanggilan fullLogout manual sesuai permintaan
|
||||
|
||||
if (defaultOptions.clearStorage && process.client) {
|
||||
//console.log("Clearing localStorage and sessionStorage");
|
||||
localStorage.removeItem("user-preferences");
|
||||
localStorage.removeItem("app-state");
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
// Memanggil API logout server untuk menghandle logout Keycloak dan pembersihan session
|
||||
if (process.client) {
|
||||
// Mengambil keycloakIssuer dan redirectUri dari public runtime config
|
||||
const config = useRuntimeConfig();
|
||||
const keycloakIssuer = config.public.keycloakIssuer;
|
||||
const origin = window.location.origin;
|
||||
const redirectUri = `${origin}/auth/login`;
|
||||
|
||||
let logoutUrl = `${keycloakIssuer}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(
|
||||
redirectUri
|
||||
)}`;
|
||||
if (idToken.value) {
|
||||
logoutUrl += `&id_token_hint=${encodeURIComponent(idToken.value)}`;
|
||||
}
|
||||
|
||||
//console.log("Calling Keycloak logout URL:", logoutUrl);
|
||||
|
||||
await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ id_token_hint: idToken.value })
|
||||
});
|
||||
|
||||
// Bersihkan local storage setelah logout server
|
||||
//console.log("Clearing localStorage and sessionStorage after server logout");
|
||||
localStorage.removeItem("user-preferences");
|
||||
localStorage.removeItem("app-state");
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
await signOut({
|
||||
callbackUrl: `${defaultOptions.redirect}?reason=idle`,
|
||||
redirect: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
throw new Error("Logout failed. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
// Perbaikan fullLogout - simplified dan menggunakan logout yang sudah ada
|
||||
const fullLogout = async () => {
|
||||
try {
|
||||
console.log("Full logout initiated");
|
||||
|
||||
// 1. Stop session monitoring terlebih dahulu
|
||||
stopSessionMonitoring();
|
||||
console.log("Session monitoring stopped");
|
||||
console.log(process);
|
||||
|
||||
if (process.client) {
|
||||
// 2. Clear all storage before logout
|
||||
console.log("Clearing all storage before logout");
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem("user-preferences");
|
||||
localStorage.removeItem("app-state");
|
||||
localStorage.clear();
|
||||
|
||||
// Clear sessionStorage
|
||||
sessionStorage.clear();
|
||||
|
||||
// Log cookies and sessionStorage before clearing
|
||||
console.log("Cookies before clearing:", document.cookie);
|
||||
console.log(
|
||||
"SessionStorage before clearing:",
|
||||
JSON.stringify(sessionStorage)
|
||||
);
|
||||
|
||||
// Clear cookies if any
|
||||
const cookies = document.cookie.split(";");
|
||||
const domain = window.location.hostname;
|
||||
cookies.forEach((c, index) => {
|
||||
const eqPos = c.indexOf("=");
|
||||
const name = eqPos > -1 ? c.substr(0, eqPos).trim() : c.trim();
|
||||
|
||||
console.log(`Clearing cookie [${index}]:`, name);
|
||||
|
||||
// Clear cookie for current path
|
||||
document.cookie =
|
||||
name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/";
|
||||
|
||||
// Clear cookie for current path and domain
|
||||
document.cookie =
|
||||
name +
|
||||
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domain}`;
|
||||
|
||||
// Clear cookie for current path and domain with leading dot (for subdomains)
|
||||
if (domain.indexOf(".") !== -1) {
|
||||
const domainWithDot = "." + domain;
|
||||
document.cookie =
|
||||
name +
|
||||
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domainWithDot}`;
|
||||
}
|
||||
|
||||
// Explicitly clear Keycloak cookies if present
|
||||
const keycloakCookies = [
|
||||
"KEYCLOAK_SESSION",
|
||||
"KEYCLOAK_IDENTITY",
|
||||
"KEYCLOAK_IDENTITY_LEGACY",
|
||||
"KEYCLOAK_REMEMBER_ME",
|
||||
"KEYCLOAK_REMEMBER_ME_LEGACY"
|
||||
];
|
||||
if (keycloakCookies.includes(name)) {
|
||||
console.log(`Explicitly clearing Keycloak cookie: ${name}`);
|
||||
document.cookie =
|
||||
name +
|
||||
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domain}`;
|
||||
if (domain.indexOf(".") !== -1) {
|
||||
const domainWithDot = "." + domain;
|
||||
document.cookie =
|
||||
name +
|
||||
`=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=${domainWithDot}`;
|
||||
}
|
||||
}
|
||||
console.log("Cleared cookie:", name);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Call server API to get the correct logout URL
|
||||
const response = await $fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id_token_hint: idToken.value
|
||||
})
|
||||
});
|
||||
|
||||
if (response.success && response.logoutUrl && process.client) {
|
||||
console.log("Redirecting to Keycloak logout:", response.logoutUrl);
|
||||
localStorage.removeItem("user-preferences");
|
||||
localStorage.removeItem("app-state");
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
// Append query parameters to logoutUrl
|
||||
let logoutUrlWithParams = response.logoutUrl;
|
||||
// 4. Redirect directly to Keycloak logout endpoint with appended params
|
||||
window.location.href = logoutUrlWithParams;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to signOut if server API fails
|
||||
|
||||
// Clear local data and update status to logged out
|
||||
if (process.client) {
|
||||
// Clear localStorage and sessionStorage again to be sure
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
// Reset data and status to reflect logged out state
|
||||
internalData.value = null;
|
||||
internalStatus.value = "unauthenticated";
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("Full logout failed:", error);
|
||||
// Emergency fallback - manual logout to Keycloak
|
||||
if (process.client) {
|
||||
try {
|
||||
console.log("Executing emergency logout");
|
||||
const config = useRuntimeConfig();
|
||||
const keycloakIssuer = config.public.keycloakIssuer;
|
||||
const origin = window.location.origin;
|
||||
const redirectUri = `${origin}/auth/login?reason=emergency`;
|
||||
// Clear all storage again to ensure
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
// Build logout URL with client_id as fallback
|
||||
let emergencyLogoutUrl = `${keycloakIssuer}/protocol/openid-connect/logout?post_logout_redirect_uri=${encodeURIComponent(
|
||||
redirectUri
|
||||
)}`;
|
||||
// Add id_token_hint if available
|
||||
if (idToken.value) {
|
||||
emergencyLogoutUrl += `&id_token_hint=${encodeURIComponent(
|
||||
idToken.value
|
||||
)}`;
|
||||
} else {
|
||||
// Use client_id as alternative if id_token_hint is missing
|
||||
const clientId = config.public.keycloakClientId || "your-client-id";
|
||||
emergencyLogoutUrl += `&client_id=${encodeURIComponent(
|
||||
String(clientId)
|
||||
)}`;
|
||||
}
|
||||
console.log("Emergency logout URL:", emergencyLogoutUrl);
|
||||
window.location.href = emergencyLogoutUrl;
|
||||
} catch (emergencyError) {
|
||||
console.error("Emergency logout failed:", emergencyError);
|
||||
// Last resort - force redirect and clear storage
|
||||
if (process.client) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.href = "/auth/login?reason=force";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Force refresh session
|
||||
const forceRefreshSession = async () => {
|
||||
try {
|
||||
await getSession();
|
||||
} catch (error) {
|
||||
console.error("Force refresh session error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// New method to check if Keycloak session is active
|
||||
const isKeycloakSessionActive = async (): Promise<boolean> => {
|
||||
if (!process.client) return false;
|
||||
|
||||
try {
|
||||
const session = await getSession();
|
||||
if (
|
||||
session &&
|
||||
session.expires &&
|
||||
new Date(session.expires) > new Date()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error("Error checking Keycloak session active status:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Add computed property for accessTokenPayload
|
||||
// **PERBAIKAN: Return semua fungsi yang diperlukan termasuk session monitoring**
|
||||
return {
|
||||
// State
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
user,
|
||||
userRoles,
|
||||
clientRoles,
|
||||
accessToken,
|
||||
sessionExpires,
|
||||
isIdle: readonly(isIdle),
|
||||
|
||||
// Raw data untuk debugging
|
||||
data: internalData,
|
||||
status: internalStatus,
|
||||
|
||||
// Session data dari [...].ts
|
||||
idToken,
|
||||
refreshToken,
|
||||
accessTokenPayload,
|
||||
expiresAt,
|
||||
sessionScope,
|
||||
jwtToken,
|
||||
|
||||
// Role checking functions
|
||||
hasRole,
|
||||
hasClientRole,
|
||||
hasAnyRole,
|
||||
isAdmin,
|
||||
|
||||
// Session monitoring functions
|
||||
updateActivity,
|
||||
startSessionMonitoring,
|
||||
stopSessionMonitoring,
|
||||
|
||||
// Authentication actions
|
||||
login,
|
||||
logout,
|
||||
fullLogout,
|
||||
refresh: async () => {
|
||||
await getSession();
|
||||
},
|
||||
forceRefreshSession,
|
||||
isKeycloakSessionActive
|
||||
};
|
||||
};
|
||||
@@ -1,246 +0,0 @@
|
||||
[
|
||||
"Dr.",
|
||||
"dr.",
|
||||
"DR.",
|
||||
"Dr",
|
||||
"dr",
|
||||
"Prof.",
|
||||
"prof.",
|
||||
"PROF.",
|
||||
"Prof",
|
||||
"Ir.",
|
||||
"ir.",
|
||||
"IR.",
|
||||
"Ir",
|
||||
"Drs.",
|
||||
"drs.",
|
||||
"DRS.",
|
||||
"Drs",
|
||||
"Dra.",
|
||||
"dra.",
|
||||
"DRA.",
|
||||
"Dra",
|
||||
"drg.",
|
||||
"drg",
|
||||
"Drg.",
|
||||
"Drg",
|
||||
"H.",
|
||||
"h.",
|
||||
"Hj.",
|
||||
"hj.",
|
||||
"HJ.",
|
||||
"Hajjah",
|
||||
"K.H.",
|
||||
"KH.",
|
||||
"K.H",
|
||||
"KH",
|
||||
"Kyai",
|
||||
"Ustadz",
|
||||
"Ustaz",
|
||||
"Ust.",
|
||||
"Ustadzah",
|
||||
"Ustazah",
|
||||
"Habib",
|
||||
"Sayyid",
|
||||
"Syarif",
|
||||
"Syarifah",
|
||||
"Pdt.",
|
||||
"Pdt",
|
||||
"Pendeta",
|
||||
"Ev.",
|
||||
"Ev",
|
||||
"Pnt.",
|
||||
"Pnt",
|
||||
"Pdp.",
|
||||
"Pdp",
|
||||
"Pdm.",
|
||||
"Pdm",
|
||||
"Pastor",
|
||||
"Romo",
|
||||
"Rm.",
|
||||
"Rm",
|
||||
"Suster",
|
||||
"Sr.",
|
||||
"Sr",
|
||||
"Bruder",
|
||||
"Br.",
|
||||
"Br",
|
||||
"Ida",
|
||||
"I",
|
||||
"Pedanda",
|
||||
"Bhagawan",
|
||||
"Rsi",
|
||||
"Bhikkhu",
|
||||
"Bhikkhuni",
|
||||
"Samanera",
|
||||
"Samaneri",
|
||||
"Prada",
|
||||
"Pratu",
|
||||
"Praka",
|
||||
"Kopda",
|
||||
"Koptu",
|
||||
"Kopka",
|
||||
"Serda",
|
||||
"Sertu",
|
||||
"Serka",
|
||||
"Serma",
|
||||
"Pelda",
|
||||
"Peltu",
|
||||
"Letda",
|
||||
"Lettu",
|
||||
"Kapten",
|
||||
"Mayor",
|
||||
"Letkol",
|
||||
"Kolonel",
|
||||
"Brigjen",
|
||||
"Mayjen",
|
||||
"Letjen",
|
||||
"Jenderal",
|
||||
"Klk",
|
||||
"Kls",
|
||||
"Kld",
|
||||
"Kpm",
|
||||
"Kpt",
|
||||
"Kps",
|
||||
"Pel",
|
||||
"Pem",
|
||||
"Let",
|
||||
"Lettu",
|
||||
"Letda",
|
||||
"Kapt",
|
||||
"May",
|
||||
"Letkol",
|
||||
"Kol",
|
||||
"Laksda",
|
||||
"Laksma",
|
||||
"Laksdya",
|
||||
"Prada",
|
||||
"Pratu",
|
||||
"Praka",
|
||||
"Kopda",
|
||||
"Koptu",
|
||||
"Kopka",
|
||||
"Serda",
|
||||
"Sertu",
|
||||
"Serka",
|
||||
"Serma",
|
||||
"Pelda",
|
||||
"Peltu",
|
||||
"Letda",
|
||||
"Lettu",
|
||||
"Kapten",
|
||||
"Mayor",
|
||||
"Letkol",
|
||||
"Kolonel",
|
||||
"Marsda",
|
||||
"Marsma",
|
||||
"Marsdya",
|
||||
"Bhayangkara",
|
||||
"Bharada",
|
||||
"Bharatu",
|
||||
"Bharaka",
|
||||
"Abrip",
|
||||
"Abriptu",
|
||||
"Abripda",
|
||||
"Brigadir",
|
||||
"Bripka",
|
||||
"Briptu",
|
||||
"Bripda",
|
||||
"Aipda",
|
||||
"Aiptu",
|
||||
"Ipda",
|
||||
"Iptu",
|
||||
"AKP",
|
||||
"Kompol",
|
||||
"AKBP",
|
||||
"Kombes",
|
||||
"Brigjen",
|
||||
"Irjen",
|
||||
"Komjen",
|
||||
"Jenderal",
|
||||
"Raden",
|
||||
"R.",
|
||||
"R",
|
||||
"Raden Mas",
|
||||
"R.M.",
|
||||
"RM.",
|
||||
"Raden Ayu",
|
||||
"R.A.",
|
||||
"RA.",
|
||||
"Raden Ajeng",
|
||||
"R.Aj.",
|
||||
"Kanjeng",
|
||||
"K.",
|
||||
"Gusti",
|
||||
"Sultan",
|
||||
"Sunan",
|
||||
"Pangeran",
|
||||
"Teuku",
|
||||
"Cut",
|
||||
"Andi",
|
||||
"Daeng",
|
||||
"Puang",
|
||||
"La",
|
||||
"Wa",
|
||||
"Baginda",
|
||||
"Tuanku",
|
||||
"Tengku",
|
||||
"Raja",
|
||||
"Datuk",
|
||||
"Sutan",
|
||||
"Bapak",
|
||||
"Bpk.",
|
||||
"Bpk",
|
||||
"Bp.",
|
||||
"Bp",
|
||||
"Pak",
|
||||
"Ibu",
|
||||
"Bu",
|
||||
"Saudara",
|
||||
"Sdr.",
|
||||
"Sdr",
|
||||
"Saudari",
|
||||
"Sdri.",
|
||||
"Sdri",
|
||||
"Tuan",
|
||||
"Tn.",
|
||||
"Tn",
|
||||
"Nyonya",
|
||||
"Ny.",
|
||||
"Ny",
|
||||
"Nona",
|
||||
"Nn.",
|
||||
"Nn",
|
||||
"Mas",
|
||||
"Mbak",
|
||||
"Mba",
|
||||
"Dik",
|
||||
"Adik",
|
||||
"Kakak",
|
||||
"Kak",
|
||||
"Bang",
|
||||
"Abang",
|
||||
"Uda",
|
||||
"Uni",
|
||||
"Yang Mulia",
|
||||
"YM.",
|
||||
"Yang Terhormat",
|
||||
"Yth.",
|
||||
"Yang Berbahagia",
|
||||
"Yb.",
|
||||
"Notaris",
|
||||
"Advokat",
|
||||
"Adv.",
|
||||
"S.H.",
|
||||
"SH.",
|
||||
"Ak.",
|
||||
"CA.",
|
||||
"CPA.",
|
||||
"CMA.",
|
||||
"Insinyur",
|
||||
"Ins.",
|
||||
"Dokter",
|
||||
"Doktor",
|
||||
"Guru",
|
||||
"Dosen"
|
||||
]
|
||||
@@ -1,198 +0,0 @@
|
||||
[
|
||||
"S.Kom",
|
||||
"S.Kom.",
|
||||
"S.T",
|
||||
"S.T.",
|
||||
"S.E",
|
||||
"S.E.",
|
||||
"S.H",
|
||||
"S.H.",
|
||||
"S.Pd",
|
||||
"S.Pd.",
|
||||
"S.Ked",
|
||||
"S.Ked.",
|
||||
"S.Psi",
|
||||
"S.Psi.",
|
||||
"S.S",
|
||||
"S.S.",
|
||||
"S.Farm",
|
||||
"S.Farm.",
|
||||
"S.Si",
|
||||
"S.Si.",
|
||||
"S.Sos",
|
||||
"S.Sos.",
|
||||
"S.IP",
|
||||
"S.IP.",
|
||||
"S.I.Kom",
|
||||
"S.I.Kom.",
|
||||
"S.Hum",
|
||||
"S.Hum.",
|
||||
"S.Sn",
|
||||
"S.Sn.",
|
||||
"S.Ag",
|
||||
"S.Ag.",
|
||||
"S.Th.I",
|
||||
"S.Th.I.",
|
||||
"S.Fil",
|
||||
"S.Fil.",
|
||||
"S.Ars",
|
||||
"S.Ars.",
|
||||
"S.Kep",
|
||||
"S.Kep.",
|
||||
"S.Gz",
|
||||
"S.Gz.",
|
||||
"S.KM",
|
||||
"S.KM.",
|
||||
"S.K.M",
|
||||
"S.K.M.",
|
||||
"S.Mat",
|
||||
"S.Mat.",
|
||||
"S.Stat",
|
||||
"S.Stat.",
|
||||
"S.Kel",
|
||||
"S.Kel.",
|
||||
"S.Pi",
|
||||
"S.Pi.",
|
||||
"S.Hut",
|
||||
"S.Hut.",
|
||||
"S.TP",
|
||||
"S.TP.",
|
||||
"S.P",
|
||||
"S.P.",
|
||||
"S.Pt",
|
||||
"S.Pt.",
|
||||
"S.K.G",
|
||||
"S.K.G.",
|
||||
"S.Ds",
|
||||
"S.Ds.",
|
||||
"M.Kom",
|
||||
"M.Kom.",
|
||||
"M.T",
|
||||
"M.T.",
|
||||
"M.M",
|
||||
"M.M.",
|
||||
"M.Si",
|
||||
"M.Si.",
|
||||
"M.Pd",
|
||||
"M.Pd.",
|
||||
"M.Kes",
|
||||
"M.Kes.",
|
||||
"M.H",
|
||||
"M.H.",
|
||||
"M.A",
|
||||
"M.A.",
|
||||
"M.Sc",
|
||||
"M.Sc.",
|
||||
"MBA",
|
||||
"M.B.A",
|
||||
"M.B.A.",
|
||||
"M.Sos",
|
||||
"M.Sos.",
|
||||
"M.Hum",
|
||||
"M.Hum.",
|
||||
"M.Ag",
|
||||
"M.Ag.",
|
||||
"M.Fil",
|
||||
"M.Fil.",
|
||||
"M.Sn",
|
||||
"M.Sn.",
|
||||
"M.Ars",
|
||||
"M.Ars.",
|
||||
"M.Kep",
|
||||
"M.Kep.",
|
||||
"M.Farm",
|
||||
"M.Farm.",
|
||||
"M.E",
|
||||
"M.E.",
|
||||
"M.Ak",
|
||||
"M.Ak.",
|
||||
"M.IP",
|
||||
"M.IP.",
|
||||
"M.I.Kom",
|
||||
"M.I.Kom.",
|
||||
"M.Psi",
|
||||
"M.Psi.",
|
||||
"Ph.D",
|
||||
"Ph.D.",
|
||||
"PhD",
|
||||
"Dr.",
|
||||
"Dr",
|
||||
"dr.",
|
||||
"dr",
|
||||
"drg.",
|
||||
"drg",
|
||||
"Sp.A",
|
||||
"Sp.A.",
|
||||
"Sp.OG",
|
||||
"Sp.OG.",
|
||||
"Sp.PD",
|
||||
"Sp.PD.",
|
||||
"Sp.B",
|
||||
"Sp.B.",
|
||||
"Sp.JP",
|
||||
"Sp.JP.",
|
||||
"Sp.M",
|
||||
"Sp.M.",
|
||||
"Sp.THT",
|
||||
"Sp.THT.",
|
||||
"Sp.An",
|
||||
"Sp.An.",
|
||||
"Sp.Rad",
|
||||
"Sp.Rad.",
|
||||
"Sp.KJ",
|
||||
"Sp.KJ.",
|
||||
"Sp.S",
|
||||
"Sp.S.",
|
||||
"Sp.U",
|
||||
"Sp.U.",
|
||||
"Sp.BS",
|
||||
"Sp.BS.",
|
||||
"Sp.BA",
|
||||
"Sp.BA.",
|
||||
"Sp.KK",
|
||||
"Sp.KK.",
|
||||
"Sp.P",
|
||||
"Sp.P.",
|
||||
"Sp.PA",
|
||||
"Sp.PA.",
|
||||
"Sp.PK",
|
||||
"Sp.PK.",
|
||||
"Sp.RM",
|
||||
"Sp.RM.",
|
||||
"Sp.Or",
|
||||
"Sp.Or.",
|
||||
"Sp.KFR",
|
||||
"Sp.KFR.",
|
||||
"Sp.G",
|
||||
"Sp.G.",
|
||||
"A.Md",
|
||||
"A.Md.",
|
||||
"A.Ma",
|
||||
"A.Ma.",
|
||||
"A.Md.Keb",
|
||||
"A.Md.Keb.",
|
||||
"A.Md.Kep",
|
||||
"A.Md.Kep.",
|
||||
"A.Md.Kom",
|
||||
"A.Md.Kom.",
|
||||
"A.Md.T",
|
||||
"A.Md.T.",
|
||||
"A.Md.Gz",
|
||||
"A.Md.Gz.",
|
||||
"A.Md.Farm",
|
||||
"A.Md.Farm.",
|
||||
"A.P",
|
||||
"A.P.",
|
||||
"Apt.",
|
||||
"Apt",
|
||||
"Ners",
|
||||
"Ners.",
|
||||
"Prof.",
|
||||
"Prof",
|
||||
"Ir.",
|
||||
"Ir",
|
||||
"Drs.",
|
||||
"Drs",
|
||||
"Dra.",
|
||||
"Dra"
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"menus": [
|
||||
{
|
||||
"id": "1",
|
||||
"title": "Medis",
|
||||
"url": "",
|
||||
"icon": "mdi-medical-bag",
|
||||
"parentId": null,
|
||||
"order": 1,
|
||||
"isActive": true,
|
||||
"reference": "referensi",
|
||||
"children": [
|
||||
{
|
||||
"id": "1-1",
|
||||
"title": "Diagnosa",
|
||||
"url": "bridging/referensi/medis",
|
||||
"icon": "",
|
||||
"parentId": "1",
|
||||
"order": 1,
|
||||
"isActive": true,
|
||||
"reference": "referensi"
|
||||
},
|
||||
{
|
||||
"id": "1-2",
|
||||
"title": "Poliklinik",
|
||||
"url": "bridging/referensi/medis",
|
||||
"icon": "",
|
||||
"parentId": "1",
|
||||
"order": 2,
|
||||
"isActive": true,
|
||||
"reference": "referensi"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"references": ["Referensi", "Main", "Admin"],
|
||||
"menuOptions": ["Select a menu", "Main Menu", "Side Menu", "Footer Menu"]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Dashboard",
|
||||
"path": "/dashboard",
|
||||
"templateType": "dashboard",
|
||||
"metadata": {
|
||||
"title": "Dashboard",
|
||||
"description": "Main dashboard page",
|
||||
"keywords": ["dashboard", "admin"]
|
||||
},
|
||||
"content": {},
|
||||
"createdBy": "admin",
|
||||
"createdAt": "2025-06-30T05:32:00Z",
|
||||
"updatedAt": "2025-06-30T05:32:00Z",
|
||||
"status": "published"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"roles": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Admin",
|
||||
"description": "Full system access",
|
||||
"permissions": ["create", "read", "update", "delete"],
|
||||
"directories": ["/pages/admin/*", "/pages/content/*", "/pages/roles/*"],
|
||||
"createdAt": "2025-06-30T05:32:00Z"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Editor",
|
||||
"description": "Content management access",
|
||||
"permissions": ["create", "read", "update"],
|
||||
"directories": ["/pages/content/*", "/pages/blog/*"],
|
||||
"createdAt": "2025-06-30T05:32:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// export default defineNuxtRouteMiddleware((to, from) => {
|
||||
// const authStore = useAuthStore()
|
||||
|
||||
// // Pastikan user sudah login
|
||||
// if (!authStore.isAuthenticated) {
|
||||
// return navigateTo('/login')
|
||||
// }
|
||||
|
||||
// // Ambil role yang dibutuhkan dari meta page
|
||||
// const requiredRoles = to.meta.roles as string[] | undefined
|
||||
// const requiredPermissions = to.meta.permissions as string[] | undefined
|
||||
|
||||
// // Cek role
|
||||
// if (requiredRoles && requiredRoles.length > 0) {
|
||||
// const hasRequiredRole = requiredRoles.some(role =>
|
||||
// authStore.hasRole(role)
|
||||
// )
|
||||
|
||||
// if (!hasRequiredRole) {
|
||||
// throw createError({
|
||||
// statusCode: 403,
|
||||
// statusMessage: 'Anda tidak memiliki akses ke halaman ini'
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Cek permissions
|
||||
// if (requiredPermissions && requiredPermissions.length > 0) {
|
||||
// const hasRequiredPermission = requiredPermissions.some(permission =>
|
||||
// authStore.hasPermission(permission)
|
||||
// )
|
||||
|
||||
// if (!hasRequiredPermission) {
|
||||
// throw createError({
|
||||
// statusCode: 403,
|
||||
// statusMessage: 'Anda tidak memiliki izin untuk mengakses halaman ini'
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
+12
-88
@@ -18,39 +18,14 @@ export default defineNuxtConfig({
|
||||
]
|
||||
}
|
||||
},
|
||||
// css: [
|
||||
// "vuetify/lib/styles/main.sass", // Core Vuetify styles
|
||||
// "@mdi/font/css/materialdesignicons.min.css", // Material Design Icons
|
||||
// "~/scss/style.scss", // Custom application styles
|
||||
// ],
|
||||
/**
|
||||
* Modules dan plugins
|
||||
* Integrasi dengan ecosystem Nuxt.js
|
||||
*/
|
||||
|
||||
modules: [
|
||||
"@pinia/nuxt", // State management dengan Pinia
|
||||
[
|
||||
"@sidebase/nuxt-auth",
|
||||
{
|
||||
// Add auth module options here as per Sidebase auth guide
|
||||
auth: {
|
||||
isEnabled: true,
|
||||
baseURL:
|
||||
process.env.AUTH_ORIGIN || "http://meninjar.dev.rssa.id:3001",
|
||||
provider: {
|
||||
type: "authjs"
|
||||
// Additional provider options can be added here
|
||||
},
|
||||
globalAppMiddleware: {
|
||||
isEnabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"@pinia/nuxt",
|
||||
|
||||
],
|
||||
|
||||
plugins: [
|
||||
"~/plugins/vuetify.ts" // Setup Vuetify dengan konfigurasi custom
|
||||
"~/plugins/vuetify.ts"
|
||||
],
|
||||
ssr: false,
|
||||
|
||||
@@ -66,78 +41,27 @@ export default defineNuxtConfig({
|
||||
define: {
|
||||
"process.env.DEBUG": false
|
||||
},
|
||||
server: {
|
||||
allowedHosts: ["meninjar.dev.rssa.id"]
|
||||
}
|
||||
// css: {
|
||||
// preprocessorOptions: {
|
||||
// scss: {
|
||||
// // Global SCSS variables yang dapat diakses di semua komponen
|
||||
// additionalData: `@import "~/scss/variables.scss";`,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
|
||||
},
|
||||
|
||||
nitro: {
|
||||
serveStatic: true,
|
||||
compressPublicAssets: true, // Kompresi asset untuk faster loading,
|
||||
|
||||
/**
|
||||
* Prerender routes untuk static generation
|
||||
* Halaman yang akan di-generate saat build time
|
||||
*/
|
||||
compressPublicAssets: true,
|
||||
prerender: {
|
||||
// routes: [
|
||||
// '/', // Landing page
|
||||
// '/auth/login', // Halaman login
|
||||
// '/auth/register' // Halaman register
|
||||
// ]
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Route protection untuk production
|
||||
* Middleware untuk security dan rate limiting
|
||||
*/
|
||||
// routeRules: {
|
||||
// '/admin/**': {
|
||||
// headers: { 'X-Robots-Tag': 'noindex' }
|
||||
// // index: false // ❌ Property ini tidak valid - sudah dihapus
|
||||
// }
|
||||
// }
|
||||
|
||||
},
|
||||
runtimeConfig: {
|
||||
authSecret: process.env.AUTH_SECRET,
|
||||
keycloakClientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
||||
keycloakClientId: process.env.KEYCLOAK_CLIENT_ID,
|
||||
// Move keycloakIssuer to public to expose on client side
|
||||
public: {
|
||||
authUrl: process.env.AUTH_ORIGIN || "http://meninjar.dev.rssa.id:3000",
|
||||
keycloakIssuer:
|
||||
process.env.KEYCLOAK_ISSUER || "https://auth.rssa.top/realms/sandbox"
|
||||
}
|
||||
},
|
||||
// auth: {
|
||||
// isEnabled: true,
|
||||
// baseURL: process.env.AUTH_ORIGIN,
|
||||
// provider: {
|
||||
// type: "authjs",
|
||||
// },
|
||||
// globalAppMiddleware: {
|
||||
// isEnabled: true,
|
||||
// },
|
||||
// },
|
||||
|
||||
devServerHandlers: [],
|
||||
|
||||
hooks: {},
|
||||
/**
|
||||
* Development tools configuration
|
||||
* Tools untuk meningkatkan developer experience
|
||||
*/
|
||||
|
||||
devtools: {
|
||||
enabled: true, // Enable Nuxt DevTools untuk debugging
|
||||
enabled: true,
|
||||
timeline: {
|
||||
enabled: true // Enable timeline untuk performance monitoring
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Generated
-164
@@ -1251,17 +1251,6 @@
|
||||
"integrity": "sha512-4wMPu9iN3/HL97QblBsBay3E1etIciR84izI3U+4iALY+JHCrI+a2jO0qbAZ/nxKoegypYEaiiqWXylm+/zfrw==",
|
||||
"license": "Apache 2"
|
||||
},
|
||||
"node_modules/@netlify/blobs": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@netlify/blobs/-/blobs-8.2.0.tgz",
|
||||
"integrity": "sha512-9djLZHBKsoKk8XCgwWSEPK9QnT8qqxEQGuYh48gFIcNLvpBKkLnHbDZuyUxmNemCfDz7h0HnMXgSPnnUVgARhg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^14.16.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@netlify/dev-utils": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@netlify/dev-utils/-/dev-utils-2.2.0.tgz",
|
||||
@@ -1422,159 +1411,6 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.9.tgz",
|
||||
"integrity": "sha512-pVyd8/1y1l5atQRvOaLOvfbmRwefxLhqQOzYo/M7FQ5eaRwA1+wuCn7t39VwEgDd7Aw1+AIWwd+MURXUeXhwDw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.9.tgz",
|
||||
"integrity": "sha512-DwdeJqP7v8wmoyTWPbPVodTwCybBZa02xjSJ6YQFIFZFZ7dFgrieKW4Eo0GoIcOJq5+JxkQyejmI+8zwDp3pwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.9.tgz",
|
||||
"integrity": "sha512-wdQsKsIsGSNdFojvjW3Ozrh8Q00+GqL3wTaMjDkQxVtRbAqfFBtrLPO0IuWChVUP2UeuQcHpVeUvu0YgOP00+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.9.tgz",
|
||||
"integrity": "sha512-6VpS+bodQqzOeCwGxoimlRoosiWlSc0C224I7SQWJZoyJuT1ChNCo+45QQH+/GtbR/s7nhaUqmiHdzZC9TXnXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.9.tgz",
|
||||
"integrity": "sha512-XxG3yj61WDd28NA8gFASIR+2viQaYZEFQagEodhI/R49gXWnYhiflTeeEmCn7Vgnxa/OfK81h1gvhUZ66lozpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.9.tgz",
|
||||
"integrity": "sha512-/dnscWqfO3+U8asd+Fc6dwL2l9AZDl7eKtPNKW8mKLh4Y4wOpjJiamhe8Dx+D+Oq0GYVjuW0WwjIxYWVozt2bA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.9.tgz",
|
||||
"integrity": "sha512-T/iPnyurOK5a4HRUcxAlss8uzoEf5h9tkd+W2dSWAfzxv8WLKlUgbfk+DH43JY3Gc2xK5URLuXrxDZ2mGfk/jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.9.tgz",
|
||||
"integrity": "sha512-BLiPKJomaPrTAb7ykjA0LPcuuNMLDVK177Z1xe0nAem33+9FIayU4k/OWrtSn9SAJW/U60+1hoey5z+KCHdRLQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.9.tgz",
|
||||
"integrity": "sha512-/72/dZfjXXNY/u+n8gqZDjI6rxKMpYsgBBYNZKWOQw0BpBF7WCnPflRy3ZtvQ2+IYI3ZH2bPyj7K+6a6wNk90Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<!-- Breadcrumb -->
|
||||
<v-breadcrumbs :items="breadcrumbs" divider=">" class="pa-0 mb-4">
|
||||
<template v-slot:item="{ item }">
|
||||
<v-breadcrumbs-item :to="item.to" :disabled="item.disabled">
|
||||
{{ item.title }}
|
||||
</v-breadcrumbs-item>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
|
||||
<!-- Page Title -->
|
||||
<h1 class="text-h5 mb-6">Buat Registrasi</h1>
|
||||
|
||||
<!-- Registration Form -->
|
||||
<v-form ref="form" v-model="valid" lazy-validation>
|
||||
<!-- Registration Type Section -->
|
||||
<RegistrationType
|
||||
v-model:registrationData="formData.registrationType"
|
||||
@update="updateSection('registrationType')"
|
||||
/>
|
||||
|
||||
<!-- Patient Data Section -->
|
||||
<PatientData
|
||||
v-model:patientData="formData.patient"
|
||||
@update="updateSection('patient')"
|
||||
class="mt-6"
|
||||
/>
|
||||
|
||||
<!-- Appointment Schedule Section -->
|
||||
<AppointmentSchedule
|
||||
v-model:scheduleData="formData.schedule"
|
||||
@update="updateSection('schedule')"
|
||||
class="mt-6"
|
||||
/>
|
||||
|
||||
<!-- Risk Assessment Section -->
|
||||
<RiskAssessment
|
||||
v-model:riskData="formData.riskAssessment"
|
||||
@update="updateSection('riskAssessment')"
|
||||
class="mt-6"
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<v-row class="mt-8">
|
||||
<v-col cols="12" class="d-flex justify-end gap-3">
|
||||
<v-btn variant="outlined" color="primary" @click="resetForm">
|
||||
Reset
|
||||
</v-btn>
|
||||
<v-btn color="primary" :loading="loading" @click="submitForm">
|
||||
Simpan Registrasi
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import RegistrationType from "~/components/apps/registration/form/RegistrationType.vue";
|
||||
import PatientData from "~/components/apps/registration/form/PatientData.vue";
|
||||
import AppointmentSchedule from "~/components/apps/registration/form/AppointmentSchedule.vue";
|
||||
import RiskAssessment from "~/components/apps/registration/form/RiskAssessment.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const form = ref();
|
||||
const valid = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const breadcrumbs = [
|
||||
{ title: "Rawat Jalan", to: "/appointment", disabled: false },
|
||||
{ title: "Registrasi", to: "/appointment/registration", disabled: false },
|
||||
{ title: "Buat Registrasi", to: "", disabled: true }
|
||||
];
|
||||
|
||||
const formData = reactive({
|
||||
registrationType: {
|
||||
visitType: "",
|
||||
treatmentType: "Rawat Jalan",
|
||||
triage: ""
|
||||
},
|
||||
patient: {
|
||||
name: "",
|
||||
medicalRecordNumber: "",
|
||||
category: "",
|
||||
birthDate: "",
|
||||
gender: "",
|
||||
address: "",
|
||||
phoneCode: "+62",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
identityType: "",
|
||||
identityNumber: ""
|
||||
},
|
||||
schedule: {
|
||||
paymentMethod: "Pribadi",
|
||||
referralNumber: "",
|
||||
polyclinicName: "",
|
||||
doctorName: "",
|
||||
consultationDate: "",
|
||||
consultationTime: "",
|
||||
slotAvailable: true
|
||||
},
|
||||
riskAssessment: {
|
||||
walkingAbility: null,
|
||||
supportWhileSitting: null,
|
||||
score: 0
|
||||
}
|
||||
});
|
||||
|
||||
const updateSection = (section) => {
|
||||
console.log(`Section ${section} updated:`, formData[section]);
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
const { valid } = await form.value.validate();
|
||||
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
// API call simulation
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
console.log("Form submitted:", formData);
|
||||
|
||||
// Navigate to success page or show notification
|
||||
router.push("/appointment/registration");
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value.reset();
|
||||
};
|
||||
</script>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/*Call Components*/
|
||||
import RevenueCard from '@/components/dashboard/RevenueCard.vue';
|
||||
import NewCustomer from '@/components/dashboard/NewCustomer.vue';
|
||||
import Totalincome from '@/components/dashboard/TotalIncome.vue';
|
||||
import RevenueProduct from '@/components/dashboard/RevenueProducts.vue';
|
||||
import DailyActivities from '@/components/dashboard/DailyActivities.vue';
|
||||
import BlogCards from '@/components/dashboard/BlogCards.vue';
|
||||
// definePageMeta({
|
||||
// middleware: 'role',
|
||||
// });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" lg="8"><RevenueCard /></v-col>
|
||||
<v-col cols="12" lg="4"
|
||||
><NewCustomer class="mb-6" />
|
||||
<Totalincome />
|
||||
</v-col>
|
||||
<v-col cols="12" lg="8"><RevenueProduct/></v-col>
|
||||
<v-col cols="12" lg="4"><DailyActivities/> </v-col>
|
||||
<v-col cols="12"><BlogCards/></v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
@@ -1,183 +0,0 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<!-- Breadcrumb -->
|
||||
<v-breadcrumbs :items="breadcrumbs" class="pa-0 mb-4">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-home" size="small"></v-icon>
|
||||
</template>
|
||||
</v-breadcrumbs>
|
||||
|
||||
<!-- Form Container -->
|
||||
<v-card elevation="2">
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="submitForm">
|
||||
<!-- Header Section -->
|
||||
<v-card-title class="bg-grey-lighten-3 py-3">
|
||||
<span class="text-subtitle-1 font-weight-medium"
|
||||
>DATA PASIEN</span
|
||||
>
|
||||
</v-card-title>
|
||||
|
||||
<!-- Data Diri Section -->
|
||||
<DataDiriSection
|
||||
v-model:formData="formData.dataDiri"
|
||||
v-model:isBayiBaru="formData.isBayiBaru"
|
||||
/>
|
||||
|
||||
<!-- Alamat Section -->
|
||||
<AlamatSection
|
||||
v-model:alamatIdentitas="formData.alamatIdentitas"
|
||||
v-model:alamatDomisili="formData.alamatDomisili"
|
||||
v-model:samaDenganIdentitas="formData.samaDenganIdentitas"
|
||||
/>
|
||||
|
||||
<!-- Kesehatan Section -->
|
||||
<KesehatanSection v-model:formData="formData.kesehatan" />
|
||||
|
||||
<!-- Pembayaran Section -->
|
||||
<PembayaranSection v-model:formData="formData.pembayaran" />
|
||||
|
||||
<!-- Sosial Section -->
|
||||
<SosialSection v-model:formData="formData.sosial" />
|
||||
|
||||
<!-- Penanggung Jawab Section -->
|
||||
<PenanggungJawabSection
|
||||
v-model:formData="formData.penanggungJawab"
|
||||
v-model:samaDenganPasien="formData.samaDenganPasien"
|
||||
:alamatPasien="formData.alamatDomisili"
|
||||
/>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<v-card-actions class="pa-4 bg-grey-lighten-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="outlined" color="grey" @click="resetForm">
|
||||
Reset
|
||||
</v-btn>
|
||||
<v-btn
|
||||
type="submit"
|
||||
variant="flat"
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
Simpan Pasien
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import DataDiriSection from "~/components/apps/patient/form/DataDiriSection.vue";
|
||||
import AlamatSection from "~/components/apps/patient/form/AlamatSection.vue";
|
||||
import KesehatanSection from "~/components/apps/patient/form/KesehatanSection.vue";
|
||||
import PembayaranSection from "~/components/apps/patient/form/PembayaranSection.vue";
|
||||
import SosialSection from "~/components/apps/patient/form/SosialSection.vue";
|
||||
import PenanggungJawabSection from "~/components/apps/patient/form/PenanggungJawabSection.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const form = ref();
|
||||
const valid = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const breadcrumbs = [
|
||||
{
|
||||
title: "Pasien",
|
||||
disabled: false,
|
||||
href: "/patient"
|
||||
},
|
||||
{
|
||||
title: "Buat Pasien",
|
||||
disabled: true,
|
||||
href: "/patient/create"
|
||||
}
|
||||
];
|
||||
|
||||
const formData = reactive({
|
||||
isBayiBaru: false,
|
||||
samaDenganIdentitas: false,
|
||||
samaDenganPasien: false,
|
||||
dataDiri: {
|
||||
photo: null,
|
||||
namaLengkap: "",
|
||||
nomorTeleponSelular: "",
|
||||
nomorTeleponRumah: "",
|
||||
email: "",
|
||||
jenisKelamin: "",
|
||||
tempatLahir: "",
|
||||
tanggalLahir: "",
|
||||
jenisIdentitas: "",
|
||||
nomorIdentitas: "",
|
||||
namaIbuKandung: "",
|
||||
nomorRekamMedis: ""
|
||||
},
|
||||
alamatIdentitas: {
|
||||
desa: "",
|
||||
alamatLengkap: "",
|
||||
rt: "",
|
||||
rw: "",
|
||||
kodePos: ""
|
||||
},
|
||||
alamatDomisili: {
|
||||
desa: "",
|
||||
alamatLengkap: "",
|
||||
rt: "",
|
||||
rw: "",
|
||||
kodePos: ""
|
||||
},
|
||||
kesehatan: {
|
||||
golonganDarah: "",
|
||||
statusPerokok: "",
|
||||
jenisPenyakit: ""
|
||||
},
|
||||
pembayaran: {
|
||||
jenisPembayaran: ""
|
||||
},
|
||||
sosial: {
|
||||
agama: "",
|
||||
statusPernikahan: "",
|
||||
pendidikanTerakhir: "",
|
||||
pekerjaan: "",
|
||||
bahasaDikuasai: "",
|
||||
sukuEtnis: ""
|
||||
},
|
||||
penanggungJawab: {
|
||||
namaLengkap: "",
|
||||
jenisKelamin: "",
|
||||
hubunganDenganPasien: "",
|
||||
nomorTelepon: "",
|
||||
pekerjaan: "",
|
||||
alamat: ""
|
||||
}
|
||||
});
|
||||
|
||||
const submitForm = async () => {
|
||||
const validation = await form.value.validate();
|
||||
if (!validation.valid) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
// Simulasi API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
console.log("Submitting form data:", formData);
|
||||
|
||||
// Redirect ke halaman list pasien
|
||||
router.push("/patient");
|
||||
} catch (error) {
|
||||
console.error("Error saving patient:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.value.reset();
|
||||
};
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/*-For Set Blank Layout-*/
|
||||
definePageMeta({
|
||||
layout: "blank",
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="d-flex justify-center align-center text-center h-100">
|
||||
<div>
|
||||
<img src="~/assets/images/backgrounds/maintenance.svg" width="500" alt="under_construction" />
|
||||
<h1 class="text-h1">Opps!!!</h1>
|
||||
<h4 class="text-h4 my-8">Website is Under Construction. Check back later!</h4>
|
||||
<v-btn flat color="primary" class="mb-4" to="/">Go Back to Home</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,58 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import LogoIcon from '~/components/layout/full/logo/Logo.vue';
|
||||
/* Register form */
|
||||
import RegisterForm from '~/components/auth/RegisterForm.vue';
|
||||
/*-For Set Blank Layout-*/
|
||||
definePageMeta({
|
||||
layout: "blank",
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="pa-3">
|
||||
<v-row class="h-100vh mh-100 auth">
|
||||
<v-col cols="12" lg="5" xl="4" class="bg-surface auth">
|
||||
<div class="d-flex justify-center align-center h-100">
|
||||
<div class="mt-xl-0 mt-5 auth-card">
|
||||
<LogoIcon />
|
||||
<h2 class="text-h3 my-3 heading">Sign Up</h2>
|
||||
<div class="mb-6">Your Admin Dashboard</div>
|
||||
<RegisterForm />
|
||||
<p class="d-flex align-center justify-center textSecondary mt-6 font-weight-medium">
|
||||
Already have an Account?
|
||||
<RouterLink
|
||||
class="pl-0 text-primary opacity-1 pl-2 font-weight-medium text-decoration-none"
|
||||
height="auto"
|
||||
to="/auth/login"
|
||||
variant="plain"
|
||||
>Sign in</RouterLink
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" lg="7" xl="8" class="d-lg-flex d-none align-center justify-center authentication bg-darkgray position-relative">
|
||||
<div class="circle-top"></div>
|
||||
<div>
|
||||
<LogoIcon class="circle-bottom" />
|
||||
</div>
|
||||
<div class="d-flex justify-center align-center w-100 h-n80">
|
||||
<v-row class="justify-center z-index-2">
|
||||
<v-col xl="6" lg="7">
|
||||
<h1 class="text-h1 text-white lh-normal">
|
||||
Welcome to
|
||||
<br />
|
||||
MatDash
|
||||
</h1>
|
||||
<p class="text-h6 text-white opacity-80 font-weight-regular mt-4 lh-md">
|
||||
MatDash helps developers to build organized and well<br />
|
||||
coded dashboards full of beautiful and rich modules.
|
||||
</p>
|
||||
<v-btn to="/" size="large" color="primary" class="mt-5"> Learn More </v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,410 +0,0 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<!-- Information Panel - Responsive -->
|
||||
<v-col cols="12" lg="6" xl="5">
|
||||
<v-card elevation="2" class="h-100">
|
||||
<v-card-title class="bg-primary text-white">
|
||||
<v-icon left>mdi-information</v-icon>
|
||||
INFORMATION
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-form ref="formRef" v-model="isFormValid">
|
||||
<!-- Title Menu -->
|
||||
<v-select
|
||||
v-model="menuForm.titleMenu"
|
||||
:items="menuOptions"
|
||||
label="Title Menu"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
prepend-inner-icon="mdi-format-title"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
|
||||
<!-- Side Menu -->
|
||||
<v-select
|
||||
v-model="menuForm.sideMenu"
|
||||
:items="menuOptions"
|
||||
label="Side Menu"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
prepend-inner-icon="mdi-menu"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
|
||||
<!-- Name Menu -->
|
||||
<v-text-field
|
||||
v-model="menuForm.nameMenu"
|
||||
label="Name Menu"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
prepend-inner-icon="mdi-tag"
|
||||
:rules="[rules.required]"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<!-- Link URL Menu -->
|
||||
<v-text-field
|
||||
v-model="menuForm.linkUrlMenu"
|
||||
label="Link URL Menu"
|
||||
variant="outlined"
|
||||
class="mb-3"
|
||||
prepend-inner-icon="mdi-link"
|
||||
placeholder="/example/path"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<!-- Icon Menu -->
|
||||
<v-text-field
|
||||
v-model="menuForm.iconMenu"
|
||||
label="Icon Menu"
|
||||
variant="outlined"
|
||||
class="mb-4"
|
||||
prepend-inner-icon="mdi-emoticon"
|
||||
placeholder="mdi-home"
|
||||
clearable
|
||||
>
|
||||
<template #append-inner>
|
||||
<v-icon
|
||||
v-if="menuForm.iconMenu"
|
||||
color="primary"
|
||||
>
|
||||
{{ menuForm.iconMenu }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-form>
|
||||
|
||||
<!-- Add Menu Button -->
|
||||
<v-btn
|
||||
color="teal"
|
||||
variant="elevated"
|
||||
@click="addMenuItem"
|
||||
:disabled="!isFormValid"
|
||||
:loading="isAdding"
|
||||
block
|
||||
size="large"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
Add Menu
|
||||
</v-btn>
|
||||
|
||||
<!-- Clear Form Button -->
|
||||
<v-btn
|
||||
color="grey-darken-1"
|
||||
variant="outlined"
|
||||
@click="clearForm"
|
||||
block
|
||||
>
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
Clear Form
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Menu Management Panel - Responsive -->
|
||||
<v-col cols="12" lg="6" xl="7">
|
||||
<v-card elevation="2" class="h-100">
|
||||
<v-card-title class="bg-blue text-white d-flex justify-space-between align-center">
|
||||
<div>
|
||||
<v-icon left>mdi-menu-open</v-icon>
|
||||
CREATE CUSTOM MENU
|
||||
</div>
|
||||
<v-chip color="white" text-color="blue" size="small">
|
||||
{{ menuItems.length }} items
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<!-- Action Bar -->
|
||||
<v-row class="mb-4" align="center">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="selectedReference"
|
||||
:items="references"
|
||||
label="Reference"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
prepend-inner-icon="mdi-database"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" class="d-flex justify-end">
|
||||
<v-btn
|
||||
color="teal"
|
||||
variant="elevated"
|
||||
@click="reloadMenu"
|
||||
:loading="isLoading"
|
||||
class="me-2"
|
||||
>
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
<span class="d-none d-sm-inline">Reload</span>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="elevated"
|
||||
@click="saveAllMenus"
|
||||
:loading="isSaving"
|
||||
>
|
||||
<v-icon left>mdi-content-save</v-icon>
|
||||
<span class="d-none d-sm-inline">Save</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Instructions -->
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
density="compact"
|
||||
>
|
||||
<v-icon>mdi-drag</v-icon>
|
||||
Drag The Menu List To Re-Order, Click Update Menu To Save The Position, To Add Item On Menu, Use Form Below
|
||||
</v-alert>
|
||||
|
||||
<!-- Menu Tree Header -->
|
||||
<div class="menu-header bg-blue text-white pa-2 rounded-t">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="6" sm="4">
|
||||
<strong>MENU TITLE</strong>
|
||||
</v-col>
|
||||
<v-col cols="4" sm="5" class="d-none d-sm-block">
|
||||
<strong>URL LINK</strong>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="3" class="text-right">
|
||||
<strong>ACTION</strong>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<!-- Menu Tree -->
|
||||
<div class="menu-tree-container" style="max-height: 400px; overflow-y: auto;">
|
||||
<MenuTreeItem
|
||||
v-for="item in menuItems"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
:level="0"
|
||||
@edit="editMenuItem"
|
||||
@delete="deleteMenuItem"
|
||||
@toggle="toggleMenuItem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="menuItems.length === 0" class="text-center pa-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-menu-off</v-icon>
|
||||
<p class="text-grey-darken-1 mt-2">No menu items created yet</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Snackbar for notifications -->
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:color="snackbar.color"
|
||||
:timeout="3000"
|
||||
location="top right"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
<template #actions>
|
||||
<v-btn icon="mdi-close" @click="snackbar.show = false" />
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MenuItem, MenuForm } from '../../types/menu'
|
||||
import type { VForm } from 'vuetify/components'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'default',
|
||||
title: 'Menu Builder'
|
||||
})
|
||||
|
||||
// Reactive data
|
||||
const isFormValid = ref(false)
|
||||
const isAdding = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const formRef = ref<VForm | null>(null)
|
||||
|
||||
const menuForm = ref<MenuForm>({
|
||||
titleMenu: '',
|
||||
sideMenu: '',
|
||||
nameMenu: '',
|
||||
linkUrlMenu: '',
|
||||
iconMenu: ''
|
||||
})
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'success'
|
||||
})
|
||||
|
||||
// Form validation rules
|
||||
const rules = {
|
||||
required: (value: string) => !!value || 'This field is required'
|
||||
}
|
||||
|
||||
// Use composables
|
||||
const {
|
||||
menuItems,
|
||||
menuOptions,
|
||||
references,
|
||||
selectedReference,
|
||||
loadMenus,
|
||||
saveMenu,
|
||||
deleteMenu,
|
||||
updateMenuOrder
|
||||
} = useMenuManagement()
|
||||
|
||||
// Load initial data
|
||||
onMounted(async () => {
|
||||
await loadMenus()
|
||||
})
|
||||
|
||||
// Methods
|
||||
const addMenuItem = async () => {
|
||||
if (!isFormValid.value) return
|
||||
|
||||
isAdding.value = true
|
||||
try {
|
||||
const newItem: MenuItem = {
|
||||
id: `menu-${Date.now()}`,
|
||||
title: menuForm.value.titleMenu || menuForm.value.nameMenu,
|
||||
url: menuForm.value.linkUrlMenu,
|
||||
icon: menuForm.value.iconMenu || 'mdi-circle-outline',
|
||||
parentId: undefined,
|
||||
order: [...menuItems.value].length + 1,
|
||||
isActive: true,
|
||||
reference: selectedReference.value,
|
||||
children: [] as MenuItem[]
|
||||
}
|
||||
|
||||
await saveMenu({
|
||||
...newItem,
|
||||
children: [...(newItem.children ?? [])]
|
||||
})
|
||||
clearForm()
|
||||
showSnackbar('Menu item added successfully!', 'success')
|
||||
} catch (error) {
|
||||
showSnackbar('Failed to add menu item', 'error')
|
||||
} finally {
|
||||
isAdding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearForm = () => {
|
||||
menuForm.value = {
|
||||
titleMenu: '',
|
||||
sideMenu: '',
|
||||
nameMenu: '',
|
||||
linkUrlMenu: '',
|
||||
iconMenu: ''
|
||||
}
|
||||
if (formRef.value) {
|
||||
formRef.value.resetValidation()
|
||||
}
|
||||
}
|
||||
|
||||
const editMenuItem = (item: MenuItem) => {
|
||||
menuForm.value = {
|
||||
titleMenu: item.title,
|
||||
sideMenu: '',
|
||||
nameMenu: item.title,
|
||||
linkUrlMenu: item.url ?? '',
|
||||
iconMenu: item.icon
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMenuItem = async (itemId: string) => {
|
||||
try {
|
||||
await deleteMenu(itemId)
|
||||
showSnackbar('Menu item deleted successfully!', 'success')
|
||||
} catch (error) {
|
||||
showSnackbar('Failed to delete menu item', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleMenuItem = async (itemId: string) => {
|
||||
const item = findMenuItem(itemId)
|
||||
if (item) {
|
||||
item.isActive = !item.isActive
|
||||
await saveMenu({
|
||||
...item,
|
||||
children: [...(item.children ?? [])]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const reloadMenu = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
await loadMenus()
|
||||
showSnackbar('Menu reloaded successfully!', 'success')
|
||||
} catch (error) {
|
||||
showSnackbar('Failed to reload menu', 'error')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveAllMenus = async () => {
|
||||
isSaving.value = true
|
||||
try {
|
||||
// Save all menu changes
|
||||
for (const item of [...menuItems.value]) {
|
||||
// Cast children to mutable array if readonly
|
||||
const mutableItem = {
|
||||
...item,
|
||||
children: [...(item.children ?? [])]
|
||||
}
|
||||
await saveMenu(mutableItem)
|
||||
}
|
||||
showSnackbar('All menus saved successfully!', 'success')
|
||||
} catch (error) {
|
||||
showSnackbar('Failed to save menus', 'error')
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const findMenuItem = (id: string): MenuItem | null => {
|
||||
const findInItems = (items: MenuItem[], id: string): MenuItem | null => {
|
||||
for (const item of items) {
|
||||
if (item.id === id) return item
|
||||
if (item.children) {
|
||||
const found = findInItems(item.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return findInItems(menuItems.value, id)
|
||||
}
|
||||
|
||||
const showSnackbar = (message: string, color: string) => {
|
||||
snackbar.value = { show: true, message, color }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.menu-tree-container {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
</style>
|
||||
+8
-16
@@ -1,22 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
/*Call Components*/
|
||||
import RevenueCard from '@/components/dashboard/RevenueCard.vue';
|
||||
import NewCustomer from '@/components/dashboard/NewCustomer.vue';
|
||||
import Totalincome from '@/components/dashboard/TotalIncome.vue';
|
||||
import RevenueProduct from '@/components/dashboard/RevenueProducts.vue';
|
||||
import DailyActivities from '@/components/dashboard/DailyActivities.vue';
|
||||
import BlogCards from '@/components/dashboard/BlogCards.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" lg="8"><RevenueCard /></v-col>
|
||||
<v-col cols="12" lg="4"
|
||||
><NewCustomer class="mb-6" />
|
||||
<Totalincome />
|
||||
</v-col>
|
||||
<v-col cols="12" lg="8"><RevenueProduct/></v-col>
|
||||
<v-col cols="12" lg="4"><DailyActivities/> </v-col>
|
||||
<v-col cols="12"><BlogCards/></v-col>
|
||||
</v-row>
|
||||
<div class="d-flex justify-center align-center text-center h-100vh">
|
||||
<div>
|
||||
<img src="@/assets/images/backgrounds/errorimg.svg" width="500" alt="404" />
|
||||
<h1 class="text-h1 pt-3">Opps!!!</h1>
|
||||
<h4 class="text-h4 my-8">This page you are looking for could not be found.</h4>
|
||||
<v-btn flat color="primary" class="mb-4" to="/">Go Back to Home</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,49 +0,0 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { menuId, reference } = await readBody<{
|
||||
menuId: string
|
||||
reference: string
|
||||
}>(event)
|
||||
|
||||
if (!menuId || !reference) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'MenuId and reference are required'
|
||||
})
|
||||
}
|
||||
|
||||
const filePath = path.join(process.cwd(), 'matdash', 'data', 'menus.json')
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8')
|
||||
const data = JSON.parse(fileContent)
|
||||
|
||||
// Remove menu item and its children recursively
|
||||
const removeMenuItem = (items: any[], id: string): any[] => {
|
||||
return items.filter(item => {
|
||||
if (item.id === id) return false
|
||||
if (item.children) {
|
||||
item.children = removeMenuItem(item.children, id)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
data.menus = removeMenuItem(data.menus, menuId)
|
||||
|
||||
// Save updated data
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Menu deleted successfully'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting menu:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to delete menu'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), 'matdash', 'data', 'menus.json')
|
||||
|
||||
// Check if file exists, create if not
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
} catch {
|
||||
const defaultData = {
|
||||
menus: [],
|
||||
references: ["Referensi", "Main", "Admin"],
|
||||
menuOptions: ["Select a menu", "Main Menu", "Side Menu", "Footer Menu"]
|
||||
}
|
||||
await fs.writeFile(filePath, JSON.stringify(defaultData, null, 2))
|
||||
}
|
||||
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8')
|
||||
const data = JSON.parse(fileContent)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading menus:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to load menus'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,57 +0,0 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import type { MenuItem } from '../../../types/menu'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { menuItem, reference } = await readBody<{
|
||||
menuItem: MenuItem
|
||||
reference: string
|
||||
}>(event)
|
||||
|
||||
if (!menuItem || !reference) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'MenuItem and reference are required'
|
||||
})
|
||||
}
|
||||
|
||||
const filePath = path.join(process.cwd(), 'matdash', 'data', 'menus.json')
|
||||
|
||||
// Read current data
|
||||
let data
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8')
|
||||
data = JSON.parse(fileContent)
|
||||
} catch {
|
||||
data = { menus: [], references: [], menuOptions: [] }
|
||||
}
|
||||
|
||||
// Update or add menu item
|
||||
const existingIndex = data.menus.findIndex((item: MenuItem) => item.id === menuItem.id)
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
data.menus[existingIndex] = { ...menuItem, updatedAt: new Date().toISOString() }
|
||||
} else {
|
||||
data.menus.push({
|
||||
...menuItem,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// Save to file
|
||||
await fs.writeFile(filePath, JSON.stringify(data, null, 2))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: menuItem
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving menu:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to save menu'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,111 +0,0 @@
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const pageData = await readBody(event)
|
||||
|
||||
if (!pageData.name || !pageData.path) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Page name and path are required'
|
||||
})
|
||||
}
|
||||
|
||||
// Generate Vue file content
|
||||
const vueTemplate = `<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card elevation="2">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon left v-if="pageData.content?.icon">{{ pageData.content.icon }}</v-icon>
|
||||
<span>{{ pageData.metadata?.title || 'Page Title' }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p>{{ pageData.metadata?.description || 'Page description goes here.' }}</p>
|
||||
|
||||
<!-- Add your custom content here -->
|
||||
<v-alert type="info" variant="tonal">
|
||||
This page was automatically generated from the menu system.
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: '${pageData.metadata?.title || 'Generated Page'}',
|
||||
description: '${pageData.metadata?.description || ''}',
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
// Page data
|
||||
const pageData = ${JSON.stringify(pageData, null, 2)}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add your custom styles here */
|
||||
</style>
|
||||
`
|
||||
|
||||
// Determine file path
|
||||
const fileName = `${pageData.name.toLowerCase().replace(/\s+/g, '-')}.vue`
|
||||
const pagesDir = path.join(process.cwd(), 'matdash', 'pages')
|
||||
const filePath = path.join(pagesDir, fileName)
|
||||
|
||||
// Ensure pages directory exists
|
||||
await fs.mkdir(pagesDir, { recursive: true })
|
||||
|
||||
// Write Vue file
|
||||
await fs.writeFile(filePath, vueTemplate)
|
||||
|
||||
// Save page data to JSON
|
||||
const dataPath = path.join(process.cwd(), 'matdash', 'data', 'pages.json')
|
||||
let pagesData
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(dataPath, 'utf-8')
|
||||
pagesData = JSON.parse(fileContent)
|
||||
} catch {
|
||||
pagesData = { pages: [] }
|
||||
}
|
||||
|
||||
// Add or update page data
|
||||
const existingIndex = pagesData.pages.findIndex((page: any) => page.path === pageData.path)
|
||||
const pageRecord = {
|
||||
...pageData,
|
||||
id: pageData.id || `page-${Date.now()}`,
|
||||
fileName,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
pagesData.pages[existingIndex] = pageRecord
|
||||
} else {
|
||||
pagesData.pages.push(pageRecord)
|
||||
}
|
||||
|
||||
await fs.writeFile(dataPath, JSON.stringify(pagesData, null, 2))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
fileName,
|
||||
filePath: filePath.replace(process.cwd(), ''),
|
||||
pageData: pageRecord
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating page:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to generate page'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
// types/auth.ts
|
||||
export interface ExtendedUser {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
preferred_username: string;
|
||||
email_verified: boolean;
|
||||
roles: string[];
|
||||
client_roles: string[];
|
||||
permissions?: string[];
|
||||
given_name?: string;
|
||||
family_name?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface ExtendedSession {
|
||||
user: ExtendedUser;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
-264
@@ -1,264 +0,0 @@
|
||||
// types/simrs-roles.ts
|
||||
|
||||
interface Roles {
|
||||
nama_role: string;
|
||||
deskripsi: string;
|
||||
tanggung_jawab: string[];
|
||||
}
|
||||
|
||||
interface KonfigurasiKeamanan {
|
||||
autentikasi: string;
|
||||
enkripsi: string;
|
||||
audit_trail: boolean;
|
||||
session_timeout: number;
|
||||
}
|
||||
|
||||
interface KonfigurasiSistem {
|
||||
level_akses: string[];
|
||||
modul_utama: string[];
|
||||
keamanan: KonfigurasiKeamanan;
|
||||
}
|
||||
|
||||
interface SIMRSRoles {
|
||||
role_administratif: Roles[];
|
||||
role_klinis: Roles[];
|
||||
role_penunjang_medis: Roles[];
|
||||
role_manajemen: Roles[];
|
||||
role_teknis: Roles[];
|
||||
konfigurasi_sistem: KonfigurasiSistem;
|
||||
}
|
||||
|
||||
// Enum untuk level akses
|
||||
enum LevelAkses {
|
||||
SUPER_ADMIN = "Super Admin",
|
||||
ADMIN_DEPARTEMEN = "Admin Departemen",
|
||||
USER_KLINIS = "User Klinis",
|
||||
USER_ADMINISTRATIF = "User Administratif",
|
||||
GUEST_READ_ONLY = "Guest/Read Only"
|
||||
}
|
||||
|
||||
// Enum untuk modul utama
|
||||
enum ModulUtama {
|
||||
PENDAFTARAN_PASIEN = "Pendaftaran Pasien",
|
||||
REKAM_MEDIS_ELEKTRONIK = "Rekam Medis Elektronik",
|
||||
FARMASI = "Farmasi",
|
||||
LABORATORIUM = "Laboratorium",
|
||||
RADIOLOGI = "Radiologi",
|
||||
KEUANGAN = "Keuangan",
|
||||
INVENTORY = "Inventory",
|
||||
REPORTING = "Reporting"
|
||||
}
|
||||
|
||||
// Data konstanta untuk SIMRS roles
|
||||
const simrsRoles: SIMRSRoles = {
|
||||
role_administratif: [
|
||||
{
|
||||
nama_role: "Administrator Pendaftaran",
|
||||
deskripsi: "Mengelola proses pendaftaran pasien dan penjadwalan dalam sistem SIMRS",
|
||||
tanggung_jawab: [
|
||||
"Mengelola pendaftaran pasien dan penjadwalan dokter",
|
||||
"Mengatur penjadwalan operasi atau pemeriksaan",
|
||||
"Mengelola antrian dan mengurangi waktu tunggu pasien",
|
||||
"Memastikan data pasien terinput dengan benar"
|
||||
]
|
||||
},
|
||||
{
|
||||
nama_role: "Staff Administrasi Keuangan",
|
||||
deskripsi: "Mengelola aspek keuangan dan penagihan dalam sistem rumah sakit",
|
||||
tanggung_jawab: [
|
||||
"Mengelola penagihan dan klaim asuransi",
|
||||
"Mengintegrasikan sistem dengan BPJS Kesehatan",
|
||||
"Mengelola administrasi keuangan rumah sakit",
|
||||
"Memproses pembayaran dan verifikasi biaya"
|
||||
]
|
||||
},
|
||||
{
|
||||
nama_role: "Petugas Rekam Medis",
|
||||
deskripsi: "Mengelola dan memelihara rekam medis elektronik pasien",
|
||||
tanggung_jawab: [
|
||||
"Mengelola Electronic Medical Records (EMR)",
|
||||
"Memastikan dokumentasi pasien yang akurat",
|
||||
"Menjaga keamanan dan kerahasiaan data pasien",
|
||||
"Melakukan backup dan archiving data medis"
|
||||
]
|
||||
}
|
||||
],
|
||||
role_klinis: [
|
||||
{
|
||||
nama_role: "Dokter/Tenaga Medis",
|
||||
deskripsi: "Profesional medis yang menggunakan sistem untuk diagnosis dan pengobatan",
|
||||
tanggung_jawab: [
|
||||
"Mengakses riwayat medis, alergi, dan catatan pengobatan pasien",
|
||||
"Menggunakan modul pendukung pengambilan keputusan klinis",
|
||||
"Melakukan pemantauan pasien secara berkelanjutan",
|
||||
"Menginput diagnosis dan resep obat ke sistem"
|
||||
]
|
||||
},
|
||||
{
|
||||
nama_role: "Perawat",
|
||||
deskripsi: "Tenaga keperawatan yang melakukan perawatan langsung kepada pasien",
|
||||
tanggung_jawab: [
|
||||
"Mencatat parameter vital dan perkembangan pasien",
|
||||
"Mengakses informasi terintegrasi dari berbagai departemen",
|
||||
"Melakukan pemantauan pasien rawat inap",
|
||||
"Mengelola jadwal pemberian obat dan tindakan keperawatan"
|
||||
]
|
||||
},
|
||||
{
|
||||
nama_role: "Apoteker/Staff Farmasi",
|
||||
deskripsi: "Mengelola obat-obatan dan memastikan keamanan farmasi",
|
||||
tanggung_jawab: [
|
||||
"Mengelola persediaan obat dan bahan medis",
|
||||
"Memastikan ketersediaan obat yang sesuai",
|
||||
"Mencegah risiko kekurangan atau kelebihan stok",
|
||||
"Melakukan verifikasi resep dan dispensing obat"
|
||||
]
|
||||
}
|
||||
],
|
||||
role_penunjang_medis: [
|
||||
{
|
||||
nama_role: "Petugas Laboratorium",
|
||||
deskripsi: "Mengelola pemeriksaan laboratorium dan hasilnya",
|
||||
tanggung_jawab: [
|
||||
"Menginput hasil tes laboratorium ke sistem",
|
||||
"Mengintegrasikan data laboratorium dengan rekam medis",
|
||||
"Melakukan quality control hasil laboratorium",
|
||||
"Mengelola sampel dan alat laboratorium"
|
||||
]
|
||||
},
|
||||
{
|
||||
nama_role: "Petugas Radiologi",
|
||||
deskripsi: "Mengelola pemeriksaan radiologi dan imaging",
|
||||
tanggung_jawab: [
|
||||
"Mengelola hasil pemeriksaan radiologi",
|
||||
"Mengintegrasikan data radiologi dengan sistem",
|
||||
"Melakukan penjadwalan pemeriksaan radiologi",
|
||||
"Memastikan kualitas gambar dan interpretasi"
|
||||
]
|
||||
}
|
||||
],
|
||||
role_manajemen: [
|
||||
{
|
||||
nama_role: "Manajer Rumah Sakit",
|
||||
deskripsi: "Mengelola operasional rumah sakit secara keseluruhan",
|
||||
tanggung_jawab: [
|
||||
"Mengakses laporan dan analisis kinerja",
|
||||
"Melakukan monitoring operasional rumah sakit",
|
||||
"Menggunakan modul prediktif untuk perencanaan sumber daya",
|
||||
"Membuat keputusan strategis berdasarkan data sistem"
|
||||
]
|
||||
},
|
||||
{
|
||||
nama_role: "Supervisor Departemen",
|
||||
deskripsi: "Mengawasi dan mengoordinasikan departemen spesifik",
|
||||
tanggung_jawab: [
|
||||
"Mengelola koordinasi antar-departemen",
|
||||
"Memastikan integrasi berbagai aspek operasional",
|
||||
"Monitoring kinerja staff departemen",
|
||||
"Melakukan evaluasi dan pelaporan departemen"
|
||||
]
|
||||
}
|
||||
],
|
||||
role_teknis: [
|
||||
{
|
||||
nama_role: "Administrator Sistem SIMRS",
|
||||
deskripsi: "Mengelola infrastruktur dan keamanan sistem SIMRS",
|
||||
tanggung_jawab: [
|
||||
"Mengelola keamanan sistem dan data terenkripsi",
|
||||
"Melakukan maintenance dan troubleshooting sistem",
|
||||
"Menangani masalah infrastruktur teknologi",
|
||||
"Mengatur hak akses user dan permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
nama_role: "Staff IT Support",
|
||||
deskripsi: "Memberikan dukungan teknis kepada pengguna sistem",
|
||||
tanggung_jawab: [
|
||||
"Memberikan pelatihan teknologi kepada pengguna",
|
||||
"Menangani masalah teknis harian",
|
||||
"Melakukan monitoring sistem dan evaluasi",
|
||||
"Membantu troubleshooting masalah user"
|
||||
]
|
||||
}
|
||||
],
|
||||
konfigurasi_sistem: {
|
||||
level_akses: [
|
||||
LevelAkses.SUPER_ADMIN,
|
||||
LevelAkses.ADMIN_DEPARTEMEN,
|
||||
LevelAkses.USER_KLINIS,
|
||||
LevelAkses.USER_ADMINISTRATIF,
|
||||
LevelAkses.GUEST_READ_ONLY
|
||||
],
|
||||
modul_utama: [
|
||||
ModulUtama.PENDAFTARAN_PASIEN,
|
||||
ModulUtama.REKAM_MEDIS_ELEKTRONIK,
|
||||
ModulUtama.FARMASI,
|
||||
ModulUtama.LABORATORIUM,
|
||||
ModulUtama.RADIOLOGI,
|
||||
ModulUtama.KEUANGAN,
|
||||
ModulUtama.INVENTORY,
|
||||
ModulUtama.REPORTING
|
||||
],
|
||||
keamanan: {
|
||||
autentikasi: "Multi-factor Authentication",
|
||||
enkripsi: "AES-256",
|
||||
audit_trail: true,
|
||||
session_timeout: 30
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Utility functions
|
||||
class SIMRSRoleManager {
|
||||
private roles: SIMRSRoles;
|
||||
|
||||
constructor(roles: SIMRSRoles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
// Mendapatkan semua role berdasarkan kategori
|
||||
getRolesByCategory(category: keyof Omit<SIMRSRoles, 'konfigurasi_sistem'>): Roles[] {
|
||||
return this.roles[category];
|
||||
}
|
||||
|
||||
// Mencari role berdasarkan nama
|
||||
findRoleByName(name: string): Roles | undefined {
|
||||
const allCategories = [
|
||||
...this.roles.role_administratif,
|
||||
...this.roles.role_klinis,
|
||||
...this.roles.role_penunjang_medis,
|
||||
...this.roles.role_manajemen,
|
||||
...this.roles.role_teknis
|
||||
];
|
||||
|
||||
return allCategories.find(role => role.nama_role === name);
|
||||
}
|
||||
|
||||
// Mendapatkan konfigurasi keamanan
|
||||
getSecurityConfig(): KonfigurasiKeamanan {
|
||||
return this.roles.konfigurasi_sistem.keamanan;
|
||||
}
|
||||
|
||||
// Mendapatkan semua level akses
|
||||
getAccessLevels(): string[] {
|
||||
return this.roles.konfigurasi_sistem.level_akses;
|
||||
}
|
||||
}
|
||||
|
||||
export type {
|
||||
Roles,
|
||||
KonfigurasiKeamanan,
|
||||
KonfigurasiSistem,
|
||||
SIMRSRoles,
|
||||
};
|
||||
|
||||
export {
|
||||
LevelAkses,
|
||||
ModulUtama,
|
||||
SIMRSRoleManager,
|
||||
simrsRoles as default,
|
||||
};
|
||||
|
||||
// Contoh penggunaan
|
||||
export const roleManager = new SIMRSRoleManager(simrsRoles);
|
||||
|
||||
Reference in New Issue
Block a user