first commit

This commit is contained in:
Warunee Tamkoo 2023-09-06 14:51:44 +07:00
commit eb2f504652
32490 changed files with 5731109 additions and 0 deletions

15
src/App.vue Normal file
View file

@ -0,0 +1,15 @@
<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>

29
src/api/index.ts Normal file
View file

@ -0,0 +1,29 @@
/**config api */
import { ref } from "vue";
const env = ref<string>(process.env.NODE_ENV || "development");
// if (process.env.VUE_APP_TEST) {
// env = "test";
// }
const config = ref<any>({
development: {
// API_URI: "https://localhost:7260/api",
API_URI: "https://bma-ehr.frappet.synology.me/api/v1",
},
test: {
MEET_URI: "meet.frappet.com",
},
production: {
// API_URI: "https://localhost:5010",
API_URI: `${window.location.protocol}//${window.location.host}/api/v1`,
},
});
const API_URI = ref<string>(config.value[env.value].API_URI);
export default {
env: env.value,
config: config.value,
API_URI: API_URI.value,
};

8
src/app.config.ts Normal file
View file

@ -0,0 +1,8 @@
/**ใช้รวมไฟล์ย่อยๆ ของ api แต่ละไฟล์ */
const API = {
};
export default {
API: API,
};

BIN
src/assets/05_modules.pdf Normal file

Binary file not shown.

BIN
src/assets/avatar_user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

0
src/assets/base.css Normal file
View file

BIN
src/assets/ex_slip.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
src/assets/krungthai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

1
src/assets/logo.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 308 B

35
src/assets/main.css Normal file
View file

@ -0,0 +1,35 @@
/* @import './base.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;
}
} */

View file

@ -0,0 +1,11 @@
const chartData = {
personID: "", // Unique ID เป็นเลข ID ที่อ้างอิงไปที่ตัวบุคคล
name: "-", // ขื่อบุคคล
positionName: "-", // ชื่อตำแหน่ง
positionNum: "", // เลขที่ประจำตำแหน่งในโครงสร้าง
departmentName: "", // ชื่อหน่วยงานที่สังกัด
avatar: "", // ภาพถ่าย
children: [], // บุคคลอื่น ๆ ที่อยู่ภายใต้คนนี้ มีโครงสร้างเหมือนกัน
};
export default chartData;

View file

@ -0,0 +1,275 @@
/**
* Root TreeList TreeList
* - Object
* - deptID : Unique ID // Int String
* - departmentName : ชื่อสำนัก//
* - totalPositionCount : จำนวนตำแหน่งทั้งหมดในหน่วยงาน
* - totalPositionVacant : จำนวนตำแหน่งว่างทั้งหมดในหน่วยงาน
* - Object 3
* - heads : เป็น Array Object
* - offcier : เป็น Array Object
* - children : เป็น Array Root children Object
* - Object
* - positionID : Unique ID deptID
* - positionName : ชื่อตำแหน่ง
* - positionNum : เลขที่ประจำตำแหน่ง .
* - totalPositionCount : จำนวนตำแหน่งทั้งหมด
* - totalPositionVacant : จำนวนตำแหน่งว่างทั้งหมด
*/
const chartData = {
// root ของข้อมูลเป็น Object (ไม่ใช่ Array แบบ TreeList)
deptID: 1, // Unique ID ของสำนัก/ฝ่าย/กอง (ถ้ามี?)
departmentName: "สำนักงานเลขานุการผู้ว่าราชการกรุงเทพมหานคร", // ชื่อสำนัก/ฝ่าย/กอง
totalPositionCount: 75, // จำนวนตำแหน่งทั้งหมดในหน่วยงาน
totalPositionVacant: 2, // จำนวนตำแหน่งว่างทั้งหมดในหน่วยงาน
heads: [
// Array ของผู้บริหารในหน่วยงานนั้น ๆ (ผู้อำนวยการ/หัวหน้า)
{
positionID: 2, // Unique ID ของตำแหน่งประเภทนั้น (ถ้ามี?)
positionName: "ผู้อำนวยการสูง", // ชื่อตำแหน่ง
positionNum: "(หัวหน้าสำนักงาน)", // เลขที่ประจำตำแหน่ง (ถ้ามี) เช่นพวก กทข.
totalPositionCount: 1, // จำนวนตำแหน่งทั้งหมด
totalPositionVacant: 0, // จำนวนตำแหน่งว่างทั้งหมด
},
],
officer: [
// Array ของเจ้าหน้าที่ในหน่วยงาน (รายการตำแหน่งในหน่วยงานที่ขึ้นตรงกับ deptID นั้น ๆ)
],
children: [
// Array ของหน่วยงานย่อย เช่น ฝ่าย กอง
{
// โครงสร้างที่เหลือจะเหมือนกับส่วน Root ทุกอย่าง คือแต่ละหน่วยงานย่อย มี heads ไว้ระบุหัวหน้า ผู้อำนวยการ มี Officer ไว้ระบุรายการตำแหน่งในหน่วยงาน มี Children ไว้ระบุหน่วยงานย่อย
deptID: 3,
departmentName: "ฝ่ายบริหารทั่วไป",
totalPositionCount: 14,
totalPositionVacant: 0,
heads: [
{
positionID: 2,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้าฝ่าย)",
totalPositionCount: 1,
totalPositionVacant: 0,
},
],
officer: [
{
positionID: 3,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก",
totalPositionCount: 1,
totalPositionVacant: 0,
},
{
positionID: 4,
positionName: "นักทรัพยากรบุคคล",
positionNum: "ปก./ชก.",
totalPositionCount: 1,
totalPositionVacnt: 0,
},
{
positionID: 5,
positionName: "นักวิชาการเงินและบัญชี",
positionNum: "ปก./ชก.",
totalPositionCount: 2,
totalPositionVacnt: 0,
},
{
positionID: 6,
positionName: "นักวิชาการพัสดุ",
positionNum: "ปก./ชก.",
totalPositionCount: 1,
totalPositionVacnt: 0,
},
{
positionID: 7,
positionName: "เจ้าพนักงานการเงินและบัญชี",
positionNum: "ปง./ชง.",
totalPositionCount: 3,
totalPositionVacnt: 0,
},
{
positionID: 8,
positionName: "เจ้าพนักงานพัสดุ",
positionNum: "ปง./ชง.",
totalPositionCount: 1,
totalPositionVacnt: 0,
},
{
positionID: 9,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 4,
totalPositionVacnt: 0,
},
],
},
{
deptID: 4,
departmentName: "ส่วนประสานนโยบาย",
totalPositionCount: 15,
totalPositionVacant: 0,
heads: [
{
positionID: 10,
positionName: "ผู้อำนวยการต้น",
positionNum: "(ผู้อำนวยการส่วน)",
totalPositionCount: 1,
totalPositionVacant: 0,
},
],
children: [
{
deptID: 11,
departmentName: "กลุ่มงานประชุม",
totalPositionCount: 6,
totalPositionVacant: 0,
heads: [
{
positionID: 11,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้ากลุ่มงาน)",
totalPositionCount: 1,
totalPositionVacant: 0,
},
],
officer: [
{
positionID: 12,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก.",
totalPositionCount: 4,
totalPositionVacant: 0,
},
{
positionID: 13,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 1,
totalPositionVacant: 0,
},
],
},
{
deptID: 12,
departmentName: "กลุ่มงานการเมืองและประสานนโยบาย",
totalPositionCount: 8,
totalPositionVacant: 0,
heads: [
{
positionID: 14,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้ากลุ่มงาน)",
totalPositionCount: 1,
totalPositionVacant: 0,
},
],
officer: [
{
positionID: 15,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก.",
totalPositionCount: 4,
totalPositionVacant: 0,
},
{
positionID: 16,
positionName: "เจ้าพนักงานสถิติ",
positionNum: "ปง./ชง.",
totalPositionCount: 1,
totalPositionVacant: 0,
},
{
positionID: 17,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 2,
totalPositionVacant: 0,
},
],
},
],
},
{
deptID: 14,
departmentName: "ส่วนเรื่องราวร้องทุกข์",
totalPositionCount: 15,
totalPositionVacant: 0,
heads: [
{
positionID: 18,
positionName: "ผู้อำนวยการต้น",
positionNum: "(ผู้อำนวยการส่วน)",
totalPositionCount: 1,
totalPositionVacant: 0,
},
],
children: [
{
deptID: 15,
departmentName: "กลุ่มงานรับเรื่องราวร้องทุกข์",
totalPositionCount: 7,
totalPositionVacant: 0,
heads: [
{
positionID: 19,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้ากลุ่มงาน)",
totalPositionCount: 1,
totalPositionVacant: 0,
},
],
officer: [
{
positionID: 20,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก.",
totalPositionCount: 4,
totalPositionVacant: 0,
},
{
positionID: 21,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 2,
totalPositionVacant: 0,
},
],
},
{
deptID: 16,
departmentName: "กลุ่มงานตรวจสอบ ติดตามและประมวลผล",
totalPositionCount: 7,
totalPositionVacant: 0,
heads: [
{
positionID: 22,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ชพ. (หัวหน้ากลุ่มงาน)",
totalPositionCount: 1,
totalPositionVacant: 0,
},
],
officer: [
{
positionID: 23,
positionName: "นักจัดการงานทั่วไป",
positionNum: "ปก./ชก.",
totalPositionCount: 4,
totalPositionVacant: 0,
},
{
positionID: 24,
positionName: "เจ้าพนักงานธุรการ",
positionNum: "ปง./ชง.",
totalPositionCount: 2,
totalPositionVacant: 0,
},
],
},
],
},
],
};
export default chartData;

8321
src/assets/tree.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,90 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row">
<div class="q-pr-md">
<q-avatar
:icon="icon"
size="lg"
font-size="25px"
color="blue-1"
:text-color="color"
/>
</div>
<div class="col text-dark">
<span class="text-bold">{{ title }}</span>
<br />
<span>{{ message }}</span>
</div>
</q-card-section>
<q-card-actions
align="right"
class="bg-white text-teal"
v-if="onlycancel"
>
<q-btn label="ตกลง" flat color="grey-8" @click="onDialogCancel" />
<!-- <q-btn :label="textOk" :color="color" @click="onOKClick" /> -->
</q-card-actions>
<q-card-actions align="right" class="bg-white text-teal" v-else>
<q-btn label="ยกเลิก" flat color="grey-8" @click="onDialogCancel" />
<q-btn :label="textOk" :color="color" @click="onOKClick" />
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { useDialogPluginComponent } from "quasar";
const props = defineProps({
color: {
type: String,
default: "primary",
},
textOk: {
type: String,
default: "ตกลง",
},
title: {
type: String,
default: "หัวข้อ?",
},
message: {
type: String,
default: "ข้อความ",
},
icon: {
type: String,
default: "question_mark",
},
onlycancel: {
type: Boolean,
default: false,
},
});
defineEmits([
// REQUIRED; need to specify some events that your
// component will emit through useDialogPluginComponent()
...useDialogPluginComponent.emits,
]);
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
// dialogRef - Vue ref to be applied to QDialog
// onDialogHide - Function to be used as handler for @hide on QDialog
// onDialogOK - Function to call to settle dialog with "ok" outcome
// example: onDialogOK() - no payload
// example: onDialogOK({ /*...*/ }) - with payload
// onDialogCancel - Function to call to settle dialog with "cancel" outcome
// this is part of our example (so not required)
function onOKClick() {
// on OK, it is REQUIRED to
// call onDialogOK (with optional payload)
onDialogOK();
// or with payload: onDialogOK({ ... })
// ...and it will also hide the dialog automatically
}
</script>

View file

@ -0,0 +1,24 @@
<!-- แสดง ui การโหลด -->
<template>
<q-inner-loading :showing="loaderVisibility" class="loader">
<q-spinner-cube size="80px" color="primary" />
</q-inner-loading>
</template>
<script setup lang="ts">
import { watch, ref } from "vue";
const props = defineProps({
visibility: Boolean,
});
const loaderVisibility = ref<boolean>(props.visibility);
watch(props, (count, prevCount) => {
loaderVisibility.value = props.visibility;
});
</script>
<style lang="sass">
.loader
z-index: 1000
</style>

View file

@ -0,0 +1,19 @@
<template>
<q-select v-bind="attrs">
<template v-for="(_, slot) in slots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope || {}" />
</template>
<template v-slot:no-option>
<q-item>
<q-item-section class="text-black">
ไมพบขอมลทนหา
</q-item-section>
</q-item>
</template>
</q-select>
</template>
<script setup lang="ts">
import { ref, useAttrs, useSlots } from "vue";
const attrs = ref<any>(useAttrs());
const slots = ref<any>(useSlots());
</script>

88
src/components/Table.vue Normal file
View file

