remove website contents

This commit is contained in:
bagus-arie05
2025-11-27 09:51:40 +07:00
parent d8685ccf10
commit bbc6346057
58 changed files with 117 additions and 7208 deletions

No files matched your search

+1 -1
View File
@@ -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>
-73
View File
@@ -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>
-61
View File
@@ -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>
-75
View File
@@ -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>
-61
View File
@@ -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>
-15
View File
@@ -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>
-18
View File
@@ -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>
-186
View File
@@ -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>
-45
View File
@@ -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>
-35
View File
@@ -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>
-21
View File
@@ -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>
-115
View File
@@ -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>
-54
View File
@@ -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>
-68
View File
@@ -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;
-134
View File
@@ -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
}
}
-131
View File
@@ -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
// };
// };
-606
View File
@@ -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
};
};
-246
View File
@@ -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"
]
-198
View File
@@ -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"
]
-38
View File
@@ -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"]
}
-20
View File
@@ -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"
}
]
}
-20
View File
@@ -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"
}
]
}
-40
View File
@@ -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
View File
@@ -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
}
},
-164
View File
@@ -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>
-25
View File
@@ -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>
-183
View File
@@ -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>
-16
View File
@@ -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>
-58
View File
@@ -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>
-410
View File
@@ -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
View File
@@ -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>
-49
View File
@@ -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'
})
}
})
-34
View File
@@ -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'
})
}
})
-57
View File
@@ -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'
})
}
})
-111
View File
@@ -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'
})
}
})
-21
View File
@@ -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
View File
@@ -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);