first commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
max_line_length = 80
|
||||
@@ -0,0 +1,10 @@
|
||||
NUXT_OAUTH_GITHUB_CLIENT_ID=""
|
||||
NUXT_OAUTH_GITHUB_CLIENT_SECRET=""
|
||||
NUXT_SESSION_PASSWORD=""
|
||||
|
||||
KEYCLOAK_ID="coba-pendaftaran"
|
||||
KEYCLOAK_SECRET="32HslhZ8Hn97SsbxcmowhXvmNZ9cPGNE"
|
||||
KEYCLOAK_ISSUER="https://auth.rssa.top/realms/sandbox"
|
||||
|
||||
#MONGODB_URL="mongodb://admin:stim*rs54@10.10.123.206:27017/?retryWrites=true&loadBalanced=false&serverSelectionTimeoutMS=5000&connectTimeoutMS=10000&authSource=admin&authMechanism=SCRAM-SHA-1"
|
||||
MONGODB_URL="mongodb://localhost:27017/simrs_fhir"
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Node modules
|
||||
node_modules/
|
||||
.nuxt/
|
||||
dist/
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Build output
|
||||
*.tgz
|
||||
*.zip
|
||||
|
||||
# IDE and editor specific files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
@@ -0,0 +1 @@
|
||||
pnpm-lock.yaml
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023-PRESENT Yue JIN <https://github.com/kingyue737>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,146 @@
|
||||
<p align="center">
|
||||
<img alt="Vitify - Opinionated Vuetify Admin Starter Template" src="public/vitify-nuxt.svg" width=100px/>
|
||||
</p>
|
||||
<h1 align="center">Vitify Nuxt</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/vuejs/vue">
|
||||
<img src="https://img.shields.io/badge/nuxt-3-brightgreen.svg" alt="vue">
|
||||
</a>
|
||||
<a href="https://github.com/vuetifyjs/vuetify">
|
||||
<img src="https://img.shields.io/badge/vuetify-3-blue.svg" alt="vuetify">
|
||||
</a>
|
||||
<a href="https://github.com/kingyue737/vitify-admin/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align='center'>
|
||||
<b>Vuetify</b> + <b>Nuxt</b>, Opinionated Admin Starter Template<br><br>
|
||||
</p>
|
||||
|
||||
<p align='center'>
|
||||
<a href="https://vitify-nuxt.netlify.app/">Live Demo<br><br></a>
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
- 💚 [Nuxt](https://nuxt.com/) - SPA, ESR, File-based routing, components auto importing, modules, etc
|
||||
|
||||
- 💥 SSR out of the box - powered by [Vuetify Nuxt module](https://github.com/vuetifyjs/nuxt-module)
|
||||
|
||||
- ⚡️ [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [ESBuild](https://github.com/evanw/esbuild) - born with fastness
|
||||
|
||||
- 🍍 [State Management via Pinia](https://pinia.vuejs.org/)
|
||||
|
||||
- 📥 APIs auto importing - for Composition API, VueUse and custom composables
|
||||
|
||||
- ☁️ Deploy on [Netlify](https://www.netlify.com/), zero-config
|
||||
|
||||
- 🦾 TypeScript 100%
|
||||
|
||||
- 🧪 Unit, Component and E2E Testing with [@nuxt/test-utils](https://github.com/nuxt/test-utils)
|
||||
|
||||
<br>
|
||||
|
||||
### Admin Starter Template
|
||||
|
||||
- 🪟 Default layout with drawer, header and footer
|
||||
|
||||
- 🧭 Auto-generated navigation drawer and breadcrumbs based on routes
|
||||
|
||||
- 🔔 Notification store
|
||||
|
||||
- 📉 Data visualization with [nuxt-echarts](https://github.com/kingyue737/nuxt-echarts)
|
||||
|
||||
- 🎨 Theme color customization and dark mode
|
||||
|
||||
- 📱 Responsive layout
|
||||
|
||||
- 🛡️ Authentication backed-in using [nuxt-auth-utils](https://github.com/Atinux/nuxt-auth-utils)
|
||||
|
||||
## Variants
|
||||
|
||||
- [vitify-next](https://github.com/kingyue737/vitify-next) - Lightweight Vue 3 version without Nuxt
|
||||
|
||||
- [vitify-electron](https://github.com/kingyue737/vitify-electron) - Vuetify + Nuxt + Electron starter
|
||||
- [vitify-admin](https://github.com/kingyue737/vitify-admin) - Vue 2.7 with i18n, browser compatibility and mock server
|
||||
|
||||
## Pre-packed
|
||||
|
||||
### Nuxt Modules
|
||||
|
||||
- [Vuetify Nuxt Module](https://github.com/vuetifyjs/nuxt-module) - Zero-config Nuxt Module for Vuetify
|
||||
- [VueUse](https://github.com/vueuse/vueuse) - Collection of useful composition APIs
|
||||
- [Pinia](https://github.com/vuejs/pinia) - Intuitive, type-safe, light and flexible Store for Vue
|
||||
- [Nuxt Icon](https://github.com/nuxt/icon) - Icon module for Nuxt with 200,000+ ready to use icons from Iconify
|
||||
- [Nuxt ECharts](https://github.com/kingyue737/nuxt-echarts) - Nuxt module for Apache ECharts™
|
||||
- [Nuxt Auth Utils](https://github.com/Atinux/nuxt-auth-utils) - Minimalist Authentication module for Nuxt
|
||||
|
||||
### Coding Style
|
||||
|
||||
- [Prettier](https://prettier.io/), single quotes, no semi
|
||||
- [ESLint flat config](https://eslint.org/docs/latest/use/configure/configuration-files-new) with adapted [@nuxt/eslint](https://github.com/nuxt/eslint), future-proof
|
||||
|
||||
### Dev tools
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [pnpm](https://pnpm.js.org/) - Fast, disk space efficient package manager
|
||||
- [Netlify](https://www.netlify.com/) - zero-config deployment
|
||||
- [VS Code Extensions](./.vscode/extensions.json)
|
||||
- [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - TypeScript support inside Vue SFCs
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Find and fix problems in your code
|
||||
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) - Code formatter
|
||||
- [EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig)
|
||||
|
||||
## Try it now!
|
||||
|
||||
### GitHub Template
|
||||
|
||||
[Create a repo from this template on GitHub](https://github.com/kingyue737/vitify-nuxt/generate).
|
||||
|
||||
### Clone to local
|
||||
|
||||
If you prefer to do it manually with the cleaner git history
|
||||
|
||||
```bash
|
||||
npx degit kingyue737/vitify-nuxt my-vitify-app
|
||||
cd my-vitify-app
|
||||
pnpm i
|
||||
```
|
||||
|
||||
### Authentication Setup
|
||||
|
||||
> You can switch to any [OAuth Providers](https://github.com/Atinux/nuxt-auth-utils#supported-oauth-providers) supported by [Nuxt Auth Utils](https://github.com/Atinux/nuxt-auth-utils) or write your own.
|
||||
|
||||
Create a [GitHub OAuth Application](https://github.com/settings/applications/new) with:
|
||||
|
||||
- Homepage url: `http://localhost:3000`
|
||||
- Callback url: `http://localhost:3000/api/auth/github`
|
||||
|
||||
Add the variables in the `.env` file:
|
||||
|
||||
```bash
|
||||
NUXT_OAUTH_GITHUB_CLIENT_ID="my-github-oauth-app-id"
|
||||
NUXT_OAUTH_GITHUB_CLIENT_SECRET="my-github-oauth-app-secret"
|
||||
```
|
||||
|
||||
To create sealed sessions, you also need to add `NUXT_SESSION_SECRET` in the `.env` with at least 32 characters:
|
||||
|
||||
```bash
|
||||
NUXT_SESSION_SECRET=your-super-long-secret-for-session-encryption
|
||||
```
|
||||
|
||||
Nuxt Auth Utils generates one for you when running Nuxt in development the first time if no `NUXT_SESSION_PASSWORD` is set.
|
||||
|
||||
### Development
|
||||
|
||||
Start the development server on http://localhost:3000
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](./LICENSE)
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
const theme = useTheme()
|
||||
provide(
|
||||
THEME_KEY,
|
||||
computed(() => (theme.current.value.dark ? 'dark' : undefined)),
|
||||
)
|
||||
const route = useRoute()
|
||||
const title = computed(() => {
|
||||
return route.meta?.title || route.matched[0].meta?.title || ''
|
||||
})
|
||||
useHead({
|
||||
title,
|
||||
titleTemplate: (t) => (t ? `${t} | Vitify Admin` : 'Vitify Admin'),
|
||||
htmlAttrs: { lang: 'en' },
|
||||
link: [{ rel: 'icon', href: '/favicon.ico' }],
|
||||
})
|
||||
useSeoMeta({
|
||||
viewport: 'width=device-width, initial-scale=1, maximum-scale=1',
|
||||
description: 'Vuetify 3 + Nuxt 3, Opinionated Admin Starter Template',
|
||||
ogImage: '/social-image.png',
|
||||
twitterImage: '/social-image.png',
|
||||
twitterCard: 'summary_large_image',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* replace padding with margin to limit scrollbar in v-main */
|
||||
.v-main {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-top: var(--v-layout-top);
|
||||
margin-bottom: var(--v-layout-bottom);
|
||||
height: calc(100vh - var(--v-layout-top) - var(--v-layout-bottom));
|
||||
overflow-y: auto;
|
||||
transition-property: padding;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<svg
|
||||
class="nuxt-spa-loading"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 37 37"
|
||||
fill="none"
|
||||
width="80"
|
||||
>
|
||||
<g transform="rotate(180 18.778 13.7)">
|
||||
<path
|
||||
d="M24.236 22.006h10.742L25.563 5.822l-8.979 14.31a4 4 0 0 1-3.388 1.874H2.978l16-27.713 6 10.392"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.nuxt-spa-loading {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.nuxt-spa-loading > g > path {
|
||||
fill: none;
|
||||
stroke: #248fe4;
|
||||
stroke-width: 4px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
/* Stroke-dasharray property */
|
||||
stroke-dasharray: 128;
|
||||
stroke-dashoffset: 128;
|
||||
animation: nuxt-spa-loading-move 3s linear infinite;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: #121212;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nuxt-spa-loading-move {
|
||||
100% {
|
||||
stroke-dashoffset: -128;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
After Width: | Height: | Size: 945 B |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"value": "ktp",
|
||||
"label": "KTP",
|
||||
"placeholder": "Masukkan 14 Nomor NIK",
|
||||
"regex": "regex:^\\d{14}$"
|
||||
},
|
||||
{
|
||||
"value": "nip",
|
||||
"label": "NIP",
|
||||
"placeholder": "Masukkan Nomor NIP",
|
||||
"regex": "regex:^\\d{10}$"
|
||||
},
|
||||
{
|
||||
"value": "niptt",
|
||||
"label": "NIPTT",
|
||||
"placeholder": "Masukkan NIPTT",
|
||||
"regex": "regex:^\\d{13}$"
|
||||
},
|
||||
{
|
||||
"value": "sip",
|
||||
"label": "SIP",
|
||||
"placeholder": "Masukkan Nomor Surat Ijin Praktek",
|
||||
"regex": "regex:^\\d{2}$"
|
||||
},
|
||||
{
|
||||
"value": "jkn",
|
||||
"label": "JKN",
|
||||
"placeholder": "Masukkan Nomor Surat Ijin Praktek",
|
||||
"regex": "regex:^\\d{10}$"
|
||||
},
|
||||
{
|
||||
"value": "no_rm",
|
||||
"label": "NO RM",
|
||||
"placeholder": "Masukkan Nomor Surat Ijin Praktek",
|
||||
"regex": "regex:^\\d{2}$"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 56 56">
|
||||
<defs>
|
||||
<path id="inner-pie" d="M10,26.4A19.5,19.5,0,0,0,26.4,10H10V26.4Z" />
|
||||
<path id="outer-ring" d="M2,26.7A26.7,26.7,0,0,1,26.7,2v-2A28,28,0,0,0,0,26.7h2Z" />
|
||||
<radialGradient id="radial-gradient" cx="28" cy="28" r="26.2" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.269" stop-color="currentColor" stop-opacity="0" />
|
||||
<stop offset="0.567" stop-color="currentColor" stop-opacity="0.8" />
|
||||
<stop offset="0.857" stop-color="currentColor" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<g fill="url(#radial-gradient)">
|
||||
<use xlink:href="#inner-pie" />
|
||||
<use xlink:href="#inner-pie" transform="rotate(90, 28, 28)" />
|
||||
<use xlink:href="#inner-pie" transform="rotate(180, 28, 28)" />
|
||||
<use xlink:href="#inner-pie" transform="rotate(270, 28, 28)" />
|
||||
</g>
|
||||
<g filter="brightness(0.8)">
|
||||
<use xlink:href="#outer-ring" />
|
||||
<use xlink:href="#outer-ring" transform="rotate(90, 28, 28)" />
|
||||
<use xlink:href="#outer-ring" transform="rotate(180, 28, 28)" />
|
||||
<use xlink:href="#outer-ring" transform="rotate(270, 28, 28)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 37 37" fill="none">
|
||||
<g transform="rotate(180 18.778 13.7)">
|
||||
<path stroke="currentColor" fill="none" stroke-width="4px" stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M24.236 22.006h10.742L25.563 5.822l-8.979 14.31a4 4 0 0 1-3.388 1.874H2.978l16-27.713 6 10.392" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
@@ -0,0 +1,64 @@
|
||||
html {
|
||||
overflow-y: auto;
|
||||
&.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
.v-card--variant-elevated:not(.v-card--flat) {
|
||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.v-theme--light {
|
||||
&.v-icon--clickable,
|
||||
&.v-btn--icon {
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
--v-border-opacity: 0.09 !important;
|
||||
--v-theme-background: 243, 243, 243 !important;
|
||||
}
|
||||
|
||||
.v-app-bar,
|
||||
.v-navigation-drawer,
|
||||
.v-footer {
|
||||
background-color: rgb(var(--v-theme-background)) !important;
|
||||
}
|
||||
|
||||
.v-overlay__content > .v-card {
|
||||
overflow-y: overlay !important;
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
text-transform: initial !important;
|
||||
}
|
||||
|
||||
.v-data-table-footer {
|
||||
.v-field__input {
|
||||
min-height: 36px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.v-tab {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.v-list-item-title {
|
||||
text-overflow: unset !important;
|
||||
}
|
||||
|
||||
.v-breadcrumbs-divider {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.v-field--focused {
|
||||
.v-field__prepend-inner .v-icon {
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
}
|
||||
|
||||
.v-icon .iconify {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@import 'global';
|
||||
@import 'scrollbar';
|
||||
@@ -0,0 +1,21 @@
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(149, 149, 149, 0.4);
|
||||
background-clip: content-box;
|
||||
min-height: 28px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(149, 149, 149, 0.4);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
// See https://github.com/nuxt/nuxt/releases/tag/v3.13.0
|
||||
import type {
|
||||
ComponentCustomOptions as _ComponentCustomOptions,
|
||||
ComponentCustomProperties as _ComponentCustomProperties,
|
||||
} from 'vue'
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface ComponentCustomProperties extends _ComponentCustomProperties {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface ComponentCustomOptions extends _ComponentCustomOptions {}
|
||||
}
|
||||
|
||||
declare module '#app' {
|
||||
interface PageMeta {
|
||||
icon?: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
drawerIndex?: number
|
||||
}
|
||||
}
|
||||
|
||||
declare module '#auth-utils' {
|
||||
interface User {
|
||||
login: string
|
||||
avatar_url: string
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { mergeProps } from 'vue'
|
||||
|
||||
const theme = useTheme()
|
||||
const drawer = useState('drawer')
|
||||
const route = useRoute()
|
||||
const breadcrumbs = computed(() => {
|
||||
return route!.matched
|
||||
.filter((item) => item.meta && item.meta.title)
|
||||
.map((r) => ({
|
||||
title: r.meta.title!,
|
||||
disabled: r.path === route.path || false,
|
||||
to: r.path,
|
||||
}))
|
||||
})
|
||||
const isDark = computed({
|
||||
get() {
|
||||
return theme.global.name.value === 'dark' ? true : false
|
||||
},
|
||||
set(v) {
|
||||
theme.global.name.value = v ? 'dark' : 'light'
|
||||
},
|
||||
})
|
||||
// const { loggedIn, clear, user } = useUserSession()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar flat>
|
||||
<v-app-bar-nav-icon @click="drawer = !drawer" />
|
||||
<v-breadcrumbs :items="breadcrumbs" />
|
||||
<v-spacer />
|
||||
<div id="app-bar" />
|
||||
<v-switch
|
||||
v-model="isDark"
|
||||
color=""
|
||||
hide-details
|
||||
density="compact"
|
||||
inset
|
||||
false-icon="mdi-white-balance-sunny"
|
||||
true-icon="mdi-weather-night"
|
||||
class="opacity-80"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
href="https://github.com/kingyue737/vitify-nuxt"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
target="_blank"
|
||||
>
|
||||
<v-icon size="30" icon="mdi-github" />
|
||||
</v-btn>
|
||||
<v-menu location="bottom">
|
||||
<template #activator="{ props: menu }">
|
||||
<v-tooltip location="bottom">
|
||||
<template #activator="{ props: tooltip }">
|
||||
<v-btn icon v-bind="mergeProps(menu, tooltip)" class="ml-1">
|
||||
<!-- <v-icon v-if="!loggedIn" icon="mdi-account-circle" size="36" /> -->
|
||||
<!-- <v-avatar v-else color="primary" size="36"> -->
|
||||
<!-- <v-img :src="user?.avatar_url" /> -->
|
||||
<!-- </v-avatar> -->
|
||||
</v-btn>
|
||||
</template>
|
||||
<!-- <span>{{ loggedIn ? user!.login : 'User' }}</span> -->
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-list>
|
||||
<!-- <v-list-item
|
||||
v-if="!loggedIn"
|
||||
title="Login"
|
||||
prepend-icon="mdi-github"
|
||||
href="/api/auth/github"
|
||||
/> -->
|
||||
<!-- <v-list-item
|
||||
v-else
|
||||
title="Logout"
|
||||
prepend-icon="mdi-logout"
|
||||
@click="clear"
|
||||
/> -->
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
</template>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const routes = router.getRoutes().filter((r) => r.path.lastIndexOf('/') === 0)
|
||||
const drawerState = useState('drawer', () => true)
|
||||
|
||||
const { mobile, lgAndUp, width } = useDisplay()
|
||||
const drawer = computed({
|
||||
get() {
|
||||
return drawerState.value || !mobile.value
|
||||
},
|
||||
set(val: boolean) {
|
||||
drawerState.value = val
|
||||
},
|
||||
})
|
||||
const rail = computed(() => !drawerState.value && !mobile.value)
|
||||
routes.sort((a, b) => (a.meta?.drawerIndex ?? 99) - (b.meta?.drawerIndex ?? 98))
|
||||
|
||||
drawerState.value = lgAndUp.value && width.value !== 1280
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:expand-on-hover="rail"
|
||||
:rail="rail"
|
||||
floating
|
||||
>
|
||||
<template #prepend>
|
||||
<v-list>
|
||||
<v-list-item class="pa-1">
|
||||
<template #prepend>
|
||||
<v-icon
|
||||
icon="custom:vitify-nuxt"
|
||||
size="x-large"
|
||||
class="drawer-header-icon"
|
||||
color="primary"
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title
|
||||
class="text-h5 font-weight-bold"
|
||||
style="line-height: 2rem"
|
||||
>
|
||||
Vitify <span class="text-primary">Admin</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
<v-list nav density="compact">
|
||||
<AppDrawerItem v-for="route in routes" :key="route.name" :item="route" />
|
||||
</v-list>
|
||||
<v-spacer />
|
||||
<template #append>
|
||||
<v-list-item class="drawer-footer px-0 d-flex flex-column justify-center">
|
||||
<div class="text-caption pt-6 pt-md-0 text-center text-no-wrap">
|
||||
© Copyright 2023
|
||||
<a
|
||||
href="https://github.com/kingyue737"
|
||||
class="font-weight-bold text-primary"
|
||||
target="_blank"
|
||||
>Yue JIN</a
|
||||
>
|
||||
<span> & </span>
|
||||
<a
|
||||
href="https://www.nustarnuclear.com/"
|
||||
class="font-weight-bold text-primary"
|
||||
target="_blank"
|
||||
>NuStar</a
|
||||
>
|
||||
</div>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.v-navigation-drawer {
|
||||
transition-property:
|
||||
box-shadow, transform, visibility, width, height, left, right, top, bottom,
|
||||
border-radius !important;
|
||||
overflow: hidden;
|
||||
&.v-navigation-drawer--rail {
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
&.v-navigation-drawer--is-hovering {
|
||||
border-top-right-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
box-shadow:
|
||||
0px 1px 2px 0px rgb(0 0 0 / 30%),
|
||||
0px 1px 3px 1px rgb(0 0 0 / 15%);
|
||||
}
|
||||
&:not(.v-navigation-drawer--is-hovering) {
|
||||
.drawer-footer {
|
||||
transform: translateX(-160px);
|
||||
}
|
||||
.drawer-header-icon {
|
||||
height: 1em !important;
|
||||
width: 1em !important;
|
||||
}
|
||||
.v-list-group {
|
||||
--list-indent-size: 0px;
|
||||
--prepend-width: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.v-navigation-drawer__content {
|
||||
overflow-y: hidden;
|
||||
@supports (scrollbar-gutter: stable) {
|
||||
scrollbar-gutter: stable;
|
||||
> .v-list--nav {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
overflow-y: overlay;
|
||||
}
|
||||
}
|
||||
.drawer-footer {
|
||||
transition: all 0.2s;
|
||||
min-height: 30px;
|
||||
}
|
||||
.drawer-header-icon {
|
||||
opacity: 1 !important;
|
||||
height: 1.2em !important;
|
||||
width: 1.2em !important;
|
||||
transition: all 0.2s;
|
||||
margin-right: -10px;
|
||||
}
|
||||
.v-list-group {
|
||||
--prepend-width: 10px;
|
||||
}
|
||||
.v-list-item {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const { item } = defineProps<{
|
||||
item: RouteRecordRaw
|
||||
}>()
|
||||
const visibleChildren = computed(() =>
|
||||
item.children
|
||||
?.filter((child) => child.meta?.icon)
|
||||
.sort((a, b) => (a.meta?.drawerIndex ?? 99) - (b.meta?.drawerIndex ?? 98)),
|
||||
)
|
||||
const visibleChildrenNum = computed(() => visibleChildren.value?.length || 0)
|
||||
const isItem = computed(() => !item.children || visibleChildrenNum.value <= 1)
|
||||
const title = toRef(() => item.meta?.title)
|
||||
const icon = toRef(() => item.meta?.icon)
|
||||
// @ts-expect-error unknown type miss match
|
||||
const to = computed<RouteRecordRaw>(() => ({
|
||||
name: item.name || visibleChildren.value?.[0].name,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item
|
||||
v-if="isItem && icon"
|
||||
:to="to"
|
||||
:prepend-icon="icon"
|
||||
active-class="text-primary"
|
||||
:title="title"
|
||||
/>
|
||||
<v-list-group v-else-if="icon" :prepend-icon="icon" color="primary">
|
||||
<template #activator="{ props: vProps }">
|
||||
<v-list-item :title="title" v-bind="vProps" />
|
||||
</template>
|
||||
<AppDrawerItem
|
||||
v-for="child in visibleChildren"
|
||||
:key="child.name"
|
||||
:item="child"
|
||||
/>
|
||||
</v-list-group>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<v-footer app>
|
||||
<v-spacer />
|
||||
<v-defaults-provider
|
||||
:defaults="{ VBtn: { variant: 'text', size: 'x-small' } }"
|
||||
>
|
||||
<AppNotification />
|
||||
<AppSettings />
|
||||
</v-defaults-provider>
|
||||
</v-footer>
|
||||
</template>
|
||||
<style>
|
||||
.v-footer {
|
||||
padding: 0px 10px !important;
|
||||
> .v-btn--icon {
|
||||
.v-icon {
|
||||
height: 1.25em;
|
||||
width: 1.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
const notificationStore = useNotificationStore()
|
||||
const { notifications } = storeToRefs(notificationStore)
|
||||
const notificationsShown = computed(() =>
|
||||
notifications.value.filter((notification) => notification.show).reverse(),
|
||||
)
|
||||
const showAll = ref(false)
|
||||
const timeout = computed(() => (showAll.value ? -1 : 5000))
|
||||
function deleteNotification(id: number) {
|
||||
notificationStore.delNotification(id)
|
||||
}
|
||||
function emptyNotifications() {
|
||||
notificationStore.$reset()
|
||||
}
|
||||
function toggleAll() {
|
||||
showAll.value = !showAll.value
|
||||
notifications.value.forEach((m) => {
|
||||
m.show = showAll.value
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-btn
|
||||
v-tooltip="{ text: 'Notification' }"
|
||||
:icon="notifications.length ? 'mdi-bell-badge-outline' : 'mdi-bell-outline'"
|
||||
:rounded="0"
|
||||
@click="toggleAll"
|
||||
/>
|
||||
<ClientOnly>
|
||||
<teleport to="body">
|
||||
<v-card
|
||||
elevation="6"
|
||||
width="400"
|
||||
class="d-flex flex-column notification-card"
|
||||
:class="{ 'notification-card--open': showAll }"
|
||||
>
|
||||
<v-toolbar flat density="compact">
|
||||
<v-toolbar-title
|
||||
class="font-weight-light text-body-1"
|
||||
:text="
|
||||
notifications.length ? 'Notification' : 'No New Notifications'
|
||||
"
|
||||
/>
|
||||
<v-btn
|
||||
v-tooltip="{ text: 'Clear All Notifications' }"
|
||||
size="small"
|
||||
icon="mdi-bell-remove"
|
||||
@click="emptyNotifications"
|
||||
/>
|
||||
<v-btn
|
||||
v-tooltip="{ text: 'Hide Notifications' }"
|
||||
class="mr-0"
|
||||
size="small"
|
||||
icon="$expand"
|
||||
@click="toggleAll"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<v-slide-y-reverse-transition
|
||||
tag="div"
|
||||
class="d-flex flex-column notification-box"
|
||||
group
|
||||
hide-on-leave
|
||||
>
|
||||
<div
|
||||
v-for="notification in notificationsShown"
|
||||
:key="notification.id"
|
||||
class="notification-item-wrapper"
|
||||
>
|
||||
<AppNotificationItem
|
||||
v-model="notification.show"
|
||||
:notification="notification"
|
||||
:timeout="timeout"
|
||||
class="notification-item"
|
||||
@close="deleteNotification(notification.id)"
|
||||
/>
|
||||
</div>
|
||||
</v-slide-y-reverse-transition>
|
||||
</v-card>
|
||||
</teleport>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notification-item {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
.notification-card {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
right: 15px;
|
||||
bottom: 48px;
|
||||
max-height: 100vh;
|
||||
overflow: visible;
|
||||
visibility: hidden;
|
||||
&.notification-card--open {
|
||||
visibility: visible;
|
||||
overflow: hidden;
|
||||
max-height: calc(100vh - 200px);
|
||||
.notification-box {
|
||||
overflow-y: overlay;
|
||||
pointer-events: auto;
|
||||
.notification-item-wrapper {
|
||||
transition: none !important;
|
||||
.notification-item {
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
border-top: 1px solid #5656563d !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.notification-box {
|
||||
overflow-y: visible;
|
||||
visibility: visible;
|
||||
pointer-events: none;
|
||||
.notification-item {
|
||||
pointer-events: initial;
|
||||
user-select: initial;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import type { Notification } from '~/stores/notification'
|
||||
|
||||
const props = defineProps<{
|
||||
timeout: number
|
||||
notification: Notification
|
||||
}>()
|
||||
const emit = defineEmits(['close'])
|
||||
const isShow = defineModel<boolean>({ default: false })
|
||||
const timeout = toRef(props, 'timeout')
|
||||
const { start, stop } = useTimeoutFn(() => (isShow.value = false), timeout, {
|
||||
immediate: false,
|
||||
})
|
||||
watch(timeout, (v) => (v !== -1 ? start() : stop()), { immediate: true })
|
||||
const variant = computed(() => timeout.value === -1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-alert
|
||||
:border="variant ? 'start' : false"
|
||||
:variant="variant ? 'outlined' : undefined"
|
||||
:density="variant ? 'compact' : undefined"
|
||||
:theme="variant ? undefined : 'dark'"
|
||||
:elevation="variant ? 0 : 3"
|
||||
:type="notification.type"
|
||||
:text="notification.text"
|
||||
:title="notification.time.toLocaleString()"
|
||||
>
|
||||
<template #close>
|
||||
<v-btn icon="$close" @click="emit('close')" />
|
||||
</template>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.v-alert-title) {
|
||||
line-height: 1.25rem;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { mergeProps } from 'vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
const theme = useTheme()
|
||||
const primary = useStorage('theme-primary', '#1697f6')
|
||||
const color = computed({
|
||||
get() {
|
||||
return theme.themes.value.light.colors.primary
|
||||
},
|
||||
set(val: string) {
|
||||
primary.value = val
|
||||
theme.themes.value.light.colors.primary = val
|
||||
theme.themes.value.dark.colors.primary = val
|
||||
},
|
||||
})
|
||||
const colors = [
|
||||
['#1697f6', '#ff9800'],
|
||||
['#4CAF50', '#FF5252'],
|
||||
['#9C27b0', '#E91E63'],
|
||||
['#304156', '#3f51b5'],
|
||||
['#002FA7', '#492d22'],
|
||||
]
|
||||
const menuShow = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-menu
|
||||
v-model="menuShow"
|
||||
:close-on-content-click="false"
|
||||
location="top right"
|
||||
offset="15"
|
||||
>
|
||||
<template #activator="{ props: menu }">
|
||||
<v-tooltip location="top" text="Theme Palette">
|
||||
<template #activator="{ props: tooltip }">
|
||||
<v-btn
|
||||
icon="mdi-palette-outline"
|
||||
v-bind="mergeProps(menu, tooltip)"
|
||||
:rounded="0"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-card width="320">
|
||||
<v-card-text class="text-center">
|
||||
<v-label class="mb-3"> Theme Palette </v-label>
|
||||
<v-color-picker
|
||||
v-model="color"
|
||||
show-swatches
|
||||
elevation="0"
|
||||
width="288"
|
||||
mode="rgb"
|
||||
:modes="['rgb', 'hex', 'hsl']"
|
||||
:swatches="colors"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
const option: ECOption = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'pageA',
|
||||
type: 'bar',
|
||||
stack: 'vistors',
|
||||
barWidth: '60%',
|
||||
data: [79, 52, 200, 334, 390, 330, 220],
|
||||
},
|
||||
{
|
||||
name: 'pageB',
|
||||
type: 'bar',
|
||||
stack: 'vistors',
|
||||
barWidth: '60%',
|
||||
data: [80, 52, 200, 334, 390, 330, 220],
|
||||
},
|
||||
{
|
||||
name: 'pageC',
|
||||
type: 'bar',
|
||||
stack: 'vistors',
|
||||
barWidth: '60%',
|
||||
data: [30, 52, 200, 334, 390, 330, 220],
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-chart :option="option" autoresize />
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
const data = [
|
||||
['2022-06-05', 116],
|
||||
['2022-06-06', 129],
|
||||
['2022-06-07', 135],
|
||||
['2022-06-08', 86],
|
||||
['2022-06-09', 73],
|
||||
['2022-06-10', 85],
|
||||
['2022-06-11', 73],
|
||||
['2022-06-12', 68],
|
||||
['2022-06-13', 92],
|
||||
['2022-06-14', 130],
|
||||
['2022-06-15', 245],
|
||||
['2022-06-16', 139],
|
||||
['2022-06-17', 115],
|
||||
['2022-06-18', 111],
|
||||
['2022-06-19', 309],
|
||||
['2022-06-20', 206],
|
||||
['2022-06-21', 137],
|
||||
['2022-06-22', 128],
|
||||
['2022-06-23', 85],
|
||||
['2022-06-24', 94],
|
||||
['2022-06-25', 71],
|
||||
['2022-06-26', 106],
|
||||
['2022-06-27', 84],
|
||||
['2022-06-28', 93],
|
||||
['2022-06-29', 85],
|
||||
['2022-06-30', 73],
|
||||
['2022-07-01', 83],
|
||||
['2022-07-02', 125],
|
||||
['2022-07-03', 107],
|
||||
['2022-07-04', 82],
|
||||
['2022-07-05', 44],
|
||||
['2022-07-06', 72],
|
||||
['2022-07-07', 106],
|
||||
['2022-07-08', 107],
|
||||
['2022-07-09', 66],
|
||||
['2022-07-10', 91],
|
||||
['2022-07-11', 92],
|
||||
['2022-07-12', 113],
|
||||
['2022-07-13', 107],
|
||||
['2022-07-14', 131],
|
||||
['2022-07-15', 111],
|
||||
['2022-07-16', 64],
|
||||
['2022-07-17', 69],
|
||||
['2022-07-18', 88],
|
||||
['2022-07-19', 77],
|
||||
['2022-07-20', 83],
|
||||
['2022-07-21', 111],
|
||||
['2022-07-22', 57],
|
||||
['2022-07-23', 55],
|
||||
['2022-07-24', 60],
|
||||
]
|
||||
|
||||
const option: ECOption = {
|
||||
backgroundColor: 'transparent',
|
||||
dataset: { source: data },
|
||||
visualMap: {
|
||||
show: false,
|
||||
type: 'continuous',
|
||||
min: 0,
|
||||
max: 400,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
grid: {
|
||||
top: 20,
|
||||
left: '2%',
|
||||
right: '2%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'value',
|
||||
type: 'line',
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
width: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-chart :option="option" autoresize />
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
const option: ECOption = {
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
left: 'center',
|
||||
bottom: '10',
|
||||
data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts'],
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'WEEKLY WRITE ARTICLES',
|
||||
type: 'pie',
|
||||
roseType: 'radius',
|
||||
radius: [15, 95],
|
||||
center: ['50%', '38%'],
|
||||
data: [
|
||||
{ value: 320, name: 'Industries' },
|
||||
{ value: 240, name: 'Technology' },
|
||||
{ value: 149, name: 'Forex' },
|
||||
{ value: 100, name: 'Gold' },
|
||||
{ value: 59, name: 'Forecasts' },
|
||||
],
|
||||
animationEasing: 'cubicInOut',
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-chart :option="option" autoresize />
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
const option: ECOption = {
|
||||
backgroundColor: 'transparent',
|
||||
radar: {
|
||||
radius: '66%',
|
||||
center: ['50%', '42%'],
|
||||
splitNumber: 8,
|
||||
splitArea: {
|
||||
areaStyle: {
|
||||
color: 'rgba(127,95,132,.3)',
|
||||
opacity: 1,
|
||||
shadowBlur: 45,
|
||||
shadowColor: 'rgba(0,0,0,.5)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 15,
|
||||
},
|
||||
},
|
||||
indicator: [
|
||||
{ name: 'Sales' },
|
||||
{ name: 'Administration' },
|
||||
{ name: 'Technology' },
|
||||
{ name: 'Customer Support' },
|
||||
{ name: 'Development' },
|
||||
{ name: 'Marketing' },
|
||||
],
|
||||
},
|
||||
legend: {
|
||||
left: 'center',
|
||||
bottom: '10',
|
||||
data: ['Allocated Budget', 'Expected Spending', 'Actual Spending'],
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'radar',
|
||||
symbolSize: 0,
|
||||
areaStyle: {
|
||||
shadowBlur: 13,
|
||||
shadowColor: 'rgba(0,0,0,.2)',
|
||||
shadowOffsetX: 0,
|
||||
shadowOffsetY: 10,
|
||||
opacity: 1,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: [5000, 7000, 12000, 11000, 15000, 14000],
|
||||
name: 'Allocated Budget',
|
||||
},
|
||||
{
|
||||
value: [4000, 9000, 15000, 15000, 13000, 11000],
|
||||
name: 'Expected Spending',
|
||||
},
|
||||
{
|
||||
value: [5500, 5000, 12000, 15000, 8000, 6000],
|
||||
name: 'Actual Spending',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-chart :option="option" autoresize />
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
const dialog = ref(false)
|
||||
const confirmed = ref(false)
|
||||
let resolve: (value: boolean) => void
|
||||
const message = ref('')
|
||||
watch(dialog, (v) => {
|
||||
if (!v) {
|
||||
resolve(confirmed.value)
|
||||
}
|
||||
})
|
||||
function open(text: string) {
|
||||
confirmed.value = false
|
||||
dialog.value = true
|
||||
message.value = text
|
||||
return new Promise<boolean>((resolveFn) => {
|
||||
resolve = resolveFn
|
||||
})
|
||||
}
|
||||
function confirm() {
|
||||
confirmed.value = true
|
||||
dialog.value = false
|
||||
}
|
||||
function cancel() {
|
||||
confirmed.value = false
|
||||
dialog.value = false
|
||||
}
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="400px">
|
||||
<v-card>
|
||||
<v-card-text class="font-weight-bold d-flex">
|
||||
<v-icon class="mr-2" color="warning" icon="$warning" />
|
||||
<span>{{ message }}</span>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" @click="cancel"> Cancel </v-btn>
|
||||
<v-btn color="primary" @click="confirm"> Confirm </v-btn>
|
||||
<v-spacer />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: 40,
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
default: '',
|
||||
validator: (value) => ['male', 'female', ''].includes(value.toLowerCase()),
|
||||
},
|
||||
profileImageUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
class: {
|
||||
type: [String, Object, Array],
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const avatarSource = computed(() => {
|
||||
if (props.profileImageUrl) {
|
||||
return props.profileImageUrl
|
||||
}
|
||||
|
||||
switch (props.gender.toLowerCase()) {
|
||||
case 'male':
|
||||
return null // Gunakan slot default untuk v-icon
|
||||
case 'female':
|
||||
return null // Gunakan slot default untuk v-icon
|
||||
default:
|
||||
return null // Gunakan slot default untuk v-icon
|
||||
}
|
||||
})
|
||||
|
||||
const avatarIcon = computed(() => {
|
||||
if (!props.profileImageUrl) {
|
||||
switch (props.gender.toLowerCase()) {
|
||||
case 'male':
|
||||
return 'mdi-face-man'
|
||||
case 'female':
|
||||
return 'mdi-face-woman'
|
||||
default:
|
||||
return 'mdi-emoticon-confused'
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-avatar :size="size" color="primary" :class="class">
|
||||
<img v-if="avatarSource" :src="avatarSource" :alt="`Avatar`" />
|
||||
<v-icon
|
||||
v-else-if="avatarIcon"
|
||||
:size="Math.max(Number(size) * 0.66, 20)"
|
||||
color="white"
|
||||
>
|
||||
{{ avatarIcon }}
|
||||
</v-icon>
|
||||
<slot v-else name="fallback">
|
||||
<v-icon :size="Math.max(Number(size) * 0.66, 20)" color="white">
|
||||
mdi-account-circle
|
||||
</v-icon>
|
||||
</slot>
|
||||
</v-avatar>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'mdi-information',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-row align="center" no-gutters>
|
||||
<v-icon small class="mr-2">{{ icon }}</v-icon>
|
||||
<span class="text-body-2">{{ text }}</span>
|
||||
</v-row>
|
||||
</template>
|
||||
@@ -0,0 +1,268 @@
|
||||
<script setup>
|
||||
const delay = ref(null)
|
||||
delay.value = 10
|
||||
|
||||
async function getItemsSearch(query, el$) {
|
||||
const { data } = await useFetch('/api/address?search=' + query, {
|
||||
lazy: true,
|
||||
server: false,
|
||||
})
|
||||
return data.value.map((d) => {
|
||||
return {
|
||||
value: d,
|
||||
label: d.display,
|
||||
}
|
||||
})
|
||||
// const addressesString = data.value.map((value) => {
|
||||
// var result = value.villages ? 'Desa/Kel ' + value.villages + ', ' : ''
|
||||
// result += value.districts ? 'Kec ' + value.districts + ', ' : ''
|
||||
// result += value.cities ? 'Kota/Kab ' + value.cities + ', ' : ''
|
||||
// result += value.states ? 'Prov ' + value.states + ', ' : ''
|
||||
// result += value.countries ? value.countries : ''
|
||||
// return { value: value, label: result }
|
||||
// })
|
||||
// return addressesString
|
||||
}
|
||||
|
||||
async function getPostal(query) {
|
||||
const { data } = await useFetch('/api/address?search=' + query, {
|
||||
lazy: true,
|
||||
server: false,
|
||||
})
|
||||
return data.value
|
||||
}
|
||||
|
||||
function onSelectSearch(option, el$) {
|
||||
const postal = getPostal(option.value.states.regencies.districts)
|
||||
el$.$parent.$parent.children$.country.update(option.value)
|
||||
el$.$parent.$parent.children$.state.update(option.value.states)
|
||||
el$.$parent.$parent.children$.city.update(option.value.states.regencies)
|
||||
el$.$parent.$parent.children$.district.update(
|
||||
option.value.states.regencies.districts,
|
||||
)
|
||||
el$.$parent.$parent.children$.village.update(
|
||||
option.value.states.regencies.districts.villages,
|
||||
)
|
||||
el$.$parent.$parent.children$.postalCode.update(postal[0])
|
||||
}
|
||||
|
||||
function onSelectCountry(option, el$) {
|
||||
el$.$parent.$parent.children$.state.clear()
|
||||
el$.$parent.$parent.children$.city.clear()
|
||||
el$.$parent.$parent.children$.district.clear()
|
||||
el$.$parent.$parent.children$.village.clear()
|
||||
el$.$parent.$parent.children$.postalCode.clear()
|
||||
}
|
||||
|
||||
function onSelectState(option, el$) {
|
||||
el$.$parent.$parent.children$.city.clear()
|
||||
el$.$parent.$parent.children$.district.clear()
|
||||
el$.$parent.$parent.children$.village.clear()
|
||||
el$.$parent.$parent.children$.postalCode.clear()
|
||||
}
|
||||
|
||||
function onSelectCity(option, el$) {
|
||||
el$.$parent.$parent.children$.district.clear()
|
||||
el$.$parent.$parent.children$.village.clear()
|
||||
el$.$parent.$parent.children$.postalCode.clear()
|
||||
}
|
||||
|
||||
function onSelectDistrict(option, el$) {
|
||||
el$.$parent.$parent.children$.village.clear()
|
||||
el$.$parent.$parent.children$.postalCode.clear()
|
||||
console.log(el$)
|
||||
}
|
||||
|
||||
function onSelectVillage(option, el$) {
|
||||
el$.$parent.$parent.children$.postalCode.clear()
|
||||
}
|
||||
|
||||
async function onChange(input, el$) {
|
||||
delay.value = input.length > 0 ? 5000 : 10
|
||||
}
|
||||
|
||||
function formatData(name, value) {
|
||||
return { [name]: value.name }
|
||||
}
|
||||
function formatLine(name, value) {
|
||||
return { [name]: value.split('\n') }
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ListElement name="address">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement
|
||||
:name="index"
|
||||
:columns="{
|
||||
sm: {
|
||||
container: 12,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<TextareaElement
|
||||
name="line"
|
||||
placeholder="Alamat"
|
||||
:format-data="formatLine"
|
||||
/>
|
||||
<SelectElement
|
||||
name="search"
|
||||
@select="onSelectSearch"
|
||||
:search="true"
|
||||
:native="false"
|
||||
input-type="search"
|
||||
autocomplete="off"
|
||||
placeholder="🔎 Cari Alamat"
|
||||
:resolve-on-load="false"
|
||||
:delay="20"
|
||||
:items="getItemsSearch"
|
||||
:submit="false"
|
||||
no-results-text="Alamat Tidak Ditemukan"
|
||||
:object="true"
|
||||
:filter-results="false"
|
||||
:caret="false"
|
||||
/>
|
||||
<SelectElement
|
||||
name="country"
|
||||
allow-absent
|
||||
@select="onSelectCountry"
|
||||
items="/api/address/countries"
|
||||
:format-data="formatData"
|
||||
:search="true"
|
||||
:native="false"
|
||||
:object="true"
|
||||
:resolve-on-load="true"
|
||||
autocomplete="on"
|
||||
value-prop="_id"
|
||||
label-prop="name"
|
||||
input-type="search"
|
||||
placeholder="Negara"
|
||||
:conditions="[
|
||||
[
|
||||
['address.*.line', 'not_empty'],
|
||||
['address.*.search', 'not_empty'],
|
||||
],
|
||||
]"
|
||||
:columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<SelectElement
|
||||
name="state"
|
||||
allow-absent
|
||||
@select="onSelectState"
|
||||
@search-change="onChange"
|
||||
items="/api/address/states?parent={address.*.country}"
|
||||
:format-data="formatData"
|
||||
:search="true"
|
||||
:native="false"
|
||||
:object="true"
|
||||
:delay="delay"
|
||||
:resolve-on-load="false"
|
||||
autocomplete="on"
|
||||
value-prop="_id"
|
||||
label-prop="name"
|
||||
input-type="search"
|
||||
placeholder="Provinsi"
|
||||
:conditions="[['address.*.country', 'not_empty']]"
|
||||
:columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<SelectElement
|
||||
name="city"
|
||||
allow-absent
|
||||
@select="onSelectCity"
|
||||
@search-change="onChange"
|
||||
items="/api/address/cities?parent={address.*.state}"
|
||||
:format-data="formatData"
|
||||
:search="true"
|
||||
:native="false"
|
||||
:object="true"
|
||||
:delay="delay"
|
||||
:resolve-on-load="false"
|
||||
autocomplete="on"
|
||||
value-prop="_id"
|
||||
label-prop="name"
|
||||
input-type="search"
|
||||
placeholder="Kota / Kabupaten"
|
||||
:conditions="[['address.*.state', 'not_empty']]"
|
||||
:columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<SelectElement
|
||||
name="district"
|
||||
allow-absent
|
||||
@select="onSelectDistrict"
|
||||
@search-change="onChange"
|
||||
items="/api/address/districts?parent={address.*.city}"
|
||||
:format-data="formatData"
|
||||
:search="true"
|
||||
:native="false"
|
||||
:object="true"
|
||||
:delay="delay"
|
||||
:resolve-on-load="false"
|
||||
autocomplete="on"
|
||||
value-prop="_id"
|
||||
label-prop="name"
|
||||
input-type="search"
|
||||
placeholder="Kecamatan"
|
||||
:conditions="[['address.*.city', 'not_empty']]"
|
||||
:columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<SelectElement
|
||||
name="village"
|
||||
allow-absent
|
||||
@select="onSelectVillage"
|
||||
@search-change="onChange"
|
||||
items="/api/address/villages?parent={address.*.district}"
|
||||
:format-data="formatData"
|
||||
:search="true"
|
||||
:native="false"
|
||||
:object="true"
|
||||
:delay="delay"
|
||||
:resolve-on-load="false"
|
||||
autocomplete="on"
|
||||
value-prop="_id"
|
||||
label-prop="name"
|
||||
input-type="search"
|
||||
placeholder="Desa / Kelurahan"
|
||||
:conditions="[['address.*.district', 'not_empty']]"
|
||||
:columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
<SelectElement
|
||||
name="postalCode"
|
||||
allow-absent
|
||||
@search-change="onChange"
|
||||
items="/api/address/postal?parent={address.*.district}"
|
||||
:native="false"
|
||||
:object="false"
|
||||
:delay="delay"
|
||||
:resolve-on-load="false"
|
||||
autocomplete="off"
|
||||
placeholder="Kode Pos"
|
||||
:conditions="[['address.*.district', 'not_empty']]"
|
||||
:columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
}"
|
||||
/>
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<ListElement name="communication">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<GroupElement name="container2_2">
|
||||
<GroupElement name="column1" :columns="{
|
||||
container: 1,
|
||||
}">
|
||||
<ToggleElement name="preferred" :labels="{
|
||||
on: 'Aktif',
|
||||
off: 'Pasif',
|
||||
}" :default="true" size="lg" align="right" />
|
||||
</GroupElement>
|
||||
<GroupElement name="column2" :columns="{
|
||||
container: 11,
|
||||
}">
|
||||
<ListElement name="language" :controls="{
|
||||
add: false,
|
||||
remove: false,
|
||||
}">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<SelectElement name="text" :items="[
|
||||
{
|
||||
value: 'Indonesia',
|
||||
label: 'Indonesia',
|
||||
},
|
||||
{
|
||||
value: 'Inggris',
|
||||
label: 'Inggris',
|
||||
},
|
||||
{
|
||||
value: 'jawa',
|
||||
label: 'Jawa',
|
||||
},
|
||||
{
|
||||
value: 'Sunda',
|
||||
label: 'Sunda',
|
||||
},
|
||||
]" :search="true" :native="false" input-type="search" autocomplete="off" placeholder="Bahasa"
|
||||
:create="true" :append-new-option="false" />
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
</GroupElement>
|
||||
</GroupElement>
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,77 @@
|
||||
<script setup>
|
||||
const parsedName = ref({})
|
||||
const onChange = (newValue, oldValue, el$) => {
|
||||
const i = parseInt(el$.$parent.$parent.path.split('.')[1])
|
||||
parsedName.value[i] = parseName(el$.$parent.$parent.children$.text.value)
|
||||
el$.$parent.$parent.children$.prefix.update(parsedName.value[i].prefix)
|
||||
el$.$parent.$parent.children$.given.update(parsedName.value[i].given)
|
||||
el$.$parent.$parent.children$.family.update(parsedName.value[i].family)
|
||||
el$.$parent.$parent.children$.suffix.update(parsedName.value[i].suffix)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ListElement
|
||||
name="name"
|
||||
:controls="{
|
||||
add: true,
|
||||
remove: false,
|
||||
}"
|
||||
:rules="['min:1']"
|
||||
:min="1"
|
||||
:max="1"
|
||||
:initial="1"
|
||||
>
|
||||
<template #default="{ index }">
|
||||
<ObjectElement
|
||||
:name="index"
|
||||
:columns="{
|
||||
sm: {
|
||||
container: 12,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<TextElement
|
||||
@change="onChange"
|
||||
name="text"
|
||||
placeholder="Nama Lengkap"
|
||||
/>
|
||||
<HiddenElement name="prefix" :meta="true" />
|
||||
<HiddenElement name="given" :meta="true" />
|
||||
<HiddenElement name="family" :meta="true" />
|
||||
<HiddenElement name="suffix" :meta="true" />
|
||||
<!-- <FormLibParsedName :name="parsedName" :path="'address.0'" /> -->
|
||||
<StaticElement name="parsed-name" size="m">
|
||||
<div v-if="parsedName[index]" class="d-flex flex-row">
|
||||
<v-chip
|
||||
v-for="(prefix, index) in parsedName[index].prefix"
|
||||
:key="`prefix-${index}`"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ prefix }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-for="(given, index) in parsedName[index].given"
|
||||
:key="`given-${index}`"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue-darken-3"
|
||||
>{{ given }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-show="parsedName[index].family"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue"
|
||||
>{{ parsedName[index].family }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-for="(suffix, index) in parsedName[index].suffix"
|
||||
:key="`suffix-${index}`"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ suffix }}</v-chip
|
||||
>
|
||||
</div>
|
||||
</StaticElement>
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import items from '~/assets/data/identifier/practitioner.json'
|
||||
|
||||
function onChange(oldValue, newValue, el$) {
|
||||
el$.$parent.$parent.children$.value.update(el$.value)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ListElement name="identifier" :rules="['min:1']">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<GroupElement name="type">
|
||||
<SelectElement name="name" :search="true" :items="items" data-key="option" :native="false" default="ktp"
|
||||
:columns="{
|
||||
container: 3,
|
||||
}" :can-clear="false" :can-deselect="false" />
|
||||
|
||||
<TextElement v-for="item in items" :conditions="[['identifier.*.type.name', item.value]]" name="value_element"
|
||||
@change="onChange" :rules="['nullable', item.regex]" :messages="{
|
||||
regex: 'Format Nomor ID salah!',
|
||||
}" :placeholder="item.placeholder" :columns="{
|
||||
container: 9,
|
||||
}" :submit="false" />
|
||||
|
||||
<HiddenElement name="value" :meta="true" />
|
||||
</GroupElement>
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup>
|
||||
// Define props with validation
|
||||
const props = defineProps({
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (value) => {
|
||||
// Basic validation to ensure data has a name property
|
||||
return value && value.name
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StaticElement name="parsed-name" size="m">
|
||||
<div v-show="name" class="d-flex flex-row">
|
||||
<span> {{ index }}</span>
|
||||
<v-chip
|
||||
v-for="(prefix, index) in name.value[path].prefix"
|
||||
:key="`prefix-${index}`"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ prefix }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-for="(given, index) in name.value[path].given"
|
||||
:key="`given-${index}`"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue-darken-3"
|
||||
>{{ given }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-show="name.value[path].family"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue"
|
||||
>{{ name.family }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-for="(suffix, index) in name.value[path].suffix"
|
||||
:key="`suffix-${index}`"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ suffix }}</v-chip
|
||||
>
|
||||
</div>
|
||||
</StaticElement>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<ListElement name="telecom">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<SelectElement name="system" :items="[
|
||||
{
|
||||
value: 'phone',
|
||||
label: '📞',
|
||||
},
|
||||
{
|
||||
value: 'email',
|
||||
label: '📧',
|
||||
},
|
||||
{
|
||||
value: 'url',
|
||||
label: '🌐',
|
||||
},
|
||||
]" :search="true" :native="false" input-type="search" autocomplete="off" :can-deselect="false"
|
||||
:can-clear="false" default="phone" :columns="{
|
||||
default: {
|
||||
container: 2,
|
||||
},
|
||||
sm: {
|
||||
container: 1,
|
||||
},
|
||||
}" :caret="false" />
|
||||
<SelectElement name="use" :items="[
|
||||
{
|
||||
value: 'home',
|
||||
label: '🏠',
|
||||
},
|
||||
{
|
||||
value: 'mobile',
|
||||
label: '📱',
|
||||
},
|
||||
{
|
||||
value: 'work',
|
||||
label: '🏢',
|
||||
},
|
||||
]" :search="true" :native="false" input-type="search" autocomplete="off" :columns="{
|
||||
default: {
|
||||
container: 2,
|
||||
},
|
||||
sm: {
|
||||
container: 1,
|
||||
},
|
||||
}" :caret="false" :can-deselect="false" :can-clear="false" default="home" />
|
||||
<GroupElement name="value" :columns="{
|
||||
default: {
|
||||
container: 8,
|
||||
},
|
||||
sm: {
|
||||
container: 10,
|
||||
},
|
||||
}">
|
||||
<PhoneElement name="phone" :allow-incomplete="true" :unmask="true" default="+62"
|
||||
:conditions="[['telecom.*.system', 'in', ['phone']]]" />
|
||||
<TextElement name="email" input-type="email" :rules="['nullable', 'email']" placeholder="eg. example@mail.com"
|
||||
:conditions="[['telecom.*.system', 'in', ['email']]]" />
|
||||
<TextElement name="url" input-type="url" :rules="['nullable', 'url']" placeholder="eg. http(s)://domain.com"
|
||||
:floating="false" :conditions="[['telecom.*.system', 'in', ['url']]]" />
|
||||
</GroupElement>
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
definePageMeta({
|
||||
title: 'Vue Form',
|
||||
icon: 'mdi-checkbox-blank-off-outline',
|
||||
drawerIndex: 0,
|
||||
})
|
||||
|
||||
const data = ref({})
|
||||
const humanName = ref({})
|
||||
const onChange = () => {
|
||||
humanName.value = parseName(data.value.nama)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ma-4">
|
||||
<Vueform v-model="data" @change="onChange" sync>
|
||||
<TextElement
|
||||
name="nama"
|
||||
placeholder="Nama Lengkap"
|
||||
:rules="['required', 'min:3', 'max:100']"
|
||||
/>
|
||||
<StaticElement name="parsed-name" size="sm">
|
||||
<div class="d-flex flex-row">
|
||||
<v-chip
|
||||
v-for="prefix in humanName.prefix"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ prefix }}</v-chip
|
||||
><v-chip
|
||||
v-for="given in humanName.given"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue-darken-3"
|
||||
>{{ given }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-show="humanName.family"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue"
|
||||
>{{ humanName.family }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-for="suffix in humanName.suffix"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ suffix }}</v-chip
|
||||
>
|
||||
</div>
|
||||
</StaticElement>
|
||||
<RadiogroupElement
|
||||
name="gender"
|
||||
view="tabs"
|
||||
label="Jenis Kelamin"
|
||||
:items="[
|
||||
{
|
||||
value: 'unknown',
|
||||
label: '⭕',
|
||||
},
|
||||
{
|
||||
value: 'male',
|
||||
label: '♂️ Laki-laki',
|
||||
},
|
||||
{
|
||||
value: 'female',
|
||||
label: '♀️ Perempuan',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
label: '⚧️ Lainnya',
|
||||
},
|
||||
]"
|
||||
default="unknown"
|
||||
/>
|
||||
</Vueform>
|
||||
</div>
|
||||
<span>{{ data }}</span>
|
||||
<span>{{ humanName }}</span>
|
||||
</template>
|
||||
@@ -0,0 +1,222 @@
|
||||
<script>
|
||||
import { FormLibAddress, FormLibHumanName, FormLibTelecom } from '#components'
|
||||
import Identifier from '../Lib/Identifier.vue'
|
||||
|
||||
const data = ref('')
|
||||
const handleResponse = (response, form$) => {
|
||||
console.log(response) // axios response
|
||||
console.log(response.status) // HTTP status code
|
||||
console.log(response.data) // response data
|
||||
|
||||
console.log(form$) // <Vueform> instance
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Vueform v-model="data" validate-on="change|step" endpoint="/api/patient/create" method="post"
|
||||
@response="handleResponse">
|
||||
<StaticElement name="identifierTitle" tag="h2" content="Nomor Identitas" />
|
||||
<FormLibIdentifier />
|
||||
<StaticElement name="divider" tag="hr" />
|
||||
<StaticElement name="personalInfoTitle" tag="h2" content="Personal Info" />
|
||||
<FormLibHumanName />
|
||||
<!-- <ListElement name="nestedList" :controls="{
|
||||
add: false,
|
||||
remove: false,
|
||||
}">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<SelectElement name="humanNameUse" :items="[
|
||||
{
|
||||
value: 'official',
|
||||
label: '👨💼 Resmi',
|
||||
},
|
||||
{
|
||||
value: 'usual',
|
||||
label: '👨🦱 Biasa',
|
||||
},
|
||||
{
|
||||
value: 'temp',
|
||||
label: '🦲 Sementara',
|
||||
},
|
||||
// {
|
||||
// value: 'nickname',
|
||||
// label: '🙋♂️ Panggilan',
|
||||
// },
|
||||
// {
|
||||
// value: 'anonymous',
|
||||
// label: '👤 Anonim',
|
||||
// },
|
||||
// {
|
||||
// value: 'old',
|
||||
// label: '💇♂️ Nama Lama',
|
||||
// },
|
||||
// {
|
||||
// value: 'maiden',
|
||||
// label: '👧 Nama Gadis',
|
||||
// },
|
||||
]" :columns="{
|
||||
default: {
|
||||
container: 5,
|
||||
},
|
||||
sm: {
|
||||
container: 3,
|
||||
},
|
||||
lg: {
|
||||
container: 2,
|
||||
},
|
||||
}" :rules="['required']" :native="false" :can-deselect="false" :can-clear="false" :close-on-select="false"
|
||||
:caret="false" default="official" />
|
||||
<TextElement name="text" :columns="{
|
||||
default: {
|
||||
container: 7,
|
||||
},
|
||||
sm: {
|
||||
container: 9,
|
||||
},
|
||||
lg: {
|
||||
container: 10,
|
||||
},
|
||||
}" placeholder="Nama Lengkap" />
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement> -->
|
||||
<GroupElement name="container2">
|
||||
<GroupElement name="column1" :columns="{
|
||||
container: 6,
|
||||
}">
|
||||
<RadiogroupElement name="gender" view="tabs" label="Jenis Kelamin" :items="[
|
||||
{
|
||||
value: 'male',
|
||||
label: 'Laki-laki',
|
||||
},
|
||||
{
|
||||
value: 'female',
|
||||
label: 'Perempuan',
|
||||
},
|
||||
// {
|
||||
// value: 'other',
|
||||
// label: 'Lainnya',
|
||||
// },
|
||||
// {
|
||||
// value: 'unknown',
|
||||
// label: ' ',
|
||||
// },
|
||||
]" default="unknown" />
|
||||
</GroupElement>
|
||||
<GroupElement name="column_2" :columns="{
|
||||
container: 6,
|
||||
}">
|
||||
<ListElement name="maritalStatus" :controls="{
|
||||
add: false,
|
||||
remove: false,
|
||||
}">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<ListElement name="coding" :controls="{
|
||||
add: false,
|
||||
remove: false,
|
||||
}">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<SelectElement name="select" :items="[
|
||||
{
|
||||
value: 'UNK',
|
||||
label: 'Tidak Tahu',
|
||||
},
|
||||
{
|
||||
value: 'U',
|
||||
label: 'Belum Menikah',
|
||||
},
|
||||
{
|
||||
value: 'M',
|
||||
label: 'Menikah',
|
||||
},
|
||||
{
|
||||
value: 'D',
|
||||
label: 'Cerai Hidup',
|
||||
},
|
||||
{
|
||||
value: 'W',
|
||||
label: 'Cerai Mati',
|
||||
},
|
||||
]" :search="true" :native="false" label="Status Perkawinan" input-type="search" autocomplete="off"
|
||||
:can-deselect="false" :can-clear="false" default="UNK" />
|
||||
<HiddenElement name="system" default="http://terminology.hl7.org/CodeSystem/v3-MaritalStatus" />
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
</GroupElement>
|
||||
</GroupElement>
|
||||
<SelectElement name="birthPlace" :search="true" :native="false" input-type="search" autocomplete="on" :items="[
|
||||
{
|
||||
value: 'malang',
|
||||
label: 'Malang',
|
||||
},
|
||||
{
|
||||
value: 'surabaya',
|
||||
label: 'Surabaya',
|
||||
},
|
||||
]" placeholder="Tempat Lahir" :columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
lg: {
|
||||
container: 6,
|
||||
},
|
||||
}" />
|
||||
<DateElement name="birthDate" placeholder="Tanggal Lahir" :columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
lg: {
|
||||
container: 6,
|
||||
},
|
||||
}" />
|
||||
<StaticElement name="divider_1" tag="hr" />
|
||||
<StaticElement name="addressTitle" tag="h2" content="Alamat" />
|
||||
<FormLibAddress />
|
||||
|
||||
<StaticElement name="divider_2" tag="hr" />
|
||||
<StaticElement name="contactTitle" tag="h4" content="Kontak" />
|
||||
<FormLibTelecom />
|
||||
<StaticElement name="divider_3" tag="hr" />
|
||||
<StaticElement name="communicationTitle" tag="h2" content="Komunikasi" />
|
||||
<FormLibCommunication />
|
||||
<GroupElement name="container" :columns="{
|
||||
default: {
|
||||
container: 7,
|
||||
},
|
||||
sm: {
|
||||
container: 8,
|
||||
},
|
||||
lg: {
|
||||
container: 9,
|
||||
},
|
||||
}" />
|
||||
<GroupElement name="container2_3" :columns="{
|
||||
default: {
|
||||
container: 5,
|
||||
},
|
||||
sm: {
|
||||
container: 4,
|
||||
},
|
||||
lg: {
|
||||
container: 3,
|
||||
},
|
||||
}">
|
||||
<GroupElement name="column1" :columns="{
|
||||
container: 6,
|
||||
}">
|
||||
<ButtonElement name="secondaryButton" button-label="Batal" :secondary="true" align="center" size="lg" />
|
||||
</GroupElement>
|
||||
<GroupElement name="column2" :columns="{
|
||||
container: 6,
|
||||
}">
|
||||
<ButtonElement name="submit" button-label="Simpan" :submits="true" align="center" size="lg" />
|
||||
</GroupElement>
|
||||
</GroupElement>
|
||||
</Vueform>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
definePageMeta({
|
||||
title: 'Vue Form',
|
||||
icon: 'mdi-checkbox-blank-off-outline',
|
||||
drawerIndex: 0,
|
||||
})
|
||||
|
||||
const data = ref({})
|
||||
const humanName = ref({})
|
||||
const onChange = () => {
|
||||
humanName.value = parseName(data.value.nama)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ma-4">
|
||||
<Vueform v-model="data" @change="onChange" sync>
|
||||
<TextElement
|
||||
name="nama"
|
||||
placeholder="Nama Lengkap"
|
||||
:rules="['required', 'min:3', 'max:100']"
|
||||
/>
|
||||
<StaticElement name="parsed-name" size="sm">
|
||||
<div class="d-flex flex-row">
|
||||
<v-chip
|
||||
v-for="prefix in humanName.prefix"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ prefix }}</v-chip
|
||||
><v-chip
|
||||
v-for="given in humanName.given"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue-darken-3"
|
||||
>{{ given }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-show="humanName.family"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue"
|
||||
>{{ humanName.family }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-for="suffix in humanName.suffix"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ suffix }}</v-chip
|
||||
>
|
||||
</div>
|
||||
</StaticElement>
|
||||
<RadiogroupElement
|
||||
name="gender"
|
||||
view="tabs"
|
||||
label="Jenis Kelamin"
|
||||
:items="[
|
||||
{
|
||||
value: 'male',
|
||||
label: '♂️ Laki-laki',
|
||||
},
|
||||
{
|
||||
value: 'female',
|
||||
label: '♀️ Perempuan',
|
||||
},
|
||||
]"
|
||||
default="unknown"
|
||||
/>
|
||||
</Vueform>
|
||||
</div>
|
||||
<span>{{ data }}</span>
|
||||
<span>{{ humanName }}</span>
|
||||
</template>
|
||||
@@ -0,0 +1,146 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const data = ref({ name: '' })
|
||||
|
||||
const handleResponse = (response, form$) => {
|
||||
console.log(response) // axios response
|
||||
console.log(response.status) // HTTP status code
|
||||
console.log(response.data) // response data
|
||||
|
||||
console.log(form$) // <Vueform> instance
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Vueform v-model="data" validate-on="change|step" endpoint="/api/practitioner/basic" method="post"
|
||||
@response="handleResponse">
|
||||
<StaticElement name="identifierTitle" tag="h3" content="Nomor Identitas" />
|
||||
<FormLibIdentifier />
|
||||
<StaticElement name="divider" tag="hr" />
|
||||
<StaticElement name="nameTitle" tag="h4" content="Personal Info" />
|
||||
<FormLibHumanName />
|
||||
<RadiogroupElement name="gender" view="tabs" :items="[
|
||||
{
|
||||
value: 'unknown',
|
||||
label: '⭕',
|
||||
},
|
||||
{
|
||||
value: 'male',
|
||||
label: '♂️',
|
||||
},
|
||||
{
|
||||
value: 'female',
|
||||
label: '♀️',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
label: '⚧️',
|
||||
},
|
||||
]" default="unknown" />
|
||||
<SelectElement name="birthPlace" :search="true" :native="false" input-type="search" autocomplete="on"
|
||||
placeholder="Tempat Lahir" :columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
}" items="/jsondata/birth-place.json" value-prop="name" label-prop="name" search-param="name" />
|
||||
<DateElement name="birthDate" placeholder="Tanggal Lahir" :columns="{
|
||||
sm: {
|
||||
container: 6,
|
||||
},
|
||||
}" />
|
||||
<FileElement name="photo" accept="image/*" view="image" :rules="[
|
||||
'mimetypes:image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/tiff',
|
||||
]" :urls="{}" :drop="true" label="Pas Foto" />
|
||||
<StaticElement name="divider_1" tag="hr" />
|
||||
<StaticElement name="addressTitle" tag="h4" content="Alamat" />
|
||||
<FormLibAddress />
|
||||
<StaticElement name="divider_2" tag="hr" />
|
||||
<StaticElement name="contactTitle" tag="h4" content="Kontak" />
|
||||
<ListElement name="telecom">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<SelectElement name="system" :items="[
|
||||
{
|
||||
value: 'phone',
|
||||
label: '📞',
|
||||
},
|
||||
{
|
||||
value: 'email',
|
||||
label: '📧',
|
||||
},
|
||||
{
|
||||
value: 'url',
|
||||
label: '🌐',
|
||||
},
|
||||
]" :search="true" :native="false" input-type="search" autocomplete="off" :can-deselect="false"
|
||||
:can-clear="false" default="phone" :columns="{
|
||||
default: {
|
||||
container: 2,
|
||||
},
|
||||
sm: {
|
||||
container: 1,
|
||||
},
|
||||
}" :caret="false" />
|
||||
<SelectElement name="use" :items="[
|
||||
{
|
||||
value: 'home',
|
||||
label: '🏠',
|
||||
},
|
||||
{
|
||||
value: 'mobile',
|
||||
label: '📱',
|
||||
},
|
||||
{
|
||||
value: 'work',
|
||||
label: '🏢',
|
||||
},
|
||||
]" :search="true" :native="false" input-type="search" autocomplete="off" :columns="{
|
||||
default: {
|
||||
container: 2,
|
||||
},
|
||||
sm: {
|
||||
container: 1,
|
||||
},
|
||||
}" :caret="false" :can-deselect="false" :can-clear="false" default="home" />
|
||||
<GroupElement name="value" :columns="{
|
||||
default: {
|
||||
container: 8,
|
||||
},
|
||||
sm: {
|
||||
container: 10,
|
||||
},
|
||||
}">
|
||||
<PhoneElement name="phone" :allow-incomplete="true" :unmask="true" default="+62"
|
||||
:conditions="[['telecom.*.system', 'in', ['phone']]]" />
|
||||
<TextElement name="email" input-type="email" :rules="['nullable', 'email']"
|
||||
placeholder="eg. example@mail.com" :conditions="[['telecom.*.system', 'in', ['email']]]" />
|
||||
<TextElement name="url" input-type="url" :rules="['nullable', 'url']" placeholder="eg. http(s)://domain.com"
|
||||
:floating="false" :conditions="[['telecom.*.system', 'in', ['url']]]" />
|
||||
</GroupElement>
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
<StaticElement name="divider_3" tag="hr" />
|
||||
<StaticElement name="communicationTitle" tag="h4" content="Komunikasi" />
|
||||
<ListElement name="communication">
|
||||
<template #default="{ index }">
|
||||
<ObjectElement :name="index">
|
||||
<ObjectElement name="language">
|
||||
<SelectElement name="text" :items="[
|
||||
{
|
||||
value: 'INDONESIA',
|
||||
label: 'INDONESIA',
|
||||
},
|
||||
]" :search="true" :native="false" input-type="search" autocomplete="off" placeholder="Bahasa" />
|
||||
</ObjectElement>
|
||||
<HiddenElement name="preferred" :default="true" :meta="true" />
|
||||
</ObjectElement>
|
||||
</template>
|
||||
</ListElement>
|
||||
<ToggleElement name="active" text="Catatan praktisi ini digunakan secara aktif" :default="true" />
|
||||
|
||||
<ButtonElement name="submit" button-label="Submit" :submits="true" align="right" />
|
||||
</Vueform>
|
||||
<p>{{ data }}</p>
|
||||
<!-- <p>{{ data.value.name }}</p> -->
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<ObjectElement name="test">
|
||||
<TextElement name="text" placeholder="Test Element" />
|
||||
</ObjectElement>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
const items = computed(() =>
|
||||
route.matched
|
||||
.filter((v) => v.path === route.path)[0]
|
||||
.children.filter((c) => c.path)
|
||||
.toSorted(
|
||||
(a, b) => (a.meta?.drawerIndex ?? 99) - (b.meta?.drawerIndex ?? 98),
|
||||
)
|
||||
.map((c) => ({
|
||||
title: c.meta?.title,
|
||||
to: c.name ? c : `${route.path}/${c.path}`,
|
||||
prependIcon: c.meta?.icon,
|
||||
subtitle: c.meta?.subtitle,
|
||||
})),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card v-for="item in items" :key="item.title" class="mb-1">
|
||||
<v-list-item v-bind="item" append-icon="mdi-chevron-right" :ripple="false" class="py-4" />
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
icon: string
|
||||
iconClass?: string
|
||||
color: string
|
||||
title: string
|
||||
value: number | null
|
||||
unit?: string
|
||||
formatter?: (v: number) => string
|
||||
}>(),
|
||||
{
|
||||
iconClass: '',
|
||||
value: null,
|
||||
unit: '',
|
||||
formatter: (v: number) => v.toString(),
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="stats-card v-alert--border-top">
|
||||
<v-icon
|
||||
size="x-large"
|
||||
class="stats-icon"
|
||||
:color="color"
|
||||
:class="iconClass"
|
||||
:icon="icon"
|
||||
/>
|
||||
<div class="card-title ml-auto text-right">
|
||||
<span
|
||||
class="card-title--name font-weight-bold"
|
||||
:class="`text-${color}`"
|
||||
v-text="title"
|
||||
/>
|
||||
<h3
|
||||
class="font-weight-regular d-inline-block ml-2"
|
||||
style="font-size: 18px"
|
||||
>
|
||||
{{ value != null ? formatter(value) : '' }}
|
||||
<small v-if="unit">{{ unit }}</small>
|
||||
</h3>
|
||||
<v-divider />
|
||||
</div>
|
||||
<div class="v-alert__border" :class="`text-${color}`" />
|
||||
<div
|
||||
v-if="$slots.footer"
|
||||
class="text-grey text-right stats-footer text-caption"
|
||||
>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stats-card {
|
||||
padding: 5px;
|
||||
padding-top: 10px;
|
||||
.card-title {
|
||||
width: fit-content;
|
||||
.card-title--name {
|
||||
display: inline-block;
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
}
|
||||
.caption {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.stats-icon {
|
||||
position: absolute;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.stats-footer {
|
||||
:deep(span) {
|
||||
display: inline-block;
|
||||
font-size: 12px !important;
|
||||
letter-spacing: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||
|
||||
export default withNuxt({
|
||||
rules: {
|
||||
'vue/valid-v-slot': ['error', { allowModifiers: true }], // allow vuetify slot modifier
|
||||
'vue/html-self-closing': ['error', { html: { void: 'any' } }], // not conflict with prettier
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<!-- <AuthLayout> -->
|
||||
<slot />
|
||||
<!-- </AuthLayout> -->
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<AppDrawer />
|
||||
<AppBar />
|
||||
<v-main>
|
||||
<slot />
|
||||
</v-main>
|
||||
<AppFooter />
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient()
|
||||
}
|
||||
|
||||
declare const globalThis: {
|
||||
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
|
||||
} & typeof global;
|
||||
|
||||
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||
|
||||
export default prisma
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
|
||||
@@ -0,0 +1,94 @@
|
||||
import { aliases } from 'vuetify/iconsets/mdi'
|
||||
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true },
|
||||
modules: [
|
||||
'@pinia/nuxt',
|
||||
'@vueuse/nuxt',
|
||||
'vuetify-nuxt-module',
|
||||
// 'nuxt-auth-utils',
|
||||
'@sidebase/nuxt-auth',
|
||||
'nuxt-echarts',
|
||||
'@nuxt/icon',
|
||||
'@nuxt/eslint',
|
||||
'@nuxt/test-utils/module',
|
||||
'@vueform/nuxt',
|
||||
'@prisma/nuxt',
|
||||
],
|
||||
css: ['~/assets/styles/index.css'],
|
||||
experimental: { typedPages: true },
|
||||
typescript: { shim: false, strict: true },
|
||||
vue: { propsDestructure: true },
|
||||
vueuse: { ssrHandlers: true },
|
||||
vuetify: {
|
||||
moduleOptions: {
|
||||
ssrClientHints: {
|
||||
viewportSize: true,
|
||||
prefersColorScheme: true,
|
||||
prefersColorSchemeOptions: {},
|
||||
reloadOnFirstRequest: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
clientBundle: {
|
||||
icons: Object.values(aliases).map((v) =>
|
||||
(v as string).replace(/^mdi-/, 'mdi:'),
|
||||
),
|
||||
scan: true,
|
||||
// scan all components in the project and include icons
|
||||
// scan: true,
|
||||
},
|
||||
customCollections: [
|
||||
{
|
||||
prefix: 'custom',
|
||||
dir: './assets/icons',
|
||||
},
|
||||
],
|
||||
},
|
||||
echarts: {
|
||||
charts: ['LineChart', 'BarChart', 'PieChart', 'RadarChart'],
|
||||
renderer: 'svg',
|
||||
components: [
|
||||
'DataZoomComponent',
|
||||
'LegendComponent',
|
||||
'TooltipComponent',
|
||||
'ToolboxComponent',
|
||||
'GridComponent',
|
||||
'TitleComponent',
|
||||
'DatasetComponent',
|
||||
'VisualMapComponent',
|
||||
],
|
||||
},
|
||||
build: {
|
||||
transpile: ['vuetify'],
|
||||
},
|
||||
// vite: {
|
||||
// build: { sourcemap: false },
|
||||
// },
|
||||
auth: {
|
||||
isEnabled: true,
|
||||
baseURL: process.env.AUTH_ORIGIN,
|
||||
provider: {
|
||||
type: 'authjs',
|
||||
},
|
||||
globalAppMiddleware: {
|
||||
isEnabled: true,
|
||||
},
|
||||
},
|
||||
mongoose: {
|
||||
// uri: 'process.env.MONGODB_URI',
|
||||
// options: {},
|
||||
modelsDir: 'models',
|
||||
devtools: true,
|
||||
},
|
||||
compatibilityDate: '2024-08-05',
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
keycloakClient: 'coba-pendaftaran',
|
||||
keycloakIssuer: 'https://auth.rssa.top/realms/sandbox',
|
||||
},
|
||||
},
|
||||
})
|
||||
Generated
+20372
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.6.2",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev -p 3001 --host",
|
||||
"generate": "nuxt generate",
|
||||
"postinstall": "nuxt prepare",
|
||||
"preview": "nuxt preview",
|
||||
"typecheck": "nuxt typecheck",
|
||||
"test": "vitest",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@nuxt/eslint": "^1.2.0",
|
||||
"@nuxt/icon": "^1.11.0",
|
||||
"@nuxt/test-utils": "^3.17.2",
|
||||
"@sidebase/nuxt-auth": "^0.10.1",
|
||||
"@types/node": "^22.13.13",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"eslint": "^9.22.0",
|
||||
"happy-dom": "^17.4.4",
|
||||
"nuxt": "^3.16.0",
|
||||
"nuxt-echarts": "^0.2.6",
|
||||
"nuxt-mongoose": "1.0.6",
|
||||
"playwright-core": "^1.51.0",
|
||||
"prettier": "^3.5.3",
|
||||
"vitest": "^3.0.8",
|
||||
"vue-tsc": "^2.2.8",
|
||||
"vuetify-nuxt-module": "^0.18.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.10.1",
|
||||
"@prisma/nuxt": "^0.3.0",
|
||||
"@vueform/nuxt": "^1.11.0",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@vueuse/integrations": "^13.0.0",
|
||||
"@vueuse/nuxt": "^13.0.0",
|
||||
"echarts": "^5.6.0",
|
||||
"fuse.js": "^7.1.0",
|
||||
"nuxt-auth-utils": "^0.5.16",
|
||||
"pinia": "^3.0.1",
|
||||
"vuetify": "~3.6.15"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<v-empty-state
|
||||
headline="Whoops, 404"
|
||||
title="Page not found"
|
||||
text="The page you were looking for does not exist"
|
||||
icon="custom:nustar"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
icon: 'mdi-security',
|
||||
title: 'Auth',
|
||||
drawerIndex: 4,
|
||||
})
|
||||
|
||||
const { data, status, getCsrfToken, getProviders, signOut } = useAuth()
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
const providers = await getProviders()
|
||||
const csrfToken = await getCsrfToken()
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
// const returnTo = encodeURIComponent('http://localhost:3000/auth/login');
|
||||
const returnTo = encodeURIComponent(window.location.origin);
|
||||
|
||||
const logoutUrl = `${runtimeConfig.public.keycloakIssuer}/protocol/openid-connect/logout?client_id=${runtimeConfig.public.keycloakClient}&post_logout_redirect_uri=${returnTo}`;
|
||||
window.open(logoutUrl, '_blank'); // Sign out dari aplikasi sebelum redirect
|
||||
await signOut({ callbackUrl: '/auth/login' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-item>
|
||||
<v-card-title>Authentication Overview</v-card-title>
|
||||
<v-card-subtitle>See all available authentication & session information
|
||||
below</v-card-subtitle>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<pre v-if="status"><span>Status:</span> {{ status }}</pre>
|
||||
<pre v-if="data"><span>Data:</span> {{ data }}</pre>
|
||||
<pre v-if="csrfToken"><span>CSRF Token:</span> {{ csrfToken }}</pre>
|
||||
<pre v-if="providers"><span>Providers:</span> {{ providers }}</pre>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn text="Logout" @click="handleLogout"></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<!-- <VMain class="main d-flex align-center justify-center"> -->
|
||||
<VRow no-gutters class="main">
|
||||
<VCol cols="6" class="d-flex align-center justify-center">
|
||||
<VCard class="card" title="Login Area" width="400" rounded="lg">
|
||||
<VCardText>
|
||||
<VBtn v-for="provider in providers" :key="provider.id" @click="signIn(provider.id)" class="mb-8" color="blue"
|
||||
size="large" block>
|
||||
Sign in with {{ provider.name }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<!-- </VMain> -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// import { useField, useForm } from 'vee-validate'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'auth',
|
||||
auth: {
|
||||
unauthenticatedOnly: true,
|
||||
navigateAuthenticatedTo: '/homepage',
|
||||
},
|
||||
})
|
||||
|
||||
const { signIn, getProviders } = useAuth()
|
||||
const providers = await getProviders()
|
||||
|
||||
// const { handleSubmit, handleReset } = useForm({
|
||||
// validationSchema: {
|
||||
// username (value) {
|
||||
// if (value?.length >= 2) return true
|
||||
|
||||
// return 'Name needs to be at least 2 characters.'
|
||||
// },
|
||||
// password (value) {
|
||||
// return true
|
||||
// if (value?.length > 9 && /[0-9-]+/.test(value)) return true
|
||||
|
||||
// return 'Phone number needs to be at least 9 digits.'
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
|
||||
// const username = useField('username')
|
||||
// const password = useField('password')
|
||||
|
||||
// const submit = handleSubmit(values => {
|
||||
// alert(JSON.stringify(values, null, 2))
|
||||
// })
|
||||
|
||||
// const show = ref(false)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
min-height: 300px;
|
||||
background-image: url('/background.jpg');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.card {
|
||||
/* From https://css.glass */
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(11.1px);
|
||||
-webkit-backdrop-filter: blur(11.1px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
icon: 'mdi-monitor-dashboard',
|
||||
title: 'Dashboard',
|
||||
drawerIndex: 1,
|
||||
})
|
||||
const stats = ref([
|
||||
{
|
||||
icon: 'mdi-web',
|
||||
title: 'Bandwidth',
|
||||
value: 23,
|
||||
unit: 'GB',
|
||||
color: 'primary',
|
||||
caption: 'Up: 13, Down: 10',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-rss',
|
||||
title: 'Submissions',
|
||||
value: 108,
|
||||
color: 'primary',
|
||||
caption: 'Too young, too naive',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-send',
|
||||
title: 'Requests',
|
||||
value: 1238,
|
||||
color: 'warning',
|
||||
caption: 'Limit: 1320',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-bell',
|
||||
title: 'Messages',
|
||||
value: 9042,
|
||||
color: 'primary',
|
||||
caption: 'Warnings: 300, erros: 47',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-github',
|
||||
title: 'Github Stars',
|
||||
value: NaN,
|
||||
color: 'grey',
|
||||
caption: 'API has no response',
|
||||
},
|
||||
{
|
||||
icon: 'mdi-currency-cny',
|
||||
title: 'Total Fee',
|
||||
value: 2300,
|
||||
unit: '¥',
|
||||
color: 'error',
|
||||
caption: 'Upper Limit: 2000 ¥',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col
|
||||
v-for="stat in stats"
|
||||
:key="stat.title"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="2"
|
||||
>
|
||||
<StatsCard
|
||||
:title="stat.title"
|
||||
:unit="stat.unit"
|
||||
:color="stat.color"
|
||||
:icon="stat.icon"
|
||||
:value="stat.value"
|
||||
>
|
||||
<template #footer>
|
||||
{{ stat.caption }}
|
||||
</template>
|
||||
</StatsCard>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" lg="12">
|
||||
<v-card class="pa-2">
|
||||
<ChartLine />
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<v-card class="pa-2">
|
||||
<ChartRadar />
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<v-card class="pa-2">
|
||||
<ChartPie />
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6" lg="4">
|
||||
<v-card class="pa-2">
|
||||
<ChartBar />
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-card:not(.stats-card) {
|
||||
height: 340px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Forms Routes',
|
||||
icon: 'mdi-view-list',
|
||||
drawerIndex: 4,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<IndexPage />
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import { VCard } from 'vuetify/components'
|
||||
|
||||
definePageMeta({
|
||||
title: 'Vue Form',
|
||||
icon: 'mdi-account-injury-outline',
|
||||
drawerIndex: 2,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<v-card
|
||||
class="mx-auto"
|
||||
prepend-icon="mdi-account-injury-outline"
|
||||
subtitle="Isi dan lengkapi dengan data yang sesuai"
|
||||
width="600"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<span class="font-weight-black">Formulir Pasien Baru</span>
|
||||
</template>
|
||||
|
||||
<v-card-text class="bg-surface-light pt-4">
|
||||
<FormPatient />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
import { VCard } from 'vuetify/components'
|
||||
|
||||
definePageMeta({
|
||||
title: 'Practitioner Basic',
|
||||
icon: 'mdi-account-injury-outline',
|
||||
drawerIndex: 4,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<v-card
|
||||
class="mx-auto"
|
||||
prepend-icon="mdi-account-injury-outline"
|
||||
subtitle="Isi dan lengkapi dengan data yang sesuai"
|
||||
width="80%"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<span class="font-weight-black">Formulir Praktisi Baru</span>
|
||||
</template>
|
||||
|
||||
<v-card-text class="bg-surface-light pt-4">
|
||||
<FormPractitionerBasic />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
definePageMeta({
|
||||
title: 'Vue Form',
|
||||
icon: 'mdi-checkbox-blank-off-outline',
|
||||
drawerIndex: 0,
|
||||
})
|
||||
|
||||
const data = ref({})
|
||||
const humanName = ref({})
|
||||
const onChange = () => {
|
||||
humanName.value = parseName(data.value.nama)
|
||||
}
|
||||
|
||||
const handleResponse = (response, form$) => {
|
||||
console.log(response) // axios response
|
||||
console.log(response.status) // HTTP status code
|
||||
console.log(response.data) // response data
|
||||
|
||||
console.log(form$) // <Vueform> instance
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ma-4">
|
||||
<Vueform
|
||||
v-model="data"
|
||||
@change="onChange"
|
||||
endpoint="/api/practitioner/test"
|
||||
method="post"
|
||||
@response="handleResponse"
|
||||
sync
|
||||
>
|
||||
<TextElement
|
||||
name="nama"
|
||||
placeholder="Nama Lengkap"
|
||||
:rules="['required', 'min:3', 'max:100']"
|
||||
/>
|
||||
<StaticElement name="parsed-name" size="sm">
|
||||
<div class="d-flex flex-row">
|
||||
<v-chip
|
||||
v-for="prefix in humanName.prefix"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ prefix }}</v-chip
|
||||
><v-chip
|
||||
v-for="given in humanName.given"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue-darken-3"
|
||||
>{{ given }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-show="humanName.family"
|
||||
size="x-small"
|
||||
class="mr-1 bg-blue"
|
||||
>{{ humanName.family }}</v-chip
|
||||
>
|
||||
<v-chip
|
||||
v-for="suffix in humanName.suffix"
|
||||
size="x-small"
|
||||
class="mr-1 bg-indigo-lighten-3"
|
||||
>{{ suffix }}</v-chip
|
||||
>
|
||||
</div>
|
||||
</StaticElement>
|
||||
<RadiogroupElement
|
||||
name="gender"
|
||||
view="tabs"
|
||||
label="Jenis Kelamin"
|
||||
:items="[
|
||||
{
|
||||
value: 'unknown',
|
||||
label: '⭕',
|
||||
},
|
||||
{
|
||||
value: 'male',
|
||||
label: '♂️ Laki-laki',
|
||||
},
|
||||
{
|
||||
value: 'female',
|
||||
label: '♀️ Perempuan',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
label: '⚧️ Lainnya',
|
||||
},
|
||||
]"
|
||||
default="unknown"
|
||||
/>
|
||||
|
||||
<ButtonElement
|
||||
name="submit"
|
||||
button-label="Submit"
|
||||
:submits="true"
|
||||
align="right"
|
||||
/>
|
||||
</Vueform>
|
||||
</div>
|
||||
<span>{{ data }}</span>
|
||||
</template>
|
||||
+237
@@ -0,0 +1,237 @@
|
||||
<script setup lang="ts">
|
||||
import type { UseFuseOptions } from './index'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
import { useFuse } from './index'
|
||||
|
||||
interface DataItem {
|
||||
firstName: string
|
||||
lastName: string
|
||||
}
|
||||
|
||||
const data = shallowRef<DataItem[]>([
|
||||
{
|
||||
firstName: 'Roslyn',
|
||||
lastName: 'Mitchell',
|
||||
},
|
||||
{
|
||||
firstName: 'Cathleen',
|
||||
lastName: 'Matthews',
|
||||
},
|
||||
{
|
||||
firstName: 'Carleton',
|
||||
lastName: 'Harrelson',
|
||||
},
|
||||
{
|
||||
firstName: 'Allen',
|
||||
lastName: 'Moores',
|
||||
},
|
||||
{
|
||||
firstName: 'John',
|
||||
lastName: 'Washington',
|
||||
},
|
||||
{
|
||||
firstName: 'Brooke',
|
||||
lastName: 'Colton',
|
||||
},
|
||||
{
|
||||
firstName: 'Mary',
|
||||
lastName: 'Rennold',
|
||||
},
|
||||
{
|
||||
firstName: 'Nanny',
|
||||
lastName: 'Field',
|
||||
},
|
||||
{
|
||||
firstName: 'Chasity',
|
||||
lastName: 'Michael',
|
||||
},
|
||||
{
|
||||
firstName: 'Oakley',
|
||||
lastName: 'Giles',
|
||||
},
|
||||
{
|
||||
firstName: 'Johanna',
|
||||
lastName: 'Shepherd',
|
||||
},
|
||||
{
|
||||
firstName: 'Maybelle',
|
||||
lastName: 'Wilkie',
|
||||
},
|
||||
{
|
||||
firstName: 'Dawson',
|
||||
lastName: 'Rowntree',
|
||||
},
|
||||
{
|
||||
firstName: 'Manley',
|
||||
lastName: 'Pond',
|
||||
},
|
||||
{
|
||||
firstName: 'Lula',
|
||||
lastName: 'Sawyer',
|
||||
},
|
||||
{
|
||||
firstName: 'Hudson',
|
||||
lastName: 'Hext',
|
||||
},
|
||||
{
|
||||
firstName: 'Alden',
|
||||
lastName: 'Senior',
|
||||
},
|
||||
{
|
||||
firstName: 'Tory',
|
||||
lastName: 'Hyland',
|
||||
},
|
||||
{
|
||||
firstName: 'Constance',
|
||||
lastName: 'Josephs',
|
||||
},
|
||||
{
|
||||
firstName: 'Larry',
|
||||
lastName: 'Kinsley',
|
||||
},
|
||||
])
|
||||
|
||||
const search = shallowRef('')
|
||||
const filterBy = shallowRef('both')
|
||||
const keys = computed(() => {
|
||||
if (filterBy.value === 'first') return ['firstName']
|
||||
else if (filterBy.value === 'last') return ['lastName']
|
||||
else return ['firstName', 'lastName']
|
||||
})
|
||||
|
||||
const resultLimit = shallowRef<number | undefined>(undefined)
|
||||
const resultLimitString = shallowRef<string>('')
|
||||
watch(resultLimitString, () => {
|
||||
if (resultLimitString.value === '') {
|
||||
resultLimit.value = undefined
|
||||
} else {
|
||||
const float = Number.parseFloat(resultLimitString.value)
|
||||
if (!Number.isNaN(float)) {
|
||||
resultLimit.value = Math.round(float)
|
||||
resultLimitString.value = resultLimit.value.toString()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const exactMatch = shallowRef(false)
|
||||
const isCaseSensitive = shallowRef(false)
|
||||
const matchAllWhenSearchEmpty = shallowRef(true)
|
||||
|
||||
const options = computed<UseFuseOptions<DataItem>>(() => ({
|
||||
fuseOptions: {
|
||||
keys: keys.value,
|
||||
isCaseSensitive: isCaseSensitive.value,
|
||||
threshold: exactMatch.value ? 0 : undefined,
|
||||
},
|
||||
resultLimit: resultLimit.value,
|
||||
matchAllWhenSearchEmpty: matchAllWhenSearchEmpty.value,
|
||||
}))
|
||||
|
||||
const { results } = useFuse(search, data, options)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<input
|
||||
v-model="search"
|
||||
placeholder="Search for someone..."
|
||||
type="text"
|
||||
w-full
|
||||
/>
|
||||
<div flex flex-wrap>
|
||||
<div
|
||||
bg="dark:(dark-300) light-700"
|
||||
mr-2
|
||||
border="1 light-900 dark:(dark-700)"
|
||||
rounded
|
||||
relative
|
||||
flex
|
||||
items-center
|
||||
>
|
||||
<i i-carbon-filter absolute left-2 opacity-70 />
|
||||
<select v-model="filterBy" px-8 bg-transparent>
|
||||
<option bg="dark:(dark-300) light-700" value="both">Full Name</option>
|
||||
<option bg="dark:(dark-300) light-700" value="first">
|
||||
First Name
|
||||
</option>
|
||||
<option bg="dark:(dark-300) light-700" value="last">Last Name</option>
|
||||
</select>
|
||||
<i
|
||||
i-carbon-chevron-down
|
||||
absolute
|
||||
right-2
|
||||
pointer-events-none
|
||||
opacity-70
|
||||
/>
|
||||
</div>
|
||||
<span flex-1 />
|
||||
<div flex flex-row flex-wrap gap-x-4>
|
||||
<label class="checkbox">
|
||||
<input v-model="exactMatch" type="checkbox" />
|
||||
<span>Exact Match</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input v-model="isCaseSensitive" type="checkbox" />
|
||||
<span>Case Sensitive</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input v-model="matchAllWhenSearchEmpty" type="checkbox" />
|
||||
<span>Match all when empty</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mt-4>
|
||||
<template v-if="results.length > 0">
|
||||
<div
|
||||
v-for="result in results"
|
||||
:key="result.item.firstName + result.item.lastName"
|
||||
py-2
|
||||
>
|
||||
<div flex flex-col>
|
||||
<span> {{ result.item.firstName }} {{ result.item.lastName }} </span>
|
||||
<span text-sm opacity-50> Score Index: {{ result.refIndex }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div text-center pt-8 pb-4 opacity-80>No Results Found</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
input {
|
||||
--tw-ring-offset-width: 1px !important;
|
||||
--tw-ring-color: #8885 !important;
|
||||
--tw-ring-offset-color: transparent !important;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@apply inline-flex items-center my-auto cursor-pointer select-none;
|
||||
}
|
||||
|
||||
.checkbox input {
|
||||
appearance: none;
|
||||
padding: 0;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
background-origin: border-box;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
@apply bg-gray-400/30;
|
||||
@apply rounded-md h-4 w-4 select-none;
|
||||
}
|
||||
|
||||
.checkbox input:checked {
|
||||
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.checkbox span {
|
||||
@apply ml-1.5 text-13px opacity-70;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
const name = ref('')
|
||||
function sayHi() {
|
||||
Notify.success(`Hi, ${name.value}!`)
|
||||
}
|
||||
function warning() {
|
||||
Notify.warning(`How dare you refuse me, ${name.value}.`)
|
||||
}
|
||||
definePageMeta({
|
||||
icon: 'mdi-home',
|
||||
title: 'Homepage',
|
||||
drawerIndex: 0,
|
||||
})
|
||||
// const { user } = useUserSession()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container
|
||||
fluid
|
||||
class="d-flex align-items-center justify-center fill-height"
|
||||
>
|
||||
<div class="text-center">
|
||||
<!-- <div>Welcome {{ user?.login }}!</div> -->
|
||||
<v-icon
|
||||
icon="custom:vitify-nuxt"
|
||||
size="3em"
|
||||
color="primary"
|
||||
class="mb-4"
|
||||
/>
|
||||
<p>Opinionated Starter Template</p>
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
max-width="300"
|
||||
placeholder="Hello World"
|
||||
label="What's your name?"
|
||||
class="mt-8"
|
||||
/>
|
||||
<v-btn :disabled="!name" class="mr-2" color="primary" @click="sayHi">
|
||||
Confirm
|
||||
</v-btn>
|
||||
<v-btn :disabled="!name" @click="warning"> Cancel </v-btn>
|
||||
<VIcon icon="i-simple-icons-keycloak" />
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
redirect: 'homepage',
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Nested Routes',
|
||||
icon: 'mdi-view-list',
|
||||
drawerIndex: 2,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<IndexPage />
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { useFormTest } from '~/composables/forms/test'
|
||||
import { JsonForms } from '@jsonforms/vue'
|
||||
import { vuetifyRenderers } from '@jsonforms/vue-vuetify'
|
||||
|
||||
definePageMeta({
|
||||
title: 'Test Form',
|
||||
icon: 'mdi-animation',
|
||||
})
|
||||
|
||||
const { schema, uischema, data } = useFormTest()
|
||||
|
||||
const onChange = (event: JsonFormsChangeEvent) => {
|
||||
data.value = event.data
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img alt="Vue logo" src="./assets/logo.png" />
|
||||
<h1>JSON Forms Vue 3</h1>
|
||||
<div class="myform">
|
||||
<json-forms
|
||||
:data="data"
|
||||
:renderers="vuetifyRenderers"
|
||||
:schema="schema"
|
||||
:uischema="uischema"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<pre>{{ data }}</pre>
|
||||
<!-- <pre>{{ typeof schema }}</pre>
|
||||
<pre>{{ schema }}</pre> -->
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { JsonForms } from '@jsonforms/vue'
|
||||
import { vuetifyRenderers } from '@jsonforms/vue-vuetify'
|
||||
import { useFormBasic } from '~/composables/forms/basic'
|
||||
|
||||
definePageMeta({
|
||||
title: 'Basic Form',
|
||||
icon: 'mdi-animation',
|
||||
})
|
||||
|
||||
const { schema, uischema, data } = useFormBasic()
|
||||
|
||||
const onChange = (event: JsonFormsChangeEvent) => {
|
||||
data.value = event.data
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img alt="Vue logo" src="./assets/logo.png" />
|
||||
<h1>JSON Forms Vue 3</h1>
|
||||
<div class="myform">
|
||||
<json-forms
|
||||
:data="data"
|
||||
:renderers="vuetifyRenderers"
|
||||
:schema="schema"
|
||||
:uischema="uischema"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
<pre>{{ data }}</pre>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
definePageMeta({
|
||||
title: 'Menu 3',
|
||||
icon: 'mdi-animation',
|
||||
})
|
||||
</script>
|
||||
<template></template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<IndexPage />
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Menu1 Form',
|
||||
icon: 'mdi-animation',
|
||||
})
|
||||
|
||||
const api_data = useFetch('/api/forms/basic/data')
|
||||
const api_i18n = useFetch('/api/forms/basic/i18n')
|
||||
const api_schema = useFetch('/api/forms/basic/schema')
|
||||
const api_uischema = useFetch('/api/forms/basic/uischema')
|
||||
|
||||
// In Vue 3, markRaw is used directly without needing to import it from a specific package
|
||||
// const renderers = markRaw([
|
||||
// ...extendedVuetifyRenderers,
|
||||
// // here you can add custom renderers
|
||||
// ]);
|
||||
|
||||
// Using ref for reactive data in Vue 3
|
||||
const data = ref(api_data) // You'll need to replace this with your actual data
|
||||
const schema = ref(api_schema) // Replace with your actual schema
|
||||
const uischema = ref(api_uischema) // Replace with your actual UI schema
|
||||
|
||||
// Event handlers are defined as functions in the setup
|
||||
// const onChange = (event) => {
|
||||
// data.value = event.data;
|
||||
// };
|
||||
|
||||
const demo = {
|
||||
properties: {
|
||||
firstName: {
|
||||
type: 'string',
|
||||
description: "The person's first name.",
|
||||
},
|
||||
lastName: {
|
||||
type: 'string',
|
||||
description: "The person's last name.",
|
||||
},
|
||||
age: {
|
||||
description: 'Age in years which must be equal to or greater than zero.',
|
||||
type: 'integer',
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var form = document.getElementById('vuetify-json-forms')
|
||||
// form.setAttribute("schema", JSON.stringify(demo));
|
||||
|
||||
// type="module"
|
||||
// src="https://cdn.jsdelivr.net/npm/@chobantonov/jsonforms-vuetify-webcomponent@3.5.1/dist/vuetify-json-forms.min.js"
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<vuetify-json-forms id="vuetify-json-forms"></vuetify-json-forms>
|
||||
<v-container fluid> {{ data.data }} </v-container>
|
||||
<v-container fluid> {{ api_i18n.data }} </v-container>
|
||||
<v-container fluid> {{ schema.data }} </v-container>
|
||||
<v-container fluid> {{ uischema.data }} </v-container>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Menu 2-2',
|
||||
icon: 'mdi-animation',
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<v-container fluid> empty page </v-container>
|
||||
</template>
|
||||
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Vuetify Form',
|
||||
icon: 'mdi-animation',
|
||||
})
|
||||
|
||||
import { ref, provide, onBeforeMount } from 'vue'
|
||||
import { JsonForms } from '@jsonforms/vue'
|
||||
|
||||
import {
|
||||
vuetifyRenderers,
|
||||
defaultStyles,
|
||||
mergeStyles,
|
||||
} from '@jsonforms/vue-vuetify'
|
||||
|
||||
const renderers = Object.freeze([
|
||||
...vuetifyRenderers,
|
||||
// here you can add custom renderers
|
||||
])
|
||||
|
||||
var uischema = useFetch('/api/forms/example/uischema').data
|
||||
var schema = useFetch('/api/forms/example/schema').data
|
||||
|
||||
console.log(JSON.parse(schema.trim))
|
||||
|
||||
// onBeforeMount(async () => {
|
||||
// uischema = JSON.stringify(useFetch('/api/forms/example/uischema').data)
|
||||
// console.log(uischema)
|
||||
// // schema = JSON.stringify(useFetch('/api/forms/example/schema'))
|
||||
// // console.log(schema)
|
||||
// })
|
||||
|
||||
const data = ref({
|
||||
name: 'Send email to Adrian',
|
||||
description: 'Confirm if you have passed the subject\nHereby ...',
|
||||
done: true,
|
||||
recurrence: 'Daily',
|
||||
rating: 3,
|
||||
})
|
||||
|
||||
const onChange = (event: JsonFormsChangeEvent) => {
|
||||
data.value = event.data
|
||||
}
|
||||
|
||||
// mergeStyles combines all classes from both styles definitions into one
|
||||
const myStyles = mergeStyles(defaultStyles, { control: { label: 'mylabel' } })
|
||||
|
||||
// Provide styles to child components
|
||||
provide('styles', myStyles)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img alt="Vue logo" src="./assets/logo.png" />
|
||||
<h1>JSON Forms Vue 3</h1>
|
||||
<!-- <div class="myform">
|
||||
<json-forms
|
||||
:data="data"
|
||||
:renderers="renderers"
|
||||
:schema="schema"
|
||||
:uischema="uischema"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div> -->
|
||||
<pre>{{ data }}</pre>
|
||||
<pre>{{ typeof schema }}</pre>
|
||||
<pre>{{ schema }}</pre>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Pasien',
|
||||
icon: 'mdi-account-injury-outline',
|
||||
drawerIndex: 4,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script>
|
||||
|
||||
definePageMeta({
|
||||
title: 'Data Pasien',
|
||||
icon: 'mdi-account-injury-outline',
|
||||
drawerIndex: 0,
|
||||
})
|
||||
const randomData = ref(generateRandomDataPasien(10));
|
||||
|
||||
const headers = [
|
||||
{ text: 'No RM' },
|
||||
{ text: 'Nama' },
|
||||
{ text: 'tanggal Lahir' },
|
||||
{ text: 'Alamat' },
|
||||
{ text: 'No KTP' },
|
||||
{ text: 'No JKN' },
|
||||
{ text: 'Kelamin' },
|
||||
{ text: 'Aksi' },
|
||||
// { text: 'Aksi' },
|
||||
];
|
||||
|
||||
|
||||
const searchQuery = ref('');
|
||||
const filteredData = computed(() => {
|
||||
if (searchQuery.value.length >= 3) {
|
||||
return randomData.value.filter(item =>
|
||||
item.norm.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
item.no_ktp.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
item.nama.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
);
|
||||
}
|
||||
return randomData.value;
|
||||
});
|
||||
|
||||
console.log(randomData.value)
|
||||
console.log(randomData.value.length)
|
||||
</script>
|
||||
<template>
|
||||
<v-container>
|
||||
<v-card>
|
||||
<div class="py-4 px-3">
|
||||
<div class="d-flex justify-space-between mb-6">
|
||||
<h5 class="text-h5 ">Kunjungan Pasien</h5>
|
||||
<div class="w-sm-25">
|
||||
<v-text-field label="Cari" v-model="searchField" variant="outlined" density="compact" type="text"
|
||||
hide-details color="primary"></v-text-field>
|
||||
</div>
|
||||
</div>
|
||||
<v-row>
|
||||
<div class="d-flex flex-wrap">
|
||||
<!-- <v-col v-for="item in filteredData" :key="item.id" cols="12" md="4">
|
||||
<v-lazy :min-height="200" :options="{ 'threshold': 1 }" transition="fade-transition">
|
||||
<v-card>
|
||||
<template v-slot:append>
|
||||
<h3>{{ item.norm }}</h3>
|
||||
</template>
|
||||
|
||||
<template v-slot:prepend>
|
||||
<v-avatar size="50" class="rounded-md bg-lightsecondary">
|
||||
<Icon icon="solar:user-circle-broken" class="text-secondary" height="36" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<template v-slot:title>
|
||||
<h4>{{ capitalizeEachWord(item.nama) }}</h4>
|
||||
</template>
|
||||
|
||||
<template v-slot:subtitle>
|
||||
<p>{{ item.no_ktp }}</p>
|
||||
</template>
|
||||
|
||||
<template v-slot:text>
|
||||
<p>{{ capitalizeEachWord(item.alamat) }}</p>
|
||||
</template>
|
||||
|
||||
<template v-slot:actions>
|
||||
<div class="d-flex justify-end">
|
||||
<v-btn color="primary" @click="editItem(item.norm)">Ubah</v-btn>
|
||||
<v-btn color="error" @click="deleteItem(item.norm)">Hapus</v-btn>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</v-card>
|
||||
</v-lazy>
|
||||
</v-col> -->
|
||||
<div v-for="(data, i) in filteredData" :key="i">
|
||||
<p>s</p>
|
||||
|
||||
</div>
|
||||
<v-card class="mx-4">
|
||||
<template v-slot:prepend>
|
||||
left
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
right
|
||||
</template>
|
||||
</v-card>
|
||||
<v-card>
|
||||
<template v-slot:prepend>
|
||||
left
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
right
|
||||
</template>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<IndexPage />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
|
||||
definePageMeta({
|
||||
title: 'Kunjungan Pasien',
|
||||
icon: 'mdi-account-injury-outline',
|
||||
drawerIndex: 1,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<pre>dddddddd</pre>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script>
|
||||
import { VCard } from 'vuetify/components'
|
||||
|
||||
definePageMeta({
|
||||
title: 'Tambah Pasien',
|
||||
icon: 'mdi-account-injury-outline',
|
||||
drawerIndex: 2,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<v-card class="mx-auto" prepend-icon="mdi-account-injury-outline" subtitle="Isi dan lengkapi dengan data yang sesuai"
|
||||
width="80%">
|
||||
<template v-slot:title>
|
||||
<span class="font-weight-black">Formulir Pasien Baru</span>
|
||||
</template>
|
||||
|
||||
<v-card-text class="bg-surface-light pt-4">
|
||||
<FormPatientCreate />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -0,0 +1,234 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableHeaders } from '~/plugins/vuetify'
|
||||
|
||||
definePageMeta({
|
||||
icon: 'mdi-table',
|
||||
title: 'Data Praktisi',
|
||||
drawerIndex: 3,
|
||||
})
|
||||
|
||||
const search = ref('')
|
||||
const dialogDelete = useTemplateRef('dialogDelete')
|
||||
function showDialogDelete(name: string) {
|
||||
dialogDelete.value
|
||||
?.open('Are you sure you want to delete this dessert?')
|
||||
.then(async (confirmed: boolean) => {
|
||||
if (confirmed) {
|
||||
try {
|
||||
const index = desserts.value!.findIndex((v) => v.name === name)
|
||||
desserts.value!.splice(index, 1)
|
||||
Notify.success('Deleted')
|
||||
} catch (e) {
|
||||
Notify.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const practitioners = ref([])
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'info',
|
||||
})
|
||||
|
||||
// Mengambil data dari API
|
||||
const fetchPractitioners = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
const response = await fetch('/api/practitioner')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error: ${response.status} - ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
console.log(data)
|
||||
practitioners.value = data
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch practitioners:', err)
|
||||
error.value = `Gagal memuat data praktisi: ${err.message}`
|
||||
showSnackbar('Gagal memuat data praktisi', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Mendapatkan nomor telepon praktisi
|
||||
const getPractitionerPhone = (practitioner) => {
|
||||
if (!practitioner.telecom || practitioner.telecom.length === 0) {
|
||||
return 'Tidak ada data'
|
||||
}
|
||||
|
||||
const phone = practitioner.telecom.find((t) => t.system === 'phone')
|
||||
return phone ? phone.value : 'Tidak ada data'
|
||||
}
|
||||
|
||||
// Mendapatkan email praktisi
|
||||
const getPractitionerEmail = (practitioner) => {
|
||||
if (!practitioner.telecom || practitioner.telecom.length === 0) {
|
||||
return 'Tidak ada data'
|
||||
}
|
||||
|
||||
const email = practitioner.telecom.find((t) => t.system === 'email')
|
||||
return email ? email.value : 'Tidak ada data'
|
||||
}
|
||||
|
||||
// Menampilkan detail praktisi
|
||||
const showDetail = (practitioner) => {
|
||||
console.log('Menampilkan detail praktisi:', getPractitionerName(practitioner))
|
||||
showSnackbar(`Detail ${getPractitionerName(practitioner)}`, 'primary')
|
||||
// Di sini bisa ditambahkan navigasi ke halaman detail atau membuka dialog
|
||||
}
|
||||
|
||||
// Menampilkan snackbar
|
||||
const showSnackbar = (text, color = 'success') => {
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
text,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
// Panggil API saat komponen dimuat
|
||||
onMounted(() => {
|
||||
fetchPractitioners()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card>
|
||||
<client-only>
|
||||
<teleport to="#app-bar">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
label="Search"
|
||||
single-line
|
||||
hide-details
|
||||
density="compact"
|
||||
class="mr-2"
|
||||
rounded="xl"
|
||||
flat
|
||||
variant="solo"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</teleport>
|
||||
</client-only>
|
||||
<v-container>
|
||||
<h1 class="text-h4 mb-4">Daftar Praktisi</h1>
|
||||
|
||||
<v-alert v-if="error" type="error" class="mb-4">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<v-progress-circular
|
||||
v-if="loading"
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
class="my-8 mx-auto d-block"
|
||||
></v-progress-circular>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
v-for="(practitioner, index) in practitioners"
|
||||
:key="index"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
>
|
||||
<v-card
|
||||
class="mx-auto mb-4"
|
||||
max-width="400"
|
||||
elevation="3"
|
||||
shaped
|
||||
>
|
||||
<v-card-title class="text-h5">
|
||||
{{ joinName(practitioner.name, ['official', 'usual']) }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row align="center" class="mx-0">
|
||||
<DivAvatar
|
||||
size="80"
|
||||
class="mr-3"
|
||||
:gender="practitioner.gender"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<DivIconText
|
||||
icon="mdi-map-marker"
|
||||
:text="practitioner.birthPlace || 'Tidak ada data'"
|
||||
/>
|
||||
<DivIconText
|
||||
icon="mdi-calendar"
|
||||
:text="formatDate(practitioner.birthDate, 'full')"
|
||||
/>
|
||||
<DivIconText
|
||||
icon="mdi-phone"
|
||||
:text="
|
||||
getContactPoints(
|
||||
practitioner.telecom,
|
||||
'phone',
|
||||
).join(',')
|
||||
"
|
||||
/>
|
||||
<DivIconText
|
||||
icon="mdi-email"
|
||||
:text="
|
||||
getContactPoints(
|
||||
practitioner.telecom,
|
||||
'email',
|
||||
).join(',')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
text
|
||||
@click="showDetail(practitioner)"
|
||||
>
|
||||
Detail
|
||||
<v-icon small class="ml-1">mdi-arrow-right</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar.show"
|
||||
:color="snackbar.color"
|
||||
:timeout="3000"
|
||||
>
|
||||
{{ snackbar.text }}
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
<DialogConfirm ref="dialogDelete" />
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-card {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.v-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
</style>
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableHeaders } from '~/plugins/vuetify'
|
||||
|
||||
definePageMeta({
|
||||
icon: 'mdi-table',
|
||||
title: 'Data Table',
|
||||
drawerIndex: 3,
|
||||
})
|
||||
|
||||
const search = ref('')
|
||||
const dialogDelete = useTemplateRef('dialogDelete')
|
||||
function showDialogDelete(name: string) {
|
||||
dialogDelete.value
|
||||
?.open('Are you sure you want to delete this dessert?')
|
||||
.then(async (confirmed: boolean) => {
|
||||
if (confirmed) {
|
||||
try {
|
||||
const index = desserts.value!.findIndex((v) => v.name === name)
|
||||
desserts.value!.splice(index, 1)
|
||||
Notify.success('Deleted')
|
||||
} catch (e) {
|
||||
Notify.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const headers: DataTableHeaders = [
|
||||
{
|
||||
title: 'Dessert (100g serving)',
|
||||
key: 'name',
|
||||
},
|
||||
{ title: 'Calories', key: 'calories' },
|
||||
{ title: 'Fat (g)', key: 'fat' },
|
||||
{ title: 'Carbs (g)', key: 'carbs' },
|
||||
{ title: 'Protein (g)', key: 'protein' },
|
||||
{ title: 'Iron (%)', key: 'iron' },
|
||||
{ title: 'Actions', key: 'actions', sortable: false },
|
||||
]
|
||||
const desserts = ref([
|
||||
{
|
||||
name: 'Frozen Yogurt',
|
||||
calories: 159,
|
||||
fat: 6.0,
|
||||
carbs: 24,
|
||||
protein: 4.0,
|
||||
iron: '1',
|
||||
},
|
||||
{
|
||||
name: 'Jelly bean',
|
||||
calories: 375,
|
||||
fat: 0.0,
|
||||
carbs: 94,
|
||||
protein: 0.0,
|
||||
iron: '0',
|
||||
},
|
||||
{
|
||||
name: 'KitKat',
|
||||
calories: 518,
|
||||
fat: 26.0,
|
||||
carbs: 65,
|
||||
protein: 7,
|
||||
iron: '6',
|
||||
},
|
||||
{
|
||||
name: 'Eclair',
|
||||
calories: 262,
|
||||
fat: 16.0,
|
||||
carbs: 23,
|
||||
protein: 6.0,
|
||||
iron: '7',
|
||||
},
|
||||
{
|
||||
name: 'Gingerbread',
|
||||
calories: 356,
|
||||
fat: 16.0,
|
||||
carbs: 49,
|
||||
protein: 3.9,
|
||||
iron: '16',
|
||||
},
|
||||
{
|
||||
name: 'Ice cream sandwich',
|
||||
calories: 237,
|
||||
fat: 9.0,
|
||||
carbs: 37,
|
||||
protein: 4.3,
|
||||
iron: '1',
|
||||
},
|
||||
{
|
||||
name: 'Lollipop',
|
||||
calories: 392,
|
||||
fat: 0.2,
|
||||
carbs: 98,
|
||||
protein: 0,
|
||||
iron: '2',
|
||||
},
|
||||
{
|
||||
name: 'Cupcake',
|
||||
calories: 305,
|
||||
fat: 3.7,
|
||||
carbs: 67,
|
||||
protein: 4.3,
|
||||
iron: '8',
|
||||
},
|
||||
{
|
||||
name: 'Honeycomb',
|
||||
calories: 408,
|
||||
fat: 3.2,
|
||||
carbs: 87,
|
||||
protein: 6.5,
|
||||
iron: '45',
|
||||
},
|
||||
{
|
||||
name: 'Donut',
|
||||
calories: 452,
|
||||
fat: 25.0,
|
||||
carbs: 51,
|
||||
protein: 4.9,
|
||||
iron: '22',
|
||||
},
|
||||
{
|
||||
name: 'Donut2',
|
||||
calories: 452,
|
||||
fat: 25.0,
|
||||
carbs: 51,
|
||||
protein: 4.9,
|
||||
iron: '22',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-card>
|
||||
<client-only>
|
||||
<teleport to="#app-bar">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
label="Search"
|
||||
single-line
|
||||
hide-details
|
||||
density="compact"
|
||||
class="mr-2"
|
||||
rounded="xl"
|
||||
flat
|
||||
variant="solo"
|
||||
style="width: 250px"
|
||||
/>
|
||||
</teleport>
|
||||
</client-only>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="desserts"
|
||||
item-value="name"
|
||||
:search="search"
|
||||
>
|
||||
<template #item.actions="{ item }">
|
||||
<v-defaults-provider
|
||||
:defaults="{
|
||||
VBtn: {
|
||||
size: 20,
|
||||
rounded: 'sm',
|
||||
variant: 'text',
|
||||
class: 'ml-1',
|
||||
color: '',
|
||||
},
|
||||
VIcon: {
|
||||
size: 20,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<v-btn
|
||||
v-tooltip="{ text: 'Delete', location: 'top' }"
|
||||
icon="mdi-delete-outline"
|
||||
@click.stop="showDialogDelete(item.name)"
|
||||
/>
|
||||
</v-defaults-provider>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<DialogConfirm ref="dialogDelete" />
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { IconProps } from 'vuetify'
|
||||
import { Icon } from '#components'
|
||||
import type { VDataTable } from 'vuetify/components'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { aliases } from 'vuetify/iconsets/mdi'
|
||||
|
||||
export type DataTableHeaders = VDataTable['$props']['headers']
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.hook('vuetify:configuration', ({ vuetifyOptions }) => {
|
||||
vuetifyOptions.icons = {
|
||||
defaultSet: 'nuxtIcon',
|
||||
sets: {
|
||||
nuxtIcon: {
|
||||
component: ({ icon, tag, ...rest }: IconProps) =>
|
||||
h(tag, rest, [h(Icon, { name: aliases[icon as string] ?? icon })]),
|
||||
},
|
||||
},
|
||||
aliases,
|
||||
}
|
||||
const primary = useStorage('theme-primary', '#1697f6').value
|
||||
vuetifyOptions.theme = {
|
||||
themes: {
|
||||
light: { colors: { primary } },
|
||||
dark: { colors: { primary } },
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
Generated
+10178
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
export default {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// model Country {
|
||||
// id String @id @map("_id")
|
||||
// name String @unique
|
||||
// }
|
||||
|
||||
// model Address {
|
||||
// id String @id @map("_id")
|
||||
// name String @unique
|
||||
// alt_name String?
|
||||
// latitude Float?
|
||||
// longitude Float?
|
||||
// regencies Regency[]
|
||||
// }
|
||||
|
||||
// type Regency {
|
||||
// id String @map("_id")
|
||||
// province_id String
|
||||
// name String
|
||||
// alt_name String?
|
||||
// latitude Float?
|
||||
// longitude Float?
|
||||
// districts District[]
|
||||
// }
|
||||
|
||||
// type District {
|
||||
// id String @map("_id")
|
||||
// regency_id String
|
||||
// name String
|
||||
// alt_name String?
|
||||
// latitude Float?
|
||||
// longitude Float?
|
||||
// villages Village[]
|
||||
// }
|
||||
|
||||
// type Village {
|
||||
// id String @map("_id")
|
||||
// district_id String
|
||||
// name String
|
||||
// }
|
||||
|
||||
// model Postal {
|
||||
// id Int @id @map("_id")
|
||||
// name String?
|
||||
// postal Int[]
|
||||
// }
|
||||
@@ -0,0 +1,67 @@
|
||||
// model Practitioner {
|
||||
// id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||
// active Boolean @default(true)
|
||||
// identifier Identifier[]
|
||||
// // name HumanName[]
|
||||
// // telecom ContactPoint[]
|
||||
// birthDate DateTime?
|
||||
// birthPlace String?
|
||||
// // deceased Deceased?
|
||||
// // address AddressType[]
|
||||
// // communication BackboneElementCommunication[]
|
||||
// }
|
||||
|
||||
// type Identifier {
|
||||
// name String
|
||||
// value String
|
||||
// }
|
||||
|
||||
// type HumanName {
|
||||
// use String @default("usual")
|
||||
// text String?
|
||||
// family String?
|
||||
// given String[]
|
||||
// prefix String[]
|
||||
// suffix String[]
|
||||
// period Period
|
||||
// }
|
||||
|
||||
// type Period {
|
||||
// start DateTime @default(now())
|
||||
// end DateTime?
|
||||
// }
|
||||
|
||||
// type ContactPoint {
|
||||
// system String @default("phone")
|
||||
// use String @default("home")
|
||||
// value String
|
||||
// period Period
|
||||
// }
|
||||
|
||||
// type Deceased {
|
||||
// deceasedBoolean Boolean?
|
||||
// deceasedDateTime DateTime?
|
||||
// }
|
||||
|
||||
// type AddressType {
|
||||
// use String @default("home")
|
||||
// type String @default("physical")
|
||||
// text String?
|
||||
// line String[]
|
||||
// village String?
|
||||
// district String?
|
||||
// city String?
|
||||
// state String?
|
||||
// country String?
|
||||
// postalCode String
|
||||
// period Period
|
||||
// }
|
||||
|
||||
// type BackboneElementCommunication {
|
||||
// language CodeableConceptLanguage
|
||||
// preferred Boolean?
|
||||
// }
|
||||
|
||||
// type CodeableConceptLanguage {
|
||||
// text String
|
||||
// }
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
||||
[
|
||||
{
|
||||
"id": "11",
|
||||
"name": "ACEH",
|
||||
"alt_name": "ACEH",
|
||||
"latitude": 4.36855,
|
||||
"longitude": 97.0253
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"name": "SUMATERA UTARA",
|
||||
"alt_name": "SUMATERA UTARA",
|
||||
"latitude": 2.19235,
|
||||
"longitude": 99.38122
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"name": "SUMATERA BARAT",
|
||||
"alt_name": "SUMATERA BARAT",
|
||||
"latitude": -1.34225,
|
||||
"longitude": 100.0761
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"name": "RIAU",
|
||||
"alt_name": "RIAU",
|
||||
"latitude": 0.50041,
|
||||
"longitude": 101.54758
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"name": "JAMBI",
|
||||
"alt_name": "JAMBI",
|
||||
"latitude": -1.61157,
|
||||
"longitude": 102.7797
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
"name": "SUMATERA SELATAN",
|
||||
"alt_name": "SUMATERA SELATAN",
|
||||
"latitude": -3.12668,
|
||||
"longitude": 104.09306
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
"name": "BENGKULU",
|
||||
"alt_name": "BENGKULU",
|
||||
"latitude": -3.51868,
|
||||
"longitude": 102.53598
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"name": "LAMPUNG",
|
||||
"alt_name": "LAMPUNG",
|
||||
"latitude": -4.8555,
|
||||
"longitude": 105.0273
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"name": "KEPULAUAN BANGKA BELITUNG",
|
||||
"alt_name": "KEPULAUAN BANGKA BELITUNG",
|
||||
"latitude": -2.75775,
|
||||
"longitude": 107.58394
|
||||
},
|
||||
{
|
||||
"id": "21",
|
||||
"name": "KEPULAUAN RIAU",
|
||||
"alt_name": "KEPULAUAN RIAU",
|
||||
"latitude": -0.15478,
|
||||
"longitude": 104.58037
|
||||
},
|
||||
{
|
||||
"id": "31",
|
||||
"name": "DKI JAKARTA",
|
||||
"alt_name": "DKI JAKARTA",
|
||||
"latitude": 6.1745,
|
||||
"longitude": 106.8227
|
||||
},
|
||||
{
|
||||
"id": "32",
|
||||
"name": "JAWA BARAT",
|
||||
"alt_name": "JAWA BARAT",
|
||||
"latitude": -6.88917,
|
||||
"longitude": 107.64047
|
||||
},
|
||||
{
|
||||
"id": "33",
|
||||
"name": "JAWA TENGAH",
|
||||
"alt_name": "JAWA TENGAH",
|
||||
"latitude": -7.30324,
|
||||
"longitude": 110.00441
|
||||
},
|
||||
{
|
||||
"id": "34",
|
||||
"name": "DI YOGYAKARTA",
|
||||
"alt_name": "DI YOGYAKARTA",
|
||||
"latitude": 7.7956,
|
||||
"longitude": 110.3695
|
||||
},
|
||||
{
|
||||
"id": "35",
|
||||
"name": "JAWA TIMUR",
|
||||
"alt_name": "JAWA TIMUR",
|
||||
"latitude": -6.96851,
|
||||
"longitude": 113.98005
|
||||
},
|
||||
{
|
||||
"id": "36",
|
||||
"name": "BANTEN",
|
||||
"alt_name": "BANTEN",
|
||||
"latitude": -6.44538,
|
||||
"longitude": 106.13756
|
||||
},
|
||||
{
|
||||
"id": "51",
|
||||
"name": "BALI",
|
||||
"alt_name": "BALI",
|
||||
"latitude": -8.23566,
|
||||
"longitude": 115.12239
|
||||
},
|
||||
{
|
||||
"id": "52",
|
||||
"name": "NUSA TENGGARA BARAT",
|
||||
"alt_name": "NUSA TENGGARA BARAT",
|
||||
"latitude": -8.12179,
|
||||
"longitude": 117.63696
|
||||
},
|
||||
{
|
||||
"id": "53",
|
||||
"name": "NUSA TENGGARA TIMUR",
|
||||
"alt_name": "NUSA TENGGARA TIMUR",
|
||||
"latitude": -8.56568,
|
||||
"longitude": 120.69786
|
||||
},
|
||||
{
|
||||
"id": "61",
|
||||
"name": "KALIMANTAN BARAT",
|
||||
"alt_name": "KALIMANTAN BARAT",
|
||||
"latitude": -0.13224,
|
||||
"longitude": 111.09689
|
||||
},
|
||||
{
|
||||
"id": "62",
|
||||
"name": "KALIMANTAN TENGAH",
|
||||
"alt_name": "KALIMANTAN TENGAH",
|
||||
"latitude": -1.49958,
|
||||
"longitude": 113.29033
|
||||
},
|
||||
{
|
||||
"id": "63",
|
||||
"name": "KALIMANTAN SELATAN",
|
||||
"alt_name": "KALIMANTAN SELATAN",
|
||||
"latitude": -2.94348,
|
||||
"longitude": 115.37565
|
||||
},
|
||||
{
|
||||
"id": "64",
|
||||
"name": "KALIMANTAN TIMUR",
|
||||
"alt_name": "KALIMANTAN TIMUR",
|
||||
"latitude": 0.78844,
|
||||
"longitude": 116.242
|
||||
},
|
||||
{
|
||||
"id": "65",
|
||||
"name": "KALIMANTAN UTARA",
|
||||
"alt_name": "KALIMANTAN UTARA",
|
||||
"latitude": 2.72594,
|
||||
"longitude": 116.911
|
||||
},
|
||||
{
|
||||
"id": "71",
|
||||
"name": "SULAWESI UTARA",
|
||||
"alt_name": "SULAWESI UTARA",
|
||||
"latitude": 0.65557,
|
||||
"longitude": 124.09015
|
||||
},
|
||||
{
|
||||
"id": "72",
|
||||
"name": "SULAWESI TENGAH",
|
||||
"alt_name": "SULAWESI TENGAH",
|
||||
"latitude": -1.69378,
|
||||
"longitude": 120.80886
|
||||
},
|
||||
{
|
||||
"id": "73",
|
||||
"name": "SULAWESI SELATAN",
|
||||
"alt_name": "SULAWESI SELATAN",
|
||||
"latitude": -3.64467,
|
||||
"longitude": 119.94719
|
||||
},
|
||||
{
|
||||
"id": "74",
|
||||
"name": "SULAWESI TENGGARA",
|
||||
"alt_name": "SULAWESI TENGGARA",
|
||||
"latitude": -3.54912,
|
||||
"longitude": 121.72796
|
||||
},
|
||||
{
|
||||
"id": "75",
|
||||
"name": "GORONTALO",
|
||||
"alt_name": "GORONTALO",
|
||||
"latitude": 0.71862,
|
||||
"longitude": 122.45559
|
||||
},
|
||||
{
|
||||
"id": "76",
|
||||
"name": "SULAWESI BARAT",
|
||||
"alt_name": "SULAWESI BARAT",
|
||||
"latitude": -2.49745,
|
||||
"longitude": 119.3919
|
||||
},
|
||||
{
|
||||
"id": "81",
|
||||
"name": "MALUKU",
|
||||
"alt_name": "MALUKU",
|
||||
"latitude": -3.11884,
|
||||
"longitude": 129.42078
|
||||
},
|
||||
{
|
||||
"id": "82",
|
||||
"name": "MALUKU UTARA",
|
||||
"alt_name": "MALUKU UTARA",
|
||||
"latitude": 0.63012,
|
||||
"longitude": 127.97202
|
||||
},
|
||||
{
|
||||
"id": "91",
|
||||
"name": "PAPUA BARAT",
|
||||
"alt_name": "PAPUA BARAT",
|
||||
"latitude": -1.38424,
|
||||
"longitude": 132.90253
|
||||
},
|
||||
{
|
||||
"id": "94",
|
||||
"name": "PAPUA",
|
||||
"alt_name": "PAPUA",
|
||||
"latitude": -3.98857,
|
||||
"longitude": 138.34853
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "John Doe",
|
||||
"vegetarian": false,
|
||||
"birthDate": "1985-06-02",
|
||||
"personalData": {
|
||||
"age": 34
|
||||
},
|
||||
"postalCode": "12345"
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"en": {
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"description": "The name of the person"
|
||||
},
|
||||
"vegetarian": {
|
||||
"label": "Vegetarian",
|
||||
"description": "Whether the person is a vegetarian"
|
||||
},
|
||||
"birth": {
|
||||
"label": "Birth Date",
|
||||
"description": ""
|
||||
},
|
||||
"nationality": {
|
||||
"label": "Nationality",
|
||||
"description": ""
|
||||
},
|
||||
"personal-data": {
|
||||
"age": {
|
||||
"label": "Age"
|
||||
},
|
||||
"driving": {
|
||||
"label": "Driving Skill",
|
||||
"description": "Indicating experience level"
|
||||
}
|
||||
},
|
||||
"height": {
|
||||
"label": "Height"
|
||||
},
|
||||
"occupation": {
|
||||
"label": "Occupation",
|
||||
"description": ""
|
||||
},
|
||||
"postal-code": {
|
||||
"label": "Postal Code"
|
||||
},
|
||||
"error": {
|
||||
"required": "field is required"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"description": "Der Name der Person"
|
||||
},
|
||||
"vegetarian": {
|
||||
"label": "Vegetarier",
|
||||
"description": "Isst die Person vegetarisch?"
|
||||
},
|
||||
"birth": {
|
||||
"label": "Geburtsdatum",
|
||||
"description": ""
|
||||
},
|
||||
"nationality": {
|
||||
"label": "Nationalität",
|
||||
"description": "",
|
||||
"Other": "Andere"
|
||||
},
|
||||
"personal-data": {
|
||||
"age": {
|
||||
"label": "Alter"
|
||||
},
|
||||
"driving": {
|
||||
"label": "Fahrkenntnisse",
|
||||
"description": "Fahrerfahrung der Person"
|
||||
}
|
||||
},
|
||||
"height": {
|
||||
"label": "Größe"
|
||||
},
|
||||
"occupation": {
|
||||
"label": "Beruf",
|
||||
"description": ""
|
||||
},
|
||||
"postal-code": {
|
||||
"label": "Postleitzahl"
|
||||
},
|
||||
"error": {
|
||||
"required": "Pflichtfeld"
|
||||
},
|
||||
"Additional Information": "Zusätzliche Informationen"
|
||||
},
|
||||
"bg": {
|
||||
"name": {
|
||||
"label": "Име",
|
||||
"description": "Името на лицето"
|
||||
},
|
||||
"vegetarian": {
|
||||
"label": "Вегетарианец",
|
||||
"description": "Дали човекът е вегетарианец"
|
||||
},
|
||||
"birth": {
|
||||
"label": "Рождена дата",
|
||||
"description": ""
|
||||
},
|
||||
"nationality": {
|
||||
"label": "Националност",
|
||||
"description": ""
|
||||
},
|
||||
"personal-data": {
|
||||
"age": {
|
||||
"label": "Възраст",
|
||||
"description": "Моля, въведете вашата възраст."
|
||||
},
|
||||
"driving": {
|
||||
"label": "Шофьорски умения",
|
||||
"description": "Показва ниво на опит"
|
||||
}
|
||||
},
|
||||
"height": {
|
||||
"label": "Височина"
|
||||
},
|
||||
"occupation": {
|
||||
"label": "Професия",
|
||||
"description": ""
|
||||
},
|
||||
"postal-code": {
|
||||
"label": "Пощенски код"
|
||||
},
|
||||
"error": {
|
||||
"required": "полето е задължително"
|
||||
},
|
||||
"Additional Information": "Допълнителна информация"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"description": "Please enter your name",
|
||||
"i18n": "name"
|
||||
},
|
||||
"vegetarian": {
|
||||
"type": "boolean",
|
||||
"i18n": "vegetarian"
|
||||
},
|
||||
"birthDate": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"i18n": "birth"
|
||||
},
|
||||
"nationality": {
|
||||
"type": "string",
|
||||
"enum": ["DE", "IT", "JP", "US", "RU", "Other"],
|
||||
"i18n": "nationality"
|
||||
},
|
||||
"personalData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"age": {
|
||||
"type": "integer",
|
||||
"description": "Please enter your age.",
|
||||
"i18n": "personal-data.age"
|
||||
},
|
||||
"height": {
|
||||
"type": "number",
|
||||
"i18n": "height"
|
||||
},
|
||||
"drivingSkill": {
|
||||
"type": "number",
|
||||
"maximum": 10,
|
||||
"minimum": 1,
|
||||
"default": 7,
|
||||
"i18n": "personal-data.driving"
|
||||
}
|
||||
},
|
||||
"required": ["age", "height"]
|
||||
},
|
||||
"occupation": {
|
||||
"type": "string",
|
||||
"i18n": "occupation"
|
||||
},
|
||||
"postalCode": {
|
||||
"type": "string",
|
||||
"maxLength": 5,
|
||||
"i18n": "postal-code"
|
||||
}
|
||||
},
|
||||
"required": ["occupation", "nationality"]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"type": "VerticalLayout",
|
||||
"elements": [
|
||||
{
|
||||
"type": "HorizontalLayout",
|
||||
"elements": [
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/name"
|
||||
},
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/personalData/properties/age"
|
||||
},
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/birthDate"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Label",
|
||||
"text": "Additional Information"
|
||||
},
|
||||
{
|
||||
"type": "HorizontalLayout",
|
||||
"elements": [
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/personalData/properties/height"
|
||||
},
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/nationality"
|
||||
},
|
||||
{
|
||||
"type": "Control",
|
||||
"scope": "#/properties/occupation",
|
||||
"options": {
|
||||
"suggestion": [
|
||||
"Accountant",
|
||||
"Engineer",
|
||||
"Freelancer",
|
||||
"Journalism",
|
||||
"Physician",
|
||||
"Student",
|
||||
"Teacher",
|
||||
"Other"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 37 37" fill="none">
|
||||
<g transform="rotate(180 18.778 13.7)">
|
||||
<path class="spa-loading-path"
|
||||
d="M24.236 22.006h10.742L25.563 5.822l-8.979 14.31a4 4 0 0 1-3.388 1.874H2.978l16-27.713 6 10.392" />
|
||||
</g>
|
||||
<style>
|
||||
.spa-loading-path {
|
||||
fill: none;
|
||||
stroke: #248fe4;
|
||||
stroke-width: 4px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
/* Stroke-dasharray property */
|
||||
stroke-dasharray: 128;
|
||||
stroke-dashoffset: 128;
|
||||
animation: nuxt-spa-loading-move 1.5s linear;
|
||||
animation-fill-mode: forwards
|
||||
}
|
||||
|
||||
@keyframes nuxt-spa-loading-move {
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 729 B |
@@ -0,0 +1,24 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event)
|
||||
const parent = JSON.parse(query.parent)
|
||||
|
||||
try {
|
||||
const cities = await prisma.addressCities.findMany({
|
||||
where: {
|
||||
Parent: parent.Code,
|
||||
},
|
||||
})
|
||||
return cities.map((c) => {
|
||||
return {
|
||||
Code: c.Code,
|
||||
Name: c.Name,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
// Return error if fetching users fails
|
||||
return {
|
||||
status: 500,
|
||||
body: { message: 'Failed to fetch cities' },
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useToNumber, useToString } from '@vueuse/core'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event)
|
||||
const limit = useToNumber(useToString(query.limit).value).value || 5
|
||||
const search = useToString(query.search)
|
||||
|
||||
try {
|
||||
const countries = await prisma.addressCountries.findMany({
|
||||
// where: {
|
||||
// Name: {
|
||||
// startsWith: search.value,
|
||||
// mode: 'insensitive',
|
||||
// },
|
||||
// },
|
||||
// take: limit,
|
||||
})
|
||||
return countries.map((c) => {
|
||||
return {
|
||||
Code: c.Code,
|
||||
Name: c.Name,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
// Return error if fetching users fails
|
||||
return {
|
||||
status: 500,
|
||||
body: { message: 'Failed to fetch countries' },
|
||||
}
|
||||
}
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user