@ -0,0 +1,88 @@
<template>
<q-table
ref="table"
flat
bordered
class="custom-header-table"
v-bind="attrs"
virtual-scroll
:virtual-scroll-sticky-size-start="48"
dense
:pagination-label="paginationLabel"
v-model:pagination="pagination"
>
<template v-slot:pagination="scope">
<q-pagination
v-model="pagination.page"
active-color="primary"
color="dark"
:max="scope.pagesNumber"
:max-pages="5"
size="sm"
boundary-links
direction-links
></q-pagination>
</template>
<template v-for="(_, slot) in slots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope || {}" />
</template>
</q-table>
</template>
<script setup lang="ts">
import { ref, useAttrs, useSlots } from "vue";
const attrs = ref<any>(useAttrs());
const slots = ref<any>(useSlots());
const props = defineProps({
paging: {
type: Boolean,
defualt: false,
},
});
const pagination = ref({
sortBy: "desc",
descending: false,
page: 1,
rowsPerPage: 10,
});
const paginationLabel = (start: string, end: string, total: string) => {
if (props.paging == true)
return " " + start + " ใน " + end + " จากจำนวน " + total + " รายการ";
else return start + "-" + end + " ใน " + total;
};
</script>
<style lang="scss">
.icon-color {
color: #4154b3;
}
.custom-header-table {
height: auto;
.q-table tr:nth-child(odd) td {
background: white;
}
.q-table tr:nth-child(even) td {
background: #f8f8f8;
}
.q-table thead tr {
background: #ecebeb;
}
.q-table thead tr th {
position: sticky;
z-index: 1;
}
/* this will be the loading indicator */
.q-table thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
.q-table thead tr:first-child th {
top: 0;
}
}
</style>

View file

@ -0,0 +1,570 @@
<template>
<div class="q-px-md q-pb-md">
<!-- header บน table นหา แสดงคอลมน มแกไข เพ เผยแพรอม ยกเล (status nornmalData false) -->
<div class="col-12 row q-py-sm" v-if="nornmalData == false">
<q-btn
v-if="!editvisible == true && publicNoBtn == false"
flat
round
:disabled="editvisible == true"
:color="editvisible == true ? 'grey-7' : 'primary'"
@click="edit"
icon="mdi-pencil-outline"
>
<q-tooltip>แกไขขอม</q-tooltip>
</q-btn>
<!-- ยกเล แสดงเม กดปมแกไข -->
<q-btn
v-else
flat
round
:disabled="editvisible == false"
:outline="editvisible == false"
:color="editvisible == false ? 'grey-7' : 'red'"
@click="cancel()"
icon="mdi-undo"
>
<q-tooltip>ยกเล</q-tooltip>
</q-btn>
<!-- <q-separator vertical /> -->
<div class="q-px-sm">
<q-btn
flat
round
:disabled="editvisible == false"
:color="editvisible == false ? 'grey-7' : 'add'"
@click="add"
icon="mdi-plus"
>
<q-tooltip>เพมขอม</q-tooltip>
</q-btn>
<!-- นทกราง แสดงเม กดปมแกไข อมลมการเปนแปลงหร งไมเผยแพรอม -->
<q-btn
flat
round
:disabled="!(editvisible == true && updateData == true)"
:color="
!(editvisible == true && updateData == true) ? 'grey-7' : 'public'
"
@click="checkSave"
v-if="saveNoDraft == false"
icon="mdi-content-save-outline"
>
<q-tooltip>นทกราง</q-tooltip>
</q-btn>
<!-- ลบบนทกราง แสดงเม นทกรางแล -->
<q-btn
flat
round
:disabled="publicData == true"
:color="publicData == true ? 'grey-7' : 'deep-orange'"
@click="DeleteModal"
icon="mdi-file-remove-outline"
v-if="publicNoBtn == false"
>
<q-tooltip>ลบบนทกราง</q-tooltip>
</q-btn>
</div>
<!-- เผยแพร -->
<q-btn
flat
round
:disabled="!(publicData == false || updateData == true)"
:color="
!(publicData == false || updateData == true) ? 'grey-7' : 'public'
"
@click="publishModal"
icon="mdi-cloud-upload-outline"
v-if="publicNoBtn == false"
>
<q-tooltip>เผยแพร</q-tooltip>
</q-btn>
<div class="items-center" style="display: flex">
<div
class="row items-center"
style="display: flex"
v-if="publicData == false && publicNoBtn == false"
>
<div class="text-public text-body2 text-weight-medium q-px-sm">
อมลยงไมเผยแพร
</div>
</div>
</div>
<q-space />
<div class="items-center" style="display: flex">
<!-- อความสถานะเผยแพร โดยใช parameter publicData เปนตวกำหนดขอความ -->
<!-- <div class="row items-center" style="display: flex" v-if="publicData == true">
<q-icon cener size="20px" name="label_important" class="icon-color" />
<div class="text-size">อมลเผยแพรแล</div>
</div> -->
<div
class="row items-center"
style="display: flex"
v-if="publicData == false && publicNoBtn == false"
>
<!-- <q-icon cener size="20px" name="label_important" color="amber" />
<div class="text-grey-7 text-body2 text-weight-medium q-px-sm">
อมลยงไมเผยแพร
</div> -->
</div>
<!-- นหาขอความใน table -->
<q-input
standout
dense
:model-value="inputfilter"
ref="filterRef"
@update:model-value="updateInput"
outlined
debounce="300"
placeholder="ค้นหา"
style="max-width: 200px"
class="q-ml-sm"
>
<template v-slot:append>
<q-icon v-if="inputfilter == ''" name="search" />
<q-icon
v-if="inputfilter !== ''"
name="clear"
class="cursor-pointer"
@click="resetFilter"
/>
</template>
</q-input>
<!-- แสดงคอลมนใน table -->
<q-select
:model-value="inputvisible"
@update:model-value="updateVisible"
:display-value="$q.lang.table.columns"
multiple
outlined
dense
:options="attrs.columns"
options-dense
option-value="name"
map-options
emit-value
style="min-width: 150px"
class="gt-xs q-ml-sm"
/>
</div>
</div>
<!-- header บน table นหา แสดงคอลมน (status nornmalData true) -->
<div class="col-12 row q-py-sm items-center" v-if="nornmalData == true">
<span class="text-subtitle1">{{ titleText }}</span>
<!-- <q-select
:model-value="inputvisibleFilter"
:options="optionsFilter"
style="min-width: 150px"
class="gt-xs q-ml-sm"
/> -->
<q-select
dense
outlined
:model-value="inputvisibleFilter"
:options="optionsFilter"
class="col-xs-12 col-sm-4 col-md-3"
option-value="id"
option-label="name"
map-options
emit-value
@update:model-value="updateVisibleFilter"
v-if="optionsFilter != undefined && optionsFilter.length > 0"
/>
<q-space />
<div class="items-center" style="display: flex">
<!-- นหาขอความใน table -->
<q-input
standout
dense
:model-value="inputfilter"
ref="filterRef"
@update:model-value="updateInput"
outlined
debounce="300"
placeholder="ค้นหา"
style="max-width: 200px"
class="q-ml-sm"
>
<template v-slot:append>
<q-icon v-if="inputfilter == ''" name="search" />
<q-icon
v-if="inputfilter !== ''"
name="clear"
class="cursor-pointer"
@click="resetFilter"
/>
</template>
</q-input>
<!-- แสดงคอลมนใน table -->
<q-select
:model-value="inputvisible"
@update:model-value="updateVisible"
:display-value="$q.lang.table.columns"
multiple
outlined
dense
:options="attrs.columns"
options-dense
option-value="name"
map-options
emit-value
style="min-width: 150px"
class="gt-xs q-ml-sm"
/>
</div>
</div>
<q-table
ref="table"
flat
bordered
class="custom-header-table"
v-bind="attrs"
virtual-scroll
:virtual-scroll-sticky-size-start="48"
dense
:pagination-label="paginationLabel"
v-model:pagination="pagination"
>
<!-- :rows-per-page-options="paging == true ? [25, 50, 100, 500] : []" -->
<!-- :rows-per-page-options="[0]" -->
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width v-if="boss == true" />
<q-th v-for="col in props.cols" :key="col.name" :props="props">
<span class="text-weight-medium">{{ col.label }}</span>
</q-th>
<q-th
auto-width
v-if="
editvisible == true || nextPageVisible == true || history == true
"
/>
</q-tr>
</template>
<template v-slot:pagination="scope">
<q-pagination
v-model="pagination.page"
active-color="primary"
color="dark"
:max="scope.pagesNumber"
:max-pages="5"
size="sm"
boundary-links
direction-links
></q-pagination>
</template>
<!-- สำหรบเรยกใช template วขางนอก -->
<template #body="props">
<slot v-bind="props" name="columns"></slot>
</template>
</q-table>
</div>
<!-- อมลการเผยแพรอม -->
<!-- <notifyPublishDraft v-model:showModal="modalPublish" :ok="publish" /> -->
<!-- <q-dialog v-model="modalPublish" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row">
<div class="q-pr-md">
<q-avatar
icon="public"
size="lg"
font-size="25px"
color="blue-1"
text-color="public"
/>
</div>
<div class="col text-dark">
<span class="text-bold">องการเผยแพรอมลนหรอไม?</span>
<br />
<span>อมลทกำลงถกเผยแพรจะมผลใชงานทนท</span>
</div>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn label="ยกเลิก" flat color="grey-8" v-close-popup />
<q-btn
label="เผยแพร่"
color="public"
@click="publish()"
v-close-popup
/>
</q-card-actions>
</q-card>
</q-dialog> -->
<!-- อมลการลบเผยแพรอม -->
<!-- <notifyDeleteDraft v-model:showModal="modalDelete" :ok="deleted" /> -->
<!-- <q-dialog v-model="modalDelete" persistent>
<q-card class="q-pa-sm">
<q-card-section class="row">
<div class="q-pr-md">
<q-avatar
icon="mdi-file-remove-outline"
size="lg"
font-size="25px"
color="red-1"
text-color="deep-orange"
/>
</div>
<div class="col text-dark">
<span class="text-bold">องการลบขอมลบนทกรางนหรอไม?</span>
<br />
<span>อมลบนทกรางทกำลงถกลบนจะมผลใชงานทนท</span>
</div>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn label="ยกเลิก" flat color="grey-8" v-close-popup />
<q-btn label="ลบบันทึก" color="red" @click="deleted()" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog> -->
</template>
<script setup lang="ts">
import { ref, useAttrs, computed } from "vue";
import { useCounterMixin } from "@/stores/mixin";
import { useQuasar } from "quasar";
const $q = useQuasar();
const mixin = useCounterMixin(); //
const { dialogMessage } = mixin;
const rows = ref<any>([]);
const attrs = ref<any>(useAttrs());
const table = ref<any>(null);
const filterRef = ref<any>(null);
const modalPublish = ref<boolean>(false);
const modalDelete = ref<boolean>(false);
const pagination = ref({
sortBy: "desc",
descending: false,
page: 1,
rowsPerPage: 10,
});
const pagesNumber = computed(() => {
return Math.ceil(rows.value.length / pagination.value.rowsPerPage);
});
const paginationLabel = (start: string, end: string, total: string) => {
if (props.paging == true)
return " " + start + " ใน " + end + " จากจำนวน " + total + " รายการ";
else return start + "-" + end + " ใน " + total;
};
const props = defineProps({
inputfilter: String,
inputvisible: Array,
inputvisibleFilter: String,
editvisible: Boolean,
titleText: String,
optionsFilter: {
type: Array,
defualt: [],
},
boss: {
type: Boolean,
defualt: false,
},
saveNoDraft: {
type: Boolean,
defualt: false,
},
history: {
type: Boolean,
defualt: false,
},
paging: {
type: Boolean,
defualt: false,
},
nornmalData: {
type: Boolean,
defualt: false,
},
refreshData: {
type: Boolean,
defualt: false,
},
nextPageVisible: {
type: Boolean,
defualt: false,
},
publicData: {
type: Boolean,
defualt: true,
required: false,
},
updateData: {
type: Boolean,
defualt: true,
required: false,
},
publicNoBtn: {
type: Boolean,
defualt: false,
},
add: {
type: Function,
default: () => console.log("not function"),
},
edit: {
type: Function,
default: () => console.log("not function"),
},
save: {
type: Function,
default: () => console.log("not function"),
},
deleted: {
type: Function,
default: () => console.log("not function"),
},
cancel: {
type: Function,
default: () => console.log("not function"),
},
publish: {
type: Function,
default: () => console.log("not function"),
},
validate: {
type: Function,
default: () => console.log("not function"),
},
});
const initialPagination = ref<any>({
// descending: false,
rowsPerPage: props.paging == true ? 25 : 0,
});
const emit = defineEmits([
"update:inputfilter",
"update:inputvisible",
"update:editvisible",
"update:titleText",
"update:inputvisibleFilter",
]);
const updateEdit = (value: any) => {
emit("update:editvisible", value);
};
const updateInput = (value: any) => {
emit("update:inputfilter", value);
};
const updateVisible = (value: any) => {
emit("update:inputvisible", value);
};
const updateVisibleFilter = (value: any) => {
emit("update:inputvisibleFilter", value);
};
const checkSave = () => {
props.validate();
props.save();
// if (myForm.value !== null) {
// myForm.value.validate().then((success) => {
// if (success) {
// }
// });
// }
};
const publishModal = () => {
props.validate();
const filter = attrs.value.rows.filter((r: any) => r.name == "");
if (filter.length == 0 || attrs.value.rows.length == 0) {
// modalPublish.value = true;
dialogMessage(
$q,
"ต้องการเผยแพร่ข้อมูลนี้หรือไม่?",
"ข้อมูลที่กำลังถูกเผยแพร่นี้จะมีผลใช้งานทันที",
"public",
"เผยแพร่",
"public",
props.publish,
undefined
);
}
};
const DeleteModal = () => {
// modalDelete.value = true;
dialogMessage(
$q,
"ต้องการลบข้อมูลบันทึกร่างนี้หรือไม่?",
"ข้อมูลบันทึกร่างที่กำลังถูกลบนี้จะมีผลใช้งานทันที",
"mdi-file-remove-outline",
"ลบบันทึก",
"red",
props.deleted,
undefined
);
};
const edit = async () => {
updateEdit(!props.editvisible);
props.edit();
};
const add = async () => {
// if (myForm.value !== null) {
// myForm.value.validate();
// }
props.validate();
props.add();
await table.value.lastPage();
await table.value.scrollTo(attrs.value.rows.length - 1);
};
const deleted = async () => {
// const deletedF = () => {
if (props.publicNoBtn === false) {
updateEdit(false);
}
props.deleted();
};
const resetFilter = () => {
// reset X
emit("update:inputfilter", "");
filterRef.value.focus();
};
</script>
<style lang="scss">
.icon-color {
color: #4154b3;
}
.custom-header-table {
max-height: 64vh;
.q-table tr:nth-child(odd) td {
background: white;
}
.q-table tr:nth-child(even) td {
background: #f8f8f8;
}
.q-table thead tr {
background: #ecebeb;
}
.q-table thead tr th {
position: sticky;
z-index: 1;
}
/* this will be the loading indicator */
.q-table thead tr:last-child th {
/* height of all previous header rows */
top: 48px;
}
.q-table thead tr:first-child th {
top: 0;
}
}
</style>

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

