first commit

This commit is contained in:
2025-04-22 10:56:56 +07:00
commit af123c091b
147 changed files with 778063 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts"></script>
<template>
<v-empty-state
headline="Whoops, 404"
title="Page not found"
text="The page you were looking for does not exist"
icon="custom:nustar"
/>
</template>
+47
View File
@@ -0,0 +1,47 @@
<script setup lang="ts">
definePageMeta({
icon: 'mdi-security',
title: 'Auth',
drawerIndex: 4,
})
const { data, status, getCsrfToken, getProviders, signOut } = useAuth()
const runtimeConfig = useRuntimeConfig();
const providers = await getProviders()
const csrfToken = await getCsrfToken()
const handleLogout = async () => {
try {
// const returnTo = encodeURIComponent('http://localhost:3000/auth/login');
const returnTo = encodeURIComponent(window.location.origin);
const logoutUrl = `${runtimeConfig.public.keycloakIssuer}/protocol/openid-connect/logout?client_id=${runtimeConfig.public.keycloakClient}&post_logout_redirect_uri=${returnTo}`;
window.open(logoutUrl, '_blank'); // Sign out dari aplikasi sebelum redirect
await signOut({ callbackUrl: '/auth/login' });
} catch (error) {
console.error('Logout failed:', error);
}
};
</script>
<template>
<v-card>
<v-card-item>
<v-card-title>Authentication Overview</v-card-title>
<v-card-subtitle>See all available authentication & session information
below</v-card-subtitle>
</v-card-item>
<v-card-text>
<pre v-if="status"><span>Status:</span> {{ status }}</pre>
<pre v-if="data"><span>Data:</span> {{ data }}</pre>
<pre v-if="csrfToken"><span>CSRF Token:</span> {{ csrfToken }}</pre>
<pre v-if="providers"><span>Providers:</span> {{ providers }}</pre>
</v-card-text>
<v-card-actions>
<v-btn text="Logout" @click="handleLogout"></v-btn>
</v-card-actions>
</v-card>
</template>
+72
View File
@@ -0,0 +1,72 @@
<template>
<!-- <VMain class="main d-flex align-center justify-center"> -->
<VRow no-gutters class="main">
<VCol cols="6" class="d-flex align-center justify-center">
<VCard class="card" title="Login Area" width="400" rounded="lg">
<VCardText>
<VBtn v-for="provider in providers" :key="provider.id" @click="signIn(provider.id)" class="mb-8" color="blue"
size="large" block>
Sign in with {{ provider.name }}
</VBtn>
</VCardText>
</VCard>
</VCol>
</VRow>
<!-- </VMain> -->
</template>
<script setup>
// import { useField, useForm } from 'vee-validate'
definePageMeta({
layout: 'auth',
auth: {
unauthenticatedOnly: true,
navigateAuthenticatedTo: '/homepage',
},
})
const { signIn, getProviders } = useAuth()
const providers = await getProviders()
// const { handleSubmit, handleReset } = useForm({
// validationSchema: {
// username (value) {
// if (value?.length >= 2) return true
// return 'Name needs to be at least 2 characters.'
// },
// password (value) {
// return true
// if (value?.length > 9 && /[0-9-]+/.test(value)) return true
// return 'Phone number needs to be at least 9 digits.'
// },
// },
// })
// const username = useField('username')
// const password = useField('password')
// const submit = handleSubmit(values => {
// alert(JSON.stringify(values, null, 2))
// })
// const show = ref(false)
</script>
<style scoped>
.main {
min-height: 300px;
background-image: url('/background.jpg');
background-size: cover;
}
.card {
/* From https://css.glass */
background: rgba(0, 0, 0, 0.28);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(11.1px);
-webkit-backdrop-filter: blur(11.1px);
}
</style>
+108
View File
@@ -0,0 +1,108 @@
<script setup lang="ts">
definePageMeta({
icon: 'mdi-monitor-dashboard',
title: 'Dashboard',
drawerIndex: 1,
})
const stats = ref([
{
icon: 'mdi-web',
title: 'Bandwidth',
value: 23,
unit: 'GB',
color: 'primary',
caption: 'Up: 13, Down: 10',
},
{
icon: 'mdi-rss',
title: 'Submissions',
value: 108,
color: 'primary',
caption: 'Too young, too naive',
},
{
icon: 'mdi-send',
title: 'Requests',
value: 1238,
color: 'warning',
caption: 'Limit: 1320',
},
{
icon: 'mdi-bell',
title: 'Messages',
value: 9042,
color: 'primary',
caption: 'Warnings: 300, erros: 47',
},
{
icon: 'mdi-github',
title: 'Github Stars',
value: NaN,
color: 'grey',
caption: 'API has no response',
},
{
icon: 'mdi-currency-cny',
title: 'Total Fee',
value: 2300,
unit: '¥',
color: 'error',
caption: 'Upper Limit: 2000 ¥',
},
])
</script>
<template>
<v-container fluid>
<v-row>
<v-col
v-for="stat in stats"
:key="stat.title"
cols="12"
sm="6"
md="4"
lg="2"
>
<StatsCard
:title="stat.title"
:unit="stat.unit"
:color="stat.color"
:icon="stat.icon"
:value="stat.value"
>
<template #footer>
{{ stat.caption }}
</template>
</StatsCard>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6" lg="12">
<v-card class="pa-2">
<ChartLine />
</v-card>
</v-col>
<v-col cols="12" md="6" lg="4">
<v-card class="pa-2">
<ChartRadar />
</v-card>
</v-col>
<v-col cols="12" md="6" lg="4">
<v-card class="pa-2">
<ChartPie />
</v-card>
</v-col>
<v-col cols="12" md="6" lg="4">
<v-card class="pa-2">
<ChartBar />
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>
.v-card:not(.stats-card) {
height: 340px;
}
</style>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
definePageMeta({
title: 'Forms Routes',
icon: 'mdi-view-list',
drawerIndex: 4,
})
</script>
<template>
<NuxtPage />
</template>
+3
View File
@@ -0,0 +1,3 @@
<template>
<IndexPage />
</template>
+25
View File
@@ -0,0 +1,25 @@
<script>
import { VCard } from 'vuetify/components'
definePageMeta({
title: 'Vue Form',
icon: 'mdi-account-injury-outline',
drawerIndex: 2,
})
</script>
<template>
<v-card
class="mx-auto"
prepend-icon="mdi-account-injury-outline"
subtitle="Isi dan lengkapi dengan data yang sesuai"
width="600"
>
<template v-slot:title>
<span class="font-weight-black">Formulir Pasien Baru</span>
</template>
<v-card-text class="bg-surface-light pt-4">
<FormPatient />
</v-card-text>
</v-card>
</template>
+25
View File
@@ -0,0 +1,25 @@
<script>
import { VCard } from 'vuetify/components'
definePageMeta({
title: 'Practitioner Basic',
icon: 'mdi-account-injury-outline',
drawerIndex: 4,
})
</script>
<template>
<v-card
class="mx-auto"
prepend-icon="mdi-account-injury-outline"
subtitle="Isi dan lengkapi dengan data yang sesuai"
width="80%"
>
<template v-slot:title>
<span class="font-weight-black">Formulir Praktisi Baru</span>
</template>
<v-card-text class="bg-surface-light pt-4">
<FormPractitionerBasic />
</v-card-text>
</v-card>
</template>
+101
View File
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { ref } from 'vue'
definePageMeta({
title: 'Vue Form',
icon: 'mdi-checkbox-blank-off-outline',
drawerIndex: 0,
})
const data = ref({})
const humanName = ref({})
const onChange = () => {
humanName.value = parseName(data.value.nama)
}
const handleResponse = (response, form$) => {
console.log(response) // axios response
console.log(response.status) // HTTP status code
console.log(response.data) // response data
console.log(form$) // <Vueform> instance
}
</script>
<template>
<div class="ma-4">
<Vueform
v-model="data"
@change="onChange"
endpoint="/api/practitioner/test"
method="post"
@response="handleResponse"
sync
>
<TextElement
name="nama"
placeholder="Nama Lengkap"
:rules="['required', 'min:3', 'max:100']"
/>
<StaticElement name="parsed-name" size="sm">
<div class="d-flex flex-row">
<v-chip
v-for="prefix in humanName.prefix"
size="x-small"
class="mr-1 bg-indigo-lighten-3"
>{{ prefix }}</v-chip
><v-chip
v-for="given in humanName.given"
size="x-small"
class="mr-1 bg-blue-darken-3"
>{{ given }}</v-chip
>
<v-chip
v-show="humanName.family"
size="x-small"
class="mr-1 bg-blue"
>{{ humanName.family }}</v-chip
>
<v-chip
v-for="suffix in humanName.suffix"
size="x-small"
class="mr-1 bg-indigo-lighten-3"
>{{ suffix }}</v-chip
>
</div>
</StaticElement>
<RadiogroupElement
name="gender"
view="tabs"
label="Jenis Kelamin"
:items="[
{
value: 'unknown',
label: '⭕',
},
{
value: 'male',
label: '♂️ Laki-laki',
},
{
value: 'female',
label: '♀️ Perempuan',
},
{
value: 'other',
label: '⚧️ Lainnya',
},
]"
default="unknown"
/>
<ButtonElement
name="submit"
button-label="Submit"
:submits="true"
align="right"
/>
</Vueform>
</div>
<span>{{ data }}</span>
</template>
+237
View File
@@ -0,0 +1,237 @@
<script setup lang="ts">
import type { UseFuseOptions } from './index'
import { computed, shallowRef, watch } from 'vue'
import { useFuse } from './index'
interface DataItem {
firstName: string
lastName: string
}
const data = shallowRef<DataItem[]>([
{
firstName: 'Roslyn',
lastName: 'Mitchell',
},
{
firstName: 'Cathleen',
lastName: 'Matthews',
},
{
firstName: 'Carleton',
lastName: 'Harrelson',
},
{
firstName: 'Allen',
lastName: 'Moores',
},
{
firstName: 'John',
lastName: 'Washington',
},
{
firstName: 'Brooke',
lastName: 'Colton',
},
{
firstName: 'Mary',
lastName: 'Rennold',
},
{
firstName: 'Nanny',
lastName: 'Field',
},
{
firstName: 'Chasity',
lastName: 'Michael',
},
{
firstName: 'Oakley',
lastName: 'Giles',
},
{
firstName: 'Johanna',
lastName: 'Shepherd',
},
{
firstName: 'Maybelle',
lastName: 'Wilkie',
},
{
firstName: 'Dawson',
lastName: 'Rowntree',
},
{
firstName: 'Manley',
lastName: 'Pond',
},
{
firstName: 'Lula',
lastName: 'Sawyer',
},
{
firstName: 'Hudson',
lastName: 'Hext',
},
{
firstName: 'Alden',
lastName: 'Senior',
},
{
firstName: 'Tory',
lastName: 'Hyland',
},
{
firstName: 'Constance',
lastName: 'Josephs',
},
{
firstName: 'Larry',
lastName: 'Kinsley',
},
])
const search = shallowRef('')
const filterBy = shallowRef('both')
const keys = computed(() => {
if (filterBy.value === 'first') return ['firstName']
else if (filterBy.value === 'last') return ['lastName']
else return ['firstName', 'lastName']
})
const resultLimit = shallowRef<number | undefined>(undefined)
const resultLimitString = shallowRef<string>('')
watch(resultLimitString, () => {
if (resultLimitString.value === '') {
resultLimit.value = undefined
} else {
const float = Number.parseFloat(resultLimitString.value)
if (!Number.isNaN(float)) {
resultLimit.value = Math.round(float)
resultLimitString.value = resultLimit.value.toString()
}
}
})
const exactMatch = shallowRef(false)
const isCaseSensitive = shallowRef(false)
const matchAllWhenSearchEmpty = shallowRef(true)
const options = computed<UseFuseOptions<DataItem>>(() => ({
fuseOptions: {
keys: keys.value,
isCaseSensitive: isCaseSensitive.value,
threshold: exactMatch.value ? 0 : undefined,
},
resultLimit: resultLimit.value,
matchAllWhenSearchEmpty: matchAllWhenSearchEmpty.value,
}))
const { results } = useFuse(search, data, options)
</script>
<template>
<div>
<input
v-model="search"
placeholder="Search for someone..."
type="text"
w-full
/>
<div flex flex-wrap>
<div
bg="dark:(dark-300) light-700"
mr-2
border="1 light-900 dark:(dark-700)"
rounded
relative
flex
items-center
>
<i i-carbon-filter absolute left-2 opacity-70 />
<select v-model="filterBy" px-8 bg-transparent>
<option bg="dark:(dark-300) light-700" value="both">Full Name</option>
<option bg="dark:(dark-300) light-700" value="first">
First Name
</option>
<option bg="dark:(dark-300) light-700" value="last">Last Name</option>
</select>
<i
i-carbon-chevron-down
absolute
right-2
pointer-events-none
opacity-70
/>
</div>
<span flex-1 />
<div flex flex-row flex-wrap gap-x-4>
<label class="checkbox">
<input v-model="exactMatch" type="checkbox" />
<span>Exact Match</span>
</label>
<label class="checkbox">
<input v-model="isCaseSensitive" type="checkbox" />
<span>Case Sensitive</span>
</label>
<label class="checkbox">
<input v-model="matchAllWhenSearchEmpty" type="checkbox" />
<span>Match all when empty</span>
</label>
</div>
</div>
</div>
<div mt-4>
<template v-if="results.length > 0">
<div
v-for="result in results"
:key="result.item.firstName + result.item.lastName"
py-2
>
<div flex flex-col>
<span> {{ result.item.firstName }} {{ result.item.lastName }} </span>
<span text-sm opacity-50> Score Index: {{ result.refIndex }} </span>
</div>
</div>
</template>
<template v-else>
<div text-center pt-8 pb-4 opacity-80>No Results Found</div>
</template>
</div>
</template>
<style scoped lang="postcss">
input {
--tw-ring-offset-width: 1px !important;
--tw-ring-color: #8885 !important;
--tw-ring-offset-color: transparent !important;
}
.checkbox {
@apply inline-flex items-center my-auto cursor-pointer select-none;
}
.checkbox input {
appearance: none;
padding: 0;
-webkit-print-color-adjust: exact;
color-adjust: exact;
display: inline-block;
vertical-align: middle;
background-origin: border-box;
user-select: none;
flex-shrink: 0;
height: 1rem;
width: 1rem;
@apply bg-gray-400/30;
@apply rounded-md h-4 w-4 select-none;
}
.checkbox input:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
}
.checkbox span {
@apply ml-1.5 text-13px opacity-70;
}
</style>
+45
View File
@@ -0,0 +1,45 @@
<script setup lang="ts">
const name = ref('')
function sayHi() {
Notify.success(`Hi, ${name.value}!`)
}
function warning() {
Notify.warning(`How dare you refuse me, ${name.value}.`)
}
definePageMeta({
icon: 'mdi-home',
title: 'Homepage',
drawerIndex: 0,
})
// const { user } = useUserSession()
</script>
<template>
<v-container
fluid
class="d-flex align-items-center justify-center fill-height"
>
<div class="text-center">
<!-- <div>Welcome {{ user?.login }}!</div> -->
<v-icon
icon="custom:vitify-nuxt"
size="3em"
color="primary"
class="mb-4"
/>
<p>Opinionated Starter Template</p>
<v-text-field
v-model="name"
max-width="300"
placeholder="Hello World"
label="What's your name?"
class="mt-8"
/>
<v-btn :disabled="!name" class="mr-2" color="primary" @click="sayHi">
Confirm
</v-btn>
<v-btn :disabled="!name" @click="warning"> Cancel </v-btn>
<VIcon icon="i-simple-icons-keycloak" />
</div>
</v-container>
</template>
+8
View File
@@ -0,0 +1,8 @@
<template>
<div />
</template>
<script setup lang="ts">
definePageMeta({
redirect: 'homepage',
})
</script>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
definePageMeta({
title: 'Nested Routes',
icon: 'mdi-view-list',
drawerIndex: 2,
})
</script>
<template>
<NuxtPage />
</template>
+3
View File
@@ -0,0 +1,3 @@
<template>
<IndexPage />
</template>
+33
View File
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { useFormTest } from '~/composables/forms/test'
import { JsonForms } from '@jsonforms/vue'
import { vuetifyRenderers } from '@jsonforms/vue-vuetify'
definePageMeta({
title: 'Test Form',
icon: 'mdi-animation',
})
const { schema, uischema, data } = useFormTest()
const onChange = (event: JsonFormsChangeEvent) => {
data.value = event.data
}
</script>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<h1>JSON Forms Vue 3</h1>
<div class="myform">
<json-forms
:data="data"
:renderers="vuetifyRenderers"
:schema="schema"
:uischema="uischema"
@change="onChange"
/>
</div>
<pre>{{ data }}</pre>
<!-- <pre>{{ typeof schema }}</pre>
<pre>{{ schema }}</pre> -->
</template>
+31
View File
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { JsonForms } from '@jsonforms/vue'
import { vuetifyRenderers } from '@jsonforms/vue-vuetify'
import { useFormBasic } from '~/composables/forms/basic'
definePageMeta({
title: 'Basic Form',
icon: 'mdi-animation',
})
const { schema, uischema, data } = useFormBasic()
const onChange = (event: JsonFormsChangeEvent) => {
data.value = event.data
}
</script>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<h1>JSON Forms Vue 3</h1>
<div class="myform">
<json-forms
:data="data"
:renderers="vuetifyRenderers"
:schema="schema"
:uischema="uischema"
@change="onChange"
/>
</div>
<pre>{{ data }}</pre>
</template>
+7
View File
@@ -0,0 +1,7 @@
<script>
definePageMeta({
title: 'Menu 3',
icon: 'mdi-animation',
})
</script>
<template></template>
+3
View File
@@ -0,0 +1,3 @@
<template>
<IndexPage />
</template>
+59
View File
@@ -0,0 +1,59 @@
<script setup lang="ts">
definePageMeta({
title: 'Menu1 Form',
icon: 'mdi-animation',
})
const api_data = useFetch('/api/forms/basic/data')
const api_i18n = useFetch('/api/forms/basic/i18n')
const api_schema = useFetch('/api/forms/basic/schema')
const api_uischema = useFetch('/api/forms/basic/uischema')
// In Vue 3, markRaw is used directly without needing to import it from a specific package
// const renderers = markRaw([
// ...extendedVuetifyRenderers,
// // here you can add custom renderers
// ]);
// Using ref for reactive data in Vue 3
const data = ref(api_data) // You'll need to replace this with your actual data
const schema = ref(api_schema) // Replace with your actual schema
const uischema = ref(api_uischema) // Replace with your actual UI schema
// Event handlers are defined as functions in the setup
// const onChange = (event) => {
// data.value = event.data;
// };
const demo = {
properties: {
firstName: {
type: 'string',
description: "The person's first name.",
},
lastName: {
type: 'string',
description: "The person's last name.",
},
age: {
description: 'Age in years which must be equal to or greater than zero.',
type: 'integer',
minimum: 0,
},
},
}
var form = document.getElementById('vuetify-json-forms')
// form.setAttribute("schema", JSON.stringify(demo));
// type="module"
// src="https://cdn.jsdelivr.net/npm/@chobantonov/jsonforms-vuetify-webcomponent@3.5.1/dist/vuetify-json-forms.min.js"
</script>
<template>
<vuetify-json-forms id="vuetify-json-forms"></vuetify-json-forms>
<v-container fluid> {{ data.data }} </v-container>
<v-container fluid> {{ api_i18n.data }} </v-container>
<v-container fluid> {{ schema.data }} </v-container>
<v-container fluid> {{ uischema.data }} </v-container>
</template>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
definePageMeta({
title: 'Menu 2-2',
icon: 'mdi-animation',
})
</script>
<template>
<v-container fluid> empty page </v-container>
</template>
+67
View File
@@ -0,0 +1,67 @@
<script setup lang="ts">
definePageMeta({
title: 'Vuetify Form',
icon: 'mdi-animation',
})
import { ref, provide, onBeforeMount } from 'vue'
import { JsonForms } from '@jsonforms/vue'
import {
vuetifyRenderers,
defaultStyles,
mergeStyles,
} from '@jsonforms/vue-vuetify'
const renderers = Object.freeze([
...vuetifyRenderers,
// here you can add custom renderers
])
var uischema = useFetch('/api/forms/example/uischema').data
var schema = useFetch('/api/forms/example/schema').data
console.log(JSON.parse(schema.trim))
// onBeforeMount(async () => {
// uischema = JSON.stringify(useFetch('/api/forms/example/uischema').data)
// console.log(uischema)
// // schema = JSON.stringify(useFetch('/api/forms/example/schema'))
// // console.log(schema)
// })
const data = ref({
name: 'Send email to Adrian',
description: 'Confirm if you have passed the subject\nHereby ...',
done: true,
recurrence: 'Daily',
rating: 3,
})
const onChange = (event: JsonFormsChangeEvent) => {
data.value = event.data
}
// mergeStyles combines all classes from both styles definitions into one
const myStyles = mergeStyles(defaultStyles, { control: { label: 'mylabel' } })
// Provide styles to child components
provide('styles', myStyles)
</script>
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<h1>JSON Forms Vue 3</h1>
<!-- <div class="myform">
<json-forms
:data="data"
:renderers="renderers"
:schema="schema"
:uischema="uischema"
@change="onChange"
/>
</div> -->
<pre>{{ data }}</pre>
<pre>{{ typeof schema }}</pre>
<pre>{{ schema }}</pre>
</template>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
definePageMeta({
title: 'Pasien',
icon: 'mdi-account-injury-outline',
drawerIndex: 4,
})
</script>
<template>
<NuxtPage />
</template>
+111
View File
@@ -0,0 +1,111 @@
<script>
definePageMeta({
title: 'Data Pasien',
icon: 'mdi-account-injury-outline',
drawerIndex: 0,
})
const randomData = ref(generateRandomDataPasien(10));
const headers = [
{ text: 'No RM' },
{ text: 'Nama' },
{ text: 'tanggal Lahir' },
{ text: 'Alamat' },
{ text: 'No KTP' },
{ text: 'No JKN' },
{ text: 'Kelamin' },
{ text: 'Aksi' },
// { text: 'Aksi' },
];
const searchQuery = ref('');
const filteredData = computed(() => {
if (searchQuery.value.length >= 3) {
return randomData.value.filter(item =>
item.norm.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
item.no_ktp.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
item.nama.toLowerCase().includes(searchQuery.value.toLowerCase())
);
}
return randomData.value;
});
console.log(randomData.value)
console.log(randomData.value.length)
</script>
<template>
<v-container>
<v-card>
<div class="py-4 px-3">
<div class="d-flex justify-space-between mb-6">
<h5 class="text-h5 ">Kunjungan Pasien</h5>
<div class="w-sm-25">
<v-text-field label="Cari" v-model="searchField" variant="outlined" density="compact" type="text"
hide-details color="primary"></v-text-field>
</div>
</div>
<v-row>
<div class="d-flex flex-wrap">
<!-- <v-col v-for="item in filteredData" :key="item.id" cols="12" md="4">
<v-lazy :min-height="200" :options="{ 'threshold': 1 }" transition="fade-transition">
<v-card>
<template v-slot:append>
<h3>{{ item.norm }}</h3>
</template>
<template v-slot:prepend>
<v-avatar size="50" class="rounded-md bg-lightsecondary">
<Icon icon="solar:user-circle-broken" class="text-secondary" height="36" />
</v-avatar>
</template>
<template v-slot:title>
<h4>{{ capitalizeEachWord(item.nama) }}</h4>
</template>
<template v-slot:subtitle>
<p>{{ item.no_ktp }}</p>
</template>
<template v-slot:text>
<p>{{ capitalizeEachWord(item.alamat) }}</p>
</template>
<template v-slot:actions>
<div class="d-flex justify-end">
<v-btn color="primary" @click="editItem(item.norm)">Ubah</v-btn>
<v-btn color="error" @click="deleteItem(item.norm)">Hapus</v-btn>
</div>
</template>
</v-card>
</v-lazy>
</v-col> -->
<div v-for="(data, i) in filteredData" :key="i">
<p>s</p>
</div>
<v-card class="mx-4">
<template v-slot:prepend>
left
</template>
<template v-slot:append>
right
</template>
</v-card>
<v-card>
<template v-slot:prepend>
left
</template>
<template v-slot:append>
right
</template>
</v-card>
</div>
</v-row>
</div>
</v-card>
</v-container>
</template>
+3
View File
@@ -0,0 +1,3 @@
<template>
<IndexPage />
</template>
+11
View File
@@ -0,0 +1,11 @@
<script>
definePageMeta({
title: 'Kunjungan Pasien',
icon: 'mdi-account-injury-outline',
drawerIndex: 1,
})
</script>
<template>
<pre>dddddddd</pre>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script>
import { VCard } from 'vuetify/components'
definePageMeta({
title: 'Tambah Pasien',
icon: 'mdi-account-injury-outline',
drawerIndex: 2,
})
</script>
<template>
<v-card class="mx-auto" prepend-icon="mdi-account-injury-outline" subtitle="Isi dan lengkapi dengan data yang sesuai"
width="80%">
<template v-slot:title>
<span class="font-weight-black">Formulir Pasien Baru</span>
</template>
<v-card-text class="bg-surface-light pt-4">
<FormPatientCreate />
</v-card-text>
</v-card>
</template>
+234
View File
@@ -0,0 +1,234 @@
<script setup lang="ts">
import type { DataTableHeaders } from '~/plugins/vuetify'
definePageMeta({
icon: 'mdi-table',
title: 'Data Praktisi',
drawerIndex: 3,
})
const search = ref('')
const dialogDelete = useTemplateRef('dialogDelete')
function showDialogDelete(name: string) {
dialogDelete.value
?.open('Are you sure you want to delete this dessert?')
.then(async (confirmed: boolean) => {
if (confirmed) {
try {
const index = desserts.value!.findIndex((v) => v.name === name)
desserts.value!.splice(index, 1)
Notify.success('Deleted')
} catch (e) {
Notify.error(e)
}
}
})
}
const practitioners = ref([])
const loading = ref(true)
const error = ref(null)
const snackbar = ref({
show: false,
text: '',
color: 'info',
})
// Mengambil data dari API
const fetchPractitioners = async () => {
try {
loading.value = true
error.value = null
const response = await fetch('/api/practitioner')
if (!response.ok) {
throw new Error(`Error: ${response.status} - ${response.statusText}`)
}
const data = await response.json()
console.log(data)
practitioners.value = data
} catch (err) {
console.error('Failed to fetch practitioners:', err)
error.value = `Gagal memuat data praktisi: ${err.message}`
showSnackbar('Gagal memuat data praktisi', 'error')
} finally {
loading.value = false
}
}
// Mendapatkan nomor telepon praktisi
const getPractitionerPhone = (practitioner) => {
if (!practitioner.telecom || practitioner.telecom.length === 0) {
return 'Tidak ada data'
}
const phone = practitioner.telecom.find((t) => t.system === 'phone')
return phone ? phone.value : 'Tidak ada data'
}
// Mendapatkan email praktisi
const getPractitionerEmail = (practitioner) => {
if (!practitioner.telecom || practitioner.telecom.length === 0) {
return 'Tidak ada data'
}
const email = practitioner.telecom.find((t) => t.system === 'email')
return email ? email.value : 'Tidak ada data'
}
// Menampilkan detail praktisi
const showDetail = (practitioner) => {
console.log('Menampilkan detail praktisi:', getPractitionerName(practitioner))
showSnackbar(`Detail ${getPractitionerName(practitioner)}`, 'primary')
// Di sini bisa ditambahkan navigasi ke halaman detail atau membuka dialog
}
// Menampilkan snackbar
const showSnackbar = (text, color = 'success') => {
snackbar.value = {
show: true,
text,
color,
}
}
// Panggil API saat komponen dimuat
onMounted(() => {
fetchPractitioners()
})
</script>
<template>
<v-container fluid>
<v-row>
<v-col>
<v-card>
<client-only>
<teleport to="#app-bar">
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
label="Search"
single-line
hide-details
density="compact"
class="mr-2"
rounded="xl"
flat
variant="solo"
style="width: 250px"
/>
</teleport>
</client-only>
<v-container>
<h1 class="text-h4 mb-4">Daftar Praktisi</h1>
<v-alert v-if="error" type="error" class="mb-4">
{{ error }}
</v-alert>
<v-progress-circular
v-if="loading"
indeterminate
color="primary"
size="64"
class="my-8 mx-auto d-block"
></v-progress-circular>
<v-row v-else>
<v-col
v-for="(practitioner, index) in practitioners"
:key="index"
cols="12"
sm="6"
md="4"
>
<v-card
class="mx-auto mb-4"
max-width="400"
elevation="3"
shaped
>
<v-card-title class="text-h5">
{{ joinName(practitioner.name, ['official', 'usual']) }}
</v-card-title>
<v-card-text>
<v-row align="center" class="mx-0">
<DivAvatar
size="80"
class="mr-3"
:gender="practitioner.gender"
/>
<div>
<DivIconText
icon="mdi-map-marker"
:text="practitioner.birthPlace || 'Tidak ada data'"
/>
<DivIconText
icon="mdi-calendar"
:text="formatDate(practitioner.birthDate, 'full')"
/>
<DivIconText
icon="mdi-phone"
:text="
getContactPoints(
practitioner.telecom,
'phone',
).join(',')
"
/>
<DivIconText
icon="mdi-email"
:text="
getContactPoints(
practitioner.telecom,
'email',
).join(',')
"
/>
</div>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="primary"
text
@click="showDetail(practitioner)"
>
Detail
<v-icon small class="ml-1">mdi-arrow-right</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
</v-snackbar>
</v-container>
<DialogConfirm ref="dialogDelete" />
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style scoped>
.v-card {
transition: transform 0.3s;
}
.v-card:hover {
transform: translateY(-5px);
}
</style>
+188
View File
@@ -0,0 +1,188 @@
<script setup lang="ts">
import type { DataTableHeaders } from '~/plugins/vuetify'
definePageMeta({
icon: 'mdi-table',
title: 'Data Table',
drawerIndex: 3,
})
const search = ref('')
const dialogDelete = useTemplateRef('dialogDelete')
function showDialogDelete(name: string) {
dialogDelete.value
?.open('Are you sure you want to delete this dessert?')
.then(async (confirmed: boolean) => {
if (confirmed) {
try {
const index = desserts.value!.findIndex((v) => v.name === name)
desserts.value!.splice(index, 1)
Notify.success('Deleted')
} catch (e) {
Notify.error(e)
}
}
})
}
const headers: DataTableHeaders = [
{
title: 'Dessert (100g serving)',
key: 'name',
},
{ title: 'Calories', key: 'calories' },
{ title: 'Fat (g)', key: 'fat' },
{ title: 'Carbs (g)', key: 'carbs' },
{ title: 'Protein (g)', key: 'protein' },
{ title: 'Iron (%)', key: 'iron' },
{ title: 'Actions', key: 'actions', sortable: false },
]
const desserts = ref([
{
name: 'Frozen Yogurt',
calories: 159,
fat: 6.0,
carbs: 24,
protein: 4.0,
iron: '1',
},
{
name: 'Jelly bean',
calories: 375,
fat: 0.0,
carbs: 94,
protein: 0.0,
iron: '0',
},
{
name: 'KitKat',
calories: 518,
fat: 26.0,
carbs: 65,
protein: 7,
iron: '6',
},
{
name: 'Eclair',
calories: 262,
fat: 16.0,
carbs: 23,
protein: 6.0,
iron: '7',
},
{
name: 'Gingerbread',
calories: 356,
fat: 16.0,
carbs: 49,
protein: 3.9,
iron: '16',
},
{
name: 'Ice cream sandwich',
calories: 237,
fat: 9.0,
carbs: 37,
protein: 4.3,
iron: '1',
},
{
name: 'Lollipop',
calories: 392,
fat: 0.2,
carbs: 98,
protein: 0,
iron: '2',
},
{
name: 'Cupcake',
calories: 305,
fat: 3.7,
carbs: 67,
protein: 4.3,
iron: '8',
},
{
name: 'Honeycomb',
calories: 408,
fat: 3.2,
carbs: 87,
protein: 6.5,
iron: '45',
},
{
name: 'Donut',
calories: 452,
fat: 25.0,
carbs: 51,
protein: 4.9,
iron: '22',
},
{
name: 'Donut2',
calories: 452,
fat: 25.0,
carbs: 51,
protein: 4.9,
iron: '22',
},
])
</script>
<template>
<v-container fluid>
<v-row>
<v-col>
<v-card>
<client-only>
<teleport to="#app-bar">
<v-text-field
v-model="search"
prepend-inner-icon="mdi-magnify"
label="Search"
single-line
hide-details
density="compact"
class="mr-2"
rounded="xl"
flat
variant="solo"
style="width: 250px"
/>
</teleport>
</client-only>
<v-data-table
:headers="headers"
:items="desserts"
item-value="name"
:search="search"
>
<template #item.actions="{ item }">
<v-defaults-provider
:defaults="{
VBtn: {
size: 20,
rounded: 'sm',
variant: 'text',
class: 'ml-1',
color: '',
},
VIcon: {
size: 20,
},
}"
>
<v-btn
v-tooltip="{ text: 'Delete', location: 'top' }"
icon="mdi-delete-outline"
@click.stop="showDialogDelete(item.name)"
/>
</v-defaults-provider>
</template>
</v-data-table>
<DialogConfirm ref="dialogDelete" />
</v-card>
</v-col>
</v-row>
</v-container>
</template>