feat(satusehat): add search component, card summary and date picker
- Implement search component with lucide-vue-next icon - Create card summary component for displaying summary data - Add date picker component with range selection functionality - Update list configuration and styling - Reorganize package.json structure
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
icon: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button
|
||||
variant="outline" class="text-primary border-primary bg-white hover:bg-gray-50 focus:bg-gray-150
|
||||
focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
|
||||
>
|
||||
<Icon :name="props.icon" class="size-4 me-2" />
|
||||
{{ props.text }}
|
||||
</Button>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { Summary } from '~/components/pub/base/summary-card.type';
|
||||
|
||||
const props = defineProps<{
|
||||
isLoading: boolean
|
||||
summaryData: Summary[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
|
||||
<template v-if="props.isLoading">
|
||||
<PubBaseSummaryCard v-for="n in 4" :key="n" is-skeleton :stat="summaryData[n]" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PubBaseSummaryCard v-for="card in summaryData" :key="card.title" :stat="card" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,7 +26,6 @@ export const cols: Col[] = [
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
{ width: 100 },
|
||||
]
|
||||
|
||||
export const header: Th[][] = [
|
||||
@@ -37,11 +36,10 @@ export const header: Th[][] = [
|
||||
{ label: 'Status' },
|
||||
{ label: 'Terakhir Update' },
|
||||
{ label: 'FHIR ID' },
|
||||
{ label: '' },
|
||||
],
|
||||
]
|
||||
|
||||
export const keys = ['id', 'resource_type', 'patient', 'status', 'updated_at', 'fhir_id', 'action']
|
||||
export const keys = ['id', 'resource_type', 'patient', 'status', 'updated_at', 'fhir_id']
|
||||
|
||||
export const delKeyNames: KeyLabel[] = [
|
||||
{ key: 'code', label: 'Kode' },
|
||||
@@ -72,14 +70,6 @@ export const funcComponent: RecStrFuncComponent = {
|
||||
}
|
||||
return res
|
||||
},
|
||||
action(rec, idx) {
|
||||
const res: RecComponent = {
|
||||
idx,
|
||||
rec: rec as object,
|
||||
component: action,
|
||||
}
|
||||
return res
|
||||
},
|
||||
}
|
||||
|
||||
export const funcHtml: RecStrFuncUnknown = {
|
||||
|
||||
@@ -7,13 +7,6 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PubBaseDataTable
|
||||
:rows="data"
|
||||
:cols="cols"
|
||||
:header="header"
|
||||
:keys="keys"
|
||||
:func-parsed="funcParsed"
|
||||
:func-html="funcHtml"
|
||||
:func-component="funcComponent"
|
||||
/>
|
||||
<PubBaseDataTable :rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed"
|
||||
:func-html="funcHtml" :func-component="funcComponent" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<script setup lang="ts">
|
||||
import type { DateValue } from '@internationalized/date'
|
||||
import type { DateRange } from 'radix-vue'
|
||||
import type { Grid } from 'radix-vue/date'
|
||||
import type { Ref } from 'vue'
|
||||
import { CalendarDate } from '@internationalized/date'
|
||||
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||
import { RangeCalendarRoot, useDateFormatter } from 'radix-vue'
|
||||
import { createMonth, toDate } from 'radix-vue/date'
|
||||
import { buttonVariants } from '~/components/pub/ui/button'
|
||||
|
||||
import { cn } from '~/lib/utils'
|
||||
|
||||
// Props untuk kustomisasi
|
||||
interface Props {
|
||||
modelValue?: DateRange
|
||||
placeholder?: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: 'Pick a date range',
|
||||
class: '',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: DateRange]
|
||||
}>()
|
||||
|
||||
const value = ref(props.modelValue || {
|
||||
start: new CalendarDate(2022, 1, 20),
|
||||
end: new CalendarDate(2022, 2, 9),
|
||||
}) as Ref<DateRange>
|
||||
|
||||
watch(value, (newValue) => {
|
||||
emit('update:modelValue', newValue)
|
||||
}, { deep: true })
|
||||
|
||||
const locale = ref('en-US')
|
||||
const formatter = useDateFormatter(locale.value)
|
||||
|
||||
const placeholder = ref(value.value.start) as Ref<DateValue>
|
||||
const secondMonthPlaceholder = ref(value.value.end) as Ref<DateValue>
|
||||
|
||||
// Create independent month grids
|
||||
const firstMonth = ref(
|
||||
createMonth({
|
||||
dateObj: placeholder.value,
|
||||
locale: locale.value,
|
||||
fixedWeeks: true,
|
||||
weekStartsOn: 0,
|
||||
}),
|
||||
) as Ref<Grid<DateValue>>
|
||||
|
||||
const secondMonth = ref(
|
||||
createMonth({
|
||||
dateObj: secondMonthPlaceholder.value,
|
||||
locale: locale.value,
|
||||
fixedWeeks: true,
|
||||
weekStartsOn: 0,
|
||||
}),
|
||||
) as Ref<Grid<DateValue>>
|
||||
|
||||
// Function to update months independently
|
||||
function updateMonth(reference: 'first' | 'second', months: number) {
|
||||
if (reference === 'first') {
|
||||
placeholder.value = placeholder.value.add({ months })
|
||||
} else {
|
||||
secondMonthPlaceholder.value = secondMonthPlaceholder.value.add({ months })
|
||||
}
|
||||
}
|
||||
|
||||
// Watch first month placeholder
|
||||
watch(placeholder, (_placeholder) => {
|
||||
firstMonth.value = createMonth({
|
||||
dateObj: _placeholder,
|
||||
weekStartsOn: 0,
|
||||
fixedWeeks: true,
|
||||
locale: locale.value,
|
||||
})
|
||||
})
|
||||
|
||||
// Watch second month placeholder
|
||||
watch(secondMonthPlaceholder, (_secondMonthPlaceholder) => {
|
||||
secondMonth.value = createMonth({
|
||||
dateObj: _secondMonthPlaceholder,
|
||||
weekStartsOn: 0,
|
||||
fixedWeeks: true,
|
||||
locale: locale.value,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('w-full', props.class)">
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" :class="cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!value && 'text-muted-foreground',
|
||||
)">
|
||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||
<template v-if="value.start">
|
||||
<template v-if="value.end">
|
||||
{{
|
||||
formatter.custom(toDate(value.start), {
|
||||
dateStyle: 'medium',
|
||||
})
|
||||
}}
|
||||
-
|
||||
{{
|
||||
formatter.custom(toDate(value.end), {
|
||||
dateStyle: 'medium',
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
{{
|
||||
formatter.custom(toDate(value.start), {
|
||||
dateStyle: 'medium',
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ props.placeholder }}
|
||||
</template>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
class="w-auto p-0 rounded-xl shadow-lg border bg-white dark:bg-neutral-900 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
|
||||
align="start">
|
||||
<RangeCalendarRoot v-slot="{ weekDays }" v-model="value" v-model:placeholder="placeholder" class="p-4">
|
||||
<div class="flex flex-col gap-6 sm:flex-row sm:gap-8">
|
||||
<!-- First Month -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Button :class="cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
)" @click="updateMonth('first', -1)">
|
||||
<ChevronLeft class="h-4 w-4 text-primary" />
|
||||
</Button>
|
||||
<div class="text-sm font-medium">
|
||||
{{
|
||||
formatter.fullMonthAndYear(
|
||||
toDate(firstMonth.value),
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<Button :class="cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
)" @click="updateMonth('first', 1)">
|
||||
<ChevronRight class="h-4 w-4 text-primary" />
|
||||
</Button>
|
||||
</div>
|
||||
<RangeCalendarGrid>
|
||||
<RangeCalendarGridHead>
|
||||
<RangeCalendarGridRow>
|
||||
<RangeCalendarHeadCell v-for="day in weekDays" :key="day" class="w-full">
|
||||
{{ day }}
|
||||
</RangeCalendarHeadCell>
|
||||
</RangeCalendarGridRow>
|
||||
</RangeCalendarGridHead>
|
||||
<RangeCalendarGridBody class="mt-2">
|
||||
<RangeCalendarGridRow v-for="(weekDates, index) in firstMonth.rows" :key="`weekDate-${index}`"
|
||||
class="w-full">
|
||||
<RangeCalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
|
||||
<RangeCalendarCellTrigger :day="weekDate" :month="firstMonth.value" :class="cn(
|
||||
'h-9 w-9 p-0 text-sm rounded-md transition-colors flex items-center justify-center',
|
||||
|
||||
// tanggal bulan lain (past/next month)
|
||||
'data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50',
|
||||
|
||||
// tanggal terpilih (start / end)
|
||||
'data-[selected]:bg-green-600 data-[selected]:text-white',
|
||||
|
||||
// tanggal dalam rentang
|
||||
'data-[in-range]:bg-green-500 data-[in-range]:text-white',
|
||||
|
||||
// rounded pill untuk range
|
||||
'data-[selection-start]:rounded-l-full data-[selection-end]:rounded-r-full',
|
||||
|
||||
// hover
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
)" />
|
||||
|
||||
</RangeCalendarCell>
|
||||
</RangeCalendarGridRow>
|
||||
</RangeCalendarGridBody>
|
||||
</RangeCalendarGrid>
|
||||
</div>
|
||||
|
||||
<!-- Second Month -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<Button :class="cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
)" @click="updateMonth('second', -1)">
|
||||
<ChevronLeft class="h-4 w-4 text-primary" />
|
||||
</Button>
|
||||
<div class="text-sm font-medium">
|
||||
{{
|
||||
formatter.fullMonthAndYear(
|
||||
toDate(secondMonth.value),
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<Button :class="cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
)" @click="updateMonth('second', 1)">
|
||||
<ChevronRight class="h-4 w-4 text-primary" />
|
||||
</Button>
|
||||
</div>
|
||||
<RangeCalendarGrid>
|
||||
<RangeCalendarGridHead>
|
||||
<RangeCalendarGridRow>
|
||||
<RangeCalendarHeadCell v-for="day in weekDays" :key="day" class="w-full">
|
||||
{{ day }}
|
||||
</RangeCalendarHeadCell>
|
||||
</RangeCalendarGridRow>
|
||||
</RangeCalendarGridHead>
|
||||
<RangeCalendarGridBody class="mt-2">
|
||||
<RangeCalendarGridRow v-for="(weekDates, index) in secondMonth.rows" :key="`weekDate-${index}`"
|
||||
class="w-full">
|
||||
<RangeCalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
|
||||
<RangeCalendarCellTrigger :day="weekDate" :month="secondMonth.value" :class="cn(
|
||||
'h-9 w-9 p-0 text-sm rounded-md transition-colors flex items-center justify-center',
|
||||
|
||||
// tanggal bulan lain (past/next month)
|
||||
'data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50',
|
||||
|
||||
// tanggal terpilih (start / end)
|
||||
'data-[selected]:bg-green-600 data-[selected]:text-white',
|
||||
|
||||
// tanggal dalam rentang
|
||||
'data-[in-range]:bg-green-500 data-[in-range]:text-white',
|
||||
|
||||
// rounded pill untuk range
|
||||
'data-[selection-start]:rounded-l-full data-[selection-end]:rounded-r-full',
|
||||
|
||||
// hover
|
||||
'hover:bg-accent hover:text-accent-foreground'
|
||||
)" />
|
||||
|
||||
</RangeCalendarCell>
|
||||
</RangeCalendarGridRow>
|
||||
</RangeCalendarGridBody>
|
||||
</RangeCalendarGrid>
|
||||
</div>
|
||||
</div>
|
||||
</RangeCalendarRoot>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { Search } from 'lucide-vue-next'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full max-w-sm items-center">
|
||||
<Input id="search" type="text" placeholder="Search..." class="pl-10" />
|
||||
<span class="absolute start-0 inset-y-0 flex items-center justify-center px-2">
|
||||
<Search class="size-6 text-muted-foreground" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user