View file

@ -0,0 +1,598 @@
import { readonly } from "vue";
interface ScrollType {
position: number;
direction: string;
directionChanged: boolean;
inflectionPoint: number;
delta: number;
}
interface tabType {
key: number;
label: string;
tag: string;
}
interface childrenType {
key: number;
label: string;
path?: string;
}
interface menuType {
key: number;
icon: string;
activeIcon: string;
label: string;
path: string;
children?: childrenType[];
}
interface notiType {
id: string;
sender: string;
body: string;
timereceive: Date;
}
interface optionType {
icon: string;
label: string;
value: string;
color: string;
}
const menuList = readonly<any[]>([
{
key: 1,
icon: "mdi-home-variant-outline",
activeIcon: "mdi-home-variant",
label: "หน้าแรก",
path: "dashboard",
role: "dashboard",
},
{
key: 2,
icon: "mdi-file-outline",
activeIcon: "document",
label: "คู่มือการใช้งาน",
path: "manual",
role: "metadata",
},
// {
// key: 3,
// icon: "o_groups",
// activeIcon: "groups",
// label: "โครงสร้างอัตรากำลัง",
// path: "organizational",
// role: "organization",
// children: [
// {
// key: 3.1,
// label: "จัดการตำแหน่ง",
// path: "organizationalMapping",
// },
// {
// key: 3.2,
// label: "แผนภูมิโครงสร้าง",
// path: "organizationalStructChart",
// },
// {
// key: 3.3,
// label: "แผนภูมิองค์กร",
// path: "organizationalOrgChart",
// },
// {
// key: 3.4,
// label: "ผังโครงสร้าง",
// path: "organizationalTree",
// },
// {
// key: 3.5,
// label: "ผังโครงสร้างลูกจ้าง",
// path: "organizationalEmployee",
// },
// {
// key: 3.6,
// label: "จัดการบัญชี 2",
// path: "manageReport2",
// },
// {
// key: 3.7,
// label: "รายงานบัญชี",
// path: "organizationalReport",
// },
// ],
// },
// {
// key: 4,
// icon: "o_contact_page",
// activeIcon: "contact_page",
// label: "ทะเบียนประวัติ",
// path: "registry",
// role: "registry",
// },
// {
// key: 10,
// icon: "o_contact_page",
// activeIcon: "registry_employee_page",
// label: "ทะเบียนประวัติลูกจ้าง",
// path: "registry-employee",
// role: "registryEmployee",
// },
// {
// key: 10,
// icon: "mdi-file-certificate-outline",
// activeIcon: "order",
// label: "ออกคำสั่ง",
// path: "order",
// role: "order",
// },
// {
// key: 5,
// icon: "o_search",
// activeIcon: "search",
// label: "สรรหา",
// path: "recruiting",
// role: "recruit",
// children: [
// {
// label: "ตั้งค่าเว็บสรรหา",
// path: "editorweb",
// key: 5.1,
// role: "recruit",
// },
// {
// label: "สอบแข่งขัน",
// path: "",
// key: 5.2,
// role: "recruit",
// children: [
// {
// label: "จัดการรอบสอบแข่งขัน" /* แข่งขัน */,
// path: "competePeriod",
// role: "recruit",
// },
// {
// label: "สถิติสมัครสอบแข่งขัน" /* แข่งขัน */,
// path: "competePeriodStat",
// role: "recruit",
// },
// ],
// },
// {
// label: "คัดเลือก",
// path: "",
// key: 5.3,
// role: "recruit",
// children: [
// {
// label: "จัดการรอบคัดเลือก",
// path: "qualifyPeriod",
// role: "recruit",
// },
// {
// label: "จัดการรอบคัดเลือกคนพิการ",
// path: "disablePeriod",
// role: "recruit",
// },
// {
// label: "จัดการรายชื่อคัดเลือก",
// path: "manage",
// role: "recruit",
// },
// {
// label: "สถิติสมัครคัดเลือก",
// path: "qualifyPeriodStat",
// role: "recruit",
// },
// {
// label: "สถิติสมัครคัดเลือกคนพิการ",
// path: "qualifyPeriodStatDisable",
// role: "recruit",
// },
// ],
// },
// ],
// },
// {
// key: 6,
// path: "placement",
// icon: "how_to_reg",
// activeIcon: "how_to_reg",
// label: "บรรจุ แต่งตั้ง ย้าย โอน",
// role: "placement",
// children: [
// {
// key: 6.1,
// label: "รายชื่อผู้สอบผ่าน" /* รายชื่อผู้สอบผ่าน */,
// path: "placement",
// role: "placement",
// },
// // {
// // key: 6.2,
// // label: "ออกคำสั่ง" /* ออกคำสั่งบรรจุ */,
// // path: "Orderplacement",
// // role: "placement",
// // },
// {
// key: 6.3,
// label: "การทดลองงาน" /* การทดลองงาน */,
// path: "probation",
// role: "probation",
// },
// {
// key: 6.4,
// label: "รายการขอโอน" /* */,
// path: "transfer",
// role: "placement",
// },
// {
// key: 6.5,
// label: "รายการรับโอน" /* */,
// path: "receive",
// role: "placement",
// },
// {
// key: 6.6,
// label: "รายการช่วยราชการ" /* */,
// path: "help-government",
// role: "placement",
// },
// {
// key: 6.7,
// label: "รายการส่งตัวกลับ" /* */,
// path: "repatriate",
// role: "placement",
// },
// {
// key: 6.8,
// label: "รายการแต่งตั้ง-เลื่อน" /* */,
// path: "appoint-promote",
// role: "placement",
// },
// {
// key: 6.9,
// label: "รายการย้าย" /* */,
// path: "relocation",
// role: "placement",
// },
// {
// key: 6.1,
// label: "รายการอื่นๆ" /* */,
// path: "other",
// role: "placement",
// },
// ],
// },
// {
// key: 7,
// icon: "mdi-account-cancel-outline",
// activeIcon: "mdi-account-cancel",
// label: "พ้นจากราชการ",
// path: "retirement",
// role: "retirement",
// children: [
// {
// key: 7.1,
// label: "ประกาศเกษียณ",
// path: "retirement",
// role: "retirement",
// },
// {
// key: 7.2,
// label: "รายการลาออก",
// path: "resign",
// role: "retirement",
// },
// // {
// // key: 7.3,
// // label: "คำสั่งลาออก",
// // path: "resign-order",
// // role: "retirement",
// // },
// {
// key: 7.3,
// label: "Exit interview",
// path: "exit-Interview",
// role: "retirement",
// },
// {
// key: 7.4,
// label: "รายการบันทึกการถึงแก่กรรม",
// path: "deceased",
// role: "retirement",
// },
// {
// key: 7.5,
// label: "รายการให้ออก",
// path: "dismiss-order",
// role: "retirement",
// },
// {
// key: 7.6,
// label: "รายการปลดออก",
// path: "discharged",
// role: "retirement",
// },
// {
// key: 7.7,
// label: "รายการไล่ออก",
// path: "expulsion",
// role: "retirement",
// },
// ],
// },
// {
// key: 8,
// icon: "mdi-medal-outline",
// activeIcon: "mdi-medal",
// label: "เครื่องราชฯ",
// path: "insignia",
// role: "insignia",
// children: [
// {
// key: 8.1,
// label: "รอบการเสนอขอ",
// path: "insigniaProposals",
// role: "insignia",
// },
// {
// key: 8.2,
// label: "จัดการคำขอ",
// path: "insigniaManage",
// role: "insignia",
// },
// {
// key: 8.3,
// label: "บันทึกผลการเสนอขอ",
// path: "insigniaRecord",
// role: "insignia",
// },
// {
// key: 8.4,
// label: "จัดสรรเครื่องราชฯ",
// path: "insigniaAllocate",
// role: "insignia",
// },
// {
// key: 8.6,
// label: "ยืม-คืนเครื่องราชฯ",
// path: "insigniaBorrow",
// role: "insignia",
// },
// {
// key: 8.5,
// label: "รายงาน",
// path: "insigniaReport",
// role: "insignia",
// },
// ],
// },
// {
// key: 8,
// icon: "mdi-medal-outline",
// activeIcon: "mdi-medal",
// label: "เครื่องราชฯ",
// path: "insignia",
// role: "insignia",
// children: [
// {
// key: 8.1,
// label: "เครื่องราชฯ",
// path: "",
// role: "insignia",
// children: [
// {
// label: "รอบการเสนอขอ",
// path: "insigniaProposals",
// role: "insignia",
// },
// {
// label: "จัดการคำขอ",
// path: "insigniaManage",
// role: "insignia",
// },
// {
// label: "บันทึกผลการได้รับพระราช...",
// path: "insigniaRecord",
// role: "insignia",
// },
// {
// label: "บันทึกผลการจ่ายใบกำกับ",
// path: "VatInsignia",
// role: "insignia",
// },
// {
// label: "จัดสรรเครื่องราชฯ",
// path: "insigniaAllocate",
// role: "insignia",
// },
// {
// label: "รายงาน",
// path: "insigniaReport",
// role: "insignia",
// },
// ],
// },
// {
// key: 8.2,
// label: "เหรียญจักรพรรดิมาลา",
// path: "",
// role: "coin",
// children: [
// {
// label: "รอบการเสนอขอ",
// path: "coinProposals",
// role: "coin",
// },
// {
// label: "จัดการคำขอ",
// path: "coinManage",
// role: "coin",
// },
// {
// label: "บันทึกผลการได้รับพระราชทานเหรียญจักรพรรดิมาลา",
// path: "coinReceive",
// role: "coin",
// },
// {
// label: "บันทึกผลการจ่ายใบกำกับ",
// path: "coinPayment",
// role: "coin",
// },
// {
// label:
// "รายชื่อที่ยื่นคำร้องขอแก้ไขข้อมูลการขอพระราชทานเหรียญจักรพรรดิมาลา",
// path: "coinRequest",
// role: "coin",
// },
// // {
// // label: "ประวัติการยื่นขอ",
// // path: "coinHistory",
// // role: "coin",
// // },
// {
// label: "จัดสรรเหรียญตรา",
// path: "coinAllocate",
// role: "coin",
// },
// {
// label: "รายงาน",
// path: "coinReport",
// role: "coin",
// },
// ],
// },
// ],
// },
]);
const tabList = readonly<tabType[]>([
{
key: 1,
label: "ข้อมูลทั่วไป",
tag: "information",
},
{
key: 19,
label: "ประวัติการเปลี่ยนชื่อ",
tag: "oldName",
},
{
key: 16,
label: "ข้อมูลราชการ",
tag: "government",
},
{
key: 17,
label: "ข้อมูลที่อยู่",
tag: "address",
},
{
key: 18,
label: "ข้อมูลครอบครัว",
tag: "family",
},
{
key: 15,
label: "ใบอนุญาตประกอบอาชีพ",
tag: "certicate",
},
{
key: 2,
label: "ประวัติการศึกษา",
tag: "education",
},
{
key: 3,
label: "การฝึกอบรม/ดูงาน",
tag: "training",
},
{
key: 4,
label: "เครื่องราชอิสริยาภรณ์",
tag: "insignia",
},
{
key: 5,
label: "ประกาศเกียรติคุณ",
tag: "coined",
},
{
key: 6,
label: "ผลการประเมินปฏิบัติราชการ",
tag: "assessment",
},
{
key: 7,
label: "ตำแหน่ง/เงินเดือน/ค่าจ้าง",
tag: "position",
},
{
key: 8,
label: "วินัย",
tag: "rule",
},
{
key: 9,
label: "การลา",
tag: "leave",
},
{
key: 10,
label: "ความสามารถพิเศษ",
tag: "talent",
},
{
key: 11,
label: "ปฎิบัติราชการพิเศษ",
tag: "work",
},
{
key: 12,
label: "บันทึกวันที่ไม่ได้รับเงินเดือนฯ",
tag: "record",
},
{
key: 13,
label: "อื่นๆ",
tag: "other",
},
{
key: 14,
label: "เอกสารหลักฐาน",
tag: "document",
},
]);
const tabListPlacement = readonly<tabType[]>([
{
key: 1,
label: "ข้อมูลทั่วไป",
tag: "information",
},
{
key: 2,
label: "ใบอนุญาตประกอบอาชีพ",
tag: "certicate",
},
{
key: 3,
label: "ประวัติการศึกษา",
tag: "education",
},
{
key: 4,
label: "ผลการสอบ",
tag: "examresult",
},
{
key: 5,
label: "การคัดกรองคุณสมบัติ",
tag: "qualification",
},
]);
export { menuList, tabList, tabListPlacement };
export type { ScrollType, tabType, menuType, notiType, optionType };

