Merge branch 'dev' of https://github.com/dikstub-rssa/simrs-fe into feat/user

This commit is contained in:
Abizrh
2025-08-28 12:01:55 +07:00
225 changed files with 2110 additions and 421 deletions
+30
View File
@@ -0,0 +1,30 @@
# top-most EditorConfig file
root = true
# Default settings for all files
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
# For Markdown files, don't trim trailing whitespace (karena kadang dipakai untuk line break)
[*.md]
trim_trailing_whitespace = false
# For JSON, YAML, and config files
[*.{json,yml,yaml}]
indent_style = space
indent_size = 2
# For JS, TS, Vue files
[*.{js,ts,vue}]
indent_style = space
indent_size = 2
# For CSS, SCSS, PostCSS
[*.{css,scss,pcss}]
indent_style = space
indent_size = 2
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}
+1
View File
@@ -9,6 +9,7 @@ RSSA - Front End
- [Vue Style Guide](https://vuejs.org/style-guide)
- [Nuxt Style Guide](https://nuxt.com/docs/4.x/guide)
- [Shadcn Vue @radix-ui](https://radix.shadcn-vue.com/)
## Configuration
+38 -40
View File
@@ -21,9 +21,9 @@
--muted: 210 25% 95%;
--muted-foreground: 210 15% 50%;
/* Accent - Professional Blue */
--accent: 210 100% 50%;
--accent-foreground: 0 0% 100%;
/* Accent - Neutral Gray */
--accent: 210 40% 96%;
--accent-foreground: 222.2 84% 4.9%;
--destructive: 0 75% 55%;
--destructive-foreground: 0 0% 100%;
--border: 210 20% 88%;
@@ -67,45 +67,43 @@
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217.2 91.2% 59.8%;
/* .dark { */
/* --background: 210 25% 8%; */
/* --foreground: 210 20% 95%; */
/* --card: 210 25% 10%; */
/* --card-foreground: 210 20% 95%; */
/* --popover: 210 25% 10%; */
/* --popover-foreground: 210 20% 95%; */
/* --primary: 150 75% 45%; */
/* --primary-foreground: 0 0% 100%; */
/* --primary-hover: 150 75% 50%; */
/* --secondary: 210 25% 15%; */
/* --secondary-foreground: 210 20% 90%; */
/* --muted: 210 25% 15%; */
/* --muted-foreground: 210 15% 65%; */
/* --accent: 210 100% 55%; */
/* --accent-foreground: 0 0% 100%; */
/* --destructive: 0 75% 60%; */
/* --destructive-foreground: 0 0% 100%; */
/* --border: 210 25% 20%; */
/* --input: 210 25% 15%; */
/* --ring: 150 75% 45%; */
/* --success: 150 75% 50%; */
/* --warning: 45 95% 65%; */
/* --info: 210 100% 60%; */
/* --gradient-primary: linear-gradient(135deg, hsl(150 75% 45%), hsl(150 75% 55%)); */
/* --gradient-medical: linear-gradient(135deg, hsl(150 75% 45%), hsl(210 100% 55%)); */
/* --gradient-subtle: linear-gradient(180deg, hsl(210 25% 8%), hsl(210 25% 12%)); */
/* --sidebar-background: 240 5.9% 10%; */
/* --sidebar-foreground: 240 4.8% 95.9%; */
/* --sidebar-primary: 224.3 76.3% 48%; */
/* --sidebar-primary-foreground: 0 0% 100%; */
/* --sidebar-accent: 240 3.7% 15.9%; */
/* --sidebar-accent-foreground: 240 4.8% 95.9%; */
/* --sidebar-border: 240 3.7% 15.9%; */
/* --sidebar-ring: 217.2 91.2% 59.8%; */
}
/* .dark { */
/* --background: 210 25% 8%; */
/* --foreground: 210 20% 95%; */
/* --card: 210 25% 10%; */
/* --card-foreground: 210 20% 95%; */
/* --popover: 210 25% 10%; */
/* --popover-foreground: 210 20% 95%; */
/* --primary: 150 75% 45%; */
/* --primary-foreground: 0 0% 100%; */
/* --primary-hover: 150 75% 50%; */
/* --secondary: 210 25% 15%; */
/* --secondary-foreground: 210 20% 90%; */
/* --muted: 210 25% 15%; */
/* --muted-foreground: 210 15% 65%; */
/* --accent: 210 100% 55%; */
/* --accent-foreground: 0 0% 100%; */
/* --destructive: 0 75% 60%; */
/* --destructive-foreground: 0 0% 100%; */
/* --border: 210 25% 20%; */
/* --input: 210 25% 15%; */
/* --ring: 150 75% 45%; */
/* --success: 150 75% 50%; */
/* --warning: 45 95% 65%; */
/* --info: 210 100% 60%; */
/* --gradient-primary: linear-gradient(135deg, hsl(150 75% 45%), hsl(150 75% 55%)); */
/* --gradient-medical: linear-gradient(135deg, hsl(150 75% 45%), hsl(210 100% 55%)); */
/* --gradient-subtle: linear-gradient(180deg, hsl(210 25% 8%), hsl(210 25% 12%)); */
/* --sidebar-background: 240 5.9% 10%; */
/* --sidebar-foreground: 240 4.8% 95.9%; */
/* --sidebar-primary: 224.3 76.3% 48%; */
/* --sidebar-primary-foreground: 0 0% 100%; */
/* --sidebar-accent: 240 3.7% 15.9%; */
/* --sidebar-accent-foreground: 240 4.8% 95.9%; */
/* --sidebar-border: 240 3.7% 15.9%; */
/* --sidebar-ring: 217.2 91.2% 59.8%; */
/* } */
/* Keyframes for Animations */
@keyframes accordion-down {
from {
+14 -9
View File
@@ -11,8 +11,9 @@ import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
const doctorStatus = {
const _doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
@@ -26,10 +27,10 @@ export const cols: Col[] = [
{},
{},
{},
{ width: 120 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 50 },
{},
{},
]
export const header: Th[][] = [
@@ -43,6 +44,7 @@ export const header: Th[][] = [
{ label: 'Fee Ranap' },
{ label: 'Fee Rajal' },
{ label: 'Status' },
{ label: '' },
],
]
@@ -66,7 +68,6 @@ export const delKeyNames: KeyLabel[] = [
export const funcParsed: RecStrFuncUnknown = {
name: (rec: unknown): unknown => {
console.log(rec)
const recX = rec as SmallDetailDto
return `${recX.frontTitle} ${recX.name} ${recX.endTitle}`.trim()
},
@@ -85,10 +86,6 @@ export const funcParsed: RecStrFuncUnknown = {
const recX = rec as SmallDetailDto
return Number(recX.outPatient_itemPrice.price).toLocaleString('id-ID')
},
status: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return doctorStatus[recX.status_code as keyof typeof doctorStatus]
},
}
export const funcComponent: RecStrFuncComponent = {
@@ -100,6 +97,14 @@ export const funcComponent: RecStrFuncComponent = {
}
return res
},
status(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: statusBadge,
}
return res
},
}
export const funcHtml: RecStrFuncUnknown = {
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge'
const props = defineProps<{
rec: any
idx?: number
}>()
const doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
const statusText = computed(() => {
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus]
})
const badgeVariant = computed(() => {
return props.rec.status_code === 1 ? 'default' : 'destructive'
})
</script>
<template>
<div class="flex justify-center">
<Badge :variant="badgeVariant">
{{ statusText }}
</Badge>
</div>
</template>
+12
View File
@@ -11,6 +11,7 @@ import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
const statusBadge = defineAsyncComponent(() => import('./status-badge.vue'))
export const cols: Col[] = [
{},
@@ -110,6 +111,17 @@ export const funcComponent: RecStrFuncComponent = {
}
return res
},
status(rec, idx) {
if (rec.status === null) {
rec.status_code = 0
}
const res: RecComponent = {
idx,
rec: rec as object,
component: statusBadge,
}
return res
},
}
export const funcHtml: RecStrFuncUnknown = {
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { Badge } from '~/components/pub/ui/badge'
const props = defineProps<{
rec: any
idx?: number
}>()
const doctorStatus = {
0: 'Tidak Aktif',
1: 'Aktif',
}
const statusText = computed(() => {
return doctorStatus[props.rec.status_code as keyof typeof doctorStatus]
})
const badgeVariant = computed(() => {
return props.rec.status_code === 1 ? 'default' : 'destructive'
})
</script>
<template>
<div class="flex justify-center">
<Badge :variant="badgeVariant">
{{ statusText }}
</Badge>
</div>
</template>
@@ -1,9 +1,9 @@
import type { Col, KeyLabel, RecComponent, RecStrFuncComponent, RecStrFuncUnknown, Th } from '../../pub/nav/types'
import type { Col, KeyLabel, RecComponent, RecStrFuncComponent, RecStrFuncUnknown, Th } from '~/components/pub/custom-ui/data/types'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
const action = defineAsyncComponent(() => import('~/components/pub/nav/dropdown-action-dud.vue'))
const action = defineAsyncComponent(() => import('~/components/pub/custom-ui/data/dropdown-action-dud.vue'))
export const cols: Col[] = [
{},
@@ -0,0 +1,13 @@
<script setup lang="ts">
const props = defineProps<{
rec: any
idx?: number
}>()
</script>
<template>
<div class="flex flex-col justify-center">
<p class="font-semibold text-sm">{{ props.rec.patient.name }}</p>
<p class="text-xs text-muted-foreground">{{ props.rec.patient.mrn }}</p>
</div>
</template>
@@ -0,0 +1,29 @@
<script setup lang="ts">
import Badge from './badge.vue'
import { rowStatus } from './list-cfg'
const props = defineProps<{
rec: { status: number }
idx?: number
}>()
const variants = {
0: 'error',
1: 'warning',
2: 'info',
} as const
const statusText = computed(() => {
return rowStatus[props.rec.status as keyof typeof rowStatus]
})
const badgeStatus = computed((): 'error' | 'warning' | 'success' | 'info' => {
return variants[props.rec.status as keyof typeof variants] ?? 'info'
})
</script>
<template>
<div class="flex">
<Badge :status="badgeStatus" :text="statusText" />
</div>
</template>
+36
View File
@@ -0,0 +1,36 @@
<script setup lang="ts">
import { cva } from 'class-variance-authority'
import { cn } from '~/lib/utils'
interface BadgeProps {
status: 'success' | 'info' | 'warning' | 'error'
text: string
}
const props = defineProps<BadgeProps>()
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
success: 'rounded-full border-transparent bg-green-500 text-white hover:bg-green-600',
info: 'rounded-full border-transparent bg-blue-500 text-white hover:bg-blue-600',
warning: 'rounded-full border-transparent bg-yellow-500 text-white hover:bg-yellow-600',
error: 'rounded-full border-transparent bg-red-500 text-white hover:bg-red-600',
},
},
defaultVariants: {
variant: 'info',
},
},
)
</script>
<template>
<div class="flex">
<div :class="cn(badgeVariants({ variant: props.status }))">
{{ props.text }}
</div>
</div>
</template>
@@ -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>
@@ -0,0 +1,47 @@
<script setup lang="ts">
import Block from '~/components/pub/form/block.vue'
import FieldGroup from '~/components/pub/form/field-group.vue'
import Field from '~/components/pub/form/field.vue'
import Label from '~/components/pub/form/label.vue'
</script>
<template>
<form id="entry-form">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Icon name="i-lucide-user" class="me-2" />
<span class="font-semibold">Tambah</span> Pasien
</div>
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<Block>
<FieldGroup :column="3">
<Label>Nama</Label>
<Field>
<Input type="text" name="name" />
</Field>
</FieldGroup>
<FieldGroup :column="3">
<Label>Nama</Label>
<Field>
<Input type="text" name="name" />
</Field>
</FieldGroup>
<FieldGroup :column="3">
<Label>Nomor RM</Label>
<Field>
<Input type="text" name="name" />
</Field>
</FieldGroup>
<FieldGroup>
<Label dynamic>Alamat</Label>
<Field>
<Input type="text" name="name" />
</Field>
</FieldGroup>
</Block>
</div>
<div class="my-2 flex justify-end py-2">
<PubNavFooterCsd />
</div>
</form>
</template>
+78
View File
@@ -0,0 +1,78 @@
import type { Col, KeyLabel, RecComponent, RecStrFuncComponent, RecStrFuncUnknown, Th } from '../../pub/nav/types'
import { defineAsyncComponent } from 'vue'
type SmallDetailDto = any
export const rowType = {
1: 'Patient',
2: 'Encounter',
3: 'Observation',
}
export const rowStatus = {
0: 'Gagal',
1: 'Pending',
2: 'Terkirim',
}
const patientBadge = defineAsyncComponent(() => import('./badge-patient.vue'))
const statusBadge = defineAsyncComponent(() => import('./badge-status.vue'))
export const cols: Col[] = [
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
]
export const header: Th[][] = [
[
{ label: 'ID' },
{ label: 'Jenis' },
{ label: 'Pasien' },
{ label: 'Status' },
{ label: 'Terakhir Update' },
{ label: 'FHIR ID' },
],
]
export const keys = ['id', 'resource_type', 'patient', 'status', 'updated_at', 'fhir_id']
export const delKeyNames: KeyLabel[] = [
{ key: 'code', label: 'Kode' },
{ key: 'name', label: 'Nama' },
]
export const funcParsed: RecStrFuncUnknown = {
name: (rec: unknown): unknown => {
const recX = rec as SmallDetailDto
return `${recX.firstName} ${recX.middleName || ''} ${recX.lastName || ''}`
},
}
export const funcComponent: RecStrFuncComponent = {
patient(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: patientBadge,
}
return res
},
status(rec, idx) {
const res: RecComponent = {
idx,
rec: rec as object,
component: statusBadge,
}
return res
},
}
export const funcHtml: RecStrFuncUnknown = {
patient_address(_rec) {
return '-'
},
}
+14
View File
@@ -0,0 +1,14 @@
<script setup lang="ts">
import { cols, funcComponent, funcHtml, funcParsed, header, keys } from './list-cfg'
defineProps<{
data: any[]
}>()
</script>
<template>
<PubBaseDataTable
:rows="data" :cols="cols" :header="header" :keys="keys" :func-parsed="funcParsed"
:func-html="funcHtml" :func-component="funcComponent"
/>
</template>
+51
View File
@@ -0,0 +1,51 @@
<script setup lang="ts">
import type { DateRange } from 'radix-vue'
import type { Ref } from 'vue'
import {
CalendarDate,
DateFormatter,
getLocalTimeZone,
} from '@internationalized/date'
import { Calendar as CalendarIcon } from 'lucide-vue-next'
import { cn } from '~/lib/utils'
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>
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
:class="cn(
'w-[280px] 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">
{{ df.format(value.start.toDate(getLocalTimeZone())) }} - {{ df.format(value.end.toDate(getLocalTimeZone())) }}
</template>
<template v-else>
{{ df.format(value.start.toDate(getLocalTimeZone())) }}
</template>
</template>
<template v-else>
Pick a date
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<RangeCalendar v-model="value" initial-focus :number-of-months="2" @update:start-value="(startDate) => value.start = startDate" />
</PopoverContent>
</Popover>
</template>
+12
View File
@@ -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>
+6 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Activity, CreditCard, DollarSign, Users, UserCheck, UsersRound, Calendar, Hospital } from 'lucide-vue-next'
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
const dataCard = ref({
totalRevenue: 0,
@@ -161,8 +161,11 @@ onMounted(() => {
</div>
</CardHeader>
<CardContent class="grid cursor-pointer gap-8 md:grid-cols-4 md:gap-8">
<Card v-for="item in linkItems" :key="item"
class="border-primary hover:bg-primary my-2 h-32 border transition-colors duration-200 hover:bg-gray-200">
<Card
v-for="item in linkItems"
:key="item.title"
class="border-primary hover:bg-primary my-2 h-32 border transition-colors duration-200 hover:bg-gray-200"
>
<NuxtLink :to="item.link">
<CardContent class="my-2 grid h-full grid-rows-2 place-items-center">
<Icon :name="item.icon" class="text-primary h-9 w-[60px]" />
+9 -5
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { Summary } from '~/components/pub/base/summary-card.type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/nav/types'
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
import type { Summary } from '~/components/pub/base/summary-card/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
import { Calendar, Hospital, UserCheck, UsersRound } from 'lucide-vue-next'
import SummaryCard from '~/components/pub/base/summary-card/summary-card.vue'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
@@ -20,10 +21,11 @@ const refSearchNav: RefSearchNav = {
}
// Loading state management
const isLoading = reactive({
const isLoading = reactive<DataTableLoader>({
summary: false,
table: false,
isTableLoading: false,
})
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
@@ -84,11 +86,12 @@ async function getPatientSummary() {
}
async function getPatientList() {
isLoading.isTableLoading = true
const resp = await xfetch('/api/v1/patient')
console.log('data patient', resp)
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
isLoading.isTableLoading = false
}
onMounted(() => {
@@ -99,6 +102,7 @@ onMounted(() => {
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
</script>
<template>
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { HeaderPrep, RefSearchNav } from '~/components/pub/nav/types'
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
import RehabSepProsedurList from '~/components/app/rehab/registration/sep-prosedur/list.vue'
const props = defineProps<{
@@ -21,9 +22,9 @@ const refSearchNav: RefSearchNav = {
}
// Loading state management
const isLoading = reactive({
summary: false,
table: false,
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const recId = ref<number>(0)
const recAction = ref<string>('')
@@ -40,7 +41,6 @@ const hreaderPrep: HeaderPrep = {
async function getPatientList() {
const resp = await xfetch('/api/v1/patient')
console.log('data patient', resp)
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
@@ -53,12 +53,12 @@ onMounted(() => {
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
</script>
<template>
<PubNavHeaderPrep :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
<Header :prep="{ ...hreaderPrep }" :ref-search-nav="refSearchNav" />
<div class="my-4 flex flex-1 flex-col gap-4 md:gap-8">
<RehabSepProsedurList :data="data" />
</div>
</template>
/template>
+97
View File
@@ -0,0 +1,97 @@
import type { ServiceStatus } from '~/components/pub/base/service-status/type'
import type { Summary } from '~/components/pub/base/summary-card/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
import { CircleCheckBig, CircleDashed, CircleX, Send } from 'lucide-vue-next'
export const tabs = [
{
value: 'all',
label: 'Semua Resource',
},
{
value: 'patient',
label: 'Patient',
},
{
value: 'encounter',
label: 'Encounter',
},
{
value: 'observation',
label: 'Observation',
},
]
export const actions = [
{
value: 'export',
label: 'Ekspor',
icon: 'i-lucide-download',
},
]
// Status filter options
export const statusOptions = [
{ value: '0', label: 'Failed' },
{ value: '1', label: 'Pending' },
{ value: '2', label: 'Success' },
]
export const summaryData: Summary[] = [
{
title: 'Resource Terkirim',
icon: Send,
metric: 1245,
trend: 0,
timeframe: 'daily',
},
{
title: 'Sync Success',
icon: CircleCheckBig,
metric: '97%',
trend: 0,
timeframe: 'daily',
},
{
title: 'Pending Queue',
icon: CircleDashed,
metric: 32,
trend: 0,
timeframe: 'daily',
},
{
title: 'Failed Items',
icon: CircleX,
metric: 10,
trend: 0,
timeframe: 'daily',
},
]
// SATUSEHAT Service integration
export const service = reactive<ServiceStatus>({
serviceName: 'SATUSEHAT',
serviceDesc: 'SATUSEHAT - FHIR R4 Compliant',
sessionActive: false,
status: 'connecting',
isSkeleton: false,
})
export const headerPrep: HeaderPrep = {
title: 'SATUSEHAT Integration',
icon: 'i-lucide-box',
addNav: {
label: 'Kirim Resource',
icon: 'i-lucide-send',
// onClick: () => navigateTo('/patient/add'),
},
}
export const refSearchNav: RefSearchNav = {
onClick: () => {
// open filter modal
},
onInput: (_val: string) => {
},
onClear: () => {
},
}
+232
View File
@@ -0,0 +1,232 @@
<script setup lang="ts">
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
import { useUrlSearchParams } from '@vueuse/core'
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
import { actions, headerPrep, refSearchNav, service, summaryData, tabs } from './const'
import { defaultQuery, querySchema, tabSwitcher } from './schema.query'
// State management
const data = ref([])
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
const isLoading = reactive<DataTableLoader>({
satusehatConn: true,
isTableLoading: false,
})
const queryParams = useUrlSearchParams('history', {
initialValue: defaultQuery,
removeFalsyValues: true,
})
const params = computed(() => {
const result = querySchema.safeParse(queryParams)
return result.data
})
// Pagination state
const pagination = ref({
total: 0,
page: 1,
limit: 10,
total_pages: 0,
has_next: false,
has_prev: false,
})
// API function to fetch data
async function fetchData() {
try {
isLoading.isTableLoading = true
data.value = []
const response = await xfetch('/api/v1/satusehat/list', 'POST', {
resource_type: params.value?.resource_type,
date_from: params.value?.date_from,
date_to: params.value?.date_to,
search: params.value?.q,
page: params.value?.page,
limit: params.value?.limit,
})
if (response.success) {
data.value = response.body.data
pagination.value = response.body.meta
}
} catch (error) {
console.error('Error fetching data:', error)
data.value = []
} finally {
isLoading.isTableLoading = false
}
}
async function callSatuSehat() {
try {
await new Promise((resolve) => setTimeout(resolve, 500))
service.status = 'connected'
// service.status = 'error'
service.sessionActive = true
// service.sessionActive = false
} finally {
isLoading.satusehatConn = false
}
}
// Initialize params from URL on mount
onMounted(async () => {
await callSatuSehat()
await fetchData()
})
// Watch for url param changed state and trigger refetch
watch(params, () => {
fetchData()
})
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
// Sync active tab filter with queryparams
// fallback and default to `all` filter
const activeTabFilter = computed({
get: () => {
const result = tabSwitcher.parse(queryParams.resource_type)
return result
},
set: (value) => {
queryParams.resource_type = value
queryParams.q = defaultQuery.q
queryParams.page = defaultQuery.page
queryParams.limit = defaultQuery.limit
queryParams.date_from = defaultQuery.date_from
queryParams.date_to = defaultQuery.date_to
},
})
</script>
<template>
<div class="rounded-md border p-4">
<Header :prep="headerPrep" :ref-search-nav="refSearchNav" />
<div class="my-4 flex flex-1 flex-col gap-3 md:gap-4">
<PubBaseServiceStatus v-bind="service" />
<AppSatusehatCardSummary :is-loading="isLoading.satusehatConn!" :summary-data="summaryData" />
</div>
<div class="rounded-md border p-4">
<h2 class="text-md font-semibold py-2">FHIR Resource</h2>
<Tabs v-model="activeTabFilter">
<div class="scrollbar-hide overflow-x-auto flex gap-2 justify-between">
<TabsList>
<TabsTrigger
v-for="tab in tabs" :key="tab.value" :value="tab.value"
class="flex-shrink-0 px-4 py-2 text-sm font-medium data-[state=active]:bg-green-600 data-[state=inactive]:bg-gray-100 data-[state=active]:text-white data-[state=inactive]:text-gray-700"
>
{{ tab.label }}
</TabsTrigger>
</TabsList>
<div class="flex gap-2 items-center">
<!-- Search Input -->
<AppSatusehatPicker />
<div class="relative w-full max-w-sm">
<Dialog>
<DialogTrigger>
<Input type="text" placeholder="Cari pasien..." class="pl-3 h-9" />
</DialogTrigger>
<DialogContent>
<DialogHeader class="border-b-1">
<DialogTitle class="pb-2">Pencarian</DialogTitle>
</DialogHeader>
<Form>
<FormField v-slot="{ componentField }" name="username">
<FormItem>
<FormLabel>Pasien</FormLabel>
<FormControl>
<Input type="text" placeholder="nama pasien, id pasien" v-bind="componentField" />
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="status">
<FormItem>
<FormLabel>Status</FormLabel>
<FormControl>
<Select v-bind="componentField">
<SelectTrigger class="bg-white border border-gray-300">
<SelectValue class="text-gray-400" placeholder="-- select item" />
</SelectTrigger>
<SelectContent class="bg-white ">
<SelectItem value="0">
Gagal
</SelectItem>
<SelectItem value="1">
Pending
</SelectItem>
<SelectItem value="2">
Terkirim
</SelectItem>
</SelectContent>
</Select>
</FormControl>
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="fhirId">
<FormItem>
<FormLabel>FHIR ID</FormLabel>
<FormControl>
<Input type="text" placeholder="fhir id" v-bind="componentField" />
</FormControl>
</FormItem>
</FormField>
</Form>
<DialogFooter>
<Button variant="outline">
Reset
</Button>
<Button>
Apply
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<!-- Action Buttons -->
<AppSatusehatButtonAction
v-for="action in actions" :key="action.value" :icon="action.icon"
:text="action.label"
/>
</div>
</div>
<div class="mt-4">
<TabsContent v-for="tab in tabs" :key="`content-${tab.value}`" :value="tab.value">
<div class="rounded-md border p-4">
<AppSatusehatList v-if="!isLoading.satusehatConn" :data="data" />
<!-- Pagination -->
<div
v-if="!isLoading.satusehatConn && !isLoading.isTableLoading && pagination.total > 0"
class="mt-4 flex justify-between items-center"
>
<div class="text-sm text-muted-foreground">
Menampilkan {{ ((pagination.page - 1) * pagination.limit) + 1 }} -
{{ Math.min(pagination.page * pagination.limit, pagination.total) }}
dari {{ pagination.total }} data
</div>
<div class="flex gap-2">
<Button v-if="pagination.has_prev" variant="outline" size="sm" @click="queryParams.page--">
Sebelumnya
</Button>
<Button v-if="pagination.has_next" variant="outline" size="sm" @click="queryParams.page++">
Selanjutnya
</Button>
</div>
</div>
</div>
</TabsContent>
</div>
</Tabs>
</div>
</div>
</template>
@@ -0,0 +1,23 @@
import * as z from 'zod'
const resourceTabEnum = z.enum(['all', 'patient', 'encounter', 'observation'])
export const tabSwitcher = resourceTabEnum.default('all').catch('all')
export const querySchema = z.object({
q: z.string().min(3).optional().catch(''),
resource_type: tabSwitcher,
date_from: z.string().optional().catch(''),
date_to: z.string().optional().catch(''),
page: z.coerce.number().int().min(1).default(1).catch(1),
limit: z.coerce.number().int().min(1).max(20).default(10).catch(10),
})
export const defaultQuery = {
q: '',
status: '',
resource_type: 'all',
date_from: '',
date_to: '',
page: 1,
limit: 10,
}
+17 -5
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { HeaderPrep, RefSearchNav } from '~/components/pub/nav/types'
import type { DataTableLoader } from '~/components/pub/base/data-table/type'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/prep.vue'
const data = ref([])
@@ -16,6 +17,10 @@ const refSearchNav: RefSearchNav = {
},
}
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const recId = ref<number>(0)
const recAction = ref<string>('')
const recItem = ref<any>(null)
@@ -29,13 +34,15 @@ const headerPrep: HeaderPrep = {
},
}
useAsyncData('getDoctor', () => xfetch('/api/v1/doctor'), { server: false, immediate: true })
async function getDoctorList() {
isLoading.dataListLoading = true
const resp = await xfetch('/api/v1/doctor')
if (resp.success) {
data.value = (resp.body as Record<string, any>).data
}
isLoading.dataListLoading = false
}
onMounted(() => {
@@ -45,9 +52,14 @@ onMounted(() => {
provide('rec_id', recId)
provide('rec_action', recAction)
provide('rec_item', recItem)
provide('table_data_loader', isLoading)
</script>
<template>
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
<AppDoctorList :data="data" />
<div class="rounded-md border p-4">
<Header :prep="{ ...headerPrep }" :ref-search-nav="refSearchNav" />
<div class="rounded-md border p-4">
<AppDoctorList v-if="!isLoading.dataListLoading" :data="data" />
</div>
</div>
</template>
+14 -10
View File
@@ -12,12 +12,12 @@ const teams: {
logo: string
plan: string
}[] = [
{
name: 'SIMRS - RSSA',
logo: '/rssa-logo.png',
plan: 'Saiful Anwar Hospital',
},
]
{
name: 'SIMRS - RSSA',
logo: '/rssa-logo.png',
plan: 'Saiful Anwar Hospital',
},
]
const sidebar = {
collapsible: 'offcanvas', // 'offcanvas' | 'icon' | 'none'
side: 'left', // 'left' | 'right'
@@ -59,8 +59,10 @@ async function setMenu() {
<SidebarGroupLabel>
{{ navMenu.heading }}
</SidebarGroupLabel>
<component :is="resolveNavItemComponent(item)" v-for="(item, index) in navMenu.items" :key="index" :item="item"
class="my-2 mb-2" />
<component
:is="resolveNavItemComponent(item)" v-for="(item, index) in navMenu.items" :key="index" :item="item"
class="my-2 mb-2"
/>
</SidebarGroup>
<template v-else>
<div class="p-5">
@@ -68,8 +70,10 @@ async function setMenu() {
</div>
</template>
<SidebarGroup class="mt-auto">
<component :is="resolveNavItemComponent(item)" v-for="(item, index) in navMenuBottom" :key="index" :item="item"
size="sm" />
<component
:is="resolveNavItemComponent(item)" v-for="(item, index) in navMenuBottom" :key="index" :item="item"
size="sm"
/>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { SidebarMenuButtonVariants } from '~/components/comps-pub/ui/sidebar'
import type { SidebarMenuButtonVariants } from '~/components/pub/ui/sidebar'
import { useSidebar } from '~/components/pub/ui/sidebar'
withDefaults(
@@ -1,43 +1,68 @@
<script setup lang="ts">
import type { DataTableLoader } from './type'
import { Info } from 'lucide-vue-next'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '~/components/pub/ui/table'
defineProps<{
rows: unknown[]
cols: object
header: object[]
cols: any[]
header: any[]
keys: string[]
funcParsed: object
funcHtml: object
funcComponent: object
funcParsed: Record<string, (row: any) => any>
funcHtml: Record<string, (row: any) => string>
funcComponent: Record<string, (row: any, idx: number) => any>
}>()
const loader = inject('table_data_loader') as DataTableLoader
</script>
<template>
<Table>
<TableHeader>
<TableHeader class="bg-gray-50">
<TableRow>
<TableHead
v-for="(h, idx) in header[0]"
:key="`head-${idx}`"
v-for="(h, idx) in header[0]" :key="`head-${idx}`" class="border"
:style="{ width: cols[idx]?.width ? `${cols[idx].width}px` : undefined }"
>
>
{{ h.label }}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableBody v-if="loader.isTableLoading">
<!-- Loading state with 5 skeleton rows -->
<TableRow v-for="n in 5" :key="`skeleton-${n}`">
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-skel-${n}-${cellIndex}`" class="border">
<Skeleton class="bg-gray-100 animate-pulse text-muted-foreground w-full h-6" />
</TableCell>
</TableRow>
</TableBody>
<TableBody v-else-if="rows.length === 0">
<TableRow>
<TableCell :colspan="keys.length" class="text-center py-8">
<div class="flex items-center justify-center">
<Info class="size-5 text-muted-foreground" />
<span class="ml-2">Tidak ada data tersedia</span>
</div>
</TableCell>
</TableRow>
</TableBody>
<TableBody v-else>
<TableRow v-for="(row, rowIndex) in rows" :key="`row-${rowIndex}`">
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-${rowIndex}-${cellIndex}`">
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-${rowIndex}-${cellIndex}`" class="border">
<!-- If funcComponent has a renderer -->
<component
:is="funcComponent[key](row, rowIndex).component"
v-if="funcComponent[key]"
:is="funcComponent[key](row, rowIndex).component" v-if="funcComponent[key]"
v-bind="funcComponent[key](row, rowIndex)"
/>
/>
<!-- If funcParsed or funcHtml returns a value -->
<template v-else>
{{ funcParsed[key]?.(row) ?? funcHtml[key]?.(row) ?? row[key] }}
<!-- Use v-html for funcHtml to render HTML content -->
<div v-if="funcHtml[key]" v-html="funcHtml[key]?.(row)"></div>
<!-- Use normal interpolation for funcParsed and regular data -->
<template v-else>
{{ funcParsed[key]?.(row) ?? (row as any)[key] }}
</template>
</template>
</TableCell>
</TableRow>
@@ -0,0 +1,4 @@
export interface DataTableLoader {
isTableLoading: boolean
[key: string]: boolean
}
@@ -0,0 +1,72 @@
<script setup lang="ts">
import type { ServiceStatus } from './type'
import { Loader, Loader2 } from 'lucide-vue-next'
import { cn } from '~/lib/utils'
const props = defineProps<ServiceStatus>()
const tokenStatus = computed((): string => {
return props.sessionActive ? 'Valid' : 'Invalid'
})
</script>
<template>
<Card v-if="props.isSkeleton" class="py-6">
<div class="flex gap-4 justify-between px-6">
<div class="flex gap-2 items-center">
<span class="bg-gray-100">
<Skeleton class="bg-gray-100 w-6 h-6 sm:w-8 sm:h-8" />
</span>
<div>
<Skeleton class="w-64 h-8 bg-gray-100 text-xs md:text-sm text-muted-foreground" />
</div>
</div>
<div class="text-right flex flex-col items-end">
<Skeleton class="w-32 h-2 bg-gray-100 text-xs md:text-md text-muted-foreground" />
<Skeleton class="w-32 h-3 bg-gray-100 text-xs md:text-md font-bold" />
</div>
</div>
</Card>
<Card v-else class="py-6">
<div class="flex gap-4 justify-between px-6">
<div class="flex gap-2 items-center">
<span
:class="cn(' rounded-md w-12 h-12 flex items-center justify-center',
{ 'bg-red-500': props.status === 'error' },
{ 'bg-blue-500': props.status !== 'error' },
)"
>
<Icon v-if="props.status === 'error'" name="i-lucide-cable" class="text-white w-6 h-6 sm:w-8 sm:h-8" />
<Icon v-else name="i-lucide-bring-to-front" class="text-white w-6 h-6 sm:w-8 sm:h-8" />
</span>
<div>
<p v-if="props.status === 'connected'" class="text-xs md:text-md font-bold">Koneksi {{ props.serviceName }}
Aktif</p>
<p v-if="props.status === 'connecting'" class="flex flex-row text-xs md:text-md font-bold">Menghubungkan ke
API {{
props.serviceName }}
<Loader2 class="ml-2 h-4 w-4 animate-spin" />
</p>
<p v-if="props.status === 'error'" class="text-xs md:text-md font-bold">Koneksi ke API {{ props.serviceName
}}
Gagal</p>
<p v-if="props.status === 'connected'" class="text-xs md:text-sm text-muted-foreground">Koneksi Terhubung ke
API {{ props.serviceDesc }}
</p>
</div>
</div>
<div class="text-right flex flex-col items-end">
<p class="text-xs md:text-md text-muted-foreground">Session Token</p>
<p v-if="props.status === 'connecting'">
<Loader class="ml-2 h-4 w-4 animate-spin" />
</p>
<p
v-else :class="cn('text-xs md:text-md font-bold',
{ 'text-blue-500': props.sessionActive },
{ 'text-red-500': !props.sessionActive },
)"
>{{ tokenStatus }}</p>
</div>
</div>
</Card>
</template>
@@ -0,0 +1,7 @@
export interface ServiceStatus {
serviceName: string
serviceDesc: string
sessionActive: boolean
status: 'connected' | 'connecting' | 'error' | 'disconnected'
isSkeleton?: boolean
}
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Summary } from './summary-card.type'
import type { Summary } from './type'
import { ChevronDown, ChevronUp } from 'lucide-vue-next'
import { cn } from '~/lib/utils'
@@ -41,7 +41,7 @@ const isTrending = computed<boolean>(() => (props.stat?.trend ?? 0) > 0)
</CardHeader>
<CardContent>
<Skeleton class="mb-2 h-6 w-48 bg-gray-100 text-2xl" />
<Skeleton class="h-4 w-64 bg-gray-100 text-xs font-medium" />
<Skeleton v-if="props.stat?.trend" class="h-4 w-64 bg-gray-100 text-xs font-medium" />
</CardContent>
</Card>
<Card v-else-if="props.stat && !props.isSkeleton" class="h-42">
@@ -55,9 +55,9 @@ const isTrending = computed<boolean>(() => (props.stat?.trend ?? 0) > 0)
</div>
<p v-if="props.stat.trend !== 0" class="text-muted-foreground flex items-center gap-1 text-xs">
<component
:is="isTrending ? ChevronUp : ChevronDown"
:is="isTrending ? ChevronUp : ChevronDown"
:class="cn('h-4 w-4', { 'text-green-500': isTrending }, { 'text-red-500': !isTrending })"
/>
/>
<span :class="cn('font-medium', { 'text-green-500': isTrending }, { 'text-red-500': !isTrending })">
{{ props.stat.trend.toFixed(1) }}%
<!-- {{ Math.abs(props.stat.trend).toFixed(1) }}% -->
@@ -1,7 +1,7 @@
export interface Summary {
title: string
icon: Component
metric: number
metric: number | string
trend: number
timeframe: 'yearly' | 'monthly' | 'weekly' | 'daily'
}
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { HeaderPrep, RefSearchNav } from '../types.ts'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
const props = defineProps<{
prep: HeaderPrep
@@ -11,8 +11,8 @@ const props = defineProps<{
<header>
<div class="flex items-center">
<div class="ml-3 text-lg font-bold text-gray-900">
<Icon :name="prep.icon" class="mr-2 h-4 w-4 align-middle" />
{{ prep.title }}
<Icon :name="props.prep.icon!" class="mr-2 h-4 w-4 align-middle" />
{{ props.prep.title }}
</div>
</div>
</header>
@@ -1,21 +1,21 @@
<script setup lang="ts">
import type { HeaderPrep, RefSearchNav } from '../types.ts'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
const props = defineProps<{
prep: HeaderPrep
refSearchNav: RefSearchNav
refSearchNav?: RefSearchNav
}>()
function emitSearchNavClick() {
props.refSearchNav.onClick()
props.refSearchNav?.onClick()
}
function onInput(event: Event) {
props.refSearchNav.onInput((event.target as HTMLInputElement).value)
props.refSearchNav?.onInput((event.target as HTMLInputElement).value)
}
function btnClick() {
props.prep.addNav?.onClick()
props.prep?.addNav?.onClick?.()
}
</script>
@@ -24,19 +24,17 @@ function btnClick() {
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="ml-3 text-lg font-bold text-gray-900">
<Icon :name="prep.icon" class="mr-2 h-4 w-4 align-middle" />
{{ prep.title }}
<Icon :name="props.prep.icon!" class="mr-2 size-4 md:size-6 align-middle" />
{{ props.prep.title }}
</div>
</div>
<div class="flex items-center">
<div class="ml-3 text-lg text-gray-900">
<div v-if="props.refSearchNav" class="ml-3 text-lg text-gray-900">
<Input
type="text"
placeholder="Search"
class="w-full rounded-md border bg-white px-4 py-2 text-gray-900 sm:text-sm"
@click="emitSearchNavClick"
type="text" placeholder="Search"
class="w-full rounded-md border bg-white px-4 py-2 text-gray-900 sm:text-sm" @click="emitSearchNavClick"
@input="onInput"
/>
/>
</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">
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { HeaderPrep, RefSearchNav } from '../types.ts'
import type { HeaderPrep, RefSearchNav } from '~/components/pub/custom-ui/data/types'
const props = defineProps<{
prep: HeaderPrep
@@ -15,7 +15,7 @@ function onInput(event: Event) {
}
function btnClick() {
props.prep.addNav?.onClick()
props.prep?.addNav?.onClick?.()
}
</script>
@@ -5,7 +5,6 @@ import { AccordionContent } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { AccordionItem, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -10,7 +10,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -11,7 +11,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<AlertDialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<AlertDialogContentEmits>()
@@ -8,7 +8,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<AlertDialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -5,7 +5,6 @@ import { AlertDialogTitle } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<AlertDialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -3,7 +3,6 @@ import type { HTMLAttributes } from 'vue'
import { MoreHorizontal } from 'lucide-vue-next'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -4,7 +4,6 @@ import type { HTMLAttributes } from 'vue'
import { Primitive } from 'radix-vue'
import { cn } from '~/lib/utils'
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>(), {
as: 'a',
})
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -3,7 +3,6 @@ import type { HTMLAttributes } from 'vue'
import { ChevronRight } from 'lucide-vue-next'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
+2 -3
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import type { PrimitiveProps } from 'radix-vue'
import type { HTMLAttributes } from 'vue'
import type { ButtonVariants } from '.'
import { Primitive } from 'reka-ui'
import { Primitive } from 'radix-vue'
import { cn } from '~/lib/utils'
import { buttonVariants } from '.'
@@ -19,7 +19,6 @@ const props = withDefaults(defineProps<Props>(), {
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
+11 -12
View File
@@ -4,27 +4,26 @@ import { cva } from 'class-variance-authority'
export { default as Button } from './Button.vue'
export const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
@@ -5,7 +5,6 @@ import { CalendarCell, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { CalendarGrid, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { CalendarGridRow, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { CalendarHeadCell, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { CalendarHeader, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { CalendarHeading, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
-1
View File
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
-1
View File
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -5,7 +5,6 @@ import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'radix-vue
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<CheckboxRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CheckboxRootEmits>()
@@ -5,7 +5,6 @@ import { ComboboxRoot, useForwardPropsEmits } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = withDefaults(defineProps<ComboboxRootProps & { class?: HTMLAttributes['class'] }>(), {
open: true,
modelValue: '',
@@ -5,7 +5,6 @@ import { ComboboxEmpty } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ComboboxEmptyProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { ComboboxGroup, ComboboxLabel } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ComboboxGroupProps & {
class?: HTMLAttributes['class']
heading?: string
@@ -6,7 +6,6 @@ import { ComboboxInput, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
defineOptions({
inheritAttrs: false,
})
@@ -5,7 +5,6 @@ import { ComboboxItem, useForwardPropsEmits } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ComboboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ComboboxItemEmits>()
@@ -5,7 +5,6 @@ import { ComboboxContent, useForwardPropsEmits } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = withDefaults(defineProps<ComboboxContentProps & { class?: HTMLAttributes['class'] }>(), {
dismissable: false,
})
@@ -5,7 +5,6 @@ import { ComboboxSeparator } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ComboboxSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -11,7 +11,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ContextMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ContextMenuCheckboxItemEmits>()
@@ -10,7 +10,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ContextMenuContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ContextMenuContentEmits>()
@@ -9,7 +9,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ContextMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const emits = defineEmits<ContextMenuItemEmits>()
@@ -5,7 +5,6 @@ import { ContextMenuLabel } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ContextMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = computed(() => {
@@ -11,7 +11,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ContextMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ContextMenuRadioItemEmits>()
@@ -8,7 +8,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ContextMenuSeparatorProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -9,7 +9,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuSubContentEmits>()
@@ -10,7 +10,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<ContextMenuSubTriggerProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = computed(() => {
@@ -13,7 +13,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
@@ -5,7 +5,6 @@ import { DialogDescription, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
@@ -13,7 +13,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DialogContentEmits>()
@@ -5,7 +5,6 @@ import { DialogTitle, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { DrawerDescription } from 'vaul-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DrawerDescriptionProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -2,7 +2,6 @@
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
@@ -2,7 +2,6 @@
import type { HtmlHTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{
class?: HtmlHTMLAttributes['class']
}>()
@@ -5,7 +5,6 @@ import { DrawerOverlay } from 'vaul-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DialogOverlayProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { DrawerTitle } from 'vaul-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DrawerTitleProps & { class?: HtmlHTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -11,7 +11,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
@@ -10,7 +10,6 @@ import {
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
{
@@ -5,7 +5,6 @@ import { DropdownMenuItem, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = computed(() => {
@@ -5,7 +5,6 @@ import { DropdownMenuLabel, useForwardProps } from 'radix-vue'
import { computed } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
const delegatedProps = computed(() => {

Some files were not shown because too many files have changed in this diff Show More