Merge branch 'dev' into feat/kfr-kemoterapi-174
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { type TabItem } from '../comp-tab/type'
|
||||
|
||||
const props = defineProps<{
|
||||
initialActiveMenu: string
|
||||
data: TabItem[]
|
||||
}>()
|
||||
|
||||
const activeMenu = ref(props.initialActiveMenu)
|
||||
const emit = defineEmits<{
|
||||
changeMenu: [value: string]
|
||||
}>()
|
||||
|
||||
function changeMenu(value: string) {
|
||||
activeMenu.value = value
|
||||
emit('changeMenu', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4 flex gap-4">
|
||||
<!-- Menu Sidebar -->
|
||||
<div v-if="data.length > 0" class="w-72 flex-shrink-0 rounded-md border bg-white shadow-sm dark:bg-neutral-950">
|
||||
<div class="max-h-[calc(100vh-12rem)] overflow-y-auto p-2">
|
||||
<button
|
||||
v-for="menu in data"
|
||||
:key="menu.value"
|
||||
:data-active="activeMenu === menu.value"
|
||||
class="w-full rounded-md px-4 py-3 text-left text-sm transition-colors data-[active=false]:text-gray-700 data-[active=false]:hover:bg-gray-100 data-[active=true]:bg-primary data-[active=true]:text-white dark:data-[active=false]:text-gray-300 dark:data-[active=false]:hover:bg-neutral-800"
|
||||
@click="changeMenu(menu.value)"
|
||||
>
|
||||
{{ menu.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Menu Content -->
|
||||
<div v-if="data.find((m) => m.value === activeMenu)?.component" class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
|
||||
<component
|
||||
:is="data.find((m) => m.value === activeMenu)?.component"
|
||||
v-bind="data.find((m) => m.value === activeMenu)?.props"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,5 +3,7 @@ export interface TabItem {
|
||||
label: string
|
||||
component?: any
|
||||
groups?: string[]
|
||||
classCode?: string[]
|
||||
subClassCode?: string[]
|
||||
props?: Record<string, any>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import type { Config } from './index'
|
||||
import { refDebounced } from '@vueuse/core'
|
||||
|
||||
const props = defineProps<Config>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:searchModelValue': [value: string]
|
||||
'search': [value: string]
|
||||
}>()
|
||||
|
||||
// Quick search
|
||||
const search = ref(props.quickSearchNav?.modelValue || props.refSearchNav?.modelValue || '')
|
||||
const debouncedSearch = refDebounced(search, props.quickSearchNav?.debounceDuration || 500)
|
||||
|
||||
// Computed search model for v-model
|
||||
const searchModel = computed({
|
||||
get: () => search.value,
|
||||
set: (value: string) => {
|
||||
search.value = value
|
||||
emit('update:searchModelValue', value)
|
||||
},
|
||||
})
|
||||
|
||||
// Watch for external changes to modelValue
|
||||
watch(() => props.quickSearchNav?.modelValue, (newValue) => {
|
||||
if (newValue !== props.quickSearchNav?.modelValue) {
|
||||
search.value = newValue || ''
|
||||
}
|
||||
})
|
||||
|
||||
// Watch debounced search and emit search event
|
||||
watch(debouncedSearch, (newValue) => {
|
||||
const minLength = props.quickSearchNav?.minLength || 3
|
||||
// Only search if meets minimum length or empty (to clear search)
|
||||
if (newValue.length === 0 || newValue.length >= minLength) {
|
||||
emit('search', newValue)
|
||||
props.refSearchNav?.onInput(newValue)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle clear search
|
||||
function clearSearch() {
|
||||
searchModel.value = ''
|
||||
props.quickSearchNav?.onClear()
|
||||
props.refSearchNav?.onClear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between pb-4 2xl:pb-5 ">
|
||||
<div class="flex items-center">
|
||||
<div class="ml-3 text-lg font-semibold text-gray-900">
|
||||
<Icon v-if="icon" :name="icon" class="mr-2 size-4 md:size-6 align-middle" />
|
||||
{{ title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center [&>*]:ms-2">
|
||||
<!-- Slot -->
|
||||
<slot />
|
||||
|
||||
<!-- Search Section -->
|
||||
<div v-if="quickSearchNav || refSearchNav" class="relative">
|
||||
<Input
|
||||
v-model="searchModel"
|
||||
name="search"
|
||||
type="text"
|
||||
:class="quickSearchNav?.inputClass || refSearchNav?.inputClass"
|
||||
:placeholder="quickSearchNav?.placeholder || refSearchNav?.placeholder || 'Cari (min. 3 karakter)...'"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="search.length > 0"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
type="button"
|
||||
@click="clearSearch"
|
||||
>
|
||||
<Icon name="i-lucide-x" class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="quickSearchNav && quickSearchNav.showValidationFeedback !== false && searchModel.length > 0 && searchModel.length < (quickSearchNav.minLength || 3)"
|
||||
class="absolute -bottom-6 left-0 text-xs text-amber-600 whitespace-nowrap"
|
||||
>
|
||||
Minimal {{ quickSearchNav.minLength || 3 }} karakter untuk mencari
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Button -->
|
||||
<div v-if="addNav" class="flex items-center">
|
||||
<Button
|
||||
class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm"
|
||||
:class="addNav.classVal"
|
||||
:variant="(addNav.variant as any) || 'default'"
|
||||
@click="addNav?.onClick"
|
||||
>
|
||||
<Icon :name="addNav.icon || 'i-lucide-plus'" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ addNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Button -->
|
||||
<div v-if="filterNav" class="flex items-center">
|
||||
<Button
|
||||
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||
:class="filterNav.classVal"
|
||||
:variant="(filterNav.variant as any) || 'default'"
|
||||
@click="filterNav?.onClick"
|
||||
>
|
||||
<Icon :name="filterNav.icon || 'i-lucide-filter'" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ filterNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Print Button -->
|
||||
<div v-if="printNav" class="flex items-center">
|
||||
<Button
|
||||
class="rounded-md border border-gray-300 px-4 py-2 sm:text-sm"
|
||||
:class="printNav.classVal"
|
||||
:variant="(printNav.variant as any) || 'default'"
|
||||
@click="printNav?.onClick"
|
||||
>
|
||||
<Icon :name="printNav.icon || 'i-lucide-printer'" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ printNav.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
export type ComponentWithProps = { component: Component, props: Record<string, any> }
|
||||
|
||||
export interface ButtonNav {
|
||||
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
|
||||
classVal?: string
|
||||
classValExt?: string
|
||||
icon?: string
|
||||
label: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
// can type directly
|
||||
export interface QuickSearchNav {
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
inputClass?: string
|
||||
inputPlaceHolder?: string
|
||||
minLength?: number
|
||||
btnClass?: string
|
||||
btnIcon?: string
|
||||
btnLabel?: string
|
||||
showValidationFeedback?: boolean
|
||||
debounceDuration?: number
|
||||
searchParams: object
|
||||
onSubmit?: (searchParams: object) => void
|
||||
onClear: () => void
|
||||
}
|
||||
|
||||
// callback on event
|
||||
export interface RefSearchNav {
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
inputClass?: string
|
||||
inputPlaceHolder?: string
|
||||
btnClass?: string
|
||||
btnIcon?: string
|
||||
onInput: (val: string) => void
|
||||
onClick: () => void
|
||||
onClear: () => void
|
||||
}
|
||||
|
||||
export interface RefExportNav {
|
||||
onExportPdf?: () => void
|
||||
onExportCsv?: () => void
|
||||
onExportExcel?: () => void
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
title?: string
|
||||
icon?: string
|
||||
components?: ComponentWithProps[]
|
||||
quickSearchNav?: QuickSearchNav
|
||||
refSearchNav?: RefSearchNav // either ref or quick
|
||||
filterNav?: ButtonNav
|
||||
addNav?: ButtonNav
|
||||
printNav?: ButtonNav
|
||||
}
|
||||
|
||||
export { default as ContentHeader } from './content-header.vue'
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
height?: number
|
||||
activeTab?: 1 | 2
|
||||
}>()
|
||||
|
||||
const activeTab = ref(props.activeTab || 1)
|
||||
|
||||
function handleClick(value: 1 | 2) {
|
||||
activeTab.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content-switcher" :style="`height: ${height || 200}px`">
|
||||
<div :class="`${activeTab === 1 ? 'active' : 'inactive'}`">
|
||||
<div class="content-wrapper">
|
||||
<div>
|
||||
<slot name="content1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-nav">
|
||||
<button @click="handleClick(1)">
|
||||
<Icon name="i-lucide-chevron-right" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`${activeTab === 2 ? 'active' : 'inactive'}`">
|
||||
<div class="content-nav">
|
||||
<button @click="handleClick(2)">
|
||||
<Icon name="i-lucide-chevron-left" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="content-wrapper">
|
||||
<div>
|
||||
<slot name="content2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.content-switcher {
|
||||
@apply flex overflow-hidden gap-3
|
||||
}
|
||||
.content-switcher > * {
|
||||
@apply border border-slate-300 rounded-md flex overflow-hidden
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
@apply p-4 2xl:p-5 overflow-hidden grow
|
||||
}
|
||||
.inactive .content-wrapper {
|
||||
@apply p-0 w-0
|
||||
}
|
||||
|
||||
.content-nav {
|
||||
@apply h-full flex flex-row items-center justify-center content-center !text-2xl overflow-hidden
|
||||
}
|
||||
|
||||
.content-nav button {
|
||||
@apply pt-2 px-2 h-full w-full
|
||||
}
|
||||
|
||||
/* .content-switcher .inactive > .content-wrapper {
|
||||
@apply w-0 p-0 opacity-0 transition-all duration-500 ease-in-out
|
||||
} */
|
||||
.content-switcher .inactive {
|
||||
@apply w-16 transition-all duration-500 ease-in-out
|
||||
}
|
||||
.content-switcher .inactive > .content-nav {
|
||||
@apply w-full transition-all duration-100 ease-in-out
|
||||
}
|
||||
|
||||
.content-switcher .active {
|
||||
@apply grow transition-all duration-500 ease-in-out
|
||||
}
|
||||
.content-switcher .active > .content-nav {
|
||||
@apply w-0 transition-all duration-100 ease-in-out
|
||||
}
|
||||
/* .content-switcher .active > .content-wrapper {
|
||||
@apply w-full delay-1000 transition-all duration-1000 ease-in-out
|
||||
} */
|
||||
|
||||
</style>
|
||||
@@ -71,6 +71,7 @@ export interface KeyNames {
|
||||
|
||||
export interface LinkItem {
|
||||
label: string
|
||||
value?: string
|
||||
icon?: string
|
||||
href?: string // to cover the needs of stating full external origins full url
|
||||
action?: string // for local paths
|
||||
@@ -84,6 +85,7 @@ export const ActionEvents = {
|
||||
showEdit: 'showEdit',
|
||||
showDetail: 'showDetail',
|
||||
showProcess: 'showProcess',
|
||||
showCancel: 'showCancel',
|
||||
showVerify: 'showVerify',
|
||||
showConfirmVerify: 'showConfirmVerify',
|
||||
showValidate: 'showValidate',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
const { class: classProp } = defineProps<{
|
||||
class?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-5 text-center">:</div>
|
||||
<div :class="`w-4 ps-1 ${classProp}`">:</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { type EncounterItem } from "~/handlers/encounter-init.handler";
|
||||
|
||||
const props = defineProps<{
|
||||
initialActiveMenu: string
|
||||
data: EncounterItem[]
|
||||
}>()
|
||||
|
||||
const activeMenu = ref(props.initialActiveMenu)
|
||||
const emit = defineEmits<{
|
||||
changeMenu: [value: string]
|
||||
}>()
|
||||
|
||||
function changeMenu(value: string) {
|
||||
activeMenu.value = value
|
||||
emit('changeMenu', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex">
|
||||
<!-- Menu Sidebar -->
|
||||
<div v-if="data.length > 0" class="w-56 2xl:w-64 flex-shrink-0 rounded-md border bg-white dark:bg-slate-800 shadow-sm">
|
||||
<div class="max-h-[calc(100vh-12rem)] overflow-y-auto px-2 py-3">
|
||||
<button
|
||||
v-for="menu in data"
|
||||
:key="menu.id"
|
||||
:data-active="activeMenu === menu.id"
|
||||
class="w-full rounded-md px-4 py-3 text-left text-xs 2xl:text-sm transition-colors data-[active=false]:text-gray-700 data-[active=false]:hover:bg-gray-100 data-[active=true]:bg-primary data-[active=true]:text-white dark:data-[active=false]:text-gray-300 dark:data-[active=false]:hover:bg-neutral-800"
|
||||
@click="changeMenu(menu.id)">
|
||||
{{ menu.title }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Active Menu Content -->
|
||||
<div class="p-4 2xl:p-5 flex-grow">
|
||||
<div v-if="data.find((m) => m.id === activeMenu)?.component"
|
||||
class="flex-1 rounded-md border bg-white p-4 shadow-sm dark:bg-neutral-950">
|
||||
<component
|
||||
:is="data.find((m) => m.id === activeMenu)?.component"
|
||||
v-bind="data.find((m) => m.id === activeMenu)?.props" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -20,7 +20,7 @@ function onClick(type: ClickType) {
|
||||
@click="onClick('draft')"
|
||||
class="flex items-center gap-2 rounded-full border border-orange-400 bg-orange-50 px-3 py-1 text-sm font-medium text-orange-600 hover:bg-orange-100"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{{ props.label }}
|
||||
|
||||
@@ -37,6 +37,10 @@ function btnClick() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<!-- For components as slots -->
|
||||
<slot />
|
||||
|
||||
<!-- For components passed by props -->
|
||||
<div v-if="prep.components">
|
||||
<template v-for="cwp in prep.components">
|
||||
<component
|
||||
|
||||
@@ -32,7 +32,7 @@ function btnClick() {
|
||||
/>
|
||||
</div>
|
||||
<div v-if="prep.addNav" class="m-2 flex items-center">
|
||||
<Button size="md" class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm" @click="btnClick">
|
||||
<Button size="default" class="rounded-md border border-gray-300 px-4 py-2 text-white sm:text-sm" @click="btnClick">
|
||||
<Icon name="i-lucide-plus" class="mr-2 h-4 w-4 align-middle" />
|
||||
{{ prep.addNav.label }}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user