Merge branch 'dev' into feat/prescription

This commit is contained in:
Andrian Roshandy
2025-11-19 22:19:45 +07:00
56 changed files with 2850 additions and 57 deletions
@@ -0,0 +1,27 @@
<script setup lang="ts">
import { cn } from '~/lib/utils';
const props = withDefaults(defineProps<{
text?: string
description?: string | string[]
class?: string
}>(), {
})
</script>
<template>
<div :class="cn('flex items-center gap-4 p-3 rounded-md text-orange-500 border border-orange-400 bg-orange-50',
props.class
)">
<Icon name="i-lucide-triangle-alert" class="h-12 w-12 align-middle transition-colors" />
<div class="">
<p class="font-medium text-base">{{text}}</p>
<ul class="list-disc list-inside">
<li v-for="(desc, index) in (Array.isArray(description) ? description : [description])" :key="index">
{{ desc }}
</li>
</ul>
</div>
</div>
</template>
@@ -0,0 +1,26 @@
<script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge'
import { activeStatusCodes } from '~/lib/constants';
const props = defineProps<{
rec: any
idx?: number
}>()
const statusText = computed(() => {
const code: keyof typeof activeStatusCodes = props.rec.status_code === 1 ? `active` : `inactive`
return activeStatusCodes[code]
})
const badgeVariant = computed(() => {
return props.rec.status_code === 1 ? 'default' : 'destructive'
})
</script>
<template>
<div class="flex justify-center">
<Badge :variant="badgeVariant" class="rounded-2xl text-[0.6rem]" >
{{ statusText }}
</Badge>
</div>
</template>
@@ -8,6 +8,8 @@ interface ConfirmationProps {
message?: string
confirmText?: string
cancelText?: string
noTrueSlot?: boolean
skipClosingMessage?: boolean
variant?: 'default' | 'destructive' | 'warning'
size?: 'sm' | 'md' | 'lg' | 'xl'
}
@@ -72,19 +74,21 @@ function handleCancel() {
<div class="space-y-4">
<!-- Icon dan pesan -->
<div class="flex items-start gap-3">
<div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-6 h-6 mt-1 flex-shrink-0" />
<div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-4 h-4 2xl:h-5 2xl:h-6 flex-shrink-0" />
<div class="flex-1">
<p class="text-sm text-muted-foreground leading-relaxed">
{{ message }}
</p>
{{ message }} {{ !noTrueSlot ? ' dengan informasi sebagai berikut:' : '.' }}
</div>
</div>
<!-- Slot untuk konten custom -->
<div v-if="$slots.default">
<div>
<slot />
</div>
<div v-if="!skipClosingMessage" class="">
Lanjutkan Proses?
</div>
<!-- Footer buttons -->
<div class="flex justify-end gap-3 pt-4">
<Button variant="outline" @click="handleCancel">
@@ -46,31 +46,31 @@ const actionConfig = computed(() => {
const configs = {
delete: {
title: 'Hapus Data',
message: 'Apakah Anda yakin ingin menghapus data ini? Tindakan ini tidak dapat dibatalkan.',
message: 'Akan dilakukan penghapusan data',
confirmText: 'Hapus',
variant: 'destructive' as const,
},
deactivate: {
title: 'Nonaktifkan Data',
message: 'Apakah Anda yakin ingin menonaktifkan data ini?',
message: 'Akan dilakukan peng-nonaktifkan data',
confirmText: 'Nonaktifkan',
variant: 'warning' as const,
},
activate: {
title: 'Aktifkan Data',
message: 'Apakah Anda yakin ingin mengaktifkan data ini?',
message: 'Akan dilakukan pengaktifkan data',
confirmText: 'Aktifkan',
variant: 'default' as const,
},
archive: {
title: 'Arsipkan Data',
message: 'Apakah Anda yakin ingin mengarsipkan data ini?',
message: 'Akan dilakukan pengarsipan data',
confirmText: 'Arsipkan',
variant: 'warning' as const,
},
restore: {
title: 'Pulihkan Data',
message: 'Apakah Anda yakin ingin memulihkan data ini?',
message: 'Akan dilakukan pemulihan data',
confirmText: 'Pulihkan',
variant: 'default' as const,
},
@@ -107,6 +107,8 @@ const finalCancelText = computed(() => {
function handleConfirm() {
if (props.record) {
emit('confirm', props.record, props.action)
} else {
emit('confirm', { id: 0 }, 'confirmed')
}
emit('update:open', false)
}
@@ -119,12 +121,13 @@ function handleCancel() {
<template>
<Confirmation
v-model:open="isOpen" :title="finalTitle" :message="finalMessage" :confirm-text="finalConfirmText"
:cancel-text="finalCancelText" :variant="actionConfig.variant" size="md"
v-model:open="isOpen" :variant="actionConfig.variant" size="md"
:title="finalTitle" :message="finalMessage"
:confirm-text="finalConfirmText" :cancel-text="finalCancelText"
:no-true-slot="$slots.default ? false : true"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<!-- Slot untuk informasi tambahan record -->
>
<div v-if="record && $slots.default" class="mt-4 p-3 bg-muted rounded-md">
<slot :record="record" :action="action" />
</div>
@@ -0,0 +1,95 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './types'
interface Props {
rec: ListItemDto
size?: 'default' | 'sm' | 'lg'
}
const props = withDefaults(defineProps<Props>(), {
size: 'lg',
})
const recId = inject<Ref<number>>('rec_id')!
const recAction = inject<Ref<string>>('rec_action')!
const recItem = inject<Ref<any>>('rec_item')!
const activeKey = ref<string | null>(null)
const linkItems: LinkItem[] = [
{
label: 'Detail',
onClick: () => {
detail()
},
icon: 'i-lucide-eye',
},
{
label: 'Submit',
onClick: () => {
submit()
},
icon: 'i-lucide-check',
},
{
label: 'Hapus',
onClick: () => {
del()
},
icon: 'i-lucide-trash',
},
]
function detail() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function submit() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmSubmit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
</script>
<template>
<div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<SidebarMenuButton
:size="size"
class="data-[state=open]:text-sidebar-accent-foreground data-[state=open]:bg-white dark:data-[state=open]:bg-slate-800"
>
<Icon
name="i-lucide-chevrons-up-down"
class="ml-auto size-4"
/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
class="w-[--radix-dropdown-menu-trigger-width] min-w-40 rounded-lg border border-slate-200 bg-white text-black dark:border-slate-700 dark:bg-slate-800 dark:text-white"
align="end"
>
<DropdownMenuGroup>
<DropdownMenuItem
v-for="item in linkItems"
:key="item.label"
class="hover:bg-gray-100 dark:hover:bg-slate-700"
@click="item.onClick"
@mouseenter="activeKey = item.label"
@mouseleave="activeKey = null"
>
<Icon :name="item.icon ?? ''" />
<span :class="activeKey === item.label ? 'text-sidebar-accent-foreground' : ''">{{ item.label }}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</template>
+1
View File
@@ -73,6 +73,7 @@ export interface LinkItem {
}
export const ActionEvents = {
showConfirmSubmit: 'showConfirmSubmit',
showConfirmDelete: 'showConfirmDel',
showEdit: 'showEdit',
showDetail: 'showDetail',
+1 -1
View File
@@ -64,7 +64,7 @@ const settingClass = computed(() => {
}
cls += ' [&:not(.preview)_.height-default]:pt-2 [&:not(.preview)_.height-default]:2xl:!pt-1.5 [&:not(.preview)_.height-compact]:!pt-1 '
cls += '[&_textarea]:md:text-xs [&_textarea]:2xl:!text-sm '
cls += '[&_label]:md:text-xs [&_label]:md:text-xs [&_label]:2xl:!text-sm'
cls += '[&_label]:md:text-xs [&_label]:2xl:!text-sm '
return cls
})
</script>
@@ -1,4 +1,5 @@
export { default as Block } from './block.vue'
export { default as Colon } from './colon.vue'
export { default as Cell } from './cell.vue'
export { default as Label } from './label.vue'
export { default as Field } from './field.vue'
+5 -1
View File
@@ -19,6 +19,8 @@ const props = defineProps<{
maxLength?: number
isRequired?: boolean
isDisabled?: boolean
rightLabel?: string
bottomLabel?: string
}>()
function handleInput(event: Event) {
@@ -61,7 +63,7 @@ function handleInput(event: Event) {
v-slot="{ componentField }"
:name="fieldName"
>
<FormItem>
<FormItem :class="`relative`">
<FormControl>
<Input
:disabled="isDisabled"
@@ -76,10 +78,12 @@ function handleInput(event: Event) {
spellcheck="false"
@input="handleInput"
/>
<p v-show="rightLabel" class="text-gray-400 absolute top-0 right-3">{{ rightLabel }}</p>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</DE.Field>
<p v-show="bottomLabel" class="text-gray-400">{{ bottomLabel }}</p>
</DE.Cell>
</template>
+3 -3
View File
@@ -52,8 +52,8 @@ const isOpen = computed({
>
<DialogHeader>
<DialogTitle :class="`text-sm 2xl:text-base font-semibold flex ${titleClass || ''}`">
<div class="me-2 pt-0.5">
<Icon v-if="props.titleIcon" :name="props.titleIcon" :class="`!pt-2`" />
<div v-if="props.titleIcon" class="me-2 pt-0.5">
<Icon :name="props.titleIcon" :class="`!pt-2`" />
</div>
<div>
{{ props.title }}
@@ -61,7 +61,7 @@ const isOpen = computed({
</DialogTitle>
<DialogDescription v-if="props.description">{{ props.description }}</DialogDescription>
</DialogHeader>
<DialogDescription :class="sizeClass">
<DialogDescription :class="`${sizeClass} md:text-xs 2xl:text-sm`">
<slot />
</DialogDescription>
</DialogContent>
@@ -1,10 +1,12 @@
<script setup lang="ts">
const props = defineProps<{
enableDraft?: boolean
smallMode?: boolean
defaultClass?: string
class?: string
}>()
const enableDraft = props.enableDraft ?? true
const defaultClass = props.defaultClass ?? 'm-2 flex gap-2 px-2'
const additionalClass = props.class ?? ''
const btnClass = props.smallMode ? '[&_button]:w-7 [&_button]:h-7 [&_button]:2xl:w-8 [&_button]:2xl:h-9 [&_button]:!p-0' : ''
@@ -29,7 +31,7 @@ function onClick(type: ClickType) {
</Button>
</div>
<div>
<Button variant="secondary" type="button" @click="onClick('draft')">
<Button v-show="enableDraft" variant="secondary" type="button" @click="onClick('draft')">
<Icon name="i-lucide-file" />
Draft
</Button>
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { Calendar as CalendarIcon, Filter as FilterIcon, Search } from 'lucide-vue-next'
import { ref } from 'vue'
import type { Ref } from 'vue'
import type { DateRange } from 'radix-vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { cn } from '~/lib/utils'
import type { HeaderPrep, RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
const props = defineProps<{
prep: HeaderPrep
refSearchNav?: RefSearchNav
enableExport?: boolean
refExportNav?: RefExportNav
}>()
// function emitSearchNavClick() {
// props.refSearchNav?.onClick()
// }
//
// function onInput(event: Event) {
// props.refSearchNav?.onInput((event.target as HTMLInputElement).value)
// }
//
// function btnClick() {
// props.prep?.addNav?.onClick?.()
// }
const searchQuery = ref('')
const dateRange = ref<{ from: Date | null; to: Date | null }>({
from: new Date(),
to: new Date(),
})
const df = new DateFormatter('en-US', {
dateStyle: 'medium',
})
const value = ref({
start: new CalendarDate(2022, 1, 20),
end: new CalendarDate(2022, 1, 20).add({ days: 20 }),
}) as Ref<DateRange>
function onFilterClick() {
console.log('Search:', searchQuery.value)
console.log('Date Range:', dateRange.value)
props.refSearchNav?.onClick()
}
</script>
<template>
<header>
<div class="flex items-center gap-2 mb-4 2xl:mb-5">
<Button variant="outline" class="border-orange-500 text-orange-600 hover:bg-orange-50" @click="onFilterClick">
<FilterIcon class="mr-2 size-4" />
Filter
</Button>
<DropdownMenu v-show="props.enableExport">
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto border-orange-500 text-orange-600 hover:bg-orange-50">
<Icon name="i-lucide-download" class="h-4 w-4" />
Ekspor
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem v-show="props.refExportNav?.onExportPdf"
@click="props.refExportNav?.onExportPdf">
Ekspor PDF
</DropdownMenuItem>
<DropdownMenuItem v-show="props.refExportNav?.onExportCsv"
@click="props.refExportNav?.onExportCsv">
Ekspor CSV
</DropdownMenuItem>
<DropdownMenuItem v-show="props.refExportNav?.onExportExcel"
@click="props.refExportNav?.onExportExcel">
Ekspor Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
</template>
+28 -2
View File
@@ -5,11 +5,13 @@ import type { Ref } from 'vue'
import type { DateRange } from 'radix-vue'
import { CalendarDate, DateFormatter, getLocalTimeZone } from '@internationalized/date'
import { cn } from '~/lib/utils'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/my-ui/data/types'
import type { HeaderPrep, RefExportNav, RefSearchNav } from '~/components/pub/my-ui/data/types'
const props = defineProps<{
prep: HeaderPrep
refSearchNav?: RefSearchNav
enableExport?: boolean
refExportNav?: RefExportNav
}>()
// function emitSearchNavClick() {
@@ -57,7 +59,7 @@ function onFilterClick() {
<template>
<header>
<div class="flex items-center space-x-2 mb-4 2xl:mb-5">
<div class="flex items-center gap-2 mb-4 2xl:mb-5">
<div class="relative w-64">
<Search class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-gray-400" />
<Input v-model="searchQuery" type="text" placeholder="Cari Nama /No.RM" class="pl-9" />
@@ -97,6 +99,30 @@ function onFilterClick() {
<FilterIcon class="mr-2 size-4" />
Filter
</Button>
<DropdownMenu v-show="props.enableExport">
<DropdownMenuTrigger as-child>
<Button variant="outline" class="ml-auto border-orange-500 text-orange-600 hover:bg-orange-50">
<Icon name="i-lucide-download" class="h-4 w-4" />
Ekspor
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem v-show="props.refExportNav?.onExportPdf"
@click="props.refExportNav?.onExportPdf">
Ekspor PDF
</DropdownMenuItem>
<DropdownMenuItem v-show="props.refExportNav?.onExportCsv"
@click="props.refExportNav?.onExportCsv">
Ekspor CSV
</DropdownMenuItem>
<DropdownMenuItem v-show="props.refExportNav?.onExportExcel"
@click="props.refExportNav?.onExportExcel">
Ekspor Excel
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
</template>