View file

@ -0,0 +1,46 @@
//response api เกี่ยวกับ status code กับ error
interface RequestHistoryObject {
message: String;
result: ResultHistoryObject;
status: number;
}
//rusult แนบ id version mongo
interface ResultHistoryObject {
id: String;
items: RequestPrefixHistoryObject;
version: String;
}
//ข้อมูล คำนำหน้าชื่อ
interface RequestPrefixHistoryObject {
createdAt?: Date;
createdFullName: String;
createdUserId: String;
id: String;
isActive: Boolean;
lastUpdateFullName: String;
lastUpdateUserId: String;
lastUpdatedAt?: Date;
name: String;
}
//columns
interface PrefixColumns {
[index: number]: {
name: String;
align: String;
label: String;
sortable: Boolean;
field: String;
headerStyle: String;
style: String;
};
}
export type {
RequestHistoryObject,
ResultHistoryObject,
RequestPrefixHistoryObject,
PrefixColumns,
};

View file

@ -0,0 +1,27 @@
interface ResponseInbox {
body: string;
createdAt: Date;
createdFullName: string;
createdUserId: string;
id: string;
isOpen: boolean;
lastUpdateFullName: string;
lastUpdateUserId: string;
lastUpdatedAt: Date;
openDate: Date | null;
payload: string;
receiveDate: Date;
receiverUserId: string;
subject: string;
}
interface DataInbox {
no: string;
sender: string;
subject: string;
timereceive: Date;
body: string;
ratingModel: number;
}
export type { ResponseInbox, DataInbox };

View file

@ -0,0 +1,14 @@
//ข้อมูล คำนำหน้าชื่อ
interface ResponsePrefixHistoryObject {
createdAt?: Date;
createdFullName: String;
createdUserId: String;
id: String;
isActive: Boolean;
lastUpdateFullName: String;
lastUpdateUserId: String;
lastUpdatedAt?: Date;
name: String;
}
export type { ResponsePrefixHistoryObject };

76
src/main.ts Normal file
View file

@ -0,0 +1,76 @@
import { createApp, defineAsyncComponent } from "vue";
import App from "./App.vue";
import router from "./router";
import { Dialog, Notify, Quasar, Loading } from "quasar";
import quasarUserOptions from "./quasar-user-options";
import "quasar/src/css/index.sass";
import th from "quasar/lang/th";
import http from "./plugins/http";
import { createPinia } from "pinia";
// organization
// position
// positionEmployee
//calendar
// insignia
// import './assets/main.css'
// Import GlobalFilters
import filters from "./plugins/filters";
const app = createApp(App);
const pinia = createPinia();
// เพิ่ม Global Filters ลงใน App
app.config.globalProperties.$filters = filters;
app.use(router);
app.use(pinia);
app.use(
Quasar,
{
plugins: {
Notify,
Dialog,
Loading,
}, // import Quasar plugins and add here
config: {
notify: {
/* look at QuasarConfOptions from the API card */
},
loading: {
/* look at QuasarConfOptions from the API card */
},
},
lang: th,
}
// quasarUserOptions // build ไม่ผ่านหลัง install code org&structure chart เลยต้องคอมเม้นไว้ก่อน เทสแล้วยังรันได้หลังคอมเม้น
);
//** Global Components */
app.component(
"data-table",
defineAsyncComponent(() => import("@/components/TableView.vue"))
);
app.component(
"full-loader",
defineAsyncComponent(() => import("@/components/FullLoader.vue"))
);
app.component(
"selector",
defineAsyncComponent(() => import("@/components/Selector.vue"))
);
app.component(
"d-table",
defineAsyncComponent(() => import("@/components/Table.vue"))
);
app.config.globalProperties.$http = http;
app.mount("#app");

View file

@ -0,0 +1,7 @@
<template>
<div class="toptitle text-dark col-12 row items-center">
อการเขาสระบบ (Login)
<q-space />
</div>
</template>

View file

@ -0,0 +1,10 @@
interface Pagination {
rowsPerPage: number;
}
interface DataOption {
id: string;
name: string;
}
export type { Pagination, DataOption };

View file

@ -0,0 +1,14 @@
interface DataSumCalendarObject {
id: number;
monthFull: String;
count: number;
color: String;
}
interface DataListsObject {
id: number;
count: number;
name: string;
}
export type { DataSumCalendarObject, DataListsObject };

View file

@ -0,0 +1,14 @@
const ManualHomePage = () => import("@/modules/01_manual/views/Main.vue");
export default [
{
path: "/manual",
name: "manual",
component: ManualHomePage,
meta: {
Auth: true,
Key: [1],
Role: "metadata",
},
},
];

View file

View file

@ -0,0 +1,10 @@
<script setup lang="ts">
import LoginPage from "@/modules/01_manual/components/01_login.vue";
</script>
<template>
<div>
<LoginPage />
</div>
</template>

25
src/plugins/axios.ts Normal file
View file

@ -0,0 +1,25 @@
import axios from "axios"
import config from "process"
// import { dotnetPath } from "../path/axiosPath";
// import { getToken } from "@baloise/vue-keycloak";
import keycloak from "../plugins/keycloak"
const axiosInstance = axios.create({
withCredentials: false,
})
// axiosInstance.defaults.baseURL = dotnetPath;
axiosInstance.interceptors.request.use(
async (config) => {
const token = await keycloak.token
config.headers = {
Authorization: `Bearer ${token}`,
}
return config
},
(error) => {
Promise.reject(error)
}
)
export default axiosInstance

22
src/plugins/filters.ts Normal file
View file

@ -0,0 +1,22 @@
/**
* GLOABL Filters
* - Helper Functions
*/
const filters = {
/**
* compactNumber Social Media 1,000 1K 1,000,000 1M
* : {{ $filters.compactNumber(value) }}
*
* @param val
* @returns
*/
compactNumber (val: number) {
const formatter = Intl.NumberFormat('en', { notation: 'compact'})
return formatter.format(val)
}
}
export default filters;

45
src/plugins/http.ts Normal file
View file

@ -0,0 +1,45 @@
import Axios, { type AxiosRequestConfig, type AxiosResponse } from "axios";
import keycloak from "./keycloak";
const http = Axios.create({
timeout: 1000000000, // เพิ่มค่า timeout
headers: {
"X-Requested-With": "XMLHttpRequest",
},
});
http.interceptors.request.use(
async function (config: AxiosRequestConfig<any>) {
await keycloak.updateToken(1);
config.headers = config.headers ?? {};
const token = keycloak.token;
// const token = localStorage.getItem("access_token")
// const token =
// "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIxU2VKV2dVRFVlNXZwNS13Q1ZHaG9lT2l4bDJTTkdKemthLU5ZN211NXZJIn0.eyJleHAiOjE2NzI0MTI1NDksImlhdCI6MTY3MjM3NjU0OSwiYXV0aF90aW1lIjoxNjcyMzc2NTQ5LCJqdGkiOiI1MTVhY2IwNC1jODQ3LTQzM2YtYjUxOC03ODUzMzJhY2ZjNWYiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLmZyYXBwZXQuc3lub2xvZ3kubWUvYXV0aC9yZWFsbXMvYm1hLWVociIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJlZmM5YjRlMC1mZGU2LTQ1NDQtYmU1OS1lMTA0MjEwMjUzZjAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJibWEtZWhyIiwibm9uY2UiOiI3NjMyMGI3ZS0xZTMxLTQ5ODYtYWIzOC1iOTUyYjFlODY3OGYiLCJzZXNzaW9uX3N0YXRlIjoiMDZlNTBkZjktNzAyNi00ZGIwLTkxMjgtMWY3Y2FiYTRkNDEyIiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwczovL2xvY2FsaG9zdDo3MDA2Il0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWJtYS1laHIiLCJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBlbWFpbCBwcm9maWxlIiwic2lkIjoiMDZlNTBkZjktNzAyNi00ZGIwLTkxMjgtMWY3Y2FiYTRkNDEyIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInJvbGUiOlsiZGVmYXVsdC1yb2xlcy1ibWEtZWhyIiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIl0sIm5hbWUiOiJTeXN0ZW0gQWRtaW5pc3RyYXRvciIsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIiwiZ2l2ZW5fbmFtZSI6IlN5c3RlbSIsImZhbWlseV9uYW1lIjoiQWRtaW5pc3RyYXRvciIsImVtYWlsIjoiYWRtaW5AbG9jYWxob3N0In0.xmfJ3pzI-jLYsaiFXyjTW7gfAEpvUmMVsp9BsB1CfRCVOKiGBbuZhnQY8W-1SWVAx1NjJ55L-zMHPK6hk1dRPLbEse3DlIBZw04W9j8m-Wz3eqdHf_UCjmrXb8qAwkeq0Iaxq9mVfJJeQWeKhFBi-Ff8ek4hCXTYDICXS8ny_BaC5WkyrefHQ2xBqQjwRyoxsg4IoVMjXYNb8L9A-4BNlRfs928SqgFYCRlF5h6zw_rC0XoLrGTmqeacBdpey-r3j2g_lTqWy8mQg2T9s65IDqW3kFPOsr0SVO88sjlFbN9Et0L57RmiqORk_RwzbWg-_Yb6dOuolXsnjBOhOoTzkA";
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
},
function (error: any) {
return Promise.reject(error);
}
);
http.interceptors.response.use(
function (response: AxiosResponse<any, any>) {
return response;
},
function (error: any) {
if (typeof error !== undefined) {
// eslint-disable-next-line no-prototype-builtins
if (error.hasOwnProperty("response")) {
if (error.response.status === 401 || error.response.status === 403) {
// Store.commit("SET_ERROR_MESSAGE", error.response.data.message);
// Store.commit("REMOVE_ACCESS_TOKEN")
}
}
}
return Promise.reject(error);
}
);
export default http;

23
src/plugins/keycloak.ts Normal file
View file

@ -0,0 +1,23 @@
/**
* front connect to keycloak
*/
import Keycloak from "keycloak-js";
// import config from "../app.config";
// import http from "../shared/http";
// import router from "../router";
const initOptions = {
// url: "https://keycloak.frappet.synology.me/auth/",
realm: "bma-ehr",
clientId: "bma-ehr-vue3",
url: "https://id.frappet.synology.me/",
// realm: "bma-ehr-exam",
// clientId: "bma-ehr-exam-vue3",
}; //option keycloak ที่จะ connect
const keycloak = Keycloak(initOptions);
keycloak.onAuthSuccess = () => {}; //เพิ่มlogin สำเร็จจะมาทำฟังก์ชันนี้
await keycloak.init({ onLoad: "check-sso", checkLoginIframe: false }); //ทำการ connect keycloak
export default keycloak;

View file

@ -0,0 +1,11 @@
// import "./styles/quasar.scss"
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: {},
}

79
src/router/index.ts Normal file
View file

@ -0,0 +1,79 @@
import { createRouter, createWebHistory } from "vue-router";
const MainLayout = () => import("@/views/MainLayout.vue");
const Dashboard = () => import("@/views/Dashboard.vue");
const Error404NotFound = () => import("@/views/Error404NotFound.vue");
import ModuleManual from "@/modules/01_manual/router";
// TODO: ใช้หรือไม่?
import keycloak from "@/plugins/keycloak";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: MainLayout,
children: [
{
path: "/",
name: "dashboard",
component: Dashboard,
meta: {
Auth: true,
Key: [7],
Role: "dashboard",
},
},
...ModuleManual,
],
},
/**
* 404 Not Found
* ref: https://router.vuejs.org/guide/essentials/dynamic-matching.html#catch-all-404-not-found-route
*/
{
// path: "/:catchAll(.*)*", // TODO: ใช้ pathMatch แทนตามในเอกสารแนะนำ คงไว้เผื่อจำเป็น
path: "/:pathMatch(.*)*",
component: Error404NotFound,
},
],
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else if (to.hash) {
return {
el: to.hash,
behavior: "smooth",
};
}
},
});
router.beforeEach((to, from, next) => {
if (to.meta.Auth) {
if (!keycloak.authenticated) {
keycloak.login({
redirectUri: `${window.location.protocol}//${window.location.host}${to.path}`,
locale: "th",
});
} else {
// keycloak.updateToken(60);
const role = keycloak.tokenParsed?.role;
if (role.includes(to.meta.Role)) {
next();
} else {
next({ path: "" });
// next();
}
}
} else {
next();
}
// next();
});
export default router;

