first commit
This commit is contained in:
commit
275153649c
15
.eslintrc.cjs
Normal file
15
.eslintrc.cjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-typescript',
|
||||||
|
'@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}
|
||||||
|
}
|
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# apkt
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||||
|
|
||||||
|
1. Disable the built-in TypeScript Extension
|
||||||
|
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||||
|
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||||
|
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full bg-layout">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/assets/images/pln.ico">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>APKT</title>
|
||||||
|
</head>
|
||||||
|
<body class="h-full ">
|
||||||
|
<div id="app" class="h-full"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
13335
package-lock.json
generated
Normal file
13335
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
58
package.json
Normal file
58
package.json
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"name": "apkt",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"tailwind": "tailwindcss -i ./src/assets/css/tailwind.css -o ./src/assets/css/style.css --watch",
|
||||||
|
"build": "run-p type-check build-only",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"test:unit": "vitest",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
|
"@headlessui/vue": "^1.7.16",
|
||||||
|
"@heroicons/vue": "^2.0.18",
|
||||||
|
"@lottiefiles/lottie-player": "^2.0.2",
|
||||||
|
"@types/uuid": "^9.0.2",
|
||||||
|
"devextreme": "23.1.5",
|
||||||
|
"devextreme-vue": "23.1.5",
|
||||||
|
"pinia": "^2.1.3",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-router": "^4.2.2",
|
||||||
|
"vue-tailwind-datepicker": "^1.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.5.1",
|
||||||
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"@tsconfig/node18": "^2.0.1",
|
||||||
|
"@types/jsdom": "^21.1.1",
|
||||||
|
"@types/node": "^18.16.17",
|
||||||
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||||
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
|
"@vue/test-utils": "^2.3.2",
|
||||||
|
"@vue/tsconfig": "^0.4.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.39.0",
|
||||||
|
"eslint-plugin-vue": "^9.11.0",
|
||||||
|
"jsdom": "^22.1.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"prettier": "^3.0.3",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"typescript": "~5.2.2",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"vitest": "^0.34.6",
|
||||||
|
"vue-tsc": "^1.6.5"
|
||||||
|
}
|
||||||
|
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
83
public/assets/css/loader.css
Normal file
83
public/assets/css/loader.css
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bg {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
background: var(--initial-loader-bg, #fff);
|
||||||
|
block-size: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-logo {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 40%;
|
||||||
|
inset-inline-start: calc(50% - 45px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
block-size: 55px;
|
||||||
|
border-radius: 50%;
|
||||||
|
inline-size: 55px;
|
||||||
|
inset-block-start: 50%;
|
||||||
|
inset-inline-start: calc(50% - 35px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading .effect-1,
|
||||||
|
.loading .effect-2,
|
||||||
|
.loading .effect-3 {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
block-size: 100%;
|
||||||
|
border-inline-start: 3px solid var(--initial-loader-color, #eee);
|
||||||
|
border-radius: 50%;
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading .effect-1 {
|
||||||
|
animation: rotate 1s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading .effect-2 {
|
||||||
|
animation: rotate-opacity 1s ease infinite 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading .effect-3 {
|
||||||
|
animation: rotate-opacity 1s ease infinite 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading .effects {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate-opacity {
|
||||||
|
0% {
|
||||||
|
opacity: 0.1;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(1turn);
|
||||||
|
}
|
||||||
|
}
|
BIN
public/assets/images/favicon.ico
Normal file
BIN
public/assets/images/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
public/assets/images/pln.ico
Normal file
BIN
public/assets/images/pln.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
12
src/App.vue
Normal file
12
src/App.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
import { NotificationProvider } from '@/components/Notification'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NotificationProvider>
|
||||||
|
<RouterView />
|
||||||
|
</NotificationProvider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
96
src/assets/css/base.css
Normal file
96
src/assets/css/base.css
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--vt-c-white: #ffffff;
|
||||||
|
--vt-c-white-soft: #f8f8f8;
|
||||||
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
|
--vt-c-black: #181818;
|
||||||
|
--vt-c-black-soft: #222222;
|
||||||
|
--vt-c-black-mute: #282828;
|
||||||
|
|
||||||
|
--vt-c-indigo: #2c3e50;
|
||||||
|
|
||||||
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
|
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||||
|
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||||
|
|
||||||
|
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||||
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* semantic color variables for this project */
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-white);
|
||||||
|
--color-background-soft: var(--vt-c-white-soft);
|
||||||
|
--color-background-mute: var(--vt-c-white-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-light-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-light-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-light-1);
|
||||||
|
--color-text: var(--vt-c-text-light-1);
|
||||||
|
|
||||||
|
--section-gap: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-background: var(--vt-c-black);
|
||||||
|
--color-background-soft: var(--vt-c-black-soft);
|
||||||
|
--color-background-mute: var(--vt-c-black-mute);
|
||||||
|
|
||||||
|
--color-border: var(--vt-c-divider-dark-2);
|
||||||
|
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||||
|
|
||||||
|
--color-heading: var(--vt-c-text-dark-1);
|
||||||
|
--color-text: var(--vt-c-text-dark-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition: color 0.5s, background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
* {
|
||||||
|
scrollbar-width: 4px;
|
||||||
|
scrollbar-color: var(--secondary) var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome, Edge, and Safari */
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--secondary);
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 3px solid var(--primary);
|
||||||
|
}
|
39
src/assets/css/main.css
Normal file
39
src/assets/css/main.css
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/* @import './base.css'; */
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;700&display=swap');
|
||||||
|
@import './style.css';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* #app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
font-weight: normal;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* a,
|
||||||
|
.green {
|
||||||
|
text-decoration: none;
|
||||||
|
color: hsla(160, 100%, 37%, 1);
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
a:hover {
|
||||||
|
background-color: hsla(160, 100%, 37%, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
} */
|
3117
src/assets/css/style.css
Normal file
3117
src/assets/css/style.css
Normal file
File diff suppressed because it is too large
Load Diff
3
src/assets/css/tailwind.css
Normal file
3
src/assets/css/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
BIN
src/assets/images/hero.png
Normal file
BIN
src/assets/images/hero.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/images/pln-with-text.png
Normal file
BIN
src/assets/images/pln-with-text.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/images/pln.png
Normal file
BIN
src/assets/images/pln.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
445
src/components/Aside.vue
Normal file
445
src/components/Aside.vue
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import Icon from '@/assets/images/pln-with-text.png';
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot,
|
||||||
|
} from '@headlessui/vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoltIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
ComputerDesktopIcon,
|
||||||
|
DocumentDuplicateIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
FolderArrowDownIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
XMarkIcon
|
||||||
|
} from '@heroicons/vue/24/solid'
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'Daftar Transaksi',
|
||||||
|
icon: DocumentDuplicateIcon,
|
||||||
|
href: '/daftar-transaksi',
|
||||||
|
children: [
|
||||||
|
{ name: 'Transaksi Aktif Individual', href: '/daftar-transaksi/aktif-bulanan', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Transaksi Aktif PLN Mobile', href: '/daftar-transaksi/aktif-pln-mobile', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Transaksi Aktif Periksa dan Return Order', href: '/daftar-transaksi/periksa-return-order', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Transaksi Aktif TM', href: '/daftar-transaksi/aktif-tm', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Transaksi Aktif Historis', href: '/daftar-transaksi/aktif-historis', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Alih Unit', href: '/daftar-transaksi/monitoring-alih-unit', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring All', href: '/daftar-transaksi/monitoring-all', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Tiket', href: '/daftar-transaksi/monitoring-tiket', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring History', href: '/daftar-transaksi/monitoring-history', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Aktif', href: '/daftar-transaksi/monitoring-aktif', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Close di CC', href: '/daftar-transaksi/monitoring-close-cc', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Log AutoDispatch', href: '/daftar-transaksi/monitoring-log-autodispatch', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Yang Perlu Diperhatikan', href: '/daftar-transaksi/monitoring-perlu-diperhatikan', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Log Break Regu', href: '/daftar-transaksi/monitoring-log-break-regu', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Log Alih Regu', href: '/daftar-transaksi/monitoring-log-alih-regu', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Administration',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: '/administration',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Audit Trails',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: '/administration/audit-trails',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Log Login',
|
||||||
|
href: '/administration/audit-trails/log-login',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Log Activity',
|
||||||
|
href: '/administration/audit-trails/log-aktivity',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Log Error',
|
||||||
|
href: '/administration/audit-trails/log-error',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Task Management',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: '/administration/task-management',
|
||||||
|
children: [
|
||||||
|
{ name: 'Menu Management', href: '/administration/task-management/menu-management', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'System Parameter', href: '/administration/task-management/system-parameter', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'User Profile Manager', href: '/administration/task-management/ubah-profile-manager', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'Security Projek', href: '/administration/task-management/security-projek', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'User Role Manager', href: '/administration/task-management/user-role-manager', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'Menu Authorization', href: '/administration/task-management/menu-authorization', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'Workflow Designer', href: '/administration/task-management/workflow-designer', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'APKT News', href: '/administration/task-management/apkt-news', icon: ShieldCheckIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Master Borderless',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: '/administration/master-borderless',
|
||||||
|
children: [
|
||||||
|
{ name: 'Zone Borderless', href: '/administration/master-borderless/zone-borderless', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'User Zone Borderless', href: '/administration/master-borderless/user-zone-borderless', icon: ShieldCheckIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Master Yantek',
|
||||||
|
icon: FolderOpenIcon,
|
||||||
|
href: '/master-yantek',
|
||||||
|
children: [
|
||||||
|
{ name: 'Unit', href: '/master-yantek/unit', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Unit Jaringan', href: '/master-yantek/unit-jaringan', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Unit Pelayanan', href: '/master-yantek/unit-pelayanan', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Regu Yantek', href: '/master-yantek/regu-yantek', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Gardu Induk', href: '/master-yantek/gardu-induk', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Gardu Penyulang TM', href: '/master-yantek/gardu-penyulang-tm', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Gardu Distribusi', href: '/master-yantek/gardu-distribusi', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Group Gangguan', href: '/master-yantek/group-gangguan', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Wilayah Yantek', href: '/master-yantek/wilayah-yantek', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Material', href: '/master-yantek/material', icon: FolderOpenIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Transaksi',
|
||||||
|
icon: DocumentTextIcon,
|
||||||
|
href: '/transaksi',
|
||||||
|
children: [
|
||||||
|
{ name: 'Gangguan dan Keluhan', href: '/transaksi/gangguan-dan-keluhan', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Gangguan Tegangan Menengah', href: '/transaksi/gangguan-tegangan-menengah', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Pemadaman Terencana', href: '/transaksi/pemadaman-terencana', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Aplikasi Off-Line', href: '/transaksi/aplikasi-off-line', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Penugasan Khusus', href: '/transaksi/penugasan-khusus', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Mutasi Pengaduan', href: '/transaksi/mutasi-pengaduan', icon: DocumentTextIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Saidi Saifi',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Monitor Jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/monitor-jaringan',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Dashboard Asset',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/dashboard-asset',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Manuver Topologi Jaringan',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/manuver-topologi-jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pencarian Segment Jaringan',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/pencarian-segment-jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Penormalan Manuver Jaringan',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/penormalan-manuver-jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pindah Jaringan',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/pindah-jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Monitoring SCADA',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/monitor-scada/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Monitoring Mapping Database', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Log SCADA', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Gardu Induk', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Feeder', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Zona', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Lateral', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Section', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Segment', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Sub Lateral', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Gardu Distribusi', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Trafo', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Jurusan', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Integrasi ????', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Integrasi per No Tiang', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Master Jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/master-jaringan/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Master Gardu Induk', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Feeder', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Zona', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Section', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Segement', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Lateral', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Gardu Distribusi', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Switch', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Master Perhitungan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/master-perhitungan/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Master Asumsi', href: '/saidi-saifi/master-perhitungan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Asumsi Salatiga', href: '/saidi-saifi/master-perhitungan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Faktor Daya & Beban', href: '/saidi-saifi/master-perhitungan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Data III-09', href: '/saidi-saifi/master-perhitungan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Laporan Saidi Saifi',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/laporan-saidi-saifi/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Laporan Saidi Saifi per Penyulang', href: '/saidi-saifi/laporan-saidi-saifi/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pelanggan Prioritas',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/pelanggan-prioritas/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Daftar Pelanggan Prioritas', href: '/saidi-saifi/pelanggan-prioritas/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Pelanggan Prioritas', href: '/saidi-saifi/pelanggan-prioritas/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Koreksi dan Cleansing',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/koreksi-dan-cleansing/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Monitoring Koreksi dan Cleansing', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Koreksi Transaksi Individual', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Koreksi dan Cleansing TM', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Koreksi Kode Gangguan dan ANEV', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Laporan Koreksi dan Cleansing', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Delegasi Koreksi dan Cleansing', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Utility',
|
||||||
|
icon: BoltIcon,
|
||||||
|
href: '/utility',
|
||||||
|
children: [
|
||||||
|
{ name: 'Piket Obank', href: '/utility/piket-obank', icon: BoltIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EIS',
|
||||||
|
icon: FolderArrowDownIcon,
|
||||||
|
href: '/eis',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pengaturan',
|
||||||
|
icon: Cog6ToothIcon,
|
||||||
|
href: '/pengaturan',
|
||||||
|
children: [
|
||||||
|
{ name: 'Ganti Password', href: '/pengaturan/gantu-password', icon: Cog6ToothIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const menuSelected = ref(route.fullPath);
|
||||||
|
|
||||||
|
watch(route, (to, _) => {
|
||||||
|
menuSelected.value = to.fullPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMenu = (name: string) => {
|
||||||
|
return menuSelected.value === name
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sideBarStatus: Boolean,
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['onChangeSideBarStatus']);
|
||||||
|
const localSideBarStatus = ref(props.sideBarStatus)
|
||||||
|
|
||||||
|
function closeSideBar() {
|
||||||
|
localSideBarStatus.value = false;
|
||||||
|
emits('onChangeSideBarStatus', false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<TransitionRoot as="template" :show="sideBarStatus">
|
||||||
|
<Dialog as="div" class="relative z-40 md:hidden" @close="closeSideBar">
|
||||||
|
<TransitionChild as="template" enter="transition-opacity ease-linear duration-300" enter-from="opacity-0"
|
||||||
|
enter-to="opacity-100" leave="transition-opacity ease-linear duration-300" leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0">
|
||||||
|
<div class="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-40 flex">
|
||||||
|
<TransitionChild as="template" enter="transition ease-in-out duration-300 transform"
|
||||||
|
enter-from="-translate-x-full" enter-to="translate-x-0"
|
||||||
|
leave="transition ease-in-out duration-300 transform" leave-from="translate-x-0"
|
||||||
|
leave-to="-translate-x-full">
|
||||||
|
<DialogPanel class="relative flex flex-col flex-1 w-full max-w-xs pt-5 pb-4 bg-white">
|
||||||
|
<TransitionChild as="template" enter="ease-in-out duration-300" enter-from="opacity-0"
|
||||||
|
enter-to="opacity-100" leave="ease-in-out duration-300" leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0">
|
||||||
|
<div class="absolute top-0 right-0 pt-2 -mr-12">
|
||||||
|
<button type="button"
|
||||||
|
class="flex items-center justify-center w-10 h-10 ml-1 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||||
|
@click="closeSideBar">
|
||||||
|
<span class="sr-only">Close sidebar</span>
|
||||||
|
<XMarkIcon class="w-6 h-6 text-white" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</TransitionChild>
|
||||||
|
<div class="flex items-center flex-shrink-0 px-4">
|
||||||
|
<img class="w-auto h-16" :src="Icon" alt="PLN" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 h-0 mt-5 overflow-y-auto">
|
||||||
|
<nav class="px-2 space-y-1">
|
||||||
|
<RouterLink v-for="item in navigation" :key="item.name" :to="item.href"
|
||||||
|
:class="[isMenu(item.href) ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']">
|
||||||
|
<component :is="item.icon"
|
||||||
|
:class="[isMenu(item.href) ? 'text-gray-500' : 'text-gray-400 group-hover:text-gray-500', 'mr-4 flex-shrink-0 h-6 w-6']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
{{ item.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
<div class="flex-shrink-0 w-14" aria-hidden="true">
|
||||||
|
<!-- Dummy element to force sidebar to shrink to fit close icon -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
<!-- Static sidebar for desktop -->
|
||||||
|
<div class="z-10 hidden bg-gray-600 md:fixed md:inset-y-0 md:flex md:w-80 md:flex-col">
|
||||||
|
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
||||||
|
<div class="flex items-center flex-shrink-0 h-16 px-5">
|
||||||
|
<img class="w-auto h-11" :src="Icon" alt="PLN" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-grow bg-gray-600 hover:overflow-y-auto">
|
||||||
|
<div class="flex flex-col flex-grow mt-5 ">
|
||||||
|
<nav class="flex-1 px-2 pb-4 space-y-1">
|
||||||
|
<template v-for="item in navigation" :key="item.name">
|
||||||
|
<div v-if="!item.children || item.children.length === 0">
|
||||||
|
<!-- Single-level item -->
|
||||||
|
<RouterLink :to="item.href"
|
||||||
|
:class="[isMenu(item.href) ? 'bg-primary-500 text-white font-medium' : 'text-gray-500 hover:bg-primary-100 hover:text-gray-900', 'group w-full flex items-center pl-2 py-2 text-xs font-medium rounded-md']">
|
||||||
|
<component :is="item.icon"
|
||||||
|
:class="[isMenu(item.href) ? 'text-white' : 'text-gray-500 group-hover:text-gray-900', 'mr-3 flex-shrink-0 h-6 w-6']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
{{ item.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Disclosure v-else
|
||||||
|
v-bind:default-open="item.children.find((d) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? true : false">
|
||||||
|
<!-- Nested item with children -->
|
||||||
|
<template v-slot="{ open }">
|
||||||
|
<DisclosureButton
|
||||||
|
:class="[item.children.find((d) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500 font-medium' : 'text-gray-500 hover:bg-primary-100 hover:text-gray-900', 'group w-full flex items-center pl-2 pr-1 py-2 text-left text-xs font-medium rounded-md focus:outline-none focus:ring-0 focus:ring-indigo-500']">
|
||||||
|
<component :is="item.icon"
|
||||||
|
:class="[item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500' : 'text-gray-400 group-hover:text-gray-500', 'flex-shrink-0 w-6 h-6 mr-3']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
<span class="flex-1">{{ item.name }}</span>
|
||||||
|
|
||||||
|
<svg :class="[item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500' : 'text-gray-300 group-hover:text-gray-500', open ? 'rotate-180' : '', 'ml-3 flex-shrink-0 transform transition-colors duration-150 ease-in-out ']"
|
||||||
|
width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M4.44002 8.9399C4.72127 8.659 5.10252 8.50122 5.50002 8.50122C5.89752 8.50122 6.27877 8.659 6.56002 8.9399L12 14.3799L17.44 8.9399C17.7244 8.67494 18.1005 8.53069 18.4891 8.53755C18.8777 8.54441 19.2484 8.70183 19.5233 8.97666C19.7981 9.25148 19.9555 9.62225 19.9624 10.0109C19.9692 10.3995 19.825 10.7756 19.56 11.0599L13.06 17.5599C12.7788 17.8408 12.3975 17.9986 12 17.9986C11.6025 17.9986 11.2213 17.8408 10.94 17.5599L4.44002 11.0599C4.15912 10.7787 4.00134 10.3974 4.00134 9.9999C4.00134 9.6024 4.15912 9.22115 4.44002 8.9399Z"
|
||||||
|
:fill="item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? '#14A2BA' : '#647375'" />
|
||||||
|
</svg>
|
||||||
|
</DisclosureButton>
|
||||||
|
|
||||||
|
<DisclosurePanel class="space-y-1">
|
||||||
|
<!-- Nested children -->
|
||||||
|
<template v-for="subItem in item.children" :key="subItem.name">
|
||||||
|
<div v-if="!subItem.children || subItem.children.length === 0">
|
||||||
|
<!-- Single-level child -->
|
||||||
|
<RouterLink :to="subItem.href"
|
||||||
|
:class="[isMenu(subItem.href) ? 'text-white font-medium bg-primary-500' : 'text-gray-500 hover:bg-primary-100 hover:text-gray-900', 'flex items-center w-11/12 py-2 px-2 text-xs font-medium rounded-md group ml-auto']">
|
||||||
|
{{ subItem.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<Disclosure v-else
|
||||||
|
:default-open="subItem.children.find((d: any) => d.href === menuSelected) ? true : false">
|
||||||
|
<!-- Nested child with children -->
|
||||||
|
<template v-slot="{ open }">
|
||||||
|
<DisclosureButton
|
||||||
|
:class="[subItem.children.find((d: any) => d.href === menuSelected) ? 'text-primary-500 font-medium' : 'text-gray-500 hover:bg-primary-100 hover:text-gray-900', 'group w-11/12 flex items-center px-2 py-2 text-left text-xs font-medium rounded-md focus:outline-none focus:ring-0 focus:ring-indigo-500 ml-auto']">
|
||||||
|
|
||||||
|
<span class="flex-1">{{ subItem.name }}</span>
|
||||||
|
|
||||||
|
<svg :class="[subItem.children.find((d: any) => d.href === menuSelected) ? 'text-primary-500' : 'text-gray-300 group-hover:text-gray-500', open ? 'rotate-180' : '', 'ml-3 flex-shrink-0 transform transition-colors duration-150 ease-in-out ']"
|
||||||
|
width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||||
|
aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M4.44002 8.9399C4.72127 8.659 5.10252 8.50122 5.50002 8.50122C5.89752 8.50122 6.27877 8.659 6.56002 8.9399L12 14.3799L17.44 8.9399C17.7244 8.67494 18.1005 8.53069 18.4891 8.53755C18.8777 8.54441 19.2484 8.70183 19.5233 8.97666C19.7981 9.25148 19.9555 9.62225 19.9624 10.0109C19.9692 10.3995 19.825 10.7756 19.56 11.0599L13.06 17.5599C12.7788 17.8408 12.3975 17.9986 12 17.9986C11.6025 17.9986 11.2213 17.8408 10.94 17.5599L4.44002 11.0599C4.15912 10.7787 4.00134 10.3974 4.00134 9.9999C4.00134 9.6024 4.15912 9.22115 4.44002 8.9399Z"
|
||||||
|
:fill="subItem.children.find((d: any) => d.href === menuSelected) ? '#14A2BA' : '#647375'" />
|
||||||
|
</svg>
|
||||||
|
</DisclosureButton>
|
||||||
|
<DisclosurePanel class="space-y-1 pl-11">
|
||||||
|
<!-- Nested children of nested child -->
|
||||||
|
<RouterLink v-for="nestedSubItem in subItem.children"
|
||||||
|
:key="nestedSubItem.name" :to="nestedSubItem.href"
|
||||||
|
:class="[isMenu(nestedSubItem.href) ? 'text-white font-medium bg-primary-500' : 'text-gray-500 hover:bg-primary-100 hover:text-gray-900', 'flex items-center w-12/12 py-2 px-2 text-xs font-medium rounded-md group ml-auto']">
|
||||||
|
{{ nestedSubItem.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</Disclosure>
|
||||||
|
</template>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</template>
|
||||||
|
</Disclosure>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
288
src/components/AsideWhite.vue
Normal file
288
src/components/AsideWhite.vue
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import Icon from '@/assets/images/pln-with-text.png';
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot,
|
||||||
|
} from '@headlessui/vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
BoltIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
ComputerDesktopIcon,
|
||||||
|
DocumentDuplicateIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
FolderArrowDownIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
XMarkIcon
|
||||||
|
} from '@heroicons/vue/24/outline'
|
||||||
|
import { useMenuStore } from '@/stores/menu';
|
||||||
|
|
||||||
|
const nav = useMenuStore();
|
||||||
|
const navigation = nav.navigation;
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const menuSelected = ref(route.fullPath);
|
||||||
|
|
||||||
|
watch(route, (to, _) => {
|
||||||
|
menuSelected.value = to.fullPath
|
||||||
|
closeSideBar()
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMenu = (name: string) => {
|
||||||
|
return menuSelected.value === name
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sideBarStatus: Boolean,
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['onChangeSideBarStatus']);
|
||||||
|
const localSideBarStatus = ref(props.sideBarStatus)
|
||||||
|
|
||||||
|
function closeSideBar() {
|
||||||
|
localSideBarStatus.value = false;
|
||||||
|
emits('onChangeSideBarStatus', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TransitionRoot as="template" :show="sideBarStatus">
|
||||||
|
<Dialog as="div" class="relative z-40 md:hidden" @close="closeSideBar">
|
||||||
|
<TransitionChild as="template" enter="transition-opacity ease-linear duration-300" enter-from="opacity-0"
|
||||||
|
enter-to="opacity-100" leave="transition-opacity ease-linear duration-300" leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0">
|
||||||
|
<div class="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-40 flex">
|
||||||
|
<TransitionChild as="template" enter="transition ease-in-out duration-300 transform"
|
||||||
|
enter-from="-translate-x-full" enter-to="translate-x-0"
|
||||||
|
leave="transition ease-in-out duration-300 transform" leave-from="translate-x-0"
|
||||||
|
leave-to="-translate-x-full">
|
||||||
|
<DialogPanel class="relative flex flex-col flex-1 w-full max-w-xs pt-5 pb-4 bg-white">
|
||||||
|
<TransitionChild as="template" enter="ease-in-out duration-300" enter-from="opacity-0"
|
||||||
|
enter-to="opacity-100" leave="ease-in-out duration-300" leave-from="opacity-100"
|
||||||
|
leave-to="opacity-0">
|
||||||
|
<div class="absolute top-0 right-0 pt-2 -mr-12">
|
||||||
|
<button type="button"
|
||||||
|
class="flex items-center justify-center w-10 h-10 ml-1 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||||
|
@click="closeSideBar">
|
||||||
|
<span class="sr-only">Close sidebar</span>
|
||||||
|
<XMarkIcon class="w-6 h-6 text-white" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</TransitionChild>
|
||||||
|
<div class="flex items-center flex-shrink-0 px-4">
|
||||||
|
<RouterLink to="/">
|
||||||
|
<img class="w-auto h-16" :src="Icon" alt="PLN" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 h-0 mt-5 overflow-y-auto">
|
||||||
|
<nav class="px-2 space-y-1">
|
||||||
|
<template v-for="item in navigation" :key="item.name">
|
||||||
|
<div v-if="!item.children || item.children.length === 0">
|
||||||
|
<!-- Single-level item -->
|
||||||
|
<RouterLink :to="item.href"
|
||||||
|
:class="[isMenu(item.href) ? 'bg-primary-500 text-white font-bold' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'group w-full flex items-center pl-2 py-2 text-xs rounded-lg']">
|
||||||
|
<component :is="item.icon"
|
||||||
|
:class="[isMenu(item.href) ? 'text-white' : 'text-aside group-hover:text-gray-900', 'mr-3 flex-shrink-0 h-6 w-6']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
{{ item.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Disclosure v-else
|
||||||
|
v-bind:default-open="item.children.find((d) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? true : false">
|
||||||
|
<!-- Nested item with children -->
|
||||||
|
<template v-slot="{ open }">
|
||||||
|
<DisclosureButton
|
||||||
|
:class="[item.children.find((d) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500 font-bold' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'group w-full flex items-center pl-2 pr-1 py-2 text-left text-xs rounded-lg focus:outline-none focus:ring-0 focus:ring-indigo-500']">
|
||||||
|
<component :is="item.icon"
|
||||||
|
:class="[item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500' : 'text-gray-400 group-hover:text-gray-500', 'flex-shrink-0 w-6 h-6 mr-3']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
<span class="flex-1">{{ item.name }}</span>
|
||||||
|
|
||||||
|
<svg :class="[item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500' : 'text-gray-300 group-hover:text-gray-500', open ? 'rotate-180' : '', 'ml-3 flex-shrink-0 transform transition-colors duration-150 ease-in-out ']"
|
||||||
|
width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||||
|
aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M4.44002 8.9399C4.72127 8.659 5.10252 8.50122 5.50002 8.50122C5.89752 8.50122 6.27877 8.659 6.56002 8.9399L12 14.3799L17.44 8.9399C17.7244 8.67494 18.1005 8.53069 18.4891 8.53755C18.8777 8.54441 19.2484 8.70183 19.5233 8.97666C19.7981 9.25148 19.9555 9.62225 19.9624 10.0109C19.9692 10.3995 19.825 10.7756 19.56 11.0599L13.06 17.5599C12.7788 17.8408 12.3975 17.9986 12 17.9986C11.6025 17.9986 11.2213 17.8408 10.94 17.5599L4.44002 11.0599C4.15912 10.7787 4.00134 10.3974 4.00134 9.9999C4.00134 9.6024 4.15912 9.22115 4.44002 8.9399Z"
|
||||||
|
:fill="item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? '#14A2BA' : '#647375'" />
|
||||||
|
</svg>
|
||||||
|
</DisclosureButton>
|
||||||
|
|
||||||
|
<DisclosurePanel class="space-y-1">
|
||||||
|
<!-- Nested children -->
|
||||||
|
<template v-for="subItem in item.children" :key="subItem.name">
|
||||||
|
<div v-if="!subItem.children || subItem.children.length === 0">
|
||||||
|
<!-- Single-level child -->
|
||||||
|
<RouterLink :to="subItem.href"
|
||||||
|
:class="[isMenu(subItem.href) ? 'text-white font-bold bg-primary-500' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'flex items-center w-11/12 py-2 px-2 text-xs rounded-lg group ml-auto']">
|
||||||
|
{{ subItem.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<Disclosure v-else
|
||||||
|
:default-open="subItem.children.find((d: any) => d.href === menuSelected) ? true : false">
|
||||||
|
<!-- Nested child with children -->
|
||||||
|
<template v-slot="{ open }">
|
||||||
|
<DisclosureButton
|
||||||
|
:class="[subItem.children.find((d: any) => d.href === menuSelected) ? 'text-primary-500 font-bold' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'group w-11/12 flex items-center px-2 py-2 text-left text-xs rounded-lg focus:outline-none focus:ring-0 focus:ring-indigo-500 ml-auto']">
|
||||||
|
|
||||||
|
<span class="flex-1">{{ subItem.name }}</span>
|
||||||
|
|
||||||
|
<svg :class="[subItem.children.find((d: any) => d.href === menuSelected) ? 'text-primary-500' : 'text-gray-300 group-hover:text-gray-500', open ? 'rotate-180' : '', 'ml-3 flex-shrink-0 transform transition-colors duration-150 ease-in-out ']"
|
||||||
|
width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||||
|
aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M4.44002 8.9399C4.72127 8.659 5.10252 8.50122 5.50002 8.50122C5.89752 8.50122 6.27877 8.659 6.56002 8.9399L12 14.3799L17.44 8.9399C17.7244 8.67494 18.1005 8.53069 18.4891 8.53755C18.8777 8.54441 19.2484 8.70183 19.5233 8.97666C19.7981 9.25148 19.9555 9.62225 19.9624 10.0109C19.9692 10.3995 19.825 10.7756 19.56 11.0599L13.06 17.5599C12.7788 17.8408 12.3975 17.9986 12 17.9986C11.6025 17.9986 11.2213 17.8408 10.94 17.5599L4.44002 11.0599C4.15912 10.7787 4.00134 10.3974 4.00134 9.9999C4.00134 9.6024 4.15912 9.22115 4.44002 8.9399Z"
|
||||||
|
:fill="subItem.children.find((d: any) => d.href === menuSelected) ? '#14A2BA' : '#647375'" />
|
||||||
|
</svg>
|
||||||
|
</DisclosureButton>
|
||||||
|
<DisclosurePanel class="space-y-1 pl-11">
|
||||||
|
<!-- Nested children of nested child -->
|
||||||
|
<RouterLink v-for="nestedSubItem in subItem.children"
|
||||||
|
:key="nestedSubItem.name" :to="nestedSubItem.href"
|
||||||
|
:class="[isMenu(nestedSubItem.href) ? 'text-white font-bold bg-primary-500' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'flex items-center w-12/12 py-2 px-2 text-xs rounded-lg group ml-auto']">
|
||||||
|
{{ nestedSubItem.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</Disclosure>
|
||||||
|
</template>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</template>
|
||||||
|
</Disclosure>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
<div class="flex-shrink-0 w-14" aria-hidden="true">
|
||||||
|
<!-- Dummy element to force sidebar to shrink to fit close icon -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
<!-- Static sidebar for desktop -->
|
||||||
|
<div class="z-10 hidden bg-white md:fixed md:inset-y-0 md:flex md:w-80 md:flex-col">
|
||||||
|
<!-- Sidebar component, swap this element with another sidebar if you like -->
|
||||||
|
<div class="flex items-center flex-shrink-0 h-16 px-5 border-b border-gray-100 border-dashed">
|
||||||
|
<RouterLink to="/">
|
||||||
|
<img class="w-auto h-11" :src="Icon" alt="PLN" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-grow hover:overflow-y-auto">
|
||||||
|
<div class="flex flex-col flex-grow mt-5">
|
||||||
|
<nav class="flex-1 px-2 pb-4 space-y-1">
|
||||||
|
<template v-for="item in navigation" :key="item.name">
|
||||||
|
<div v-if="!item.children || item.children.length === 0">
|
||||||
|
<!-- Single-level item -->
|
||||||
|
<RouterLink :to="item.href"
|
||||||
|
:class="[isMenu(item.href) ? 'bg-primary-500 text-white font-bold' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'group w-full flex items-center pl-2 py-2 text-xs rounded-lg']">
|
||||||
|
<component :is="item.icon"
|
||||||
|
:class="[isMenu(item.href) ? 'text-white' : 'text-aside group-hover:text-gray-900', 'mr-3 flex-shrink-0 h-6 w-6']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
{{ item.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Disclosure v-slot="{ open }" v-else
|
||||||
|
v-bind:default-open="item.children.find((d) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? true : false">
|
||||||
|
<!-- Nested item with children -->
|
||||||
|
<DisclosureButton
|
||||||
|
:class="[item.children.find((d) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500 font-bold' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'group w-full flex items-center pl-2 pr-1 py-2 text-left text-xs rounded-lg focus:outline-none focus:ring-0 focus:ring-indigo-500']">
|
||||||
|
<component :is="item.icon"
|
||||||
|
:class="[item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500' : 'text-gray-400 group-hover:text-gray-500', 'flex-shrink-0 w-6 h-6 mr-3']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
<span class="flex-1">{{ item.name }}</span>
|
||||||
|
|
||||||
|
<svg :class="[item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? 'text-primary-500' : 'text-gray-300 group-hover:text-gray-500', open ? 'rotate-180' : '', 'ml-3 flex-shrink-0 transform transition-colors duration-150 ease-in-out ']"
|
||||||
|
width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M4.44002 8.9399C4.72127 8.659 5.10252 8.50122 5.50002 8.50122C5.89752 8.50122 6.27877 8.659 6.56002 8.9399L12 14.3799L17.44 8.9399C17.7244 8.67494 18.1005 8.53069 18.4891 8.53755C18.8777 8.54441 19.2484 8.70183 19.5233 8.97666C19.7981 9.25148 19.9555 9.62225 19.9624 10.0109C19.9692 10.3995 19.825 10.7756 19.56 11.0599L13.06 17.5599C12.7788 17.8408 12.3975 17.9986 12 17.9986C11.6025 17.9986 11.2213 17.8408 10.94 17.5599L4.44002 11.0599C4.15912 10.7787 4.00134 10.3974 4.00134 9.9999C4.00134 9.6024 4.15912 9.22115 4.44002 8.9399Z"
|
||||||
|
:fill="item.children.find((d: any) => d.href === menuSelected) || item.children.find((d) => d.children.find((e) => e.href === menuSelected)) ? '#14A2BA' : '#647375'" />
|
||||||
|
</svg>
|
||||||
|
</DisclosureButton>
|
||||||
|
<!-- <transition enter-active-class="transition duration-500 transform ease"
|
||||||
|
enter-from-class="-translate-y-12 opacity-0" enter-to-class="translate-y-0 opacity-100"
|
||||||
|
leave-active-class="duration-300 transform dtransition ease"
|
||||||
|
leave-from-class="translate-y-0 opacity-100" leave-to-class="-translate-y-12 opacity-0"> -->
|
||||||
|
<transition enter-active-class="overflow-hidden transition-all duration-300"
|
||||||
|
enter-from-class="transform scale-95 opacity-0 max-h-0"
|
||||||
|
enter-to-class="transform scale-300 opacity-300 max-h-[1000px]"
|
||||||
|
leave-active-class="overflow-hidden transition-all duration-300"
|
||||||
|
leave-from-class="transform scale-300 opacity-300 max-h-[1000px]"
|
||||||
|
leave-to-class="transform scale-95 opacity-0 max-h-0">
|
||||||
|
<DisclosurePanel class="space-y-1">
|
||||||
|
<!-- Nested children -->
|
||||||
|
<template v-for="(subItem, index) in item.children" :key="subItem.href">
|
||||||
|
<div v-if="!subItem.children || subItem.children.length === 0">
|
||||||
|
<!-- Single-level child -->
|
||||||
|
<RouterLink :to="subItem.href"
|
||||||
|
:class="[isMenu(subItem.href) ? 'text-white font-bold bg-primary-500' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'flex items-center w-11/12 py-2 px-2 text-xs rounded-lg group ml-auto']">
|
||||||
|
{{ subItem.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<Disclosure :key="subItem.href + index" v-slot="{ open }" v-else
|
||||||
|
:default-open="subItem.children.find((d: any) => d.href === menuSelected) ? true : false">
|
||||||
|
<!-- Nested child with children -->
|
||||||
|
<DisclosureButton
|
||||||
|
:class="[subItem.children.find((d: any) => d.href === menuSelected) ? 'text-primary-500 font-bold' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'group w-11/12 flex items-center px-2 py-2 text-left text-xs rounded-lg focus:outline-none focus:ring-0 focus:ring-indigo-500 ml-auto']">
|
||||||
|
<span class="flex-1">{{ subItem.name }}</span>
|
||||||
|
<svg :class="[subItem.children.find((d: any) => d.href === menuSelected) ? 'text-primary-500' : 'text-gray-300 group-hover:text-gray-500', open ? 'rotate-180' : '', 'ml-3 flex-shrink-0 transform transition-colors duration-150 ease-in-out ']"
|
||||||
|
width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||||
|
aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M4.44002 8.9399C4.72127 8.659 5.10252 8.50122 5.50002 8.50122C5.89752 8.50122 6.27877 8.659 6.56002 8.9399L12 14.3799L17.44 8.9399C17.7244 8.67494 18.1005 8.53069 18.4891 8.53755C18.8777 8.54441 19.2484 8.70183 19.5233 8.97666C19.7981 9.25148 19.9555 9.62225 19.9624 10.0109C19.9692 10.3995 19.825 10.7756 19.56 11.0599L13.06 17.5599C12.7788 17.8408 12.3975 17.9986 12 17.9986C11.6025 17.9986 11.2213 17.8408 10.94 17.5599L4.44002 11.0599C4.15912 10.7787 4.00134 10.3974 4.00134 9.9999C4.00134 9.6024 4.15912 9.22115 4.44002 8.9399Z"
|
||||||
|
:fill="subItem.children.find((d: any) => d.href === menuSelected) ? '#14A2BA' : '#647375'" />
|
||||||
|
</svg>
|
||||||
|
</DisclosureButton>
|
||||||
|
<transition enter-active-class="overflow-hidden transition-all duration-300"
|
||||||
|
enter-from-class="transform scale-95 opacity-0 max-h-0"
|
||||||
|
enter-to-class="transform scale-300 opacity-300 max-h-[1000px]"
|
||||||
|
leave-active-class="overflow-hidden transition-all duration-300"
|
||||||
|
leave-from-class="transform scale-300 opacity-300 max-h-[1000px]"
|
||||||
|
leave-to-class="transform scale-95 opacity-0 max-h-0">
|
||||||
|
<DisclosurePanel class="space-y-1 pl-11">
|
||||||
|
<!-- Nested children of nested child -->
|
||||||
|
<RouterLink v-for="nestedSubItem in subItem.children"
|
||||||
|
:key="nestedSubItem.href" :to="nestedSubItem.href"
|
||||||
|
:class="[isMenu(nestedSubItem.href) ? 'text-white font-bold bg-primary-500' : 'font-semibold text-aside hover:bg-primary-100 hover:text-gray-900', 'flex items-center w-12/12 py-2 px-2 text-xs rounded-lg group ml-auto']">
|
||||||
|
{{ nestedSubItem.name }}
|
||||||
|
</RouterLink>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
|
||||||
|
</Disclosure>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</DisclosurePanel>
|
||||||
|
</transition>
|
||||||
|
|
||||||
|
</Disclosure>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
71
src/components/ButtonDropdown.vue
Normal file
71
src/components/ButtonDropdown.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
|
||||||
|
import { ChevronDownIcon } from '@heroicons/vue/20/solid'
|
||||||
|
import { ref, type PropType } from 'vue';
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
const props = defineProps({
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
textStyle: {
|
||||||
|
type: String,
|
||||||
|
default: "text-sm font-medium text-gray-700",
|
||||||
|
},
|
||||||
|
buttonStyle: {
|
||||||
|
type: String,
|
||||||
|
default: "rounded-md border border-gray-300 bg-white",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: Array as PropType<Data[]>,
|
||||||
|
default: () => [
|
||||||
|
{ name: 'Pilih Data' },
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
checked: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:checked"]);
|
||||||
|
|
||||||
|
const searchByChecked = ref(props.checked)
|
||||||
|
|
||||||
|
function changeSearchBy(index: number) {
|
||||||
|
searchByChecked.value = index
|
||||||
|
emit("update:checked", index);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Menu as="div" class="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<MenuButton
|
||||||
|
:class="['inline-flex items-center justify-center w-full px-2 py-2 rounded-md shadow-sm focus:outline-none focus:ring-0', textStyle, buttonStyle, className]">
|
||||||
|
{{ props.data[searchByChecked].name }}
|
||||||
|
<ChevronDownIcon class="w-4 h-4 ml-2 -mr-1" aria-hidden="true" />
|
||||||
|
</MenuButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<transition enter-active-class="transition duration-100 ease-out" enter-from-class="transform scale-95 opacity-0"
|
||||||
|
enter-to-class="transform scale-100 opacity-100" leave-active-class="transition duration-75 ease-in"
|
||||||
|
leave-from-class="transform scale-100 opacity-100" leave-to-class="transform scale-95 opacity-0">
|
||||||
|
<MenuItems
|
||||||
|
class="absolute left-0 z-20 w-56 mt-2 origin-top-left bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<div class="py-1">
|
||||||
|
<MenuItem v-slot="{ active }" v-for="(item, index) in props.data">
|
||||||
|
<a @click="changeSearchBy(index)"
|
||||||
|
:class="[active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', 'cursor-pointer block px-4 py-2 text-sm']">
|
||||||
|
{{ item.name }}
|
||||||
|
</a>
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</MenuItems>
|
||||||
|
</transition>
|
||||||
|
</Menu>
|
||||||
|
</template>
|
43
src/components/ButtonPrimary.vue
Normal file
43
src/components/ButtonPrimary.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
type: {
|
||||||
|
type: String as () => "button" | "submit" | "reset",
|
||||||
|
default: "button",
|
||||||
|
},
|
||||||
|
onClick: {
|
||||||
|
type: Function as unknown as () => (payload: MouseEvent) => void,
|
||||||
|
default: () => { },
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button :type="type" @click="onClick" :disabled="disabled"
|
||||||
|
:class="['px-4 py-2 mt-2 text-sm font-bold text-white border-transparent rounded-lg bg-primary-500 focus:outline-0 disabled:bg-primary-400', className]">
|
||||||
|
<slot></slot>
|
||||||
|
<span v-if="!isLoading">{{ label }}</span>
|
||||||
|
<span v-else>
|
||||||
|
<center>
|
||||||
|
<lottie-player autoplay loop mode="normal"
|
||||||
|
src="https://assets2.lottiefiles.com/packages/lf20_lNbR6W7kY6.json"
|
||||||
|
style="height:1.2rem"></lottie-player>
|
||||||
|
</center>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
188
src/components/CommandPalettes.vue
Normal file
188
src/components/CommandPalettes.vue
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
<template>
|
||||||
|
<TransitionRoot :show="open" as="template" @after-leave="onQueryChange('')" appear>
|
||||||
|
<Dialog as="div" class="relative z-10" @close="onClose">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
|
||||||
|
leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
||||||
|
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-25 backdrop-blur" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-10 p-4 overflow-y-auto sm:p-6 md:p-20">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0 scale-95"
|
||||||
|
enter-to="opacity-100 scale-100" leave="ease-in duration-200" leave-from="opacity-100 scale-100"
|
||||||
|
leave-to="opacity-0 scale-95">
|
||||||
|
<DialogPanel
|
||||||
|
class="max-w-2xl mx-auto overflow-hidden transition-all transform bg-white divide-y divide-gray-500 shadow-2xl divide-opacity-10 rounded-xl bg-opacity-80 ring-0 ring-black ring-opacity-5">
|
||||||
|
<Combobox>
|
||||||
|
<div class="relative">
|
||||||
|
<MagnifyingGlassIcon
|
||||||
|
class="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||||
|
aria-hidden="true" />
|
||||||
|
<ComboboxInput :autofocus="false"
|
||||||
|
class="w-full h-12 pr-4 text-gray-900 placeholder-gray-500 bg-white border-0 pl-11 focus:ring-0 sm:text-sm"
|
||||||
|
placeholder="Cari menu..." @change="onQueryChange($event.target.value)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ComboboxOptions v-if="query === '' || filteredMenus.length > 0" static
|
||||||
|
class="overflow-y-auto divide-y divide-gray-500 max-h-80 scroll-py-2 divide-opacity-10">
|
||||||
|
<li class="p-2" v-if="filteredMenus.length > 0 || recent.length > 0">
|
||||||
|
<h2 v-if="query === '' && recent.length > 0"
|
||||||
|
class="px-3 mt-4 mb-2 text-xs font-semibold text-gray-900">
|
||||||
|
Pencarian terakhir</h2>
|
||||||
|
<ul class="text-sm text-gray-700">
|
||||||
|
<ComboboxOption as="template" v-for="menu in query === '' ? recent : filteredMenus"
|
||||||
|
:key="menu.href" v-slot="{ active }">
|
||||||
|
<li @click="openMenu(menu)"
|
||||||
|
:class="['flex cursor-pointer select-none items-center rounded-md px-3 py-2', active && 'bg-gray-900 bg-opacity-5 text-gray-900']">
|
||||||
|
<component :is="menu.icon"
|
||||||
|
:class="['h-6 w-6 flex-none text-gray-900 text-opacity-40', active && 'text-opacity-100']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
<span class="flex-auto ml-3 truncate">
|
||||||
|
{{ menu.name }}
|
||||||
|
</span>
|
||||||
|
<span v-if="active" class="flex-none ml-3 text-gray-500">
|
||||||
|
Buka
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ComboboxOptions>
|
||||||
|
|
||||||
|
<div v-if="query !== '' && filteredMenus.length === 0" class="px-6 text-center py-14 sm:px-14">
|
||||||
|
<!-- <FolderIcon class="w-6 h-6 mx-auto text-gray-900 text-opacity-40" aria-hidden="true" /> -->
|
||||||
|
<h2 class="font-semibold text-slate-900">Tidak ada hasil</h2>
|
||||||
|
|
||||||
|
<p class="mt-2 text-sm leading-6 text-slate-600">
|
||||||
|
Kami tidak dapat menemukan menu apa pun dengan istilah itu saat ini, silahkan gunakan
|
||||||
|
kata kunci lainnya.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { MagnifyingGlassIcon } from '@heroicons/vue/20/solid'
|
||||||
|
import { FolderIcon } from '@heroicons/vue/24/outline'
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxOptions,
|
||||||
|
ComboboxOption,
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot,
|
||||||
|
} from '@headlessui/vue'
|
||||||
|
import { useMenuStore } from '@/stores/menu';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
// const menus = [
|
||||||
|
// { id: 1, name: 'Workflow Inc. / Website Redesign', url: '#' },
|
||||||
|
// ]
|
||||||
|
const nav = useMenuStore()
|
||||||
|
const menus = nav.navigation
|
||||||
|
|
||||||
|
// var recent = computed(() => readRecentMenu())
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: Boolean
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['onClose'])
|
||||||
|
|
||||||
|
function searchMenus(query: string) {
|
||||||
|
const result: typeof menus = []
|
||||||
|
menus.forEach((menu) => {
|
||||||
|
if (menu.children.length > 0) {
|
||||||
|
menu.children.forEach((child) => {
|
||||||
|
if (child.children.length > 0) {
|
||||||
|
child.children.forEach((grandChild) => {
|
||||||
|
if (grandChild.name.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
result.push(grandChild)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (child.name.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
result.push(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (menu.name.toLowerCase().includes(query.toLowerCase())) {
|
||||||
|
result.push(menu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRouter()
|
||||||
|
const recent = computed(() => query.value === '' ? readRecentMenu() : [])
|
||||||
|
const open = ref(props.open)
|
||||||
|
const query = ref('')
|
||||||
|
const filteredMenus = computed(() =>
|
||||||
|
query.value === ''
|
||||||
|
? []
|
||||||
|
: searchMenus(query.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
open.value = false
|
||||||
|
emit('onClose')
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMenuToRecent(menu: any) {
|
||||||
|
const lastRecent = readRecentMenu()
|
||||||
|
const index = lastRecent.findIndex((item: any) => item.href === menu.href)
|
||||||
|
if (index > -1) {
|
||||||
|
lastRecent.splice(index, 1)
|
||||||
|
}
|
||||||
|
lastRecent.unshift(menu)
|
||||||
|
if (lastRecent.length > 5) {
|
||||||
|
lastRecent.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('recentmenu', JSON.stringify(lastRecent))
|
||||||
|
}
|
||||||
|
let debounceTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
function onQueryChange(value: string) {
|
||||||
|
if (debounceTimeout) {
|
||||||
|
clearTimeout(debounceTimeout);
|
||||||
|
}
|
||||||
|
debounceTimeout = setTimeout(() => {
|
||||||
|
// check if value is empty or only spaces
|
||||||
|
if (value.trim() === '') {
|
||||||
|
query.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query.value = value
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readRecentMenu() {
|
||||||
|
const recent = localStorage.getItem('recentmenu')
|
||||||
|
if (recent) {
|
||||||
|
return JSON.parse(recent)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMenu(menu: typeof menus[0]) {
|
||||||
|
addMenuToRecent(menu)
|
||||||
|
route.push(menu.href)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.open, (value) => {
|
||||||
|
open.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
202
src/components/Dialogs/ActionDialog.vue
Normal file
202
src/components/Dialogs/ActionDialog.vue
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ExclamationTriangleIcon, InformationCircleIcon, QuestionMarkCircleIcon,
|
||||||
|
} from '@heroicons/vue/24/outline'
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
open: Boolean,
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
cancelText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Batal',
|
||||||
|
},
|
||||||
|
confirmText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Oke',
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'normal',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['onClose', 'onCancel', 'onConfirm']);
|
||||||
|
const open = ref(props.open)
|
||||||
|
const title = ref(props.title)
|
||||||
|
const message = ref(props.message)
|
||||||
|
const cancelText = ref(props.cancelText)
|
||||||
|
const confirmText = ref(props.confirmText)
|
||||||
|
const type = ref(props.type)
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
open.value = false;
|
||||||
|
emits('onClose', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnCancel() {
|
||||||
|
emits('onCancel');
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnConfirm() {
|
||||||
|
emits('onConfirm');
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.open, (value) => {
|
||||||
|
open.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Log Out Dialog -->
|
||||||
|
<TransitionRoot as="template" :show="open">
|
||||||
|
<Dialog as="div" class="relative z-20" @close="close">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
|
||||||
|
leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
||||||
|
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div class="flex items-center justify-center min-h-full p-4 text-center sm:p-0">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300"
|
||||||
|
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
|
||||||
|
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||||
|
<DialogPanel
|
||||||
|
:class="[type === 'error' || type === 'warning' ? 'sm:max-w-lg' : 'sm:max-w-sm sm:p-6', 'relative overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:w-full']">
|
||||||
|
<!-- Body Section -->
|
||||||
|
<div v-if="type === 'error'" class="px-4 pt-5 pb-4 bg-white sm:p-6 sm:pb-4">
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-red-100 rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<ExclamationTriangleIcon class="w-6 h-6 text-red-600" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||||
|
<DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900">
|
||||||
|
{{ title }}
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type === 'warning'" class="px-4 pt-5 pb-4 bg-white sm:p-6 sm:pb-4">
|
||||||
|
<div class="sm:flex sm:items-start">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-orange-100 rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<QuestionMarkCircleIcon class="w-6 h-6 text-orange-600" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||||
|
<DialogTitle as="h3" class="text-base font-semibold leading-6 text-gray-900">
|
||||||
|
{{ title }}
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type === 'info'">
|
||||||
|
<div class="flex items-center justify-center w-12 h-12 mx-auto bg-blue-100 rounded-full">
|
||||||
|
<InformationCircleIcon class="w-6 h-6 text-blue-600" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:mt-5">
|
||||||
|
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-gray-900">
|
||||||
|
{{ title }}
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type === 'success'">
|
||||||
|
<div class="flex items-center justify-center w-12 h-12 mx-auto bg-green-100 rounded-full">
|
||||||
|
<CheckIcon class="w-6 h-6 text-green-600" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-center sm:mt-5">
|
||||||
|
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-gray-900">
|
||||||
|
{{ title }}
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div class="mt-3 text-center sm:mt-5">
|
||||||
|
<DialogTitle as="h3" class="text-lg font-medium leading-6 text-gray-900">
|
||||||
|
{{ title }}
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="mt-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Footer Section -->
|
||||||
|
<div v-if="type === 'error'" class="px-4 py-3 bg-gray-50 sm:flex sm:flex-row-reverse sm:px-6">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex justify-center w-full px-3 py-2 text-sm font-semibold text-white bg-red-600 rounded-md shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
|
||||||
|
@click="handleOnConfirm()">{{ confirmText }}</button>
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex justify-center w-full px-3 py-2 mt-3 text-sm font-semibold text-gray-900 bg-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
||||||
|
@click="handleOnCancel()" ref="cancelButtonRef">
|
||||||
|
{{ cancelText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type === 'warning'"
|
||||||
|
class="px-4 py-3 bg-gray-50 sm:flex sm:flex-row-reverse sm:px-6">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex justify-center w-full px-3 py-2 text-sm font-semibold text-white bg-orange-600 rounded-md shadow-sm hover:bg-orange-500 sm:ml-3 sm:w-auto"
|
||||||
|
@click="handleOnConfirm()">{{ confirmText }}</button>
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex justify-center w-full px-3 py-2 mt-3 text-sm font-semibold text-gray-900 bg-white rounded-md shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
||||||
|
@click="handleOnCancel()" ref="cancelButtonRef">
|
||||||
|
{{ cancelText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type === 'info'" class="mt-5 sm:mt-6">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex justify-center w-full px-4 py-2 text-base font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:text-sm"
|
||||||
|
@click="handleOnConfirm()">
|
||||||
|
{{ confirmText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="type === 'success'" class="mt-5 sm:mt-6">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:text-sm"
|
||||||
|
@click="handleOnConfirm()">
|
||||||
|
{{ confirmText }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex justify-center w-full px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:col-start-2 sm:text-sm"
|
||||||
|
@click="handleOnConfirm()">{{ confirmText }}</button>
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex justify-center w-full px-4 py-2 mt-3 text-base font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:col-start-1 sm:mt-0 sm:text-sm"
|
||||||
|
@click="handleOnCancel()" ref="cancelButtonRef">{{ cancelText }}</button>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
148
src/components/Dialogs/ChangePasswordDialog.vue
Normal file
148
src/components/Dialogs/ChangePasswordDialog.vue
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ExclamationTriangleIcon, InformationCircleIcon, QuestionMarkCircleIcon,
|
||||||
|
} from '@heroicons/vue/24/outline'
|
||||||
|
import { XMarkIcon } from '@heroicons/vue/24/solid';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import InputText from '../InputText.vue';
|
||||||
|
import ButtonPrimary from '../ButtonPrimary.vue';
|
||||||
|
import { dispatchNotification } from '../Notification';
|
||||||
|
|
||||||
|
const props = defineProps({ open: Boolean });
|
||||||
|
const emits = defineEmits(['onClose']);
|
||||||
|
const open = ref(props.open)
|
||||||
|
const oldPassword = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const passwordConfirmation = ref('');
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
open.value = false;
|
||||||
|
emits('onClose', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoading() {
|
||||||
|
isLoading.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideLoading() {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnChangePassword() {
|
||||||
|
if (oldPassword.value == '') {
|
||||||
|
dispatchNotification({
|
||||||
|
title: 'Peringatan',
|
||||||
|
content: 'Password lama tidak boleh kosong',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} else if (password.value == '') {
|
||||||
|
dispatchNotification({
|
||||||
|
title: 'Peringatan',
|
||||||
|
content: 'Password baru tidak boleh kosong',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} else if (passwordConfirmation.value == '') {
|
||||||
|
dispatchNotification({
|
||||||
|
title: 'Peringatan',
|
||||||
|
content: 'Konfirmasi password tidak boleh kosong',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} else if (password.value != passwordConfirmation.value) {
|
||||||
|
dispatchNotification({
|
||||||
|
title: 'Peringatan',
|
||||||
|
content: 'Password baru dan konfirmasi password tidak sama',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} else if (oldPassword.value == password.value) {
|
||||||
|
dispatchNotification({
|
||||||
|
title: 'Peringatan',
|
||||||
|
content: 'Password baru tidak boleh sama dengan password lama',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showLoading();
|
||||||
|
setTimeout(() => {
|
||||||
|
hideLoading();
|
||||||
|
close();
|
||||||
|
}, 15000);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.open, (value) => {
|
||||||
|
open.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Log Out Dialog -->
|
||||||
|
<TransitionRoot as="template" :show="open">
|
||||||
|
<Dialog as="div" class="relative z-20" @close="close">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300" enter-from="opacity-0" enter-to="opacity-100"
|
||||||
|
leave="ease-in duration-200" leave-from="opacity-100" leave-to="opacity-0">
|
||||||
|
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" />
|
||||||
|
</TransitionChild>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div class="flex items-center justify-center min-h-full p-4 text-center sm:p-0">
|
||||||
|
<TransitionChild as="template" enter="ease-out duration-300"
|
||||||
|
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enter-to="opacity-100 translate-y-0 sm:scale-100" leave="ease-in duration-200"
|
||||||
|
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||||
|
<DialogPanel
|
||||||
|
class="relative p-4 overflow-hidden text-left transition-all transform bg-white rounded-lg shadow-xl sm:max-w-sm sm:p-6 sm:my-8 sm:w-full">
|
||||||
|
<form @submit.prevent="handleOnChangePassword" class="flex flex-col">
|
||||||
|
<div class="absolute top-0 right-0 pt-4 pr-4 ">
|
||||||
|
<button type="button"
|
||||||
|
class="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-0"
|
||||||
|
@click="close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<XMarkIcon class="w-6 h-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DialogTitle as="h4" class="font-medium leading-6 text-gray-900 text-md">
|
||||||
|
Ubah Password
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="flex flex-col mt-2">
|
||||||
|
<label for="password" class="mb-2 text-xs font-medium text-dark">
|
||||||
|
Password Lama
|
||||||
|
</label>
|
||||||
|
<InputText type="password" class-name="mb-3 text-sm placeholder:text-sm"
|
||||||
|
placeholder="Masukan Password lama" :value="oldPassword"
|
||||||
|
@update:value="oldPassword = $event" />
|
||||||
|
|
||||||
|
<label for="password" class="mb-2 text-xs font-medium text-dark">
|
||||||
|
Password Baru
|
||||||
|
</label>
|
||||||
|
<InputText class-name="mb-3 text-sm placeholder:text-sm" type="password"
|
||||||
|
placeholder="Masukan Password baru" :value="password"
|
||||||
|
@update:value="password = $event" />
|
||||||
|
|
||||||
|
<label for="password" class="mb-2 text-xs font-medium text-dark">
|
||||||
|
Konfirmasi Password
|
||||||
|
</label>
|
||||||
|
<InputText class-name="mb-3 text-sm placeholder:text-sm" type="password"
|
||||||
|
placeholder="Konfirmasi Password" :value="passwordConfirmation"
|
||||||
|
@update:value="passwordConfirmation = $event" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Footer Section -->
|
||||||
|
<div class="mt-2">
|
||||||
|
<ButtonPrimary
|
||||||
|
class-name="inline-flex justify-center w-full px-4 py-2 font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm text-md focus:outline-none focus:ring-0 sm:text-sm"
|
||||||
|
type="submit" label="Ubah Password" :disabled="isLoading" :is-loading="isLoading" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
178
src/components/Header.vue
Normal file
178
src/components/Header.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import CommandPalettes from '@/components/CommandPalettes.vue';
|
||||||
|
import MessageBox from '@/components/MessageBox.vue';
|
||||||
|
import ActionDialog from '@/components/Dialogs/ActionDialog.vue';
|
||||||
|
import ChangePasswordDialog from '@/components/Dialogs/ChangePasswordDialog.vue';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import { useCommandPalattesStore } from '@/stores/command';
|
||||||
|
import { useMessageStore } from '@/stores/message';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuItems,
|
||||||
|
} from '@headlessui/vue'
|
||||||
|
import {
|
||||||
|
BookOpenIcon,
|
||||||
|
ChatBubbleOvalLeftEllipsisIcon,
|
||||||
|
QuestionMarkCircleIcon,
|
||||||
|
Bars3BottomLeftIcon
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
import { MagnifyingGlassIcon } from '@heroicons/vue/24/solid';
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
import PictureInitial from './PictureInitial.vue';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const messageStore = useMessageStore();
|
||||||
|
const commandStore = useCommandPalattesStore();
|
||||||
|
|
||||||
|
const props = defineProps({ sideBarStatus: Boolean });
|
||||||
|
const status = ref(props.sideBarStatus);
|
||||||
|
const emits = defineEmits(['onChangeSideBarStatus']);
|
||||||
|
|
||||||
|
function openSideBar() {
|
||||||
|
status.value = true;
|
||||||
|
emits('onChangeSideBarStatus', true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogLogout = ref(false);
|
||||||
|
const dialogChangePassword = ref(false);
|
||||||
|
|
||||||
|
function handleOnDismissDialogChangePassword() {
|
||||||
|
dialogChangePassword.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDialogChangePassword() {
|
||||||
|
dialogChangePassword.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnDismissDialogLogout() {
|
||||||
|
dialogLogout.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDialogLogout() {
|
||||||
|
dialogLogout.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnConfirmDialogLogout() {
|
||||||
|
authStore.logout()
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// document.addEventListener('keydown', commandStore.onKeyPressed);
|
||||||
|
// document.addEventListener('keyup', commandStore.onKeyUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// document.removeEventListener('keydown', commandStore.onKeyPressed);
|
||||||
|
// document.removeEventListener('keyup', commandStore.onKeyUp);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="sticky top-0 z-10 flex flex-shrink-0 h-16 bg-white shadow-lg shadow-gray-50 md:ml-80">
|
||||||
|
<button type="button" class="px-4 text-gray-500 border-gray-200 focus:outline-none focus:ring-0 md:hidden"
|
||||||
|
@click="openSideBar">
|
||||||
|
<span class="sr-only">Open sidebar</span>
|
||||||
|
<Bars3BottomLeftIcon class="w-6 h-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<!-- <div class="flex items-center flex-shrink-0 md:hidden">
|
||||||
|
<img class="w-auto h-11" :src="Icon" alt="PLN" />
|
||||||
|
</div> -->
|
||||||
|
<!-- <div class="flex items-center flex-shrink-0 hidden my-auto ml-4 md:block">
|
||||||
|
<img class="w-auto h-11" :src="IconWithText" alt="PLN" />
|
||||||
|
</div> -->
|
||||||
|
<div class="flex justify-end w-full px-4">
|
||||||
|
<div class="flex flex-1">
|
||||||
|
<div class="flex w-full md:ml-0">
|
||||||
|
<button @click="commandStore.showCommandPalettes"
|
||||||
|
class="relative w-full text-gray-400 border-0 border-transparent rounded-xl hover:text-primary-500 ring-0 ring-transparent focus:border-transparent focus:border-0 focus:ring-0">
|
||||||
|
<div class="absolute inset-y-0 left-0 flex items-center pointer-events-none">
|
||||||
|
<MagnifyingGlassIcon class="w-5 h-5" aria-hidden="true" />
|
||||||
|
<span class="hidden ml-2 sm:text-sm md:block">Cari menu...</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center ml-4 md:ml-6">
|
||||||
|
<button type="button"
|
||||||
|
class="p-1 mr-2 text-gray-400 bg-white rounded-full hover:text-primary-500 focus:outline-none focus:ring-0">
|
||||||
|
<span class="sr-only">FAQ</span>
|
||||||
|
<QuestionMarkCircleIcon class="w-6 h-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="p-1 mr-2 text-gray-400 bg-white rounded-full hover:text-primary-500 focus:outline-none focus:ring-0">
|
||||||
|
<span class="sr-only">Manual Book</span>
|
||||||
|
<BookOpenIcon class="w-6 h-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="messageStore.showDialogMessageBox"
|
||||||
|
class="p-1 text-gray-400 bg-white rounded-full hover:text-primary-500 focus:outline-none focus:ring-0">
|
||||||
|
<span class="sr-only">Message</span>
|
||||||
|
<ChatBubbleOvalLeftEllipsisIcon class="w-6 h-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Profile dropdown -->
|
||||||
|
<Menu as="div" class="relative ml-3">
|
||||||
|
<div>
|
||||||
|
<MenuButton
|
||||||
|
class="flex items-center max-w-xs text-sm bg-white rounded-full focus:outline-none focus:ring-0">
|
||||||
|
<span class="sr-only">Open user menu</span>
|
||||||
|
<PictureInitial size-class="w-8 h-8" background-class="bg-gray-100"
|
||||||
|
font-class="font-bold text-gray-500 text-md" :name="userStore.user_name" />
|
||||||
|
</MenuButton>
|
||||||
|
</div>
|
||||||
|
<transition enter-active-class="transition duration-100 ease-out"
|
||||||
|
enter-from-class="transform scale-95 opacity-0" enter-to-class="transform scale-100 opacity-100"
|
||||||
|
leave-active-class="transition duration-75 ease-in"
|
||||||
|
leave-from-class="transform scale-100 opacity-100" leave-to-class="transform scale-95 opacity-0">
|
||||||
|
|
||||||
|
<MenuItems
|
||||||
|
class="absolute right-0 z-10 w-48 py-1 mt-2 origin-top-right bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<div class="flex-shrink-0 block px-4 py-2 border-b border-gray-50 group">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div>
|
||||||
|
<!-- <img class="inline-block rounded-full h-9 w-9" :src="userStore.user_image" alt="" /> -->
|
||||||
|
<PictureInitial class-name="nline-block" size-class="w-9 h-9"
|
||||||
|
background-class="bg-gray-100" font-class="font-bold text-gray-600 text-md"
|
||||||
|
:name="userStore.user_name" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<p class="text-sm font-medium text-gray-700 group-hover:text-gray-900">
|
||||||
|
{{ userStore.user_name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs font-medium text-gray-500 group-hover:text-gray-700">
|
||||||
|
{{ userStore.user_access }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MenuItem v-slot="{ active }">
|
||||||
|
<button @click="showDialogChangePassword"
|
||||||
|
:class="[active ? 'bg-gray-100' : '', 'w-full text-left block px-4 py-2 text-sm text-gray-700']">
|
||||||
|
Ubah Password
|
||||||
|
</button>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem v-slot="{ active }">
|
||||||
|
<button @click="showDialogLogout"
|
||||||
|
:class="[active ? 'bg-gray-100' : '', 'w-full text-left block px-4 py-2 text-sm text-gray-700']">
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItems>
|
||||||
|
</transition>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionDialog type="error" title="Keluar dari akun?" message="Apakah Anda sudah yakin akan keluar dari akun ini?"
|
||||||
|
cancel-text="Batalkan" confirm-text="Ya, keluar" :open="dialogLogout" @onClose="handleOnDismissDialogLogout"
|
||||||
|
@onConfirm="handleOnConfirmDialogLogout" />
|
||||||
|
<ChangePasswordDialog :open="dialogChangePassword" @onClose="handleOnDismissDialogChangePassword" />
|
||||||
|
<CommandPalettes :open="commandStore.open" @onClose="commandStore.handleOnDismissCommandPalettes" />
|
||||||
|
<MessageBox :open="messageStore.open" @onClose="messageStore.handleOnDismissMessageBox" />
|
||||||
|
</template>
|
17
src/components/HomeEmpty.vue
Normal file
17
src/components/HomeEmpty.vue
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center px-6 text-center whitespace-pre-wrap h-screen-80">
|
||||||
|
|
||||||
|
<DocumentArrowUpIcon class="w-12 h-12 mx-auto text-gray-400" aria-hidden="true" />
|
||||||
|
<h3 class="mt-2 font-bold text-gray-900 whitespace-pre-wrap text-md">
|
||||||
|
Cari menu? Ctrl + F
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-600 whitespace-pre-wrap ">
|
||||||
|
Gunakan kombinasi tombol Ctrl + F untuk mencari menu yang kamu inginkan.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { DocumentArrowUpIcon } from '@heroicons/vue/24/outline';
|
||||||
|
</script>
|
43
src/components/InputText.vue
Normal file
43
src/components/InputText.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: "text",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:value']);
|
||||||
|
|
||||||
|
const updateValue = (event: Event) => {
|
||||||
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
emit('update:value', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input autocomplete="off" :type="type" :placeholder="placeholder" :value="value" @input="updateValue($event)"
|
||||||
|
:disabled="disabled" :readonly="readonly"
|
||||||
|
:class="['w-full px-4 py-2 text-sm leading-6 placeholder:text-gray-400 text-gray-900 border-0 border-transparent rounded-lg outline-0 bg-gray-50 focus:outline-0 focus:border-0 focus:ring-0', className]" />
|
||||||
|
</template>
|
299
src/components/Menus/DaftarTransaksi/AktifIndividual.vue
Normal file
299
src/components/Menus/DaftarTransaksi/AktifIndividual.vue
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-auto max-w-7xl dx-viewport">
|
||||||
|
<div class="flex flex-col p-4 bg-white rounded-lg drop-shadow-sm min-h-96">
|
||||||
|
<nav class="flex flex-wrap gap-4 mb-4" aria-label="Tabs">
|
||||||
|
<a v-for="(tab, index) in tabs" :key="tab.name" @click="changeTab(index)"
|
||||||
|
:class="[tabChecked === index ? 'bg-primary-100 text-primary-900' : 'text-gray-500 hover:text-gray-700', 'cursor-pointer px-3 py-2 font-medium text-sm rounded-md']"
|
||||||
|
:aria-current="tabChecked === index ? 'page' : undefined">{{ tab.name }}</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<section aria-labelledby="filter-heading">
|
||||||
|
<h2 id="filter-heading" class="sr-only">Filters</h2>
|
||||||
|
|
||||||
|
<div class="pb-4 bg-white">
|
||||||
|
<div class="flex flex-wrap items-center px-0 mx-auto lg:justify-between max-w-7xl">
|
||||||
|
<Menu as="div"
|
||||||
|
class="relative flex-1 inline-block w-full mb-2 mr-2 text-left lg-2 lg:mb-0 md:flex-none lg:w-fit">
|
||||||
|
<MenuButton
|
||||||
|
class="flex inline-flex items-center justify-between w-full px-2 py-2 text-xs font-medium text-gray-700 border border-gray-100 rounded-md whitespace-nowrap group hover:text-gray-900">
|
||||||
|
Cari dengan : <b>{{ searchOptions.find(option => option.current === true)?.name }}</b>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="flex-shrink-0 w-3 h-3 ml-1 -mr-1 text-gray-400 group-hover:text-gray-500"
|
||||||
|
aria-hidden="true" />
|
||||||
|
</MenuButton>
|
||||||
|
|
||||||
|
<transition enter-active-class="transition duration-100 ease-out"
|
||||||
|
enter-from-class="transform scale-95 opacity-0"
|
||||||
|
enter-to-class="transform scale-100 opacity-100"
|
||||||
|
leave-active-class="transition duration-75 ease-in"
|
||||||
|
leave-from-class="transform scale-100 opacity-100"
|
||||||
|
leave-to-class="transform scale-95 opacity-0">
|
||||||
|
<MenuItems
|
||||||
|
class="absolute left-0 z-10 w-40 mt-2 origin-top-left bg-white rounded-md shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<div class="py-1">
|
||||||
|
<MenuItem v-for="(option, index) in searchOptions" :key="option.name"
|
||||||
|
v-slot="{ active }">
|
||||||
|
<a @click="handleChangeSearchOption(index)"
|
||||||
|
:class="[option.current ? 'font-medium text-gray-900' : 'text-gray-500', active ? 'bg-gray-100' : '', 'block px-4 py-2 text-xs']">{{
|
||||||
|
option.name }}</a>
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</MenuItems>
|
||||||
|
</transition>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<PopoverGroup class="items-center flex-1 mb-2 sm:mr-2 lg:mb-0 md:flex-none lg:w-fit">
|
||||||
|
<Popover class="relative inline-block w-full px-2 text-left">
|
||||||
|
<PopoverButton
|
||||||
|
class="flex inline-flex items-center justify-between w-full px-2 py-2 text-xs text-gray-700 border border-gray-100 rounded-md whitespace-nowrap group">
|
||||||
|
<span class="flex-1 text-left">Order by</span>
|
||||||
|
<span v-if="orderByOptions.filter(option => option.checked).length > 0"
|
||||||
|
class="ml-1.5 rounded bg-gray-100 py-0.2 px-1.5 text-[10px] flex-none font-bold tabular-nums text-gray-500">
|
||||||
|
{{ orderByOptions.filter(option => option.checked).length }}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="flex-none flex-shrink-0 w-3 h-3 ml-1 -mr-1 text-gray-400 group-hover:text-gray-500"
|
||||||
|
aria-hidden="true" />
|
||||||
|
</PopoverButton>
|
||||||
|
|
||||||
|
<transition enter-active-class="transition duration-100 ease-out"
|
||||||
|
enter-from-class="transform scale-95 opacity-0"
|
||||||
|
enter-to-class="transform scale-100 opacity-100"
|
||||||
|
leave-active-class="transition duration-75 ease-in"
|
||||||
|
leave-from-class="transform scale-100 opacity-100"
|
||||||
|
leave-to-class="transform scale-95 opacity-0">
|
||||||
|
<PopoverPanel
|
||||||
|
class="absolute right-0 z-10 p-4 mt-2 origin-top-right bg-white rounded-md shadow-2xl ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<form class="space-y-4">
|
||||||
|
<div v-for="(option, optionIdx) in orderByOptions" :key="option.value"
|
||||||
|
class="flex items-center">
|
||||||
|
<input :value="option.value" type="checkbox" :checked="option.checked"
|
||||||
|
@change="handleChangeOrderBy(optionIdx, !option.checked)"
|
||||||
|
class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500" />
|
||||||
|
<label
|
||||||
|
class="pr-6 ml-3 text-sm font-medium text-gray-900 whitespace-nowrap">
|
||||||
|
{{ option.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</PopoverPanel>
|
||||||
|
</transition>
|
||||||
|
</Popover>
|
||||||
|
</PopoverGroup>
|
||||||
|
|
||||||
|
<div class="relative inline-block mb-2 mr-2 text-left lg:mb-0 grow xs:mt-0 md:flex-1">
|
||||||
|
<div class="w-full md:max-w-[220px]">
|
||||||
|
<vue-tailwind-datepicker v-model="dateValue" i18n="id" :options="datePickerOptions"
|
||||||
|
:formatter="formatter" placeholder="Filter Tanggal" :auto-apply="false"
|
||||||
|
input-classes="bg-white text-gray-900 border rounded-md border-gray-100 text-xs px-2 py-2 focus:ring-0 focus:border-gray-100"
|
||||||
|
:disabled="false" as-single use-range separator=" s/d " />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmitSearch"
|
||||||
|
class="flex lg:mt-0 items-center w-full !py-1 border-0 border-gray-100 rounded-lg bg-gray-50 flex-between lg:w-fit">
|
||||||
|
<!-- <ButtonDropdown button-style="bg-primary-50" text-style="text-xs" :data="searchBy"
|
||||||
|
:checked="searchByChecked" @update:checked="handleSearchByChange" />
|
||||||
|
|
||||||
|
<div class="h-5 mx-2 border-r-2 border-gray-100"></div>
|
||||||
|
|
||||||
|
<ButtonPrimary label="Order By"
|
||||||
|
class-name="bg-primary-50 mt-0 py-2 text-xs !px-2 !text-dark font-normal rounded-md mr-4" /> -->
|
||||||
|
|
||||||
|
<InputText class-name="flex-1 bg-transparent text-xs !py-0 px-2 placeholder:text-gray-600"
|
||||||
|
placeholder="Masukan Pencarian" />
|
||||||
|
|
||||||
|
<ButtonPrimary type="submit"
|
||||||
|
class-name="mr-1 flex flex-row bg-primary-500 !mt-0 text-xs !px-2 !text-white font-normal rounded-md">
|
||||||
|
<MagnifyingGlassIcon class="w-4 h-4 lg:mr-2" aria-hidden="true" />
|
||||||
|
<span class="hidden lg:block">Cari</span>
|
||||||
|
</ButtonPrimary>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active filters -->
|
||||||
|
<div class="border-gray-100 bg-gray-50 border-x border-y"
|
||||||
|
v-if="orderByOptions.filter((e) => e.checked).length > 0">
|
||||||
|
<div class="px-2 py-3 mx-auto max-w-7xl sm:flex sm:items-center sm:px-2 lg:px-4">
|
||||||
|
<h3 class="text-xs font-medium text-gray-700">
|
||||||
|
Order by
|
||||||
|
<span class="sr-only">, active</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div aria-hidden="true" class="hidden w-px h-5 bg-gray-300 sm:ml-4 sm:block" />
|
||||||
|
|
||||||
|
<div class="mt-2 sm:mt-0 sm:ml-4">
|
||||||
|
<div class="flex flex-wrap items-center -m-1">
|
||||||
|
<span v-for="(order, index) in orderByOptions.filter((e) => e.checked)" :key="order.value"
|
||||||
|
class="m-1 inline-flex items-center rounded-full border border-gray-100 bg-white py-1.5 pl-3 pr-2 text-xs font-medium text-gray-900">
|
||||||
|
<span>{{ order.label }}</span>
|
||||||
|
<button @click="handleChangeOrderBy(index, !order.checked)" type="button"
|
||||||
|
class="inline-flex flex-shrink-0 w-4 h-4 p-1 ml-1 text-gray-400 rounded-full hover:bg-gray-200 hover:text-gray-500">
|
||||||
|
<span class="sr-only">Remove filter for {{ order.label }}</span>
|
||||||
|
<svg class="w-2 h-2" stroke="currentColor" fill="none" viewBox="0 0 8 8">
|
||||||
|
<path stroke-linecap="round" stroke-width="1.5" d="M1 1l6 6m0-6L1 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<DxDataGrid :data-source="dataSource" :remote-operations="false" :allow-column-reordering="true"
|
||||||
|
:row-alternation-enabled="true" :show-borders="true" @content-ready="onContentReady">
|
||||||
|
<!-- <DxColumn :group-index="0" data-field="Product" /> -->
|
||||||
|
<DxColumn data-field="Amount" caption="Sale Amount" data-type="number" format="currency"
|
||||||
|
alignment="right" />
|
||||||
|
<DxColumn :allow-grouping="false" data-field="Discount" caption="Discount %" data-type="number"
|
||||||
|
format="percent" alignment="right" cell-template="discountCellTemplate" css-class="bullet" />
|
||||||
|
<DxColumn data-field="SaleDate" data-type="date" />
|
||||||
|
<DxColumn data-field="Region" data-type="string" />
|
||||||
|
<DxColumn data-field="Sector" data-type="string" />
|
||||||
|
<DxColumn data-field="Channel" data-type="string" />
|
||||||
|
<DxColumn :width="150" data-field="Customer" data-type="string" />
|
||||||
|
|
||||||
|
<!-- <DxGroupPanel :visible="true" /> -->
|
||||||
|
<!-- <DxFilterRow :visible="true" /> -->
|
||||||
|
<!-- <DxSearchPanel :visible="true" :highlight-case-sensitive="true" /> -->
|
||||||
|
<DxGrouping :auto-expand-all="false" />
|
||||||
|
<DxPager :allowed-page-sizes="pageSizes" :show-page-size-selector="true" />
|
||||||
|
<DxPaging :page-size="10" />
|
||||||
|
<DxSelection mode="single" />
|
||||||
|
</DxDataGrid>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, type PropType } from 'vue';
|
||||||
|
import VueTailwindDatepicker from 'vue-tailwind-datepicker'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogPanel,
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuItems,
|
||||||
|
Popover,
|
||||||
|
PopoverButton,
|
||||||
|
PopoverGroup,
|
||||||
|
PopoverPanel,
|
||||||
|
TransitionChild,
|
||||||
|
TransitionRoot,
|
||||||
|
} from '@headlessui/vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DxDataGrid,
|
||||||
|
DxColumn,
|
||||||
|
DxGrouping,
|
||||||
|
DxGroupPanel,
|
||||||
|
DxPager,
|
||||||
|
DxPaging,
|
||||||
|
DxSearchPanel,
|
||||||
|
DxFilterRow,
|
||||||
|
DxSelection,
|
||||||
|
} from 'devextreme-vue/data-grid';
|
||||||
|
import DataSource from 'devextreme/data/data_source';
|
||||||
|
import 'devextreme/data/odata/store';
|
||||||
|
import ButtonDropdown from '@/components/ButtonDropdown.vue';
|
||||||
|
import ButtonPrimary from '@/components/ButtonPrimary.vue';
|
||||||
|
import InputText from '@/components/InputText.vue';
|
||||||
|
import { MagnifyingGlassIcon, ChevronDownIcon } from '@heroicons/vue/24/solid';
|
||||||
|
import { XMarkIcon } from '@heroicons/vue/24/outline'
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: 'Gangguan' },
|
||||||
|
{ name: 'Keluhan' },
|
||||||
|
{ name: 'Gangguan Diperhatikan' },
|
||||||
|
]
|
||||||
|
const tabChecked = ref(0)
|
||||||
|
|
||||||
|
function changeTab(index: number) {
|
||||||
|
tabChecked.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitSearch = (e: any) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
let collapsed = false;
|
||||||
|
const dataSource = new DataSource({
|
||||||
|
store: {
|
||||||
|
type: 'odata',
|
||||||
|
url: 'https://js.devexpress.com/Demos/SalesViewer/odata/DaySaleDtoes',
|
||||||
|
key: 'Id',
|
||||||
|
beforeSend(request) {
|
||||||
|
const year = new Date().getFullYear() - 1;
|
||||||
|
request.params.startDate = `${year}-05-10`;
|
||||||
|
request.params.endDate = `${year}-5-15`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageSizes = [10, 25, 50, 100];
|
||||||
|
const onContentReady = (e: any) => {
|
||||||
|
if (!collapsed) {
|
||||||
|
e.component.expandRow(['EnviroCare']);
|
||||||
|
collapsed = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchOptions = ref([
|
||||||
|
{ name: 'Nama Lapor', current: true },
|
||||||
|
{ name: 'Nama', current: false },
|
||||||
|
{ name: 'Alamat', current: false },
|
||||||
|
])
|
||||||
|
function handleChangeSearchOption(index: number) {
|
||||||
|
searchOptions.value.forEach((option, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
option.current = true
|
||||||
|
} else {
|
||||||
|
option.current = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderByOptions = ref([
|
||||||
|
{ value: 'status', label: 'Status', checked: false },
|
||||||
|
{ value: 'tanggal', label: 'Tanggal Lapor', checked: false },
|
||||||
|
])
|
||||||
|
function handleChangeOrderBy(index: number, data: boolean) {
|
||||||
|
orderByOptions.value.forEach((option, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
option.checked = data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ModelType = string | [Date, Date] | { start: string | Date; end: string | Date; } | { startDate: string | Date; endDate: string | Date; };
|
||||||
|
const emptyDate: ModelType = '';
|
||||||
|
const dateValue = ref(emptyDate)
|
||||||
|
|
||||||
|
const formatter = ref({
|
||||||
|
date: 'DD MMMM YYYY',
|
||||||
|
month: 'MMMM'
|
||||||
|
})
|
||||||
|
|
||||||
|
const datePickerOptions = ref({
|
||||||
|
shortcuts: {
|
||||||
|
today: 'Hari ini',
|
||||||
|
yesterday: 'Kemarin',
|
||||||
|
past: (period: any) => period + ' hari terakhir',
|
||||||
|
currentMonth: 'Bulan ini',
|
||||||
|
pastMonth: 'Bulan lalu'
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
apply: 'Terapkan',
|
||||||
|
cancel: 'Batal'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
21
src/components/Menus/MenuProvider.vue
Normal file
21
src/components/Menus/MenuProvider.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<main class="flex-1 px-2 py-2 sm:px-4 md:px-6 sm:py-4 md:py-6">
|
||||||
|
<div v-if="route.path !== '/'" class="mx-auto max-w-7xl">
|
||||||
|
<h1 class="text-lg font-medium md:text-2xl lg:text-3xl text-dark">{{ currentRouteName }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="my-2 sm:my-4">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
// Dapatkan objek route dari vue-router
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// Dapatkan nama rute menggunakan computed property
|
||||||
|
const currentRouteName = computed(() => route.name);
|
||||||
|
</script>
|
15
src/components/Menus/MenuSample.vue
Normal file
15
src/components/Menus/MenuSample.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mx-auto max-w-7xl">
|
||||||
|
<div class="border-4 border-gray-200 border-dashed rounded-lg h-[74vh]">
|
||||||
|
<div class="flex items-center justify-center h-full">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900">Menu Sample</h1>
|
||||||
|
<p class="mt-2 text-lg text-gray-600">This is a sample menu page.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
177
src/components/MessageBox.vue
Normal file
177
src/components/MessageBox.vue
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<TransitionRoot as="template" :show="open">
|
||||||
|
<Dialog as="div" class="relative z-10" @close="close">
|
||||||
|
<div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-25 backdrop-blur-sm" />
|
||||||
|
|
||||||
|
<div class="fixed inset-0 overflow-hidden">
|
||||||
|
<div class="absolute inset-0 overflow-hidden ">
|
||||||
|
<div class="fixed inset-y-0 right-0 flex max-w-full pointer-events-none lg:pl-10">
|
||||||
|
<TransitionChild as="template" enter="transform transition ease-in-out duration-500 sm:duration-700"
|
||||||
|
enter-from="translate-x-full" enter-to="translate-x-0"
|
||||||
|
leave="transform transition ease-in-out duration-500 sm:duration-700" leave-from="translate-x-0"
|
||||||
|
leave-to="translate-x-full">
|
||||||
|
<DialogPanel class="w-screen max-w-sm pointer-events-auto">
|
||||||
|
<div class="flex flex-col h-full bg-white shadow-xl">
|
||||||
|
<div class="flex flex-col flex-1 min-h-0 ">
|
||||||
|
<div class="flex items-center w-full h-16 px-4 bg-white sm:px-6 drop-shadow-sm">
|
||||||
|
<div class="flex items-start justify-between w-full">
|
||||||
|
<DialogTitle v-if="peopleIndexSelected !== null"
|
||||||
|
class="text-sm font-medium text-gray-900">
|
||||||
|
<PictureInitial class-name="mr-2" background-class="bg-primary-400"
|
||||||
|
font-class="font-medium text-white text-md"
|
||||||
|
:name="people[peopleIndexSelected].name" />
|
||||||
|
{{ people[peopleIndexSelected].name }}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogTitle v-else class="font-medium text-gray-900 text-md">
|
||||||
|
Team
|
||||||
|
</DialogTitle>
|
||||||
|
<div class="flex items-center ml-3 h-7 ">
|
||||||
|
<button type="button"
|
||||||
|
class="text-gray-400 bg-white rounded-md hover:text-gray-500 focus:ring-0 "
|
||||||
|
@click="peopleIndexSelected === null ? close() : selectPeople(null)">
|
||||||
|
<span class="sr-only">Close panel</span>
|
||||||
|
<XMarkIcon class="w-6 h-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto bg-layout">
|
||||||
|
<div v-if="peopleIndexSelected !== null"
|
||||||
|
class="flex flex-col items-center justify-center h-full px-4 sm:px-6"
|
||||||
|
aria-hidden="true">
|
||||||
|
<ChatBubbleLeftRightIcon class="w-12 h-12 mx-auto text-gray-400"
|
||||||
|
aria-hidden="true" />
|
||||||
|
<h3
|
||||||
|
class="mt-2 font-bold text-center text-gray-900 whitespace-pre-wrap text-md">
|
||||||
|
Tidak ada percakapan
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-center text-gray-600 whitespace-pre-wrap ">
|
||||||
|
Mulai percakapan dengan <b>{{ people[peopleIndexSelected].name }}</b>
|
||||||
|
untuk melihat percakapan disini.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="h-full bg-white" aria-hidden="true">
|
||||||
|
<ul role="list" class="flex-1 overflow-y-auto divide-y divide-gray-50">
|
||||||
|
<li v-for="(person, index) in people" :key="person.id"
|
||||||
|
class="cursor-pointer" @click="selectPeople(index)">
|
||||||
|
<div class="relative flex items-center px-5 py-3 group">
|
||||||
|
<div class="flex-1 block p-1 -m-1">
|
||||||
|
<div class="absolute inset-0 group-hover:bg-primary-100"
|
||||||
|
aria-hidden="true" />
|
||||||
|
<div class="relative flex items-center flex-1 min-w-0">
|
||||||
|
<span class="relative flex-shrink-0 inline-block">
|
||||||
|
<PictureInitial size-class="w-10 h-10"
|
||||||
|
background-class="bg-primary-400"
|
||||||
|
font-class="font-medium text-white text-md"
|
||||||
|
:name="person.name" />
|
||||||
|
<span
|
||||||
|
:class="[person.online ? 'bg-green-400' : 'bg-gray-300', 'absolute top-0 right-0 block h-2.5 w-2.5 rounded-full ring-2 ring-white']"
|
||||||
|
aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
<div class="ml-4 truncate">
|
||||||
|
<p
|
||||||
|
class="text-sm font-medium text-gray-900 truncate">
|
||||||
|
{{ person.name }}</p>
|
||||||
|
<p class="text-sm text-gray-500 truncate">
|
||||||
|
@User</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full px-4 py-4 shadow" v-if="peopleIndexSelected !== null">
|
||||||
|
<form @submit.prevent="messageStore.send"
|
||||||
|
class="flex items-center justify-between flex-shrink-0">
|
||||||
|
<InputText class-name="w-full mr-4 py-2 px-4" placeholder="Tulis pesan di sini"
|
||||||
|
:value="messageStore.message"
|
||||||
|
@update:value="messageStore.message = $event" />
|
||||||
|
|
||||||
|
<button type="submit" :disabled="messageStore.isSending"
|
||||||
|
class="text-sm font-bold text-white bg-transparent border-transparent rounded-full w-7 h-7 focus:outline-0 focus:ring-0">
|
||||||
|
<PaperAirplaneIcon class="pointer-events-none w-7 h-7 text-primary-500"
|
||||||
|
aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogPanel>
|
||||||
|
</TransitionChild>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</TransitionRoot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
|
||||||
|
import { XMarkIcon } from '@heroicons/vue/24/outline'
|
||||||
|
import { useMessageStore } from '@/stores/message';
|
||||||
|
import { ChatBubbleLeftRightIcon, DocumentArrowUpIcon, PaperAirplaneIcon } from '@heroicons/vue/24/solid';
|
||||||
|
import InputText from '@/components/InputText.vue'
|
||||||
|
import PictureInitial from '@/components/PictureInitial.vue'
|
||||||
|
|
||||||
|
const messageStore = useMessageStore()
|
||||||
|
const props = defineProps({ open: Boolean })
|
||||||
|
const emit = defineEmits(['onClose'])
|
||||||
|
const open = ref(props.open)
|
||||||
|
|
||||||
|
const peopleIndexSelected = ref(null)
|
||||||
|
|
||||||
|
const people = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Ajeng Sindia',
|
||||||
|
image: 'https://images.unsplash.com/photo-1505840717430-882ce147ef2d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
||||||
|
online: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Jamaludin',
|
||||||
|
image: 'https://images.unsplash.com/photo-1531427186611-ecfd6d936c79?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
||||||
|
online: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Kumara',
|
||||||
|
image: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
||||||
|
online: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Bellayanti',
|
||||||
|
image: 'https://images.unsplash.com/photo-1509783236416-c9ad59bae472?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
||||||
|
online: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Jefri',
|
||||||
|
image: 'https://images.unsplash.com/photo-1517070208541-6ddc4d3efbcb?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
||||||
|
online: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Kinanti',
|
||||||
|
image: 'https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
|
||||||
|
online: false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function selectPeople(value: any) {
|
||||||
|
peopleIndexSelected.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
open.value = false
|
||||||
|
emit('onClose')
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.open, (value) => {
|
||||||
|
open.value = value;
|
||||||
|
});
|
||||||
|
</script>
|
16
src/components/Navigation.vue
Normal file
16
src/components/Navigation.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Aside from '@/components/AsideWhite.vue';
|
||||||
|
import Header from '@/components/Header.vue';
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const sideBarStatus = ref(false)
|
||||||
|
|
||||||
|
function handleChangeSideBarStatus(status: boolean) {
|
||||||
|
sideBarStatus.value = status;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Header :sideBarStatus="sideBarStatus" @onChangeSideBarStatus="handleChangeSideBarStatus" />
|
||||||
|
<Aside :sideBarStatus="sideBarStatus" @onChangeSideBarStatus="handleChangeSideBarStatus" />
|
||||||
|
</template>
|
47
src/components/Notification/Notification.vue
Normal file
47
src/components/Notification/Notification.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import store from './store';
|
||||||
|
import type { Notification } from './interfaces';
|
||||||
|
import { ErrorIcon, InfoIcon, WarningIcon, SuccessIcon, CloseIcon } from './icons';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
notification: Notification,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const closeNotification = (id: string) => {
|
||||||
|
store.actions.removeNotification(id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-white shadow-lg rounded-lg pointer-events-auto">
|
||||||
|
<div class="rounded-lg shadow-xl overflow-hidden">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0" v-if="notification.type">
|
||||||
|
<SuccessIcon v-if="notification.type === 'success'" />
|
||||||
|
<InfoIcon v-else-if="notification.type === 'info'" />
|
||||||
|
<WarningIcon v-else-if="notification.type === 'warning'" />
|
||||||
|
<ErrorIcon v-else-if="notification.type === 'error'" />
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 w-0 flex-1 pt-0.5">
|
||||||
|
<p class="text-sm leading-5 font-medium text-gray-900" v-if="notification.title">
|
||||||
|
{{ notification.title }}
|
||||||
|
</p>
|
||||||
|
<p :class="`${notification.title ? 'mt-1' : ''} text-sm leading-5 text-gray-500`">
|
||||||
|
{{ notification.content }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4 flex-shrink-0 flex">
|
||||||
|
<button
|
||||||
|
@click="() => closeNotification(notification.id)"
|
||||||
|
class="inline-flex text-gray-400 focus:outline-none focus:text-gray-500 transition-all ease-in-out duration-150"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
26
src/components/Notification/NotificationProvider.vue
Normal file
26
src/components/Notification/NotificationProvider.vue
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Notification from './Notification.vue';
|
||||||
|
import store from './store';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed inset-0 z-50 flex items-start justify-end px-4 py-6 pointer-events-none sm:p-6">
|
||||||
|
<div class="w-full max-w-sm">
|
||||||
|
<TransitionGroup appear tag="div"
|
||||||
|
:enter-active-class="store.getters.getNotificationsCount() > 1 ? 'transform ease-out delay-300 duration-300 transition' : 'transform ease-out duration-300 transition'"
|
||||||
|
enter-from-class="translate-x-4 opacity-0" enter-to-class="translate-x-0 opacity-100"
|
||||||
|
leave-active-class="transition duration-100 ease-in" leave-from-class="opacity-100" leave-to-class="opacity-0"
|
||||||
|
move-class="transition duration-500 ease-in-out">
|
||||||
|
<Notification :key="notification.id" :notification="notification" :class="idx > 0 ? 'mt-4' : ''"
|
||||||
|
v-for="(notification, idx) in store.getters.getNotifications()" />
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.delay-300 {
|
||||||
|
transition-delay: 300ms;
|
||||||
|
}
|
||||||
|
</style>
|
5
src/components/Notification/icons/CloseIcon.vue
Normal file
5
src/components/Notification/icons/CloseIcon.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" class="w-5 h-5">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
5
src/components/Notification/icons/ErrorIcon.vue
Normal file
5
src/components/Notification/icons/ErrorIcon.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-red-400">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
5
src/components/Notification/icons/InfoIcon.vue
Normal file
5
src/components/Notification/icons/InfoIcon.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-blue-400">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
6
src/components/Notification/icons/SuccessIcon.vue
Normal file
6
src/components/Notification/icons/SuccessIcon.vue
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" class="h-6 w-6 text-green-400">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
5
src/components/Notification/icons/WarningIcon.vue
Normal file
5
src/components/Notification/icons/WarningIcon.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-orange-400">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
13
src/components/Notification/icons/index.ts
Normal file
13
src/components/Notification/icons/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import ErrorIcon from "./ErrorIcon.vue";
|
||||||
|
import WarningIcon from "./WarningIcon.vue";
|
||||||
|
import InfoIcon from "./InfoIcon.vue";
|
||||||
|
import SuccessIcon from "./SuccessIcon.vue";
|
||||||
|
import CloseIcon from "./CloseIcon.vue";
|
||||||
|
|
||||||
|
export {
|
||||||
|
ErrorIcon,
|
||||||
|
WarningIcon,
|
||||||
|
InfoIcon,
|
||||||
|
SuccessIcon,
|
||||||
|
CloseIcon
|
||||||
|
}
|
9
src/components/Notification/index.ts
Normal file
9
src/components/Notification/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import NotificationProvider from "./NotificationProvider.vue";
|
||||||
|
import store from "./store";
|
||||||
|
|
||||||
|
const { dispatchNotification } = store.actions;
|
||||||
|
|
||||||
|
export {
|
||||||
|
NotificationProvider,
|
||||||
|
dispatchNotification
|
||||||
|
};
|
@ -0,0 +1,8 @@
|
|||||||
|
export type NotificationType = 'success' | 'info' | 'warning' | 'error';
|
||||||
|
|
||||||
|
export interface Notification {
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
content: string;
|
||||||
|
type?: NotificationType;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
import type { NotificationType } from "./Notification.interface";
|
||||||
|
|
||||||
|
export interface NotificationConfig {
|
||||||
|
title?: string;
|
||||||
|
content: string;
|
||||||
|
duration?: number;
|
||||||
|
autoClose?: boolean;
|
||||||
|
type?: NotificationType;
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import type { Notification } from './Notification.interface';
|
||||||
|
|
||||||
|
export interface NotificationsState {
|
||||||
|
notifications: Notification[];
|
||||||
|
}
|
5
src/components/Notification/interfaces/index.ts
Normal file
5
src/components/Notification/interfaces/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import type { Notification, NotificationType } from './Notification.interface'
|
||||||
|
import type { NotificationsState } from './NotificationsState.interface'
|
||||||
|
import type { NotificationConfig } from './NotificationConfig.interface'
|
||||||
|
|
||||||
|
export type { Notification, NotificationsState, NotificationConfig, NotificationType };
|
50
src/components/Notification/store/index.ts
Normal file
50
src/components/Notification/store/index.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import type { NotificationsState, NotificationConfig } from '../interfaces';
|
||||||
|
import { reactive, readonly } from 'vue';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const state = reactive<NotificationsState>({
|
||||||
|
notifications: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
removeNotification(id: string) {
|
||||||
|
state.notifications = state.notifications.filter(notification => notification.id !== id);
|
||||||
|
},
|
||||||
|
dispatchNotification({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
type,
|
||||||
|
autoClose = true,
|
||||||
|
duration = 3000,
|
||||||
|
}: NotificationConfig) {
|
||||||
|
const id = uuidv4();
|
||||||
|
const notifications = [{
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
type,
|
||||||
|
}, ...state.notifications];
|
||||||
|
state.notifications = notifications;
|
||||||
|
|
||||||
|
if (autoClose) {
|
||||||
|
setTimeout(() => {
|
||||||
|
actions.removeNotification(id);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
getNotifications() {
|
||||||
|
return [...state.notifications].slice(0,4);
|
||||||
|
},
|
||||||
|
getNotificationsCount() {
|
||||||
|
return state.notifications.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
state: readonly(state),
|
||||||
|
actions,
|
||||||
|
getters,
|
||||||
|
}
|
51
src/components/PictureInitial.vue
Normal file
51
src/components/PictureInitial.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<span :class="['inline-flex items-center justify-center rounded-full', backgroundClass, sizeClass, className]">
|
||||||
|
<span :class="['leading-none', fontClass]">
|
||||||
|
{{ nameInitial }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
fontClass: {
|
||||||
|
type: String,
|
||||||
|
default: "text-sm font-medium text-white"
|
||||||
|
},
|
||||||
|
backgroundClass: {
|
||||||
|
type: String,
|
||||||
|
default: "bg-primary-400"
|
||||||
|
},
|
||||||
|
sizeClass: {
|
||||||
|
type: String,
|
||||||
|
default: "w-8 h-8"
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
default: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function createInitial(name: String) {
|
||||||
|
const wordCapital = name
|
||||||
|
.split(" ")
|
||||||
|
.map(word => word.charAt(0).toUpperCase())
|
||||||
|
.join("");
|
||||||
|
try {
|
||||||
|
return wordCapital.slice(0, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return wordCapital.slice(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameInitial = computed(() => {
|
||||||
|
return createInitial(props.name)
|
||||||
|
})
|
||||||
|
</script>
|
11
src/components/__tests__/HelloWorld.spec.ts
Normal file
11
src/components/__tests__/HelloWorld.spec.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
// import HelloWorld from '../HelloWorld.vue'
|
||||||
|
|
||||||
|
describe('HelloWorld', () => {
|
||||||
|
it('renders properly', () => {
|
||||||
|
// const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
|
||||||
|
// expect(wrapper.text()).toContain('Hello Vitest')
|
||||||
|
})
|
||||||
|
})
|
7
src/components/icons/IconCommunity.vue
Normal file
7
src/components/icons/IconCommunity.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
src/components/icons/IconDocumentation.vue
Normal file
7
src/components/icons/IconDocumentation.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
src/components/icons/IconEcosystem.vue
Normal file
7
src/components/icons/IconEcosystem.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
src/components/icons/IconSupport.vue
Normal file
7
src/components/icons/IconSupport.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
19
src/components/icons/IconTooling.vue
Normal file
19
src/components/icons/IconTooling.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
15
src/main.ts
Normal file
15
src/main.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'devextreme/dist/css/dx.light.css';
|
||||||
|
import "@lottiefiles/lottie-player"
|
||||||
|
import '@/assets/css/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
348
src/router/index.ts
Normal file
348
src/router/index.ts
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '@/views/HomeView.vue'
|
||||||
|
import LoginView from '@/views/LoginView.vue'
|
||||||
|
import NotFoundView from '@/views/NotFoundView.vue'
|
||||||
|
import MenuSample from '@/components/Menus/MenuSample.vue'
|
||||||
|
import AktifIndividual from '@/components/Menus/DaftarTransaksi/AktifIndividual.vue'
|
||||||
|
import HomeEmptyView from '@/components/HomeEmpty.vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
linkActiveClass: 'active',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
'meta': { requiresAuth: true },
|
||||||
|
component: HomeView,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'Home Page',
|
||||||
|
component: HomeEmptyView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'daftar-transaksi',
|
||||||
|
name: 'Daftar Transaksi',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'aktif-individual',
|
||||||
|
name: 'Transaksi Aktif Individual',
|
||||||
|
component: AktifIndividual,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'aktif-pln-mobile',
|
||||||
|
name: 'Transaksi Aktif PLN Mobile',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'periksa-return-order',
|
||||||
|
name: 'Transaksi Aktif Periksa dan Return Order',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'aktif-tm',
|
||||||
|
name: 'Transaksi Aktif TM',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'aktif-historis',
|
||||||
|
name: 'Transaksi Aktif Historis',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-alih-unit',
|
||||||
|
name: 'Monitoring Alih Unit',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-all',
|
||||||
|
name: 'Monitoring All',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-tiket',
|
||||||
|
name: 'Monitoring Tiket',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-history',
|
||||||
|
name: 'Monitoring History',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-aktif',
|
||||||
|
name: 'Monitoring Aktif',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-close-cc',
|
||||||
|
name: 'Monitoring Close CC',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-log-autodispatch',
|
||||||
|
name: 'Monitoring Log AutoDispatch',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-perlu-diperhatikan',
|
||||||
|
name: 'Monitoring yang Perlu Diperhatikan',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-log-break-regu',
|
||||||
|
name: 'Monitoring Log Break Regu',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring-log-alih-regu',
|
||||||
|
name: 'Monitoring Log Alih Regu',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'administration',
|
||||||
|
name: 'Administration',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'audit-trails',
|
||||||
|
name: 'Audit Trails',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'log-login',
|
||||||
|
name: 'Log Login',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'log-aktivity',
|
||||||
|
name: 'Log Aktivity',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'log-error',
|
||||||
|
name: 'Log Error',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'task-management',
|
||||||
|
name: 'Task Management',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'menu-management',
|
||||||
|
name: 'Menu Management',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'system-parameter',
|
||||||
|
name: 'System Parameter',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'ubah-profile-manager',
|
||||||
|
name: 'Ubah Profile Manager',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'security-projek',
|
||||||
|
name: 'Security Projek',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user-role-manager',
|
||||||
|
name: 'User Role Manager',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'menu-authorization',
|
||||||
|
name: 'Menu Authorization',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'workflow-designer',
|
||||||
|
name: 'Workflow Designer',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'apkt-news',
|
||||||
|
name: 'APKT News',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'master-borderless',
|
||||||
|
name: 'Master Borderless',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'zone-borderless',
|
||||||
|
name: 'Zone Borderless',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'user-zone-borderless',
|
||||||
|
name: 'User Zone Borderless',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'master-yantek',
|
||||||
|
name: 'Master Yantek',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'unit',
|
||||||
|
name: 'Unit',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'unit-jaringan',
|
||||||
|
name: 'Unit Jaringan',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'unit-pelayanan',
|
||||||
|
name: 'Unit Pelayanan',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'regu-yantek',
|
||||||
|
name: 'Regu Yantek',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'gardu-induk',
|
||||||
|
name: 'Gardu Induk',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'gardu-penyulang-tm',
|
||||||
|
name: 'Gardu Penyulang TM',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'gardu-distribusi',
|
||||||
|
name: 'Gardu Distribusi',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'group-gangguan',
|
||||||
|
name: 'Group Gangguan',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'wilayah-yantek',
|
||||||
|
name: 'Wilayah Yantek',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'material',
|
||||||
|
name: 'Material',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'transaksi',
|
||||||
|
name: 'Transaksi',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'gangguan-dan-keluhan',
|
||||||
|
name: 'Gangguan dan Keluhan',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'gangguan-tegangan-menengah',
|
||||||
|
name: 'Gangguan Tegangan Menengah',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pemadaman-terencana',
|
||||||
|
name: 'Pemadaman Terencana',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'aplikasi-off-line',
|
||||||
|
name: 'Aplikasi Off-Line',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'penugasan-khusus',
|
||||||
|
name: 'Penugasan Khusus',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mutasi-pengaduan',
|
||||||
|
name: 'Mutasi Pengaduan',
|
||||||
|
component: MenuSample,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: LoginView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/logout',
|
||||||
|
name: 'Logout',
|
||||||
|
beforeEnter(to, from, next) {
|
||||||
|
const auth = useAuthStore();
|
||||||
|
auth.logout();
|
||||||
|
next('/login');
|
||||||
|
},
|
||||||
|
redirect: '/logout'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/404',
|
||||||
|
name: 'Not Found',
|
||||||
|
component: NotFoundView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
name: 'Profile',
|
||||||
|
redirect: '/404',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'Settings',
|
||||||
|
redirect: '/404',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, _from) => {
|
||||||
|
// redirect to login page if not logged in and trying to access a restricted page
|
||||||
|
const publicPages = ['/login'];
|
||||||
|
const authRequired = !publicPages.includes(to.path);
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
|
// if to is not found, redirect to 404
|
||||||
|
if (to.matched.length === 0) {
|
||||||
|
return '/404';
|
||||||
|
} else {
|
||||||
|
// if to is not login and user is not logged in, redirect to login
|
||||||
|
if (!auth.isLoggedIn) {
|
||||||
|
// if to is 404, redirect to 404
|
||||||
|
if (to.path !== '/404' && to.path !== '/login') {
|
||||||
|
return '/login';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if to is login and user is logged in, redirect to home
|
||||||
|
if (to.path === '/login') {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
43
src/stores/auth.ts
Normal file
43
src/stores/auth.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { dispatchNotification } from '@/components/Notification'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
// token from localStorage
|
||||||
|
const token = ref(localStorage.getItem('token') || '')
|
||||||
|
// create a shared state
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isLoggedIn = computed(() => token.value !== '')
|
||||||
|
|
||||||
|
// define your actions
|
||||||
|
function login() {
|
||||||
|
if (username.value == '' || password.value == '') {
|
||||||
|
dispatchNotification({ title: 'Perhatian', content: 'Username atau password tidak boleh kosong', type: 'warning' })
|
||||||
|
} else {
|
||||||
|
isLoading.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
if (username.value == 'demo' && password.value == 'demo') {
|
||||||
|
// store token in localStorage
|
||||||
|
localStorage.setItem('token', 'secret-token')
|
||||||
|
dispatchNotification({ title: 'Berhasil', content: 'Login berhasil, selamat datang kembali!', type: 'success' })
|
||||||
|
// redirect to home page after login
|
||||||
|
// router.replace('/')
|
||||||
|
router.go(0)
|
||||||
|
} else {
|
||||||
|
dispatchNotification({ title: 'Login Gagal', content: 'Username atau password salah', type: 'error' })
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
// expose everything
|
||||||
|
return { token, username, password, isLoggedIn, login, logout, isLoading }
|
||||||
|
})
|
47
src/stores/command.ts
Normal file
47
src/stores/command.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCommandPalattesStore = defineStore('command_palettes', () => {
|
||||||
|
const open = ref(false);
|
||||||
|
|
||||||
|
const controlStatus = ref(false)
|
||||||
|
const keyFStatus = ref(false)
|
||||||
|
|
||||||
|
function showCommandPalettes() {
|
||||||
|
open.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOnDismissCommandPalettes() {
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyPressed(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Control') {
|
||||||
|
console.log('control pressed');
|
||||||
|
controlStatus.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'f') {
|
||||||
|
console.log('f pressed');
|
||||||
|
keyFStatus.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlStatus.value && keyFStatus.value) {
|
||||||
|
showCommandPalettes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyUp(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Control') {
|
||||||
|
console.log('control released');
|
||||||
|
controlStatus.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'f') {
|
||||||
|
console.log('f released');
|
||||||
|
keyFStatus.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { open, showCommandPalettes, handleOnDismissCommandPalettes, onKeyPressed, onKeyUp }
|
||||||
|
})
|
259
src/stores/menu.ts
Normal file
259
src/stores/menu.ts
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import {
|
||||||
|
BoltIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
ComputerDesktopIcon,
|
||||||
|
DocumentDuplicateIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
FolderArrowDownIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
ShieldCheckIcon,
|
||||||
|
} from '@heroicons/vue/24/outline'
|
||||||
|
|
||||||
|
|
||||||
|
export const useMenuStore = defineStore('menu', () => {
|
||||||
|
const navigation = [
|
||||||
|
{
|
||||||
|
name: 'Daftar Transaksi',
|
||||||
|
icon: DocumentDuplicateIcon,
|
||||||
|
href: '/daftar-transaksi',
|
||||||
|
children: [
|
||||||
|
{ name: 'Transaksi Aktif Individual', href: '/daftar-transaksi/aktif-individual', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Transaksi Aktif PLN Mobile', href: '/daftar-transaksi/aktif-pln-mobile', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Transaksi Aktif Periksa dan Return Order', href: '/daftar-transaksi/periksa-return-order', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Transaksi Aktif TM', href: '/daftar-transaksi/aktif-tm', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Transaksi Aktif Historis', href: '/daftar-transaksi/aktif-historis', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Alih Unit', href: '/daftar-transaksi/monitoring-alih-unit', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring All', href: '/daftar-transaksi/monitoring-all', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Tiket', href: '/daftar-transaksi/monitoring-tiket', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring History', href: '/daftar-transaksi/monitoring-history', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Aktif', href: '/daftar-transaksi/monitoring-aktif', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Close di CC', href: '/daftar-transaksi/monitoring-close-cc', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Log AutoDispatch', href: '/daftar-transaksi/monitoring-log-autodispatch', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Yang Perlu Diperhatikan', href: '/daftar-transaksi/monitoring-perlu-diperhatikan', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Log Break Regu', href: '/daftar-transaksi/monitoring-log-break-regu', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Log Alih Regu', href: '/daftar-transaksi/monitoring-log-alih-regu', icon: DocumentDuplicateIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Administration',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: '/administration',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Audit Trails',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: '/administration/audit-trails',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Log Login',
|
||||||
|
href: '/administration/audit-trails/log-login',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Log Activity',
|
||||||
|
href: '/administration/audit-trails/log-aktivity',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Log Error',
|
||||||
|
href: '/administration/audit-trails/log-error',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Task Management',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: '/administration/task-management',
|
||||||
|
children: [
|
||||||
|
{ name: 'Menu Management', href: '/administration/task-management/menu-management', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'System Parameter', href: '/administration/task-management/system-parameter', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'User Profile Manager', href: '/administration/task-management/ubah-profile-manager', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'Security Projek', href: '/administration/task-management/security-projek', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'User Role Manager', href: '/administration/task-management/user-role-manager', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'Menu Authorization', href: '/administration/task-management/menu-authorization', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'Workflow Designer', href: '/administration/task-management/workflow-designer', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'APKT News', href: '/administration/task-management/apkt-news', icon: ShieldCheckIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Master Borderless',
|
||||||
|
icon: ShieldCheckIcon,
|
||||||
|
href: '/administration/master-borderless',
|
||||||
|
children: [
|
||||||
|
{ name: 'Zone Borderless', href: '/administration/master-borderless/zone-borderless', icon: ShieldCheckIcon, children: [], },
|
||||||
|
{ name: 'User Zone Borderless', href: '/administration/master-borderless/user-zone-borderless', icon: ShieldCheckIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Master Yantek',
|
||||||
|
icon: FolderOpenIcon,
|
||||||
|
href: '/master-yantek',
|
||||||
|
children: [
|
||||||
|
{ name: 'Unit', href: '/master-yantek/unit', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Unit Jaringan', href: '/master-yantek/unit-jaringan', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Unit Pelayanan', href: '/master-yantek/unit-pelayanan', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Regu Yantek', href: '/master-yantek/regu-yantek', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Gardu Induk', href: '/master-yantek/gardu-induk', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Gardu Penyulang TM', href: '/master-yantek/gardu-penyulang-tm', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Gardu Distribusi', href: '/master-yantek/gardu-distribusi', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Group Gangguan', href: '/master-yantek/group-gangguan', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Wilayah Yantek', href: '/master-yantek/wilayah-yantek', icon: FolderOpenIcon, children: [], },
|
||||||
|
{ name: 'Material', href: '/master-yantek/material', icon: FolderOpenIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Transaksi',
|
||||||
|
icon: DocumentTextIcon,
|
||||||
|
href: '/transaksi',
|
||||||
|
children: [
|
||||||
|
{ name: 'Gangguan dan Keluhan', href: '/transaksi/gangguan-dan-keluhan', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Gangguan Tegangan Menengah', href: '/transaksi/gangguan-tegangan-menengah', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Pemadaman Terencana', href: '/transaksi/pemadaman-terencana', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Aplikasi Off-Line', href: '/transaksi/aplikasi-off-line', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Penugasan Khusus', href: '/transaksi/penugasan-khusus', icon: DocumentTextIcon, children: [], },
|
||||||
|
{ name: 'Mutasi Pengaduan', href: '/transaksi/mutasi-pengaduan', icon: DocumentTextIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Saidi Saifi',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Monitor Jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/monitor-jaringan',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'Dashboard Asset',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/dashboard-asset',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Manuver Topologi Jaringan',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/manuver-topologi-jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pencarian Segment Jaringan',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/pencarian-segment-jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Penormalan Manuver Jaringan',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/penormalan-manuver-jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pindah Jaringan',
|
||||||
|
href: '/saidi-saifi/monitor-jaringan/pindah-jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Monitoring SCADA',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/monitor-scada/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Monitoring Mapping Database', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Log SCADA', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Gardu Induk', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Feeder', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Zona', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Lateral', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Section', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Segment', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Sub Lateral', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Gardu Distribusi', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Trafo', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Mapping Jurusan', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Integrasi ????', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Integrasi per No Tiang', href: '/saidi-saifi/monitor-scada/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Master Jaringan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/master-jaringan/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Master Gardu Induk', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Feeder', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Zona', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Section', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Segement', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Lateral', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Gardu Distribusi', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Switch', href: '/saidi-saifi/master-jaringan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Master Perhitungan',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/master-perhitungan/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Master Asumsi', href: '/saidi-saifi/master-perhitungan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Asumsi Salatiga', href: '/saidi-saifi/master-perhitungan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Master Faktor Daya & Beban', href: '/saidi-saifi/master-perhitungan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Data III-09', href: '/saidi-saifi/master-perhitungan/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Laporan Saidi Saifi',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/laporan-saidi-saifi/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Laporan Saidi Saifi per Penyulang', href: '/saidi-saifi/laporan-saidi-saifi/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pelanggan Prioritas',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/pelanggan-prioritas/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Daftar Pelanggan Prioritas', href: '/saidi-saifi/pelanggan-prioritas/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Monitoring Pelanggan Prioritas', href: '/saidi-saifi/pelanggan-prioritas/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Koreksi dan Cleansing',
|
||||||
|
icon: ComputerDesktopIcon,
|
||||||
|
href: '/saidi-saifi/koreksi-dan-cleansing/',
|
||||||
|
children: [
|
||||||
|
{ name: 'Monitoring Koreksi dan Cleansing', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Koreksi Transaksi Individual', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Koreksi dan Cleansing TM', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Koreksi Kode Gangguan dan ANEV', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Laporan Koreksi dan Cleansing', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
{ name: 'Delegasi Koreksi dan Cleansing', href: '/saidi-saifi/koreksi-dan-cleansing/', icon: ComputerDesktopIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Utility',
|
||||||
|
icon: BoltIcon,
|
||||||
|
href: '/utility',
|
||||||
|
children: [
|
||||||
|
{ name: 'Piket Obank', href: '/utility/piket-obank', icon: BoltIcon, children: [], },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
// expose everything
|
||||||
|
return { navigation }
|
||||||
|
})
|
24
src/stores/message.ts
Normal file
24
src/stores/message.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { dispatchNotification } from '@/components/Notification'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
|
export const useMessageStore = defineStore('message_box', () => {
|
||||||
|
const message = ref('')
|
||||||
|
const isSending = ref(false)
|
||||||
|
const open = ref(false)
|
||||||
|
|
||||||
|
function handleOnDismissMessageBox() {
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDialogMessageBox() {
|
||||||
|
open.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function send() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message, send, isSending, open, handleOnDismissMessageBox, showDialogMessageBox }
|
||||||
|
})
|
10
src/stores/user.ts
Normal file
10
src/stores/user.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const user_name = computed(() => localStorage.getItem('user_name') || 'Demo User')
|
||||||
|
const user_access = computed(() => localStorage.getItem('user_access') || 'Super Admin')
|
||||||
|
const user_image = computed(() => localStorage.getItem('user_image') || 'https://iidamidamerica.org/wp-content/uploads/2020/12/male-placeholder-image.jpeg')
|
||||||
|
|
||||||
|
return { user_name, user_access, user_image }
|
||||||
|
})
|
15
src/views/HomeView.vue
Normal file
15
src/views/HomeView.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<Navigation />
|
||||||
|
<div class="flex flex-col flex-1 md:pl-80">
|
||||||
|
<MenuProvider>
|
||||||
|
<RouterView />
|
||||||
|
</MenuProvider>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MenuProvider from '@/components/Menus/MenuProvider.vue';
|
||||||
|
import Navigation from '@/components/Navigation.vue';
|
||||||
|
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
</script>
|
42
src/views/LoginView.vue
Normal file
42
src/views/LoginView.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import Icon from '@/assets/images/pln-with-text.png';
|
||||||
|
import Hero from '@/assets/images/hero.png';
|
||||||
|
import Button from '@/components/ButtonPrimary.vue';
|
||||||
|
import InputText from '@/components/InputText.vue';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-screen py-2 bg-layout">
|
||||||
|
<div class="flex flex-col px-6 py-6 bg-white shadow rounded-xl shadow-gray-50">
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<div class="flex flex-col md:mr-6">
|
||||||
|
<img :src="Icon" alt="logo" class="w-[266px] mb-8" />
|
||||||
|
<h1 class="mb-2 text-3xl text-dark">Login</h1>
|
||||||
|
<p class="mb-6 font-medium text-gray-400">Selamat datang kembali</p>
|
||||||
|
|
||||||
|
<form @submit.prevent="authStore.login" class="flex flex-col">
|
||||||
|
<label for="username" class="mb-2 text-xs font-medium text-dark">Username</label>
|
||||||
|
<InputText class-name="mb-3 text-sm placeholder:text-sm" placeholder="Masukan Username"
|
||||||
|
:value="authStore.username" @update:value="authStore.username = $event" />
|
||||||
|
|
||||||
|
<label for="password" class="mb-2 text-xs font-medium text-dark">Password</label>
|
||||||
|
<InputText class-name="mb-3 text-sm placeholder:text-sm" type="password"
|
||||||
|
placeholder="Masukan Password" :value="authStore.password"
|
||||||
|
@update:value="authStore.password = $event" />
|
||||||
|
|
||||||
|
<Button type="submit" label="Login" :disabled="authStore.isLoading"
|
||||||
|
:is-loading="authStore.isLoading" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="hidden grow md:block">
|
||||||
|
<img :src="Hero" alt="logo" class="h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
19
src/views/NotFoundView.vue
Normal file
19
src/views/NotFoundView.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="grid min-h-full px-6 py-24 bg-white place-items-center sm:py-32 lg:px-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-base font-semibold text-indigo-600">404</p>
|
||||||
|
<h1 class="mt-4 text-3xl font-bold tracking-tight text-gray-900 sm:text-5xl">Tidak ditemukan</h1>
|
||||||
|
<p class="mt-6 text-base leading-7 text-gray-600">Maaf, kami tidak menemukan halaman yang Anda cari.</p>
|
||||||
|
<div class="flex items-center justify-center mt-10 gap-x-6">
|
||||||
|
<RouterLink to="/"
|
||||||
|
class="rounded-md bg-indigo-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
|
||||||
|
Kembali ke home</RouterLink>
|
||||||
|
<a href="#" class="text-sm font-semibold text-gray-900">Hubungi bantuan <span aria-hidden="true">→</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
86
tailwind.config.js
Normal file
86
tailwind.config.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
const colors = require("tailwindcss/colors")
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
"./src/**/**/*.{vue,js,ts,jsx,tsx}",
|
||||||
|
"./node_modules/vue-tailwind-datepicker/**/*.js"
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Plus Jakarta Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
'266': '266px',
|
||||||
|
'320': '320px',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
'screen-80': '80vh'
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
"vtd-primary": {
|
||||||
|
'50': '#E8F6F8',
|
||||||
|
'100': '#B6E2EA',
|
||||||
|
'200': '#93D4DF',
|
||||||
|
'300': '#62C1D1',
|
||||||
|
'400': '#0B5966', //#43B5C8
|
||||||
|
'500': '#14A2BA',
|
||||||
|
'600': '#1293A9',
|
||||||
|
'700': '#0E7384',
|
||||||
|
'800': '#0B5966',
|
||||||
|
'900': '#0B5966',
|
||||||
|
}, // Light mode Datepicker color
|
||||||
|
// "vtd-secondary": colors.gray, // Dark mode Datepicker color
|
||||||
|
'layout': '#F3F7F9',
|
||||||
|
'dark': '#151617',
|
||||||
|
'aside': '#2E3536',
|
||||||
|
'primary': {
|
||||||
|
'50': '#E8F6F8',
|
||||||
|
'100': '#B6E2EA',
|
||||||
|
'200': '#93D4DF',
|
||||||
|
'300': '#62C1D1',
|
||||||
|
'400': '#43B5C8',
|
||||||
|
'500': '#14A2BA',
|
||||||
|
'600': '#1293A9',
|
||||||
|
'700': '#0E7384',
|
||||||
|
'800': '#0B5966',
|
||||||
|
'900': '#0B5966',
|
||||||
|
},
|
||||||
|
'accent': {
|
||||||
|
'50': '#FCFAE8',
|
||||||
|
'100': '#F5EFB6',
|
||||||
|
'200': '#F0E893',
|
||||||
|
'300': '#E9DD62',
|
||||||
|
'400': '#E5D643',
|
||||||
|
'500': '#DECC14',
|
||||||
|
'600': '#CABA12',
|
||||||
|
'700': '#9E910E',
|
||||||
|
'800': '#7A700B',
|
||||||
|
'900': '#5D5608',
|
||||||
|
},
|
||||||
|
'gray': {
|
||||||
|
'50': '#F1F2F2',
|
||||||
|
'100': '#D2D7D8',
|
||||||
|
'200': '#BCC4C5',
|
||||||
|
'300': '#9EA9AB',
|
||||||
|
'400': '#8B989A',
|
||||||
|
'500': '#6E7E81',
|
||||||
|
'600': '#647375',
|
||||||
|
'700': '#4E595C',
|
||||||
|
'800': '#3D4547',
|
||||||
|
'900': '#2E3536',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/typography'),
|
||||||
|
require('@tailwindcss/forms'),
|
||||||
|
require('@tailwindcss/aspect-ratio'),
|
||||||
|
require('@tailwindcss/container-queries'),
|
||||||
|
require('@headlessui/tailwindcss'),
|
||||||
|
require('@headlessui/tailwindcss')({ prefix: 'ui' }),
|
||||||
|
],
|
||||||
|
}
|
24
tsconfig.app.json
Normal file
24
tsconfig.app.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": [
|
||||||
|
"env.d.ts",
|
||||||
|
"src/**/*",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"src/**/**/*.vue",
|
||||||
|
"src/**/**/**/*.vue",
|
||||||
|
"src/**/**/**/**/*.vue",
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/__tests__/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"composite": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.vitest.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
}
|
||||||
|
}
|
21
tsconfig.node.json
Normal file
21
tsconfig.node.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node18/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": [
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
12
tsconfig.vitest.json
Normal file
12
tsconfig.vitest.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.app.json",
|
||||||
|
"exclude": [],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"lib": [],
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"jsdom"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
8
vercel.json
Normal file
8
vercel.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "/(.*)",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
24
vite.config.ts
Normal file
24
vite.config.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue({
|
||||||
|
template: {
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement: (tag) => ['lottie-player', 'center'].includes(tag),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
vueJsx(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
18
vitest.config.ts
Normal file
18
vitest.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { mergeConfig } from 'vite'
|
||||||
|
import { configDefaults, defineConfig } from 'vitest/config'
|
||||||
|
import viteConfig from './vite.config'
|
||||||
|
|
||||||
|
export default mergeConfig(
|
||||||
|
viteConfig,
|
||||||
|
defineConfig({
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
exclude: [...configDefaults.exclude, 'e2e/*'],
|
||||||
|
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||||
|
transformMode: {
|
||||||
|
web: [/\.[jt]sx$/]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user