client initial commit
This commit is contained in:
parent
b5f98baa2b
commit
dd1547d7c2
70 changed files with 18446 additions and 0 deletions
13
Services/client/src/App.vue
Normal file
13
Services/client/src/App.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div id="azay-admin-app">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition>
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
BIN
Services/client/src/assets/avatar_user.jpg
Normal file
BIN
Services/client/src/assets/avatar_user.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
Services/client/src/assets/logo.png
Normal file
BIN
Services/client/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
219
Services/client/src/components/FileItem.vue
Normal file
219
Services/client/src/components/FileItem.vue
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import FileItemAction from '@/components/FileItemAction.vue'
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย']
|
||||
|
||||
const { currentFolder, currentFile, currentDept } = storeToRefs(
|
||||
useTreeDataStore()
|
||||
)
|
||||
const { getFolder, createFolder, editFolder } = useTreeDataStore()
|
||||
const drawer = ref<boolean>(false)
|
||||
const drawerStatus = ref<'edit' | 'create'>('create')
|
||||
const input = ref<string>('')
|
||||
const editPathname = ref<string>('')
|
||||
const currentIcon = computed(() =>
|
||||
currentDept.value === 0
|
||||
? 'mdi-file-cabinet'
|
||||
: currentDept.value === 1
|
||||
? 'inbox'
|
||||
: 'o_folder_open'
|
||||
)
|
||||
const props = withDefaults(
|
||||
defineProps<{ action: boolean; viewMode: 'view_list' | 'view_module' }>(),
|
||||
{
|
||||
action: false,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-mt-md">
|
||||
<div class="q-gutter-md">
|
||||
<div
|
||||
:key="value.name"
|
||||
v-for="value in currentFolder"
|
||||
class="inline-block"
|
||||
>
|
||||
<div class="box border-radius-inherit">
|
||||
<q-card flat @click="() => getFolder(value.pathname)">
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon :name="currentIcon" size="6em" color="primary" />
|
||||
<div
|
||||
class="absolute"
|
||||
style="top: 0.5rem; right: 0.5rem"
|
||||
v-if="props.action"
|
||||
>
|
||||
<file-item-action
|
||||
:editname="value.name"
|
||||
:pathname="value.pathname"
|
||||
@editname="
|
||||
() => {
|
||||
drawer = !drawer
|
||||
drawerStatus = 'edit'
|
||||
editPathname = value.pathname
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-center q-pt-md">{{ value.name }}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-block" v-if="props.action && currentDept < 4">
|
||||
<div class="dashed border-radius-inherit">
|
||||
<q-card
|
||||
flat
|
||||
@click="
|
||||
() => {
|
||||
drawer = !drawer
|
||||
drawerStatus = 'create'
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon
|
||||
:name="currentIcon"
|
||||
class="add-icon"
|
||||
size="6em"
|
||||
color="primary"
|
||||
/>
|
||||
<q-btn round class="add-button" color="white" size="10px">
|
||||
<q-icon name="add" color="primary" size="1.5rem"></q-icon>
|
||||
</q-btn>
|
||||
<span class="text-center q-pt-md"
|
||||
>สร้าง{{ DEPT_NAME[currentDept] }}ใหม่</span
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="q-mt-md">
|
||||
<div class="q-gutter-md">
|
||||
<div v-for="value in currentFile" :key="value.title" class="inline-block">
|
||||
<div class="box border-radius-inherit">
|
||||
<q-card flat @click="">
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon name="description" size="6em" color="primary" />
|
||||
<div
|
||||
class="absolute"
|
||||
style="top: 0.5rem; right: 0.5rem"
|
||||
v-if="props.action"
|
||||
>
|
||||
<!-- TODO: Edit file data -->
|
||||
</div>
|
||||
<span class="text-center q-pt-md">{{ value.title }}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-block" v-if="props.action && currentDept > 2">
|
||||
<div class="dashed border-radius-inherit">
|
||||
<q-card flat @click="() => (drawer = !drawer)">
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon
|
||||
name="description"
|
||||
class="add-icon"
|
||||
size="6em"
|
||||
color="primary"
|
||||
/>
|
||||
<q-btn round class="add-button" color="white" size="10px">
|
||||
<q-icon name="add" color="primary" size="1.5rem"></q-icon>
|
||||
</q-btn>
|
||||
<span class="text-center q-pt-md">สร้างไฟล์ใหม่</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-drawer
|
||||
class="q-pa-md"
|
||||
side="right"
|
||||
v-model="drawer"
|
||||
bordered
|
||||
:width="300"
|
||||
:breakpoint="500"
|
||||
overlay
|
||||
>
|
||||
<q-toolbar class="q-mb-md q-pa-none">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold" v-if="drawerStatus == 'edit'"
|
||||
>แก้ไขชื่อ{{ DEPT_NAME[currentDept] }}</span
|
||||
>
|
||||
<span class="text-weight-bold" v-if="drawerStatus == 'create'"
|
||||
>สร้าง{{ DEPT_NAME[currentDept] }}</span
|
||||
>
|
||||
</q-toolbar-title>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
v-close-popup
|
||||
color="red"
|
||||
@click="drawer = !drawer"
|
||||
/>
|
||||
</q-toolbar>
|
||||
<span class="text-weight-bold">ชื่อ{{ DEPT_NAME[currentDept] }}</span>
|
||||
<q-input
|
||||
class="q-my-md"
|
||||
outlined
|
||||
v-model="input"
|
||||
:placeholder="`กรอกชื่อ${DEPT_NAME[currentDept]}`"
|
||||
dense
|
||||
/>
|
||||
<q-btn
|
||||
class="q-px-md"
|
||||
label="บันทึก"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
@click="
|
||||
() => {
|
||||
drawer = !drawer
|
||||
drawerStatus === 'create'
|
||||
? createFolder(input)
|
||||
: editFolder(input, editPathname)
|
||||
input = ''
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-drawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.box {
|
||||
display: inline-block;
|
||||
border: 2px solid #f1f2f4;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dashed {
|
||||
opacity: 0.4;
|
||||
display: inline-block;
|
||||
border: 2px solid #babdc3;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
margin: auto auto;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: 75px;
|
||||
right: 45px;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
92
Services/client/src/components/FileItemAction.vue
Normal file
92
Services/client/src/components/FileItemAction.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts" setup>
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const { currentDept } = storeToRefs(useTreeDataStore())
|
||||
const { deleteFolder, editFolder } = useTreeDataStore()
|
||||
defineProps<{
|
||||
pathname: string
|
||||
editname: string
|
||||
}>()
|
||||
defineEmits([
|
||||
'editname',
|
||||
'deletename',
|
||||
])
|
||||
|
||||
const editdrawer = ref(false)
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย']
|
||||
const editInput = ref<string>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-btn @click.stop icon="more_vert" color="grey" flat dense>
|
||||
<q-menu auto-close>
|
||||
<q-list dense>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => (
|
||||
$emit('editname')
|
||||
)">
|
||||
<div class="row items-center">
|
||||
<q-icon name="edit" color="positive" />
|
||||
<span class="q-ml-sm">แก้ไข</span>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable>
|
||||
<q-item-section @click="() => deleteFolder(pathname)">
|
||||
<div class="row items-center">
|
||||
<q-icon name="delete" color="negative" />
|
||||
<span class="q-ml-sm">ลบ</span>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
<!-- <q-drawer
|
||||
class="q-pa-md"
|
||||
side="left"
|
||||
v-model="editdrawer"
|
||||
bordered
|
||||
:width="300"
|
||||
:breakpoint="500"
|
||||
overlay
|
||||
>
|
||||
<q-toolbar class="q-mb-md q-pa-none">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold">แก้ไขชื่อ{{ DEPT_NAME[currentDept] }}</span>
|
||||
</q-toolbar-title>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
v-close-popup
|
||||
color="red"
|
||||
@click="editdrawer = !editdrawer"
|
||||
/>
|
||||
</q-toolbar>
|
||||
<span class="text-weight-bold">ชื่อ{{ DEPT_NAME[currentDept] }}</span>
|
||||
<q-input
|
||||
class="q-my-md"
|
||||
outlined
|
||||
v-model="editInput"
|
||||
:placeholder="`กรอกชื่อ${DEPT_NAME[currentDept]}`"
|
||||
dense
|
||||
/>
|
||||
<q-btn
|
||||
class="q-px-md"
|
||||
label="บันทึก"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
@click="
|
||||
() => {
|
||||
editdrawer = !editdrawer
|
||||
editFolder(editname,editInput)
|
||||
editInput = ''
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-drawer> -->
|
||||
</template>
|
||||
36
Services/client/src/components/FileSearched.vue
Normal file
36
Services/client/src/components/FileSearched.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
|
||||
const { foundFile } = storeToRefs(useSearchDataStore())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-mt-md" v-if="foundFile.length > 0">
|
||||
<div class="q-gutter-md">
|
||||
<div v-for="value in foundFile" :key="value.title" class="inline-block">
|
||||
<div class="box border-radius-inherit">
|
||||
<q-card flat @click="">
|
||||
<q-card-section class="column justify-center relative q-px-xl">
|
||||
<q-icon name="description" size="6em" color="primary" />
|
||||
<span class="text-center q-pt-md">{{ value.title }}</span>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md" v-if="foundFile.length == 0">
|
||||
<span>ไม่พบรายการที่ค้นหา</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.box {
|
||||
display: inline-block;
|
||||
border: 2px solid #f1f2f4;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
22
Services/client/src/components/FullLoader.vue
Normal file
22
Services/client/src/components/FullLoader.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { watch, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visibility: Boolean,
|
||||
})
|
||||
|
||||
const loaderVisibility = ref<boolean>(props.visibility)
|
||||
|
||||
watch(props, () => (loaderVisibility.value = props.visibility))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-inner-loading :showing="loaderVisibility" class="loader">
|
||||
<q-spinner-cube size="80px" color="primary" />
|
||||
</q-inner-loading>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.loader
|
||||
z-index: 1000
|
||||
</style>
|
||||
122
Services/client/src/components/PageLayout.vue
Normal file
122
Services/client/src/components/PageLayout.vue
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
import FileItem from '@/components/FileItem.vue'
|
||||
import TreeExplorer from '@/components/TreeExplorer.vue'
|
||||
import SearchBar from '@/modules/01_user/components/SearchBar.vue'
|
||||
import FileSearched from '@/components/FileSearched.vue'
|
||||
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย', 'ไฟล์']
|
||||
|
||||
const { isSearch } = storeToRefs(useSearchDataStore())
|
||||
const { data, currentDept } = storeToRefs(useTreeDataStore())
|
||||
const { getCabinet, gotoParent } = useTreeDataStore()
|
||||
const viewMode = ref<'view_list' | 'view_module'>('view_list')
|
||||
const inputSearch = ref<string>()
|
||||
const props = defineProps<{
|
||||
mode: 'admin' | 'user'
|
||||
}>()
|
||||
|
||||
onMounted(getCabinet)
|
||||
</script>
|
||||
<template>
|
||||
<section id="header" class="q-px-md q-pt-md q-pb-none">
|
||||
<div class="q-my-md row items-center">
|
||||
<div class="col">
|
||||
<h5 class="q-my-none" v-if="mode === 'admin'">จัดเก็บเอกสาร</h5>
|
||||
<h5 class="q-my-none" v-else>สืบค้นเอกสาร</h5>
|
||||
</div>
|
||||
<div class="col-3" v-if="mode === 'admin'">
|
||||
<q-input
|
||||
rounded
|
||||
outlined
|
||||
dense
|
||||
label="ค้นหา"
|
||||
bg-color="white"
|
||||
v-model="inputSearch"
|
||||
>
|
||||
<template v-slot:append><q-icon name="search" /></template>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="row q-col-gutter-md q-pa-md"
|
||||
:class="{ reverse: props.mode === 'user' }"
|
||||
id="cabinet"
|
||||
>
|
||||
<div class="col-12 col-md-3">
|
||||
<div class="bg-white rounded-borders shadow-5">
|
||||
<div
|
||||
class="q-px-md q-py-sm text-primary bg-grey-1"
|
||||
id="container-header"
|
||||
>
|
||||
<span class="block q-my-sm text-weight-bold">ตู้จัดเก็บเอกสาร</span>
|
||||
</div>
|
||||
<q-separator />
|
||||
<div class="q-pa-md">
|
||||
<tree-explorer :data="data" :level="1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<!-- <file-download /> -->
|
||||
<div class="bg-white rounded-borders shadow-5 relative">
|
||||
<search-bar v-if="mode === 'user'" />
|
||||
<div class="bg-white q-pa-md">
|
||||
<div class="row items-center justify-between">
|
||||
<span class="text-h6">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
class="q-mr-sm q-px-sm"
|
||||
v-if="currentDept > 0"
|
||||
@click="() => gotoParent()"
|
||||
>
|
||||
<q-icon name="arrow_back" size="1rem" color="primary"
|
||||
/></q-btn>
|
||||
<span>{{ DEPT_NAME[currentDept] }}</span>
|
||||
<q-btn
|
||||
v-if="mode === 'admin' && viewMode === 'view_module'"
|
||||
class="q-px-md q-ml-md"
|
||||
label="สร้างตู้เก็บเอกสาร"
|
||||
type="submit"
|
||||
color="primary"
|
||||
dense
|
||||
icon="add"
|
||||
@click=""
|
||||
/>
|
||||
</span>
|
||||
<q-btn
|
||||
flat
|
||||
color="blue-grey-2"
|
||||
:icon="viewMode"
|
||||
@click="
|
||||
() => {
|
||||
viewMode =
|
||||
viewMode === 'view_list' ? 'view_module' : 'view_list'
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<file-searched v-if="isSearch === true" />
|
||||
<file-item
|
||||
:viewMode="viewMode"
|
||||
:action="props.mode === 'admin'"
|
||||
v-if="isSearch === false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
62
Services/client/src/components/Profile.vue
Normal file
62
Services/client/src/components/Profile.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import KeyCloakService from '@/services/KeyCloakService'
|
||||
const dropdownOpen = ref<boolean>(false)
|
||||
const accountName = ref<string>()
|
||||
|
||||
accountName.value = KeyCloakService.GetUserName()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
@click="() => (dropdownOpen = !dropdownOpen)"
|
||||
class="row q-px-md cursor"
|
||||
id="app-toolbar-title"
|
||||
>
|
||||
<div class="col">
|
||||
<q-avatar>
|
||||
<img :src="`https://cdn.quasar.dev/img/avatar1.jpg`" />
|
||||
</q-avatar>
|
||||
</div>
|
||||
|
||||
<div class="cow">
|
||||
<div class="row q-pl-sm">
|
||||
<span class="text-body1">
|
||||
{{ accountName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row q-pl-sm">
|
||||
<span class="text-caption text-grey"> เจ้าหน้าที่ </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-btn-dropdown stretch flat v-model="dropdownOpen">
|
||||
<q-list>
|
||||
<q-item
|
||||
clickable
|
||||
v-close-popup
|
||||
tabindex="0"
|
||||
@click="
|
||||
() => {
|
||||
KeyCloakService.CallLogOut()
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="logout" color="primary" text-color="white" caption>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>Logout</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-separator inset spaced></q-separator>
|
||||
</q-list>
|
||||
</q-btn-dropdown>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
52
Services/client/src/components/TreeExplorer.vue
Normal file
52
Services/client/src/components/TreeExplorer.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script setup lang="ts">
|
||||
import { useTreeDataStore, type TreeDataFolder } from '@/stores/tree-data'
|
||||
const { getFolder } = useTreeDataStore()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
data: TreeDataFolder[]
|
||||
level: number
|
||||
}>(),
|
||||
{
|
||||
level: 0,
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-list v-for="folder in data" class="rounded-borders">
|
||||
<q-expansion-item
|
||||
@click="() => getFolder(folder.pathname, false)"
|
||||
:header-inset-level="level * 0.25"
|
||||
:group="level.toString()"
|
||||
:hide-expand-icon="level === 4"
|
||||
:icon="
|
||||
level === 1
|
||||
? 'mdi-file-cabinet'
|
||||
: level === 2
|
||||
? 'inbox'
|
||||
: 'o_folder_open'
|
||||
"
|
||||
:label="folder.name"
|
||||
v-model="folder.status"
|
||||
>
|
||||
<tree-explorer
|
||||
v-if="folder.folder"
|
||||
:data="folder.folder"
|
||||
:level="props.level + 1"
|
||||
/>
|
||||
</q-expansion-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.q-item[aria-expanded='true'] {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.q-item[aria-expanded='true'] .q-icon {
|
||||
color: $primary;
|
||||
}
|
||||
</style>
|
||||
44
Services/client/src/main.ts
Normal file
44
Services/client/src/main.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { createApp, defineAsyncComponent } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { Dialog, Quasar, Loading } from 'quasar'
|
||||
import th from 'quasar/lang/th'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
import HttpService from '@/services/HttpService'
|
||||
import quasarUserOptions from './quasar-user-options'
|
||||
import router from './router'
|
||||
|
||||
import 'quasar/src/css/index.sass'
|
||||
import '@vuepic/vue-datepicker/dist/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
|
||||
app.use(Quasar, {
|
||||
...quasarUserOptions,
|
||||
plugins: {
|
||||
Dialog,
|
||||
Loading,
|
||||
},
|
||||
lang: th,
|
||||
})
|
||||
|
||||
app.component(
|
||||
'full-loader',
|
||||
defineAsyncComponent(() => import('@/components/FullLoader.vue'))
|
||||
)
|
||||
|
||||
app.component(
|
||||
'datepicker',
|
||||
defineAsyncComponent(() => import('@vuepic/vue-datepicker'))
|
||||
)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
HttpService.configureAxiosKeycloak()
|
||||
|
||||
console.log(import.meta.env)
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isAdvncSearchClick = ref<boolean>(false)
|
||||
const optionsField = [
|
||||
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
|
||||
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
|
||||
]
|
||||
const optionsOp = [
|
||||
{ label: 'และ', value: 'AND' },
|
||||
{ label: 'หรือ', value: 'OR' },
|
||||
]
|
||||
const advSearchData = ref<
|
||||
{
|
||||
op: 'AND' | 'OR'
|
||||
field: 'title' | 'keyword'
|
||||
value: string
|
||||
}[]
|
||||
>([])
|
||||
const props = defineProps<{
|
||||
field: string
|
||||
value: string
|
||||
keyword?: string
|
||||
description?: string
|
||||
searchSubmit: Function
|
||||
submitSearchData: {
|
||||
AND: {
|
||||
field?: string
|
||||
value?: string
|
||||
}[]
|
||||
OR: {
|
||||
field: string
|
||||
value: string
|
||||
}[]
|
||||
}
|
||||
}>()
|
||||
|
||||
defineExpose({
|
||||
clearAdvData,
|
||||
advSearchData,
|
||||
})
|
||||
|
||||
defineEmits([
|
||||
'update:field',
|
||||
'update:value',
|
||||
'update:keyword',
|
||||
'update:description',
|
||||
])
|
||||
|
||||
function addSearchData() {
|
||||
advSearchData.value.push({
|
||||
op: 'AND',
|
||||
field: 'title',
|
||||
value: '',
|
||||
})
|
||||
}
|
||||
|
||||
function clearAdvData() {
|
||||
advSearchData.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<q-btn
|
||||
outline
|
||||
color="primary"
|
||||
icon="mdi-tools"
|
||||
label="ค้นหาขั้นสูง"
|
||||
@click="() => (isAdvncSearchClick = true)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-dialog v-model="isAdvncSearchClick">
|
||||
<q-card style="width: 1000px; max-width: 80vw">
|
||||
<q-toolbar class="bg-grey-1 q-py-sm q-px-lg">
|
||||
<q-toolbar-title>
|
||||
<span class="text-weight-bold">ค้นหาขั้นสูง</span>
|
||||
</q-toolbar-title>
|
||||
<q-btn flat round dense icon="close" v-close-popup color="red" />
|
||||
</q-toolbar>
|
||||
<q-separator />
|
||||
|
||||
<div class="col">
|
||||
<q-card-section class="row q-mt-sm">
|
||||
<div class="col">
|
||||
<div class="row q-col-gutter-md q-mb-md items-center">
|
||||
<div class="col-12 col-md-3">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:options="optionsField"
|
||||
:model-value="props.field"
|
||||
@update:model-value="(value) => $emit('update:field', value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
placeholder="เอกสาร"
|
||||
:model-value="props.value"
|
||||
@update:model-value="(value) => $emit('update:value', value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-btn
|
||||
dense
|
||||
color="teal"
|
||||
icon="mdi-plus"
|
||||
v-if="advSearchData.length === 0"
|
||||
@click="addSearchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="row q-col-gutter-md q-mb-md items-center"
|
||||
v-for="(_, index) in advSearchData"
|
||||
>
|
||||
<div class="col-4 col-md-2">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
v-model="advSearchData[index].op"
|
||||
:options="optionsOp"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-8 col-md-3">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
v-model="advSearchData[index].field"
|
||||
:options="optionsField"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
v-model="advSearchData[index].value"
|
||||
placeholder="เอกสาร"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<q-btn
|
||||
dense
|
||||
color="teal"
|
||||
icon="mdi-plus"
|
||||
v-if="index === advSearchData.length - 1"
|
||||
@click="addSearchData"
|
||||
/>
|
||||
<q-btn
|
||||
dense
|
||||
flat
|
||||
icon="mdi-minus"
|
||||
color="red"
|
||||
v-if="index === advSearchData.length - 1"
|
||||
@click="() => advSearchData.pop()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="row q-mb-md">
|
||||
<div class="col">
|
||||
<span>อื่น ๆ</span>
|
||||
<q-separator class="q-mb-lg" />
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-3 q-gutter-y-sm">
|
||||
<span class="text-weight-bold">คำสำคัญ</span>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
placeholder="กรอกคำสำคัญ"
|
||||
:model-value="props.keyword"
|
||||
@update:model-value="
|
||||
(value) => $emit('update:keyword', value)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 q-gutter-y-sm">
|
||||
<span class="text-weight-bold">รายละเอียดของเอกสาร</span>
|
||||
<q-input
|
||||
dense
|
||||
outlined
|
||||
placeholder="กรอกรายละเอียด"
|
||||
:model-value="props.description"
|
||||
@update:model-value="
|
||||
(value) => $emit('update:description', value)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
|
||||
<q-toolbar class="row justify-end">
|
||||
<div class="q-py-md">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
v-close-popup
|
||||
@click="() => props.searchSubmit()"
|
||||
/>
|
||||
</div>
|
||||
</q-toolbar>
|
||||
</div>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
188
Services/client/src/modules/01_user/components/SearchBar.vue
Normal file
188
Services/client/src/modules/01_user/components/SearchBar.vue
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<script setup lang="ts">
|
||||
import axios from 'axios'
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import type { EhrFile } from '@/stores/tree-data'
|
||||
|
||||
import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
import { useLoader } from '@/stores/loader'
|
||||
|
||||
const loaderStore = useLoader()
|
||||
const { isSearch } = storeToRefs(useSearchDataStore())
|
||||
const { getFoundFile } = useSearchDataStore()
|
||||
const advSearchComp = ref<InstanceType<typeof AdvancedSearch>>()
|
||||
const isAdvSearchCall = ref<boolean>(false)
|
||||
const optionsField = [
|
||||
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
|
||||
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
|
||||
]
|
||||
const searchData = ref<{
|
||||
field: string
|
||||
value: string
|
||||
keyword?: string
|
||||
description?: string
|
||||
}>({
|
||||
field: 'title',
|
||||
value: '',
|
||||
keyword: '',
|
||||
description: '',
|
||||
})
|
||||
const submitSearchData = ref<{
|
||||
AND: {
|
||||
field?: string
|
||||
value?: string
|
||||
}[]
|
||||
OR: { field: string; value: string }[]
|
||||
}>({
|
||||
AND: [],
|
||||
OR: [],
|
||||
})
|
||||
|
||||
function clearSearchData() {
|
||||
searchData.value = {
|
||||
field: 'title',
|
||||
value: '',
|
||||
keyword: '',
|
||||
description: '',
|
||||
}
|
||||
submitSearchData.value = {
|
||||
AND: [],
|
||||
OR: [],
|
||||
}
|
||||
advSearchComp.value?.clearAdvData()
|
||||
isAdvSearchCall.value = false
|
||||
isSearch.value = false
|
||||
}
|
||||
|
||||
async function searchSubmit() {
|
||||
submitSearchData.value = {
|
||||
AND: [],
|
||||
OR: [],
|
||||
}
|
||||
if (searchData.value.field && searchData.value) {
|
||||
submitSearchData.value?.AND.push({
|
||||
field: searchData.value.field,
|
||||
value: searchData.value.value,
|
||||
})
|
||||
if (searchData.value.keyword) {
|
||||
submitSearchData.value?.AND.push({
|
||||
field: 'keyword',
|
||||
value: searchData.value.keyword,
|
||||
})
|
||||
}
|
||||
if (searchData.value.description) {
|
||||
submitSearchData.value?.AND.push({
|
||||
field: 'description',
|
||||
value: searchData.value.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdvSearchCall.value) {
|
||||
if (
|
||||
advSearchComp.value?.advSearchData &&
|
||||
advSearchComp.value.advSearchData.length > 0
|
||||
) {
|
||||
advSearchComp.value?.advSearchData.forEach(
|
||||
(d: { field: string; value: string; op: string }) => {
|
||||
if (d.field && d.value) {
|
||||
if (d.op === 'AND') {
|
||||
submitSearchData.value.AND.push({
|
||||
field: d.field,
|
||||
value: d.value,
|
||||
})
|
||||
} else if (d.op === 'OR') {
|
||||
submitSearchData.value.OR.push({ field: d.field, value: d.value })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
try {
|
||||
loaderStore.show()
|
||||
isAdvSearchCall.value = true
|
||||
const res = await axios.post<EhrFile[]>(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}/search`,
|
||||
submitSearchData.value
|
||||
)
|
||||
getFoundFile(res.data)
|
||||
isSearch.value = true
|
||||
} catch (error) {
|
||||
console.error('Error during the request', error)
|
||||
} finally {
|
||||
loaderStore.hide()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="q-py-lg q-px-md bg-grey-1">
|
||||
<div class="row items-center q-gutter-md">
|
||||
<div class="col-12 col-md-3">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
class="bg-white"
|
||||
v-model="searchData.field"
|
||||
:options="optionsField"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-grow">
|
||||
<q-input
|
||||
class="bg-white"
|
||||
dense
|
||||
outlined
|
||||
v-model="searchData.value"
|
||||
placeholder="เอกสาร"
|
||||
@keydown.enter.prevent="searchSubmit"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isAdvSearchCall === false">
|
||||
<q-btn
|
||||
class="col-12 col-md-2"
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
@click="() => searchSubmit()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isAdvSearchCall"
|
||||
class="row items-center justify-between q-gutter-y-md q-mt-xs"
|
||||
>
|
||||
<div>
|
||||
<advanced-search
|
||||
ref="advSearchComp"
|
||||
:searchSubmit="searchSubmit"
|
||||
:submit-search-data="submitSearchData"
|
||||
v-model:field="searchData.field"
|
||||
v-model:value="searchData.value"
|
||||
v-model:keyword="searchData.keyword"
|
||||
v-model:description="searchData.description"
|
||||
/>
|
||||
</div>
|
||||
<div class="q-gutter-x-md">
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
@click="searchSubmit"
|
||||
/>
|
||||
<q-btn
|
||||
color="orange-12"
|
||||
label="ล้างข้อมูล"
|
||||
icon="close"
|
||||
@click="clearSearchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator />
|
||||
</template>
|
||||
9
Services/client/src/modules/01_user/router.ts
Normal file
9
Services/client/src/modules/01_user/router.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// const adminHomePage = () => import('@/modules/02_transfer/views/Main.vue')
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
name: 'UserHomePage',
|
||||
component: () => import('@/modules/01_user/views/homePage.vue')
|
||||
}
|
||||
]
|
||||
8
Services/client/src/modules/01_user/views/homePage.vue
Normal file
8
Services/client/src/modules/01_user/views/homePage.vue
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import PageLayout from '@/components/PageLayout.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<page-layout mode="user" />
|
||||
</template>
|
||||
<style scoped></style>
|
||||
9
Services/client/src/modules/02_admin/router.ts
Normal file
9
Services/client/src/modules/02_admin/router.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// const adminHomePage = () => import('@/modules/02_transfer/views/Main.vue')
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'AdminHomePage',
|
||||
component: () => import('@/modules/02_admin/views/homePage.vue')
|
||||
}
|
||||
]
|
||||
18
Services/client/src/modules/02_admin/views/homePage.vue
Normal file
18
Services/client/src/modules/02_admin/views/homePage.vue
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import PageLayout from '@/components/PageLayout.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<page-layout mode="admin" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
#container > [class^='col'] > * {
|
||||
border: 1px solid $separator-color;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#container-header {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
</style>
|
||||
10
Services/client/src/quasar-user-options.ts
Normal file
10
Services/client/src/quasar-user-options.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import '@quasar/extras/material-icons/material-icons.css'
|
||||
import '@quasar/extras/material-icons-outlined/material-icons-outlined.css'
|
||||
import '@quasar/extras/fontawesome-v5/fontawesome-v5.css'
|
||||
import '@quasar/extras/mdi-v4/mdi-v4.css'
|
||||
|
||||
// To be used on app.use(Quasar, { ... })
|
||||
export default {
|
||||
config: {},
|
||||
plugins: {},
|
||||
}
|
||||
55
Services/client/src/router/index.ts
Normal file
55
Services/client/src/router/index.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import UserModule from '@/modules/01_user/router'
|
||||
import AdminModule from '@/modules/02_admin/router'
|
||||
import KeyCloakService from '@/services/KeyCloakService'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'UserModule',
|
||||
component: () => import('@/views/MainLayout.vue'),
|
||||
children: [...UserModule],
|
||||
meta: {
|
||||
statusAccount: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
name: 'AdminModule',
|
||||
component: () => import('@/views/MainLayout.vue'),
|
||||
beforeEnter: (_to, _from, next) => {
|
||||
const token = KeyCloakService.GetAccesToken()
|
||||
if (token) {
|
||||
next()
|
||||
} else {
|
||||
KeyCloakService.CallLogin(() => {
|
||||
const tokenAfterLogin = KeyCloakService.GetAccesToken()
|
||||
if (tokenAfterLogin) {
|
||||
next()
|
||||
} else {
|
||||
console.error('ไม่สามารถดึง Token หลังจากล็อกอินได้')
|
||||
next('/')
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
meta: {
|
||||
statusAccount: true,
|
||||
},
|
||||
children: [...AdminModule],
|
||||
},
|
||||
/**
|
||||
* 404 Not Found
|
||||
* ref: https://router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route
|
||||
*/
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('@/views/ErrorNotFoundPage.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
12
Services/client/src/router/loader.ts
Normal file
12
Services/client/src/router/loader.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
***** DEPRECATED - Must be delete later *****
|
||||
*/
|
||||
|
||||
/**โหลดหน้าแบบ async/await
|
||||
* @param view "ชี่อไฟล์".vue
|
||||
* @param folder "folderในsrc" [Ex. /src/"folder"] default=views เมื่อไม่ส่งค่า
|
||||
*/
|
||||
export function load(view: string, folder: string = 'views') {
|
||||
// console.log(`@/${folder}/${view}.vue`);
|
||||
return async () => await import(`@/${folder}/${view}.vue`)
|
||||
}
|
||||
37
Services/client/src/services/HttpService.ts
Normal file
37
Services/client/src/services/HttpService.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { AxiosInstance, InternalAxiosRequestConfig } from 'axios'
|
||||
import axios from 'axios'
|
||||
import KeyCloakService from '@/services/KeyCloakService'
|
||||
|
||||
const HttpMethods = {
|
||||
GET: 'GET',
|
||||
POST: 'POST',
|
||||
DELETE: 'DELETE',
|
||||
}
|
||||
|
||||
const _axios = axios.create()
|
||||
const cb = (config: InternalAxiosRequestConfig) => {
|
||||
config.headers.Authorization = `Bearer ${KeyCloakService.GetAccesToken()}`
|
||||
console.log(config.headers)
|
||||
return config
|
||||
}
|
||||
|
||||
const configureAxiosKeycloak = (): void => {
|
||||
_axios.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
||||
if (KeyCloakService.IsLoggedIn()) {
|
||||
KeyCloakService.UpdateToken(cb(config))
|
||||
}
|
||||
return config
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const getAxiosClient = (): AxiosInstance => _axios
|
||||
|
||||
const HttpService = {
|
||||
HttpMethods,
|
||||
configureAxiosKeycloak,
|
||||
getAxiosClient,
|
||||
}
|
||||
|
||||
export default HttpService
|
||||
71
Services/client/src/services/KeyCloakService.ts
Normal file
71
Services/client/src/services/KeyCloakService.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import Keycloak from "keycloak-js";
|
||||
|
||||
const keycloakInstance = new Keycloak();
|
||||
|
||||
interface CallbackOneParam<T1 = void, T2 = void> {
|
||||
(param1: T1): T2;
|
||||
}
|
||||
/**
|
||||
* Initializes Keycloak instance and calls the provided callback function if successfully authenticated.
|
||||
*
|
||||
* @param onAuthenticatedCallback
|
||||
*/
|
||||
const Login = (onAuthenticatedCallback: CallbackOneParam): void => {
|
||||
keycloakInstance
|
||||
.init({ onLoad: "login-required" })
|
||||
.then(function (authenticated) {
|
||||
authenticated ? onAuthenticatedCallback() : alert("non authenticated");
|
||||
})
|
||||
.catch((e) => {
|
||||
console.dir(e);
|
||||
console.log(`keycloak init exception: ${e}`);
|
||||
});
|
||||
};
|
||||
|
||||
const UserName = (): string | undefined =>
|
||||
keycloakInstance?.tokenParsed?.preferred_username;
|
||||
|
||||
const Token = (): string | undefined => keycloakInstance?.token;
|
||||
const IdToken = (): string | undefined => keycloakInstance?.idToken;
|
||||
|
||||
const LogOut = () => keycloakInstance.logout();
|
||||
|
||||
/*
|
||||
const UserRoles = (): string[] | undefined => {
|
||||
if (keycloakInstance.resourceAccess === undefined) return undefined;
|
||||
if (keycloakInstance.resourceAccess["express-client"] === undefined) return undefined;
|
||||
|
||||
return keycloakInstance.resourceAccess["express-client"].roles;
|
||||
};
|
||||
*/
|
||||
const UserRoles = ():string[] =>{
|
||||
return DecodeToken()?.role
|
||||
}
|
||||
|
||||
|
||||
const updateToken = (successCallback: any) =>
|
||||
keycloakInstance.updateToken(5).then(successCallback).catch(doLogin);
|
||||
|
||||
const doLogin = keycloakInstance.login;
|
||||
|
||||
const isLoggedIn = () => !!keycloakInstance.token;
|
||||
|
||||
const DecodeToken = ()=>{return keycloakInstance.tokenParsed}
|
||||
const DecodeIdToken = ()=>{return keycloakInstance.idTokenParsed}
|
||||
|
||||
const KeycloakService = {
|
||||
CallLogin: Login,
|
||||
GetUserName: UserName,
|
||||
GetAccesToken: Token,
|
||||
GetIdToken: IdToken,
|
||||
CallLogOut: LogOut,
|
||||
GetUserRoles: UserRoles,
|
||||
UpdateToken: updateToken,
|
||||
IsLoggedIn: isLoggedIn,
|
||||
GetDecodeToken:DecodeToken,
|
||||
GetDecodeIdToken:DecodeIdToken
|
||||
};
|
||||
|
||||
export default KeycloakService;
|
||||
|
||||
|
||||
10
Services/client/src/shims-vue.d.ts
vendored
Normal file
10
Services/client/src/shims-vue.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable */
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
// Mocks all files ending in `.vue` showing them as plain Vue instances
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
20
Services/client/src/stores/loader.ts
Normal file
20
Services/client/src/stores/loader.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useLoader = defineStore('data', () => {
|
||||
const loader = ref<boolean>(false)
|
||||
|
||||
function show() {
|
||||
loader.value = true
|
||||
}
|
||||
|
||||
function hide() {
|
||||
loader.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
loader,
|
||||
show,
|
||||
hide,
|
||||
}
|
||||
})
|
||||
18
Services/client/src/stores/searched-data.ts
Normal file
18
Services/client/src/stores/searched-data.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { EhrFile } from '@/stores/tree-data'
|
||||
|
||||
export const useSearchDataStore = defineStore('searched', () => {
|
||||
const foundFile = ref<EhrFile[]>([])
|
||||
const isSearch = ref<Boolean>(false)
|
||||
|
||||
async function getFoundFile(data: EhrFile[]) {
|
||||
foundFile.value = data
|
||||
}
|
||||
|
||||
return {
|
||||
isSearch,
|
||||
foundFile,
|
||||
getFoundFile,
|
||||
}
|
||||
})
|
||||
246
Services/client/src/stores/tree-data.ts
Normal file
246
Services/client/src/stores/tree-data.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import { useLoader } from '@/stores/loader'
|
||||
import HttpService from '@/services/HttpService'
|
||||
|
||||
const axiosClient = HttpService.getAxiosClient()
|
||||
const apiEndpoint: string = import.meta.env.VITE_API_ENDPOINT
|
||||
|
||||
export interface EhrFolder {
|
||||
pathname: string
|
||||
name: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
export interface EhrFile {
|
||||
pathname: string
|
||||
|
||||
fileName: string
|
||||
fileSize: string
|
||||
fileType: string
|
||||
|
||||
title: string
|
||||
description: string
|
||||
category: string[]
|
||||
keyword: string[]
|
||||
|
||||
updatedAt: string
|
||||
updatedBy: string
|
||||
createdAt: string
|
||||
createdBy: string
|
||||
}
|
||||
|
||||
export interface TreeDataFolder {
|
||||
pathname: string
|
||||
name: string
|
||||
status: boolean
|
||||
folder: TreeDataFolder[]
|
||||
file?: EhrFile[]
|
||||
}
|
||||
|
||||
export const useTreeDataStore = defineStore('changeCabinet', () => {
|
||||
const loader = useLoader()
|
||||
|
||||
const data = ref<TreeDataFolder[]>([])
|
||||
const currentFolder = ref<TreeDataFolder[]>([])
|
||||
const currentFile = ref<EhrFile[]>([])
|
||||
const currentPath = ref<string>('')
|
||||
const currentDept = ref<number>(0)
|
||||
|
||||
async function getCabinet() {
|
||||
const res = await axiosClient.get<EhrFolder[]>(`${apiEndpoint}cabinet`)
|
||||
|
||||
data.value = currentFolder.value = res.data.map((v) => ({
|
||||
...v,
|
||||
status: false,
|
||||
folder: [],
|
||||
}))
|
||||
}
|
||||
|
||||
async function getFolder(pathname: string, updateStatus = true) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
currentDept.value = pathArray.length
|
||||
|
||||
if (pathArray.length >= 4) {
|
||||
currentFolder.value = []
|
||||
|
||||
await getFile(pathname)
|
||||
|
||||
let current: (typeof data.value)[number] | undefined
|
||||
current = data.value.find((v) => v.name === pathArray[0])
|
||||
if (current && updateStatus) current.status = true
|
||||
current = current?.folder.find((v) => v.name === pathArray[1])
|
||||
if (current && updateStatus) current.status = true
|
||||
current = current?.folder.find((v) => v.name === pathArray[2])
|
||||
if (current && updateStatus) current.status = true
|
||||
current = current?.folder.find((v) => v.name === pathArray[3])
|
||||
if (current && updateStatus) current.status = true
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
let requestPath = 'cabinet'
|
||||
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}/drawer`
|
||||
if (pathArray.length >= 2) requestPath += `/${pathArray[1]}/folder`
|
||||
if (pathArray.length >= 3) requestPath += `/${pathArray[2]}/subfolder`
|
||||
|
||||
currentPath.value = pathname
|
||||
|
||||
const res = await axiosClient.get<EhrFolder[]>(
|
||||
`${apiEndpoint}${requestPath}`
|
||||
)
|
||||
|
||||
const list = res.data.map((v) => ({
|
||||
...v,
|
||||
status: false,
|
||||
folder: [],
|
||||
}))
|
||||
|
||||
let current: (typeof data.value)[number] | undefined
|
||||
|
||||
if (pathArray.length >= 1) {
|
||||
current = data.value.find((v) => v.name === pathArray[0])
|
||||
if (current && updateStatus) {
|
||||
current.status = true
|
||||
}
|
||||
}
|
||||
|
||||
if (pathArray.length >= 2) {
|
||||
current = current?.folder.find((v) => v.name === pathArray[1])
|
||||
if (current && updateStatus) {
|
||||
current.status = true
|
||||
}
|
||||
}
|
||||
if (pathArray.length >= 3) {
|
||||
current = current?.folder.find((v) => v.name === pathArray[2])
|
||||
if (current && updateStatus) {
|
||||
current.status = true
|
||||
}
|
||||
}
|
||||
|
||||
if (current) current.folder = list
|
||||
|
||||
currentFolder.value = list
|
||||
|
||||
await getFile(pathname)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function getFile(pathname: string) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
if (pathArray.length <= 2) {
|
||||
currentFile.value = []
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
let requestPath = `cabinet/${pathArray[0]}/drawer/${pathArray[1]}`
|
||||
if (pathArray.length >= 3) requestPath += `/folder/${pathArray[2]}`
|
||||
if (pathArray.length >= 4) requestPath += `/subfolder/${pathArray[3]}`
|
||||
requestPath += '/file'
|
||||
|
||||
const res = await axiosClient.get<EhrFile[]>(`${apiEndpoint}${requestPath}`)
|
||||
|
||||
currentFile.value = res.data
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function createFolder(name: string | undefined) {
|
||||
loader.show()
|
||||
|
||||
if (!name) return loader.hide()
|
||||
|
||||
const pathArray: string[] = currentPath.value.split('/').filter(Boolean)
|
||||
let requestPath = 'cabinet'
|
||||
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}/drawer`
|
||||
if (pathArray.length >= 2) requestPath += `/${pathArray[1]}/folder`
|
||||
if (pathArray.length >= 3) requestPath += `/${pathArray[2]}/subfolder`
|
||||
|
||||
await axiosClient.post(`${apiEndpoint}${requestPath}`, {
|
||||
name,
|
||||
})
|
||||
|
||||
if (currentDept.value === 0) await getCabinet()
|
||||
else await getFolder(currentPath.value)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function editFolder(name: string | undefined, pathname: string) {
|
||||
loader.show()
|
||||
|
||||
if (!name) return loader.hide()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
let requestPath = 'cabinet'
|
||||
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}`
|
||||
if (pathArray.length >= 2) requestPath += `/drawer/${pathArray[1]}`
|
||||
if (pathArray.length >= 3) requestPath += `/folder/${pathArray[2]}`
|
||||
if (pathArray.length >= 4) requestPath += `/subfolder/${pathArray[3]}`
|
||||
|
||||
await axiosClient.put(`${apiEndpoint}${requestPath}`, {
|
||||
name: name,
|
||||
})
|
||||
|
||||
if (currentDept.value === 0) await getCabinet()
|
||||
else await getFolder(currentPath.value)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function deleteFolder(pathname: string) {
|
||||
loader.show()
|
||||
|
||||
const pathArray: string[] = pathname.split('/').filter(Boolean)
|
||||
|
||||
let requestPath = 'cabinet'
|
||||
if (pathArray.length >= 1) requestPath += `/${pathArray[0]}`
|
||||
if (pathArray.length >= 2) requestPath += `/drawer/${pathArray[1]}`
|
||||
if (pathArray.length >= 3) requestPath += `/folder/${pathArray[2]}`
|
||||
if (pathArray.length >= 4) requestPath += `/subfolder/${pathArray[3]}`
|
||||
|
||||
await axiosClient.delete(`${apiEndpoint}${requestPath}`)
|
||||
|
||||
if (currentDept.value === 0) await getCabinet()
|
||||
else await getFolder(currentPath.value)
|
||||
|
||||
return loader.hide()
|
||||
}
|
||||
|
||||
async function gotoParent() {
|
||||
const pathname = currentPath.value.split('/').filter(Boolean)
|
||||
|
||||
pathname.pop()
|
||||
|
||||
if (pathname.length === 0) {
|
||||
const cabinet = currentFolder.value.find((v) => v.status === true)
|
||||
if (cabinet) {
|
||||
cabinet.status = false
|
||||
}
|
||||
}
|
||||
|
||||
await getFolder(pathname.join('/') + '/')
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
currentFolder,
|
||||
currentFile,
|
||||
currentDept,
|
||||
getCabinet,
|
||||
getFolder,
|
||||
gotoParent,
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
editFolder,
|
||||
}
|
||||
})
|
||||
24
Services/client/src/style/quasar-variables.sass
Normal file
24
Services/client/src/style/quasar-variables.sass
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
$primary: #2196f3
|
||||
$secondary: #f7f9fa
|
||||
$accent: #9C27B0
|
||||
|
||||
$dark: #35473C
|
||||
|
||||
$positive: #21BA45
|
||||
$negative: #C10015
|
||||
$info: #31CCEC
|
||||
$warning: #F2C037
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+Thai:wght@100;200;300;400;500;600;700;800;900&display=swap')
|
||||
$noto-thai: 'Noto Sans Thai', sans-serif
|
||||
|
||||
body
|
||||
font-family: $noto-thai !important
|
||||
text-rendering: optimizeLegibility
|
||||
-webkit-font-smoothing: antialiased
|
||||
-moz-osx-font-smoothing: grayscale
|
||||
|
||||
$separator-color: #EDEDED !default
|
||||
|
||||
$shadow-color: #c1c1c7 !default
|
||||
|
||||
27
Services/client/src/views/ErrorNotFoundPage.vue
Normal file
27
Services/client/src/views/ErrorNotFoundPage.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Error404NotFound",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="fullscreen bg-secondary text-white text-center q-pa-md flex flex-center"
|
||||
>
|
||||
<div>
|
||||
<div class="text-h1">ไม่พบหน้าที่ต้องการ</div>
|
||||
<div class="text-h2">(404 Page Not Found)</div>
|
||||
<q-btn
|
||||
class="q-mt-xl"
|
||||
color="white"
|
||||
text-color="secondary"
|
||||
unelevated
|
||||
to="/"
|
||||
label="กลับไปหน้าหลัก"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
44
Services/client/src/views/MainLayout.vue
Normal file
44
Services/client/src/views/MainLayout.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
import { useLoader } from '@/stores/loader'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import profile from '@/components/Profile.vue'
|
||||
|
||||
const loaderStore = useLoader()
|
||||
const { loader } = storeToRefs(loaderStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-layout view="hHh lpR fFf">
|
||||
<q-header class="bg-white text-black" bordered>
|
||||
<q-toolbar class="q-py-sm">
|
||||
<q-img
|
||||
src="@/assets/logo.png"
|
||||
spinner-color="white"
|
||||
style="height: 32px; max-width: 32px"
|
||||
/>
|
||||
<div class="column q-px-md" id="app-toolbar-title">
|
||||
<span class="text-body1">ระบบทรัพยากรบุคคล</span>
|
||||
<span class="text-caption text-grey">
|
||||
จัดเก็บข้อมูลผลการประเมิน
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<q-space></q-space>
|
||||
<profile v-if="$route.meta.statusAccount" />
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-page-container>
|
||||
<QPage>
|
||||
<router-view :key="$route.fullPath" />
|
||||
</QPage>
|
||||
</q-page-container>
|
||||
<full-loader :visibility="loader" />
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.q-layout {
|
||||
background: var(--q-secondary);
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue