first commit
This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -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
|
||||
10
.env.example
Normal file
10
.env.example
Normal file
@@ -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
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@@ -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
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
pnpm-lock.yaml
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||
146
README.md
Normal file
146
README.md
Normal file
@@ -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)
|
||||
45
app.vue
Normal file
45
app.vue
Normal file
@@ -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>
|
||||
47
app/spa-loading-template.html
Normal file
47
app/spa-loading-template.html
Normal file
@@ -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 |
98873
assets/data/address/kodepos.json
Normal file
98873
assets/data/address/kodepos.json
Normal file
File diff suppressed because it is too large
Load Diff
38
assets/data/identifier/practitioner.json
Normal file
38
assets/data/identifier/practitioner.json
Normal file
@@ -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}$"
|
||||
}
|
||||
]
|
||||
23
assets/icons/nustar.svg
Normal file
23
assets/icons/nustar.svg
Normal file
@@ -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 |
6
assets/icons/vitify-nuxt.svg
Normal file
6
assets/icons/vitify-nuxt.svg
Normal file
@@ -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 |
64
assets/styles/global.css
Normal file
64
assets/styles/global.css
Normal file
@@ -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%;
|
||||
}
|
||||
2
assets/styles/index.css
Normal file
2
assets/styles/index.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import 'global';
|
||||
@import 'scrollbar';
|
||||
21
assets/styles/scrollbar.css
Normal file
21
assets/styles/scrollbar.css
Normal file
@@ -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;
|
||||
}
|
||||
30
augmentation.d.ts
vendored
Normal file
30
augmentation.d.ts
vendored
Normal file
@@ -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 {}
|
||||
82
components/App/AppBar.vue
Normal file
82
components/App/AppBar.vue
Normal file
@@ -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>
|
||||
135
components/App/AppDrawer.vue
Normal file
135
components/App/AppDrawer.vue
Normal file
@@ -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>
|
||||
40
components/App/AppDrawerItem.vue
Normal file
40
components/App/AppDrawerItem.vue
Normal file
@@ -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>
|
||||
22
components/App/AppFooter.vue
Normal file
22
components/App/AppFooter.vue
Normal file
@@ -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>
|
||||
125
components/App/AppNotification.vue
Normal file
125
components/App/AppNotification.vue
Normal file
@@ -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>
|
||||
41
components/App/AppNotificationItem.vue
Normal file
41
components/App/AppNotificationItem.vue
Normal file
@@ -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>
|
||||
60
components/App/AppSettings.vue
Normal file
60
components/App/AppSettings.vue
Normal file
@@ -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>
|
||||
62
components/Chart/ChartBar.vue
Normal file
62
components/Chart/ChartBar.vue
Normal file
@@ -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>
|
||||
95
components/Chart/ChartLine.vue
Normal file
95
components/Chart/ChartLine.vue
Normal file
@@ -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>
|
||||
34
components/Chart/ChartPie.vue
Normal file
34
components/Chart/ChartPie.vue
Normal file
@@ -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>
|
||||
64
components/Chart/ChartRadar.vue
Normal file
64
components/Chart/ChartRadar.vue
Normal file
@@ -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>
|
||||
45
components/DialogConfirm.vue
Normal file
45
components/DialogConfirm.vue
Normal file
@@ -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>
|
||||
68
components/Div/Avatar.vue
Normal file
68
components/Div/Avatar.vue
Normal file
@@ -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>
|
||||
19
components/Div/IconText.vue
Normal file
19
components/Div/IconText.vue
Normal file
@@ -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>
|
||||
268
components/Form/Lib/Address.vue
Normal file
268
components/Form/Lib/Address.vue
Normal file
@@ -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>
|
||||
56
components/Form/Lib/Communication.vue
Normal file
56
components/Form/Lib/Communication.vue
Normal file
@@ -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>
|
||||
77
components/Form/Lib/HumanName.vue
Normal file
77
components/Form/Lib/HumanName.vue
Normal file
@@ -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>
|
||||
30
components/Form/Lib/Identifier.vue
Normal file
30
components/Form/Lib/Identifier.vue
Normal file
@@ -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>
|
||||
52
components/Form/Lib/ParsedName.vue
Normal file
52
components/Form/Lib/ParsedName.vue
Normal file
@@ -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>
|
||||
72
components/Form/Lib/telecom.vue
Normal file
72
components/Form/Lib/telecom.vue
Normal file
@@ -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>
|
||||
80
components/Form/Patient.vue
Normal file
80
components/Form/Patient.vue
Normal file
@@ -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>
|
||||
222
components/Form/Patient/Create.vue
Normal file
222
components/Form/Patient/Create.vue
Normal file
@@ -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>
|
||||
72
components/Form/PatientV2.vue
Normal file
72
components/Form/PatientV2.vue
Normal file
@@ -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>
|
||||
146
components/Form/Practitioner/Basic.vue
Normal file
146
components/Form/Practitioner/Basic.vue
Normal file
@@ -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>
|
||||
5
components/Form/Practitioner/Test.vue
Normal file
5
components/Form/Practitioner/Test.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<ObjectElement name="test">
|
||||
<TextElement name="text" placeholder="Test Element" />
|
||||
</ObjectElement>
|
||||
</template>
|
||||
30
components/IndexPage.vue
Normal file
30
components/IndexPage.vue
Normal file
@@ -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>
|
||||
82
components/StatsCard.vue
Normal file
82
components/StatsCard.vue
Normal file
@@ -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>
|
||||
9
eslint.config.js
Normal file
9
eslint.config.js
Normal file
@@ -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',
|
||||
},
|
||||
})
|
||||
5
layouts/auth.vue
Normal file
5
layouts/auth.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<!-- <AuthLayout> -->
|
||||
<slot />
|
||||
<!-- </AuthLayout> -->
|
||||
</template>
|
||||
8
layouts/default.vue
Normal file
8
layouts/default.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<AppDrawer />
|
||||
<AppBar />
|
||||
<v-main>
|
||||
<slot />
|
||||
</v-main>
|
||||
<AppFooter />
|
||||
</template>
|
||||
15
lib/prisma.ts
Normal file
15
lib/prisma.ts
Normal file
@@ -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
|
||||
94
nuxt.config.ts
Normal file
94
nuxt.config.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
20372
package-lock.json
generated
Normal file
20372
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
package.json
Normal file
48
package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
10
pages/[...all].vue
Normal file
10
pages/[...all].vue
Normal file
@@ -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>
|
||||
47
pages/auth-info.vue
Normal file
47
pages/auth-info.vue
Normal file
@@ -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>
|
||||
72
pages/auth/login.vue
Normal file
72
pages/auth/login.vue
Normal file
@@ -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>
|
||||
108
pages/dashboard.vue
Normal file
108
pages/dashboard.vue
Normal file
@@ -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>
|
||||
10
pages/forms.vue
Normal file
10
pages/forms.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Forms Routes',
|
||||
icon: 'mdi-view-list',
|
||||
drawerIndex: 4,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
3
pages/forms/index.vue
Normal file
3
pages/forms/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<IndexPage />
|
||||
</template>
|
||||
25
pages/forms/patient.vue
Normal file
25
pages/forms/patient.vue
Normal file
@@ -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>
|
||||
25
pages/forms/practitioner-basic.vue
Normal file
25
pages/forms/practitioner-basic.vue
Normal file
@@ -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>
|
||||
101
pages/forms/vue-form.vue
Normal file
101
pages/forms/vue-form.vue
Normal file
@@ -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
pages/fuse.vue
Normal file
237
pages/fuse.vue
Normal file
@@ -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>
|
||||
45
pages/homepage.vue
Normal file
45
pages/homepage.vue
Normal file
@@ -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>
|
||||
8
pages/index.vue
Normal file
8
pages/index.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
redirect: 'homepage',
|
||||
})
|
||||
</script>
|
||||
10
pages/nested.vue
Normal file
10
pages/nested.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Nested Routes',
|
||||
icon: 'mdi-view-list',
|
||||
drawerIndex: 2,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
3
pages/nested/index.vue
Normal file
3
pages/nested/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<IndexPage />
|
||||
</template>
|
||||
33
pages/nested/menu1.vue
Normal file
33
pages/nested/menu1.vue
Normal file
@@ -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>
|
||||
31
pages/nested/menu2.vue
Normal file
31
pages/nested/menu2.vue
Normal file
@@ -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>
|
||||
7
pages/nested/menu3.vue
Normal file
7
pages/nested/menu3.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script>
|
||||
definePageMeta({
|
||||
title: 'Menu 3',
|
||||
icon: 'mdi-animation',
|
||||
})
|
||||
</script>
|
||||
<template></template>
|
||||
3
pages/nested/menu3/index.vue
Normal file
3
pages/nested/menu3/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<IndexPage />
|
||||
</template>
|
||||
59
pages/nested/menu3/menu1.vue
Normal file
59
pages/nested/menu3/menu1.vue
Normal file
@@ -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>
|
||||
9
pages/nested/menu3/menu2-2.vue
Normal file
9
pages/nested/menu3/menu2-2.vue
Normal file
@@ -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>
|
||||
67
pages/nested/menu3/vuetify-form.vue
Normal file
67
pages/nested/menu3/vuetify-form.vue
Normal file
@@ -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>
|
||||
10
pages/pasiens.vue
Normal file
10
pages/pasiens.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
title: 'Pasien',
|
||||
icon: 'mdi-account-injury-outline',
|
||||
drawerIndex: 4,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
111
pages/pasiens/dataPasien.vue
Normal file
111
pages/pasiens/dataPasien.vue
Normal file
@@ -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>
|
||||
3
pages/pasiens/index.vue
Normal file
3
pages/pasiens/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<IndexPage />
|
||||
</template>
|
||||
11
pages/pasiens/kunjunganPasien.vue
Normal file
11
pages/pasiens/kunjunganPasien.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
|
||||
definePageMeta({
|
||||
title: 'Kunjungan Pasien',
|
||||
icon: 'mdi-account-injury-outline',
|
||||
drawerIndex: 1,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<pre>dddddddd</pre>
|
||||
</template>
|
||||
21
pages/pasiens/tambahPasien.vue
Normal file
21
pages/pasiens/tambahPasien.vue
Normal file
@@ -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>
|
||||
234
pages/practitioner.vue
Normal file
234
pages/practitioner.vue
Normal file
@@ -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
pages/table.vue
Normal file
188
pages/table.vue
Normal file
@@ -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>
|
||||
29
plugins/vuetify.ts
Normal file
29
plugins/vuetify.ts
Normal file
@@ -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 } },
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
10178
pnpm-lock.yaml
generated
Normal file
10178
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
prettier.config.js
Normal file
5
prettier.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
export default {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
}
|
||||
45
prisma/address.prisma
Normal file
45
prisma/address.prisma
Normal file
@@ -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[]
|
||||
// }
|
||||
67
prisma/practitioner.prisma
Normal file
67
prisma/practitioner.prisma
Normal file
@@ -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
|
||||
// }
|
||||
0
prisma/schema.prisma
Normal file
0
prisma/schema.prisma
Normal file
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
56570
public/jsondata/Region_ID/districts.json
Normal file
56570
public/jsondata/Region_ID/districts.json
Normal file
File diff suppressed because it is too large
Load Diff
240
public/jsondata/Region_ID/provinces.json
Normal file
240
public/jsondata/Region_ID/provinces.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
4114
public/jsondata/Region_ID/regencies.json
Normal file
4114
public/jsondata/Region_ID/regencies.json
Normal file
File diff suppressed because it is too large
Load Diff
573547
public/jsondata/Region_ID/villages.json
Normal file
573547
public/jsondata/Region_ID/villages.json
Normal file
File diff suppressed because it is too large
Load Diff
1466
public/jsondata/birth-place.json
Normal file
1466
public/jsondata/birth-place.json
Normal file
File diff suppressed because it is too large
Load Diff
6178
public/jsondata/country-state.json
Normal file
6178
public/jsondata/country-state.json
Normal file
File diff suppressed because it is too large
Load Diff
9
public/jsonforms/basic/data.json
Normal file
9
public/jsonforms/basic/data.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "John Doe",
|
||||
"vegetarian": false,
|
||||
"birthDate": "1985-06-02",
|
||||
"personalData": {
|
||||
"age": 34
|
||||
},
|
||||
"postalCode": "12345"
|
||||
}
|
||||
126
public/jsonforms/basic/i18n.json
Normal file
126
public/jsonforms/basic/i18n.json
Normal file
@@ -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": "Допълнителна информация"
|
||||
}
|
||||
}
|
||||
57
public/jsonforms/basic/schema.json
Normal file
57
public/jsonforms/basic/schema.json
Normal file
@@ -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"]
|
||||
}
|
||||
55
public/jsonforms/basic/uischema.json
Normal file
55
public/jsonforms/basic/uischema.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
public/social-image.png
Normal file
BIN
public/social-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
26
public/vitify-nuxt.svg
Normal file
26
public/vitify-nuxt.svg
Normal file
@@ -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 |
24
server/api/address.bak/cities.get.ts
Normal file
24
server/api/address.bak/cities.get.ts
Normal file
@@ -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' },
|
||||
}
|
||||
}
|
||||
})
|
||||
31
server/api/address.bak/countries.get.ts
Normal file
31
server/api/address.bak/countries.get.ts
Normal file
@@ -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