12
src/router/loader.ts Normal file
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`);
}

102
src/stores/data.ts Normal file
View file

@ -0,0 +1,102 @@
import { ref, computed } from "vue";
import { defineStore } from "pinia";
export const useDataStore = defineStore("data", () => {
// ref() คือการประกาศ state เหมือน vuex
const count = ref<number>(0);
const loader = ref<boolean>(false);
const expandedReport2 = ref<string[]>([]);
const selectedReport2 = ref<string>("");
const expandedRegister = ref<string[]>([]);
const selectedRegister = ref<string>("");
// computed() คือการประกาศ getters เหมือน vuex
const doubleCount = computed(() => count.value * 2);
// function() คือการประกาศ actions เหมือน vuex
const increment = () => {
count.value++;
};
// tabData เป็น paramert ใช้เปรียนเทียบ tab ให้ active
const tabData = ref<string>("");
/**
* active tab
* @param val string
*/
const changeTab = (val: string) => {
tabData.value = val;
};
/**
* active tab
* @param val boolean false = close , true = open
*/
const loaderPage = (val: boolean) => {
loader.value = val;
};
/**
* tree 2
* @param val string
*/
const changeTreeReport2 = (e: string[], s: string) => {
expandedReport2.value = e;
selectedReport2.value = s;
};
/**
* tree
* @param val string
*/
const changeTreeRegister = (e: string[], s: string) => {
expandedRegister.value = e;
selectedRegister.value = s;
};
return {
count,
doubleCount,
increment,
tabData,
changeTab,
loader,
loaderPage,
expandedReport2,
selectedReport2,
changeTreeReport2,
expandedRegister,
selectedRegister,
changeTreeRegister,
};
});
// การเขียนแบบ composition api
// ตัวอย่างการใช้งาน use...Store() ตามชื่อที่ตั้ง
// import { useDataStore } from '@/stores/data'
//storeToRefs ใช้กรณีที่เราจะแปลงค่าเป็น state ใน หน้านั้น
// import { storeToRefs } from 'pinia'
// export default {
// setup() {
// const store = useDataStore()
// ***********************************
// พยายามไม่ให้ ชื่อ เหมือนกับ props ** ตัวอย่างปกติ
// const { count, doubleCount, increment } = store
// ***********************************
// ตัวอย่าง แปลงค่า store เป็น state หรือ ref
// const { name, doubleCount } = storeToRefs(store)
// ถ้าเป็น function เรียกแยกอีกทีก็ได้
// const { increment } = store
// ***********************************
//return {
// count,
// doubleCount,
// increment
// }
// },
// }

806
src/stores/mixin.ts Normal file
View file

@ -0,0 +1,806 @@
import { defineStore } from "pinia";
import moment from "moment";
import CustomComponent from "@/components/CustomDialog.vue";
import { Loading, QSpinnerCube } from "quasar";
export const useCounterMixin = defineStore("mixin", () => {
/**
*
*/
const calAge = (
srcDate: Date,
birthCal: Date = new Date(),
eng: boolean = false
) => {
const year = eng ? "years" : "ปี";
const month = eng ? "months" : "เดือน";
const day = eng ? "days" : "วัน";
if (srcDate == null) {
return `0 ${year} 0 ${month} 0 ${day}`;
}
const toDay = birthCal;
const birth = new Date(srcDate);
const yearNow = toDay.getFullYear();
const monthNow = toDay.getMonth();
const dateNow = toDay.getDate();
const yearDob = birth.getFullYear();
const monthDob = birth.getMonth();
const dateDob = birth.getDate();
const lastYear = 12;
const subtractDate: Object = moment().subtract(1, "months").endOf("month");
const lastMonths = new Date(subtractDate.toString()).getDate();
let yearAge = yearNow - yearDob;
let monthAge = 0;
let dateAge = 0;
if (monthNow >= monthDob) {
monthAge = monthNow - monthDob;
} else {
yearAge--;
monthAge = lastYear + monthNow - monthDob;
}
if (dateNow >= dateDob) {
dateAge = dateNow - dateDob;
} else {
monthAge--;
dateAge = lastMonths + dateNow - dateDob;
if (monthAge < 0) {
monthAge = 11;
yearAge--;
}
}
const age = {
years: yearAge,
months: monthAge,
days: dateAge,
};
return `${yearAge} ${year} ${monthAge} ${month} ${dateAge} ${day}`;
};
function date2Thai(
srcDate: Date,
isFullMonth: boolean = false,
isTime: boolean = false
) {
if (srcDate == null) {
return null;
`
`;
}
const date = new Date(srcDate);
const isValidDate = Boolean(+date);
if (!isValidDate) return srcDate.toString();
if (isValidDate && date.getFullYear() < 1000) return srcDate.toString();
const fullMonthThai = [
"มกราคม",
"กุมภาพันธ์",
"มีนาคม",
"เมษายน",
"พฤษภาคม",
"มิถุนายน",
"กรกฎาคม",
"สิงหาคม",
"กันยายน",
"ตุลาคม",
"พฤศจิกายน",
"ธันวาคม",
];
const abbrMonthThai = [
"ม.ค.",
"ก.พ.",
"มี.ค.",
"เม.ย.",
"พ.ค.",
"มิ.ย.",
"ก.ค.",
"ส.ค.",
"ก.ย.",
"ต.ค.",
"พ.ย.",
"ธ.ค.",
];
let dstYear = 0;
if (date.getFullYear() > 2500) {
dstYear = date.getFullYear();
} else {
dstYear = date.getFullYear() + 543;
}
let dstMonth = "";
if (isFullMonth) {
dstMonth = fullMonthThai[date.getMonth()];
} else {
dstMonth = abbrMonthThai[date.getMonth()];
}
let dstTime = "";
if (isTime) {
const H = date.getHours().toString().padStart(2, "0");
const M = date.getMinutes().toString().padStart(2, "0");
// const S = date.getSeconds().toString().padStart(2, "0")
// dstTime = " " + H + ":" + M + ":" + S + " น."
dstTime = " " + H + ":" + M + " น.";
}
return (
date.getDate().toString().padStart(2, "0") +
" " +
dstMonth +
" " +
dstYear +
dstTime
);
}
function dateMonth2Thai(srcDate: Date, isFullMonth = false, isTime = false) {
if (!srcDate) return srcDate;
const date = new Date(srcDate);
const isValidDate = Boolean(+date);
if (!isValidDate) return srcDate;
if (isValidDate && date.getFullYear() < 1000) return srcDate;
const fullMonthThai = [
"มกราคม",
"กุมภาพันธ์",
"มีนาคม",
"เมษายน",
"พฤษภาคม",
"มิถุนายน",
"กรกฎาคม",
"สิงหาคม",
"กันยายน",
"ตุลาคม",
"พฤศจิกายน",
"ธันวาคม",
];
const abbrMonthThai = [
"ม.ค.",
"ก.พ.",
"มี.ค.",
"เม.ย.",
"พ.ค.",
"มิ.ย.",
"ก.ค.",
"ส.ค.",
"ก.ย.",
"ต.ค.",
"พ.ย.",
"ธ.ค.",
];
let dstYear = 0;
if (date.getFullYear() > 2500) {
dstYear = date.getFullYear();
} else {
dstYear = date.getFullYear() + 543;
}
let dstMonth = "";
if (isFullMonth) {
dstMonth = fullMonthThai[date.getMonth()];
} else {
dstMonth = abbrMonthThai[date.getMonth()];
}
let dstTime = "";
if (isTime) {
const H = date.getHours().toString().padStart(2, "0");
const M = date.getMinutes().toString().padStart(2, "0");
// const S = date.getSeconds().toString().length === 1 ? "0" + date.getSeconds() : date.getSeconds()
// dstTime = " " + H + ":" + M + ":" + S + " น."
dstTime = " " + H + ":" + M + " น.";
}
return date.getDate().toString().padStart(2, "0") + " " + dstMonth;
}
function monthYear2Thai(month: number, year: number, isFullMonth = false) {
const date = new Date(`${year}-${month + 1}-1`);
const fullMonthThai = [
"มกราคม",
"กุมภาพันธ์",
"มีนาคม",
"เมษายน",
"พฤษภาคม",
"มิถุนายน",
"กรกฎาคม",
"สิงหาคม",
"กันยายน",
"ตุลาคม",
"พฤศจิกายน",
"ธันวาคม",
];
const abbrMonthThai = [
"ม.ค.",
"ก.พ.",
"มี.ค.",
"เม.ย.",
"พ.ค.",
"มิ.ย.",
"ก.ค.",
"ส.ค.",
"ก.ย.",
"ต.ค.",
"พ.ย.",
"ธ.ค.",
];
let dstYear = 0;
if (date.getFullYear() > 2500) {
dstYear = date.getFullYear();
} else {
dstYear = date.getFullYear() + 543;
}
let dstMonth = "";
if (isFullMonth) {
dstMonth = fullMonthThai[date.getMonth()];
} else {
dstMonth = abbrMonthThai[date.getMonth()];
}
return dstMonth + " " + dstYear;
}
function dateToISO(date: Date) {
return (
date.getFullYear() +
"-" +
appendLeadingZeroes(date.getMonth() + 1) +
"-" +
appendLeadingZeroes(date.getDate())
);
}
function appendLeadingZeroes(n: Number) {
if (n <= 9) return "0" + n;
return n;
}
function textToPhone(n: string) {
const p = n.substr(0, 3) + "-" + n.substr(3, 3) + "-" + n.substr(6, 4);
return p;
}
function textToFax(n: string) {
const p = n.substr(0, 2) + "-" + n.substr(2, 3) + "-" + n.substr(5, 4);
return p;
}
const success = (q: any, val: string) => {
// useQuasar ไม่สามารถใช้นอกไฟล์ .vue
if (val !== "") {
return q.notify({
message: val,
color: "primary",
icon: "mdi-information",
position: "bottom-right",
multiLine: true,
timeout: 1000,
badgeColor: "positive",
classes: "my-notif-class",
});
}
};
function notify(q: any, val: string) {
if (val !== "") {
q.notify({
color: "teal-10",
message: val,
icon: "mdi-information",
position: "bottom-right",
multiLine: true,
timeout: 7000,
actions: [{ label: "ปิด", color: "white", handler: () => {} }],
});
}
}
function notifyError(q: any, val: string) {
if (val !== "") {
q.notify({
color: "negative",
message: val,
icon: "mdi-alert-circle",
position: "top",
multiLine: true,
timeout: 12000,
actions: [{ label: "ปิด", color: "white", handler: () => {} }],
});
}
}
const messageError = (q: any, e: any = "") => {
// q.dialog.hide();
console.log("e.response", e.response);
if (e.response !== undefined) {
if (e.response.data.status !== undefined) {
if (e.response.data.status == 401) {
//invalid_token
q.dialog({
component: CustomComponent,
componentProps: {
title: `พบข้อผิดพลาด`,
message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`,
icon: "warning",
color: "red",
onlycancel: true,
},
});
} else {
q.dialog({
component: CustomComponent,
componentProps: {
title: `พบข้อผิดพลาด`,
message: `${e.response.data.message}`,
icon: "warning",
color: "red",
onlycancel: true,
},
});
}
} else {
if (e.response.status == 401) {
//invalid_token
q.dialog({
component: CustomComponent,
componentProps: {
title: `พบข้อผิดพลาด`,
message: `ล็อกอินหมดอายุ กรุณาล็อกอินใหม่อีกครั้ง`,
icon: "warning",
color: "red",
onlycancel: true,
},
});
} else {
q.dialog({
component: CustomComponent,
componentProps: {
title: `พบข้อผิดพลาด`,
message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`,
icon: "warning",
color: "red",
onlycancel: true,
},
});
}
}
} else {
q.dialog({
component: CustomComponent,
componentProps: {
title: `พบข้อผิดพลาด`,
message: `ข้อมูลผิดพลาดทำให้เกิดการไม่ตอบสนองต่อการเรียกใช้งานดูเว็บไซต์`,
icon: "warning",
color: "red",
onlycancel: true,
},
});
}
};
const dialogMessage = (
// ไม่เอาใส่ undefined
q: any,
title: string | undefined,
message: string | undefined,
icon: string | undefined,
textOk: string | undefined,
color: string | undefined,
ok?: Function | undefined,
cancel?: Function | undefined,
onlycancel: Boolean = false
) => {
q.dialog({
component: CustomComponent,
componentProps: {
title: title,
message: message,
icon: icon,
color: color,
textOk: textOk,
onlycancel: onlycancel,
},
})
.onOk(() => {
if (ok != undefined) ok();
})
.onCancel(() => {
if (cancel != undefined) cancel();
});
};
//*** Dialog ***//
const dialogConfirm = (
q: any,
ok?: Function,
title?: string, // ถ้ามี cancel action ใส่เป็น null
desc?: string, // ถ้ามี cancel action ใส่เป็น null
cancel?: Function
) => {
q.dialog({
component: CustomComponent,
componentProps: {
title: title && title != null ? title : "ยืนยันการบันทึก",
message:
desc && desc != null
? desc
: "ต้องการยืนยันการบันทึกข้อมูลนี้ใช่หรือไม่?",
icon: "info",
color: "public",
textOk: "ตกลง",
onlycancel: false,
},
})
.onOk(() => {
if (ok) ok();
})
.onCancel(() => {
if (cancel) cancel();
});
};
const dialogRemove = (
q: any,
ok?: Function,
title?: string, // ถ้ามี cancel action ใส่เป็น null
desc?: string, // ถ้ามี cancel action ใส่เป็น null
cancel?: Function
) => {
q.dialog({
component: CustomComponent,
componentProps: {
title: title && title != null ? title : "ยืนยันการลบข้อมูล",
message:
desc && desc != null
? desc
: "ต้องการยืนยันการลบข้อมูลนี้ใช่หรือไม่?",
icon: "delete",
color: "red",
textOk: "ตกลง",
onlycancel: false,
},
})
.onOk(() => {
if (ok) ok();
})
.onCancel(() => {
if (cancel) cancel();
});
};
const dialogMessageNotify = (
q: any,
desc?: string, // ถ้ามี cancel action ใส่เป็น null
cancel?: Function
) => {
q.dialog({
component: CustomComponent,
componentProps: {
title: "ข้อความแจ้งเตือน",
message: desc && desc != null ? desc : "กรุณากรอกข้อมูลให้ครบ",
icon: "warning",
color: "orange",
textOk: "ตกลง",
onlycancel: true,
},
}).onCancel(() => {
if (cancel) cancel();
});
};
//*** END Dialog ***//
const showLoader = () => {
Loading.show({
spinner: QSpinnerCube,
spinnerSize: 140,
spinnerColor: "primary",
backgroundColor: "white",
});
};
const hideLoader = () => {
Loading.hide();
};
function modalDelete(
q: any,
title: string,
message: string,
ok: Function,
cancel?: Function
) {
q.dialog({
title: `<span class="text-red">${title}</span>`,
message: `<span class="text-black">${message}</span>`,
cancel: {
flat: true,
color: "grey-14",
},
ok: {
color: "red-6",
},
focus: "none",
persistent: true,
html: true,
})
.onOk(() => {
ok();
})
.onCancel(() => {
if (cancel != undefined) cancel();
})
.onDismiss(() => {});
}
function modalConfirm(
q: any,
title: string,
message: string,
ok: Function,
cancel?: Function
) {
q.dialog({
title: `<span class="text-primary">${title}</span>`,
message: `<span class="text-black">${message}</span>`,
cancel: {
flat: true,
color: "grey",
},
ok: {
color: "primary",
},
focus: "none",
persistent: true,
html: true,
})
.onOk(() => {
ok();
})
.onCancel(() => {
if (cancel != undefined) cancel();
})
.onDismiss(() => {});
}
function modalWarning(q: any, title: string, message: string, ok?: Function) {
// q.dialog({
// title: `<span class="text-red">${title}</span>`,
// message: `<span class="text-black">${message}</span>`,
// ok: {
// push: true,
// color: "primary",
// },
// focus: "none",
// persistent: true,
// html: true,
// })
// .onOk(() => {
// if (ok != undefined) ok();
// })
// .onCancel(() => {})
// .onDismiss(() => {});
q.dialog({
component: CustomComponent,
componentProps: {
title: title,
message: message,
icon: "warning",
color: "warning",
onlycancel: true,
},
});
}
function modalError(q: any, title: string, message: string, ok?: Function) {
// q.dialog({
// title: `<span class="text-red">${title}</span>`,
// message: `<span class="text-black">${message}</span>`,
// ok: {
// push: true,
// color: "primary",
// },
// focus: "none",
// persistent: true,
// html: true,
// })
// .onOk(() => {
// if (ok != undefined) ok();
// })
// .onCancel(() => {})
// .onDismiss(() => {});
q.dialog({
component: CustomComponent,
componentProps: {
title: title,
message: message,
icon: "warning",
color: "red",
onlycancel: true,
},
});
}
const dateText = (val: Date) => {
if (val != null) {
return date2Thai(val);
} else {
return "-";
}
};
/**
* 2
* @param val
*/
const dateThaiRange = (val: [Date, Date]) => {
if (val === null) {
return "";
} else if (date2Thai(val[0]) === date2Thai(val[1])) {
return `${date2Thai(val[0])}`;
} else {
return `${date2Thai(val[0])} - ${date2Thai(val[1])}`;
}
};
const weekThai = (val: Number) => {
switch (val) {
case 0:
return "วันอาทิตย์";
case 1:
return "วันจันทร์";
case 2:
return "วันอังคาร";
case 3:
return "วันพุธ";
case 4:
return "วันพฤหัสบดี";
case 5:
return "วันศุกร์";
case 6:
return "วันเสาร์";
default:
return "-";
}
};
const genColor15 = (val: number) => {
val = val % 15;
switch (val) {
case 1:
return "pink";
case 2:
return "purple";
case 3:
return "deep-purple";
case 4:
return "indigo";
case 5:
return "blue";
case 6:
return "light-blue";
case 7:
return "cyan";
case 8:
return "teal";
case 9:
return "green";
case 10:
return "light-green";
case 11:
return "amber";
case 12:
return "orange";
case 13:
return "deep-orange";
case 14:
return "brown";
case 0:
return "blue-grey";
default:
return "";
}
};
const typeCategoryExam = (val: string) => {
switch (val) {
case "hygiene":
return "สำนักอนามัย";
case "physician":
return "สำนักการแพทย์";
case "city":
return "สำนักผังเมือง";
case "culture":
return "สำนักวัฒนธรรม กีฬา และการท่องเที่ยว";
default:
return "-";
}
};
const typeRetire = (val: string) => {
switch (val) {
case "retire":
return "เกษียณอายุราชการ";
case "resign":
return "ลาออก";
case "transfer":
return "ให้โอน";
case "death":
return "ถึงแก่กรรม";
case "layoff":
return "ให้ออก";
case "discharge":
return "ปลดออก";
case "dismiss":
return "ไล่ออก";
case "other":
return "อื่นๆ";
default:
return "-";
}
};
const typeChangeName = (val: string) => {
switch (val) {
case "firstName":
return "เปลี่ยนชื่อ";
case "lastName":
return "เปลี่ยนนามสกุล";
case "all":
return "เปลี่ยนชื่อ-นามสกุล";
default:
return "-";
}
};
const statusLeave = (val: string) => {
switch (val) {
case "waitting":
return "รออนุมัติ";
case "reject":
return "ไม่ผ่านการอนุมัติ";
case "approve":
return "ผ่านการอนุมัติ";
case "cancel":
return "ยกเลิก";
default:
return "-";
}
};
return {
calAge,
date2Thai,
dateToISO,
notify,
notifyError,
dateText,
monthYear2Thai,
dateMonth2Thai,
success,
weekThai,
genColor15,
typeCategoryExam,
textToPhone,
textToFax,
dateThaiRange,
modalDelete,
modalConfirm,
modalError,
dialogMessage,
messageError,
showLoader,
hideLoader,
typeRetire,
typeChangeName,
statusLeave,
modalWarning,
// common dialog
dialogConfirm,
dialogRemove,
dialogMessageNotify,
};
});

View file

@ -0,0 +1,139 @@
// FILE (create it): src/quasar-variables.sass
$primary: #02A998
$secondary: #016987
$accent: #9C27B0
// $dark: #1D1D1D
$dark: #35473C
$positive: #21BA45
$negative: #C10015
$info: #31CCEC
$warning: #F2C037
$add: #00aa86
.text-add
color: $add !important
.bg-add
background: $add !important
$edit: #019fc4
.text-edit
color: $edit !important
.bg-edit
background: $edit !important
$public: #016987
.text-public
color: $public !important
.bg-public
background: $public !important
$save: #4154b3
.text-save
color: $save !important
.bg-save
background: $save !important
$nativetab: #c8d3db
.text-nativetab
color: $nativetab !important
.bg-nativetab
background: $nativetab !important
$activetab: #4a5568
.text-activetab
color: $activetab !important
.bg-activetab
background: $activetab !important
.inputgreen .q-field__prefix,
.inputgreen .q-field__suffix,
.inputgreen .q-field__input,
.inputgreen .q-field__native
color: rgb(6, 136, 77)
@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
#azay-app,
div
font-family: $noto-thai !important
text-rendering: optimizeLegibility
-webkit-font-smoothing: antialiased
-moz-osx-font-smoothing: grayscale
$separator-color: #EDEDED !default
.bg-teal-1
background: #e0f2f1a6 !important
.table_ellipsis
max-width: 200px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
.table_ellipsis:hover
word-wrap: break-word
overflow: visible
white-space: normal
.table_ellipsis2
max-width: 25vw
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
.table_ellipsis2:hover
word-wrap: break-word
overflow: visible
white-space: normal
transition: width 2s
$muti-tab: #87d4cc
.text-muti-tab
color: $muti-tab !important
.bg-muti-tab
background: $muti-tab !important
/* editor */
.q-editor
font-size: 1rem
line-height: 1.5rem
font-weight: 400
.q-editor h1, .q-menu h1
font-size: 1.5rem
line-height: 2rem
font-weight: 400
margin-block-start: 0em
margin-block-end: 0em
.q-editor h2, .q-menu h2
font-size: 1.25rem
line-height: 1.5rem
font-weight: 400
margin-block-start: 0em
margin-block-end: 0em
.q-editor h3, .q-menu h3
font-size: 1.1rem
line-height: 1.5rem
font-weight: 400
margin-block-start: 0em
margin-block-end: 0em
.q-editor p, .q-menu p
margin: 0
/* q-tree */
.q-tree
color: #c8d3db

8
src/views/Dashboard.vue Normal file
View file

@ -0,0 +1,8 @@
<script setup lang="ts">
</script>
<!-- page:หนาแรก -->
<template>
<div class="toptitle text-dark">อการใชงานระบบ</div>
</template>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Error404NotFound",
});
</script>
<template>
<div
class="fullscreen bg-blue-10 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="blue"
unelevated
to="/"
label="กลับไปหน้าหลัก"
no-caps
/>
</div>
</div>
</template>

999
src/views/MainLayout.vue Normal file
View file

@ -0,0 +1,999 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import keycloak from "@/plugins/keycloak";
import { useRoute, useRouter } from "vue-router";
import { useDataStore } from "@/stores/data";
import { storeToRefs } from "pinia";
import { scroll, useQuasar } from "quasar";
import { useCounterMixin } from "@/stores/mixin";
import http from "@/plugins/http";
import config from "@/app.config";
import type {
ScrollType,
notiType,
optionType,
} from "../interface/request/main/main";
import {
menuList,
tabList,
tabListPlacement,
} from "../interface/request/main/main";
const { setVerticalScrollPosition, getVerticalScrollPosition } = scroll;
const store = useDataStore();
const route = useRoute();
const router = useRouter();
const link = ref<string>("");
const mixin = useCounterMixin(); //
const {
showLoader,
hideLoader,
dialogMessage,
success,
messageError,
date2Thai,
} = mixin;
const $q = useQuasar();
const { tabData, loader } = storeToRefs(store);
const { changeTab } = store;
const miniState = ref<boolean>(false);
const drawerR = ref<boolean>(false);
const rightActive = ref<boolean>(false);
const resize = ref<number>(0);
const active = ref<number>(0);
const drawerL = ref<boolean>(false);
const fullname = ref<string>("");
const role = ref<string[]>([]);
const notiTrigger = ref<boolean>(false);
const text = ref<string>("");
const notiList = ref<notiType[]>([
{
id: "1",
sender: "ท",
body: "ขอแก้ไขข้อมูลทะเบียนประวัติ",
timereceive: new Date(),
},
]);
const options = ref<optionType[]>([
{
icon: "mdi-account-cog",
label: "ผู้ดูแลระบบ",
value: "op1",
color: "primary",
},
{
icon: "mdi-account-group",
label: "เจ้าหน้าที่",
value: "op2",
color: "blue",
},
{
icon: "mdi-account-circle",
label: "บุคคล",
value: "op3",
color: "indigo",
},
]);
/**
* ใหแสดง แทปดานขวา เมอเขาหน รายละเอยดทะเบยนประว
*/
const tabScroll = () => {
return route.name == "registryDetail";
};
/**
* toggleBtnRight มย ขยาย drawer ขวา
*/
const toggleBtnRight = () => {
drawerR.value = !drawerR.value;
};
/**
* toggleBtnLeft มย ขยาย drawer าย เมอหนาจอ อถงขนาด 1024 px
* ปกตเปนการยอโดยใช Ministate
*/
const toggleBtnLeft = () => {
if (window.innerWidth < 1024) {
drawerL.value = !drawerL.value;
} else {
miniState.value = !miniState.value;
}
};
/**
* Event onScroll นำ ตำแหน top scroll
* ใช function updateScroll
*/
const onScroll = (scroll: ScrollType) => {
const { position } = scroll;
if (route.name == "PlacementPersonalDetail") {
updateScrollPlacement(position);
} else {
updateScroll(position);
}
};
/**
* updateScroll เป function active แทปดานขวา
*/
const updateScroll = (position: number) => {
// position undifind position top scroll
if (position === void 0) {
position = document.documentElement.scrollTop || document.body.scrollTop;
}
let last;
/**
* วนหา id ของ div
*/
for (const i in tabList) {
const section = tabList[i];
const item = document.getElementById(section.tag);
/**
* วนหา id ของ div หนาน
* เมอหาไมเจอใหไปตอตวตอไป
* เมอเจอแล ให ตำแหนงบนสดของ div มากกวาหรอเทาก ตำแหน top scroll บวกก แทปดานบน 155
* last เป undifind ให last เทาก tag แล หยดล
* าไมใช undifind ให last เทาก tag แล ปหาต กรณ div นยาว
*/
if (item === null) {
continue;
}
if (item.offsetTop >= position + 155) {
if (last === void 0) {
last = section.tag;
}
break;
} else {
last = section.tag;
}
}
/**
* last ไมเทาก undifind
* ใช เซ active ใหเปนแทปส
* และใช scrollIntoView ายตำแหน activeโดยการเลอนไปหา
*/
if (last !== void 0) {
changeTab(last);
const tocEl = document.getElementById("tab--" + last);
if (tocEl) {
tocEl.scrollIntoView({ block: "nearest" });
}
}
};
/**
* ใหแสดง แทปดานขวา เมอเขาหน รายละเอยดทะเบยนประว และ rightActive เทาก true
*/
const activeBtn = () => {
return route.name == "registryDetail" && rightActive.value;
};
/**
* เมอเรมตนโปรแกรมให event resize และ function myEventHandler
* set function myEventHandler เพราะ state งไมเซ , state เซทเม หนาจอเร ขยบหนาจอ
* เรมเขามา state rightActive เป state โชว มขวา
* งจ boolean งตอง set
*/
onMounted(async () => {
myEventHandler(null, false);
window.addEventListener("resize", (e: any) => {
myEventHandler(e, true);
});
});
/**
* เมอออกจากโปรแกรม ให ยกเลกการฟ event resize
*/
onUnmounted(() => {
window.removeEventListener("resize", (e: any) => {
myEventHandler(e, true);
});
});
/**
* @param e event ของ resize
* @param setSCroll เชความาจาก event หรอปาว
* set state resize = ความกวางหนาเว
* เมอความกวาง อยกว 1024( เทาก อยกว 1023) ให rightActive เป true แต rightActive true อยแลวไมอง set
* เมอความกวาง มากกวาหรอเทาก 1024 และเม rightActive เป true
* และ drawerR เป true ให rightActive เป true drawerR เป false ให rightActive เป false
* rightActive = true ; แสดงป drawer าน ขวา
* rightActive = false; ไมแสดงป drawer าน ขวา
*/
const myEventHandler = (e: any, setSCroll: boolean) => {
if (setSCroll) {
resize.value = e.target.innerWidth;
} else {
resize.value = window.innerWidth;
}
if (resize.value < 1024) {
if (!rightActive.value) {
rightActive.value = true;
}
} else {
if (rightActive.value) {
if (drawerR.value) {
rightActive.value = true;
} else {
rightActive.value = false;
}
}
}
};
/**
* ตรวจสอบ path นๆ ายงอย path แมจะเป path child แทปกงจะ active อยเช
* ปกต path จะ active เม path นม ตรงตวมนเช /main
* แตจะไม active เม path child path /main/นๆ
* @param path string
*/
const activeMenu = (path: string) => {
if (path == "dashboard" && route.fullPath == "/") return true;
if (path == "registry" && route.fullPath == "/registry-employee") return false;
if (path == "registry" && route.fullPath.includes(`/registry-employee/edit`)) return false;
if (path == "registry" && route.fullPath == "/") return false;
// if (path != "registry" && path == "registryEmployee" && route.fullPath == "/registryEmployee") return true;
const bool = route.fullPath.includes(`/${path}`);
return bool;
};
/**
* เปนฟงกนทนตลอด เพอให active ตรงตามเงอนไขใหเป true
* @param tag string เปนชอของ วนนๆ
*/
const activeTab = (tag: string) => {
return tabData.value == tag;
};
/**
* คลกเพอให router หร scroll ไปยงตำแหนงนนๆ
* @param tag string เปนชอของ วนนๆ
*/
const tagClick = (tag: string) => {
const hash = `#${tag}`;
const items = document.getElementById(tag);
const offset = Math.max(0, items == null ? 0 : items.offsetTop - 84);
// router.replace({ hash });
if (route.hash !== hash) {
const check = activeBtn();
if (check) {
// router.replace({ hash, position: { x: 0, y: 0 } });
// router.replace({ hash }).then(() => {
// setVerticalScrollPosition(window, offset, 300);
// });
drawerR.value = !drawerR.value;
setVerticalScrollPosition(window, offset, 300);
} else {
setVerticalScrollPosition(window, offset, 300);
}
} else {
setVerticalScrollPosition(window, offset, 300);
}
};
//**** Tab Right ****\\
const tabScrollPlacement = () => {
return route.name == "PlacementPersonalDetail";
};
const updateScrollPlacement = (position: number) => {
if (position === void 0) {
position = document.documentElement.scrollTop || document.body.scrollTop;
}
let last;
for (const i in tabListPlacement) {
const section = tabListPlacement[i];
const item = document.getElementById(section.tag);
if (item === null) {
continue;
}
if (item.offsetTop >= position + 100) {
if (last === void 0) {
last = section.tag;
}
break;
} else {
last = section.tag;
}
}
if (last !== void 0) {
changeTab(last);
const tocEl = document.getElementById("tab--" + last);
if (tocEl) {
tocEl.scrollIntoView({ block: "nearest" });
}
}
};
const activeBtnPlacement = () => {
return route.name == "PlacementPersonalDetail" && rightActive.value;
};
const activeTabPlacement = (tag: string) => {
return tabData.value == tag;
};
const tagClickPlacement = (tag: string) => {
const hash = `#${tag}`;
const items = document.getElementById(tag);
const offset = Math.max(0, items == null ? 0 : items.offsetTop + 50);
// router.replace({ hash });
if (route.hash !== hash) {
const checkPlacement = activeBtnPlacement();
if (checkPlacement) {
drawerR.value = !drawerR.value;
setVerticalScrollPosition(window, offset, 300);
} else {
setVerticalScrollPosition(window, offset, 300);
}
} else {
setVerticalScrollPosition(window, offset, 300);
}
};
//**** End Tab Right ****\\
/**
* logout keycloak
* confirm อนออกจากระบบ
*/
const doLogout = () => {
$q.dialog({
title: "ยืนยันการออกจากระบบ",
message: `ต้องการออกจากระบบใช้หรือไม่?`,
cancel: "ยกเลิก",
ok: "ยืนยัน",
persistent: true,
}).onOk(() => {
keycloak.logout();
});
};
/**
* งชอผใชงานจาก keycloak
*/
if (keycloak.tokenParsed != null) {
fullname.value = keycloak.tokenParsed.name;
role.value = keycloak.tokenParsed.role;
}
</script>
<!-- โครงเว -->
<template>
<!-- แบบเก design แรก -->
<!-- <q-layout view="lHh Lpr lff"> -->
<!-- ปรบใหบหน รายละเอยดทะเบยนประว -->
<q-layout view="lHh LpR lff" @scroll="onScroll">
<!-- header -->
<q-header flat class="bg-grey-2 text-dark" height-hint="7">
<q-toolbar style="padding: 0 2%">
<q-btn
size="13px"
class="bg-grey-3"
flat
dense
round
@click="toggleBtnLeft"
aria-label="Menu"
>
<q-icon
:name="miniState == false ? 'mdi-backburger' : 'mdi-menu-open'"
size="20px"
color="grey-7"
/>
</q-btn>
<q-space />
<!-- Search -->
<!-- <q-input dense rounded standout v-model="text" placeholder="ค้นหา">
<template v-slot:prepend>
<q-icon name="mdi-magnify" size="20px" color="grey-7" />
</template>
</q-input> -->
<!-- <q-btn class="bg-grey-3" flat dense round>
<q-icon name="mdi-magnify" size="20px" color="grey-7" />
</q-btn>
-->
<!-- Notification -->
<div class="row items-center no-wrap">
<q-btn-dropdown size="md" dropdown-color="grey" flat>
<template v-slot:label>
<q-item v-close-popup class="q-pa-none">
<q-item-section :avatar="$q.screen.gt.xs">
<q-avatar color="grey-3">
<!-- <img src="https://cdn.quasar.dev/img/avatar.png" /> -->
<q-icon name="mdi-account" size="22px" color="grey-7" />
</q-avatar>
</q-item-section>
<q-item-section class="text-left gt-xs">
<q-item-label class="text-caption text-weight-medium">{{
fullname
}}</q-item-label>
<!-- <q-item-label caption>เจาหนาท ..</q-item-label> -->
</q-item-section>
</q-item>
</template>
<div
class="row justify-center"
style="border-top: solid 3px #1bb19b !important; width: 273.797px"
>
<div class="column items-center col-12 q-py-md" color="grey-3">
<q-avatar size="72px" color="grey-4">
<q-icon name="mdi-account" color="grey-7" />
<!-- <img :src="require('@/assets/logo.png')" /> -->
</q-avatar>
<div class="text-subtitle2 q-mt-md q-mb-xs text-center">
{{ fullname }}
</div>
<q-btn
color="primary"
label="ออกจากระบบ"
push
size="sm"
v-close-popup
@click="doLogout"
/><!-- -->
</div>
<div class="column col-12">
<q-separator />
<div class="column q-pb-md justify-center">
<div class="text-overline text-grey q-px-md q-pt-sm">
เลอกโหมด
</div>
<!-- <q-option-group v-model="group" :options="options" color="primary"/> -->
<q-list dense v-for="op in options" :key="op.label">
<q-item clickable>
<q-item-section avatar>
<q-avatar
:color="op.color"
text-color="white"
:icon="op.icon"
size="20px"
font-size="12px"
/>
</q-item-section>
<q-item-section class="q-py-sm">{{
op.label
}}</q-item-section>
</q-item>
</q-list>
</div>
</div>
</div>
</q-btn-dropdown>
</div>
<q-btn
size="13px"
class="bg-blue-1"
v-if="activeBtn() || activeBtnPlacement()"
flat
dense
round
@click="toggleBtnRight"
aria-label="Menu"
>
<q-icon name="mdi-menu" class="rotate-180" size="20px" color="blue" />
</q-btn>
</q-toolbar>
</q-header>
<!-- end header -->
<!-- drawer -->
<q-drawer
side="left"
class="text-white"
style="background: #273238"
v-model="drawerL"
show-if-above
:width="260"
:breakpoint="1023"
:mini="miniState"
>
<!-- วนของเมน mini -->
<template v-slot:mini>
<q-scroll-area class="fit mini-slot cursor-pointer">
<q-toolbar class="q-py-md">
<q-img
src="@/assets/logo.png"
spinner-color="white"
style="height: 32px; max-width: 32px"
/>
</q-toolbar>
<q-separator color="grey-9" />
<!-- เมนอย ตอนย -->
<q-list padding>
<div v-for="(menuItem, index) in menuList" :key="index">
<div v-if="role.includes(menuItem.role)">
<q-item
clickable
v-ripple
:active="link === menuItem.label"
@click="link = menuItem.label"
active-class="text-primary menuActiveMini text-weight-medium"
v-if="menuItem.key == 3 || menuItem.key == 5"
>
<div class="row items-center no-wrap">
<q-icon :name="menuItem.icon" size="20px" class="q-ml-md" />
<q-icon
name="mdi-dots-vertical"
size="13px"
color="grey-6"
/>
</div>
<q-tooltip
anchor="center right"
self="center left"
:offset="[10, 10]"
>
{{ menuItem.label }}
</q-tooltip>
<q-menu
anchor="top right"
self="top left"
:offset="[5, 0]"
style="background: #273238; z-index: 9000"
>
<q-list class="text-white q-py-sm">
<div
v-for="(subMenu, i) in menuItem.children"
:key="i"
:to="{ name: `${subMenu.path}` }"
>
<!-- เมนอย 2 -->
<div v-if="menuItem.key == 5">
<q-item dense clickable v-if="subMenu.key !== 5.1">
<q-item-section
>{{ subMenu.label }}
</q-item-section>
<q-item-section side>
<q-icon name="keyboard_arrow_right" />
</q-item-section>
<q-menu
anchor="top end"
self="top start"
:offset="[5, 0]"
style="background: #273238; z-index: 9000"
>
<q-list class="text-white q-py-sm">
<q-item
v-for="subMenu2 in subMenu.children"
:key="subMenu2.label"
:to="{ name: `${subMenu2.path}` }"
dense
class="q-pl-md text-body2"
active-class="text-primary active-item text-weight-medium"
clickable
>
<q-item-section>
<q-item-label>{{
subMenu2.label
}}</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-item>
<q-item
v-else
dense
class="q-pl-md q-pr-xl text-body2"
active-class="text-primary active-item text-weight-medium"
clickable
:to="{ name: `${subMenu.path}` }"
>
<q-item-section>
<q-item-label>{{ subMenu.label }}</q-item-label>
</q-item-section>
</q-item>
</div>
<!-- เมนอย 1 -->
<div v-else>
<q-item
dense
class="q-pl-md q-pr-xl text-body2"
active-class="text-primary active-item text-weight-medium"
clickable
:to="{ name: `${subMenu.path}` }"
>
<q-item-section>
<q-item-label>{{ subMenu.label }}</q-item-label>
</q-item-section>
</q-item>
</div>
</div>
</q-list>
</q-menu>
</q-item>
<q-item
clickable
v-ripple
:to="{ name: `${menuItem.path}` }"
:active="link === menuItem.label"
@click="link = menuItem.label"
active-class="text-primary menuActiveMini"
v-else
>
<q-item-section avatar>
<q-avatar size="md" font-size="20px">
<q-icon :name="menuItem.icon" />
</q-avatar>
</q-item-section>
<q-tooltip
anchor="center right"
self="center left"
:offset="[10, 10]"
>
{{ menuItem.label }}
</q-tooltip>
</q-item>
</div>
</div>
</q-list>
</q-scroll-area>
</template>
<!-- จบสวนของเมน mini -->
<!-- วนของเมน -->
<q-scroll-area class="fit">
<q-toolbar class="q-py-md">
<q-toolbar-title shrink class="row items-center no-wrap">
<q-img
src="@/assets/logo.png"
spinner-color="white"
style="height: 40px; max-width: 40px"
/>
<div class="row q-ml-md">
<div
style="color: #ffffff; letter-spacing: 1px"
class="text-body2 text-weight-bolder"
>
ระบบ<span class="text-primary">ทรพยากรบคคล</span>
</div>
<div class="text-caption text-white">&nbsp;กรงเทพมหานคร</div>
</div>
</q-toolbar-title>
</q-toolbar>
<q-separator inset color="grey-9" />
<q-list padding>
<div v-for="(menuItem, index) in menuList" :key="index">
<!-- เมนอย -->
<div v-if="role.includes(menuItem.role)">
<q-expansion-item
group="somegroup"
class="menuSub"
expand-icon="mdi-chevron-down"
expanded-icon="mdi-chevron-up"
v-if="
menuItem.key == 3 ||
menuItem.key == 5 ||
menuItem.key == 6 ||
menuItem.key == 7 ||
menuItem.key == 8
"
>
<template v-slot:header>
<q-item-section avatar>
<q-avatar
:icon="menuItem.icon"
size="md"
font-size="20px"
/>
</q-item-section>
<q-item-section>{{ menuItem.label }}</q-item-section>
</template>
<!-- เมนอย 2 (สรรหา) -->
<div v-if="menuItem.key == 5">
<div v-for="(subMenu, i) in menuItem.children" :key="i">
<q-expansion-item
switch-toggle-side
dense-toggle
:label="subMenu.label"
v-if="subMenu.key !== 5.1"
class="expan2"
dense
>
<q-item
dense
class="menuSubHover"
active-class="text-primary active-item text-weight-bold menuSubAct"
clickable
v-for="subMenu2 in subMenu.children"
:key="subMenu2.key"
:to="{ name: `${subMenu2.path}` }"
>
<q-item-section>
<q-item-label class="font-400 subLabel"
>{{ subMenu2.label }}
</q-item-label>
</q-item-section>
</q-item>
</q-expansion-item>
<q-item
v-else
dense
class="menuSubHover"
active-class="text-primary active-item text-weight-bold menuSubAct"
clickable
:to="{ name: `${subMenu.path}` }"
>
<q-item-section>
<q-item-label>{{ subMenu.label }}</q-item-label>
</q-item-section>
</q-item>
</div>
</div>
<!-- เมนอย 1 -->
<q-item
v-else
dense
class="menuSubHover"
active-class="text-primary active-item text-weight-bold menuSubAct"
clickable
v-for="(subMenu, j) in menuItem.children"
:key="j"
:to="{ name: `${subMenu.path}` }"
>
<q-item-section>
<q-item-label class="font-400">{{
subMenu.label
}}</q-item-label>
</q-item-section>
</q-item>
</q-expansion-item>
<!-- เมนปกต -->
<q-item
class="text-weight-medium menu"
:active="activeMenu(menuItem.path)"
active-class="text-primary active-item text-weight-bold menuActive"
:to="{ name: `${menuItem.path}` }"
clickable
v-ripple
dense
exact
v-else
>
<q-item-section avatar>
<q-avatar size="md" font-size="20px">
<q-icon
:name="
menuItem.key === active
? menuItem.activeIcon
: menuItem.icon
"
/>
</q-avatar>
</q-item-section>
<q-item-section>
<q-item-label>{{ menuItem.label }}</q-item-label>
</q-item-section>
</q-item>
</div>
</div>
</q-list>
</q-scroll-area>
</q-drawer>
<!-- drawer page registry/:id -->
<q-drawer
side="right"
class="bg-grey-2"
show-if-above
v-if="tabScroll()"
v-model="drawerR"
:width="220"
:breakpoint="1023"
>
<q-scroll-area class="fit">
<q-list padding>
<q-item
v-for="(tabItem, index) in tabList"
:key="index"
:id="'tab--' + tabItem.tag"
class="tabNative"
active-class="text-blue-7 active-item text-weight-medium tabActive"
:active="activeTab(tabItem.tag)"
clickable
v-ripple
dense
exact
@click="tagClick(tabItem.tag)"
>
<q-item-section>
<q-item-label
><q-icon size="11px" name="mdi-circle-medium" /><span
class="q-pl-xs"
>{{ tabItem.label }}</span
></q-item-label
>
</q-item-section>
</q-item>
</q-list>
</q-scroll-area>
</q-drawer>
<!-- drawer page placement2/detail/:id -->
<q-drawer
side="right"
class="bg-grey-2"
show-if-above
v-if="tabScrollPlacement()"
v-model="drawerR"
:width="220"
:breakpoint="1023"
>
<q-scroll-area class="fit">
<q-list padding>
<q-item
v-for="(tabItem, index) in tabListPlacement"
:key="index"
:id="'tab--' + tabItem.tag"
class="tabNative"
active-class="text-blue-7 active-item text-weight-medium tabActive"
:active="activeTabPlacement(tabItem.tag)"
clickable
v-ripple
dense
exact
@click="tagClickPlacement(tabItem.tag)"
>
<q-item-section>
<q-item-label
><q-icon size="11px" name="mdi-circle-medium" /><span
class="q-pl-xs"
>{{ tabItem.label }}</span
></q-item-label
>
</q-item-section>
</q-item>
</q-list>
</q-scroll-area>
</q-drawer>
<!-- drawer -->
<q-page-container class="bg-grey-2">
<q-page style="padding: 0 2%">
<router-view :key="$route.fullPath" />
</q-page>
</q-page-container>
<full-loader :visibility="loader" />
</q-layout>
</template>
<style>
.menuSub .q-item__section--avatar,
.menu .q-item__section--avatar {
min-width: 0px;
}
.menu {
padding-bottom: 5px;
padding-top: 5px;
border-radius: 0 100px 100px 0;
margin-right: 2%;
}
.menuActive {
background: #212a2f;
border-radius: 0 100px 100px 0;
margin-right: 2%;
}
.menuActiveMini {
background: #212a2f;
}
.menuSub .q-item {
border-radius: 0 100px 100px 0;
margin-right: 2%;
font-weight: 500;
}
.expan2 .q-item {
padding-left: 10%;
}
.subLabel {
white-space: nowrap;
width: 160px;
overflow: hidden;
text-overflow: ellipsis;
}
.font-400 {
font-weight: 400;
}
.expan2 .menuSubHover {
padding-left: 30%;
border-radius: 20px;
}
.menuSubHover {
padding-left: 25%;
border-radius: 20px;
}
.menuSub .q-expansion-item__content {
background: #212a2f;
padding: 5px 0px;
margin-bottom: 5px;
}
.tabNative {
color: grey;
padding-left: 8%;
border-radius: 100px 0px 0px 100px;
}
.tabActive {
padding-left: 8%;
background: #e4f2ff;
border-radius: 100px 0px 0px 100px;
}
.q-card {
box-shadow: 3px 3px 20px -10px rgba(151, 150, 150, 0.261) !important;
}
.q-card--bordered {
border: 1px solid #ededed;
box-shadow: none !important;
}
.q-menu {
box-shadow: 3px 3px 10px 1px rgba(95, 95, 95, 0.15) !important;
}
.toptitle {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 1%;
}
.q-field--outlined .q-field__control {
border-radius: 5px;
}
.q-field--outlined .q-field__control:before {
border-color: #c8d3db;
}
.btnManu {
min-height: 48px;
border-radius: 0px 100px 100px 0px;
}
.q-field--outlined .q-icon {
color: #7474747f;
}
.q-card__actions .q-btn--rectangle {
padding: 0 14px !important;
}
/* custom scrollbar */
::-webkit-scrollbar {
width: 12px;
height: 12px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #d6dee1;
border-radius: 20px;
border: 3px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background-color: #a8bbbf;
}
</style>

249
src/views/TestView.vue Normal file
View file

@ -0,0 +1,249 @@
<template>
<div>
<div>TEST VIEW</div>
<q-btn color="primary" label="TEST" @click="testCancel" />
<data-table
:rows="rows"
:columns="columns"
:filter="filter"
:visible-columns="visibleColumns"
v-model:inputfilter="filter"
v-model:inputvisible="visibleColumns"
>
<!-- :nornmalData="true" -->
<template #columns="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
{{ col.value }}
</q-td>
</q-tr>
</template>
</data-table>
<FullCalendar
ref="fullCalendar"
class="demo-app-calendar"
:options="calendarOptions"
>
<template v-slot:eventContent="arg">
<b>{{ arg.timeText }}</b>
<i>{{ arg.event.title }}</i>
</template>
</FullCalendar>
<!-- publicData เปนสถานะ "เผยแพร่" องใสตลอด(าไมใส จะเปนสถานะ"ยังไม่เผยแพร่") ดการท state child ไดเลย -->
</div>
</template>
<script setup lang="ts">
import { defineComponent, ref } from "vue";
import { useQuasar } from "quasar";
// import { useCounterStore } from "@/stores/data";
import { useCounterMixin } from "@/stores/mixin";
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import allLocales from "@fullcalendar/core/locales-all";
import listPlugin from "@fullcalendar/list";
const props = defineProps({});
const mixin = useCounterMixin();
const { success } = mixin;
const $q = useQuasar();
const columns = ref<any>([
{
name: "name",
label: "Dessert (100g serving)",
align: "left",
field: (row: any) => row.name,
format: (val: any) => `${val}`,
sortable: true,
},
{
name: "calories",
align: "center",
label: "Calories",
field: "calories",
sortable: true,
},
{ name: "fat", label: "Fat (g)", field: "fat", sortable: true },
{ name: "carbs", label: "Carbs (g)", field: "carbs" },
{ name: "protein", label: "Protein (g)", field: "protein" },
{ name: "sodium", label: "Sodium (mg)", field: "sodium" },
{
name: "calcium",
label: "Calcium (%)",
field: "calcium",
sortable: true,
sort: (a: any, b: any) => parseInt(a, 10) - parseInt(b, 10),
},
{
name: "iron",
label: "Iron (%)",
field: "iron",
sortable: true,
sort: (a: any, b: any) => parseInt(a, 10) - parseInt(b, 10),
},
]);
const rows = ref<any>([
{
name: "Frozen Yogurt",
calories: 159,
fat: 6.0,
carbs: 24,
protein: 4.0,
sodium: 87,
calcium: "14%",
iron: "1%",
},
{
name: "Ice cream sandwich",
calories: 237,
fat: 9.0,
carbs: 37,
protein: 4.3,
sodium: 129,
calcium: "8%",
iron: "1%",
},
{
name: "Eclair",
calories: 262,
fat: 16.0,
carbs: 23,
protein: 6.0,
sodium: 337,
calcium: "6%",
iron: "7%",
},
{
name: "Cupcake",
calories: 305,
fat: 3.7,
carbs: 67,
protein: 4.3,
sodium: 413,
calcium: "3%",
iron: "8%",
},
{
name: "Gingerbread",
calories: 356,
fat: 16.0,
carbs: 49,
protein: 3.9,
sodium: 327,
calcium: "7%",
iron: "16%",
},
{
name: "Jelly bean",
calories: 375,
fat: 0.0,
carbs: 94,
protein: 0.0,
sodium: 50,
calcium: "0%",
iron: "0%",
},
{
name: "Lollipop",
calories: 392,
fat: 0.2,
carbs: 98,
protein: 0,
sodium: 38,
calcium: "0%",
iron: "2%",
},
{
name: "Honeycomb",
calories: 408,
fat: 3.2,
carbs: 87,
protein: 6.5,
sodium: 562,
calcium: "0%",
iron: "45%",
},
{
name: "Donut",
calories: 452,
fat: 25.0,
carbs: 51,
protein: 4.9,
sodium: 326,
calcium: "2%",
iron: "22%",
},
{
name: "KitKat",
calories: 518,
fat: 26.0,
carbs: 65,
protein: 7,
sodium: 54,
calcium: "12%",
iron: "6%",
},
]);
const filter = ref<string>("");
const visibleColumns = ref<String[]>([]);
["name", "calories", "fat", "carbs", "protein", "sodium", "calcium", "iron"];
const edit = ref<boolean>(false);
const calendarOptions = ref<any>({
plugins: [
dayGridPlugin,
timeGridPlugin,
interactionPlugin, // needed for dateClick
],
headerToolbar: {
left: "prev,next today",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay",
},
initialView: "dayGridMonth",
initialEvents: [
{
id: 1,
title:
"All-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day eventAll-day event",
start: new Date().toISOString().replace(/T.*$/, ""),
},
{
id: 2,
title:
"Timed event Timed eventTimed eventTimed eventTimed eventTimed eventTimed eventTimed eventTimed event Timed eventTimed eventTimed eventTimed event",
start: new Date().toISOString().replace(/T.*$/, "") + "T12:00:00",
},
], // alternatively, use the `events` setting to fetch from a feed
editable: true,
selectable: true,
selectMirror: true,
dayMaxEvents: true,
weekends: true,
// select: this.handleDateSelect,
// eventClick: this.handleEventClick,
// eventsSet: this.handleEvents
/* you can update a remote database when these fire:
eventAdd:
eventChange:
eventRemove:
*/
});
const testAdd = () => {
// console.log("test");
};
const testCancel = () => {
edit.value = false;
console.log("testCancel");
success($q, "asd");
};
</script>
<style>
.fc-event {
overflow: hidden;
}
</style>