client initial commit

This commit is contained in:
Methapon2001 2023-11-23 08:47:44 +07:00
parent b5f98baa2b
commit dd1547d7c2
No known key found for this signature in database
GPG key ID: 849924FEF46BD132
70 changed files with 18446 additions and 0 deletions

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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)

View file

@ -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>

View 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>

View 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')
}
]

View 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>

View 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')
}
]

View 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>

View 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: {},
}

View 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

View 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`)
}

View 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

View 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
View 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
}

View 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,
}
})

View 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,
}
})

View 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,
}
})

View 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

View 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>

View 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>