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