- run by vite
- change camera
17
.eslintrc.js
|
|
@ -1,20 +1,19 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
node: true,
|
||||
es2022: true,
|
||||
},
|
||||
'extends': [
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/typescript/recommended'
|
||||
|
||||
'@vue/typescript/recommended',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
rules: {
|
||||
// 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
// 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
'vue/no-mutating-props': 'off'
|
||||
}
|
||||
'vue/no-mutating-props': 'off',
|
||||
// '@typescript-eslint/no-explicit-any': 'off',
|
||||
'vue/multi-word-component-names': 'off'
|
||||
},
|
||||
}
|
||||
|
|
|
|||
5
.prettierrc.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
12
README.md
|
|
@ -1,24 +1,30 @@
|
|||
# maptest
|
||||
# BMA ระบบลงเวลาเข้า-ออกงาน
|
||||
|
||||
## Project setup
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
```
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
### Format and fixes files
|
||||
|
||||
```
|
||||
npm run lint
|
||||
npm run format
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
||||
8
cypress.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
||||
baseUrl: 'http://localhost:4173',
|
||||
},
|
||||
})
|
||||
1
env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
13
index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BMA ระบบลงเวลาเข้า-ออกงาน</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
73
package.json
|
|
@ -3,39 +3,60 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview --port 3008",
|
||||
"test:unit": "vitest --environment jsdom --root src/",
|
||||
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
|
||||
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.8",
|
||||
"@fullcalendar/daygrid": "^6.1.8",
|
||||
"@fullcalendar/interaction": "^6.1.8",
|
||||
"@fullcalendar/list": "^6.1.8",
|
||||
"@fullcalendar/timegrid": "^6.1.8",
|
||||
"@fullcalendar/vue3": "^6.1.8",
|
||||
"@googlemaps/js-api-loader": "^1.16.2",
|
||||
"@quasar/cli": "^2.3.0",
|
||||
"@quasar/extras": "^1.0.0",
|
||||
"@vuepic/vue-datepicker": "^7.2.1",
|
||||
"core-js": "^3.8.3",
|
||||
"@quasar/extras": "^1.15.8",
|
||||
"@vuepic/vue-datepicker": "^5.2.1",
|
||||
"moment": "^2.29.4",
|
||||
"pinia": "^2.1.7",
|
||||
"quasar": "^2.0.0",
|
||||
"pinia": "^2.1.4",
|
||||
"quasar": "^2.11.1",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3",
|
||||
"simple-vue-camera": "^1.1.3",
|
||||
"vite-plugin-pwa": "^0.16.7",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue3-google-map": "^0.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-pwa": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vue/eslint-config-typescript": "^9.1.0",
|
||||
"eslint": "^8.53.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"sass": "1.32.12",
|
||||
"sass-loader": "^12.0.0",
|
||||
"typescript": "~4.5.5",
|
||||
"vue-cli-plugin-quasar": "~5.0.2"
|
||||
"@quasar/vite-plugin": "^1.3.0",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/jsdom": "^20.0.1",
|
||||
"@types/node": "^18.11.12",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.0",
|
||||
"@vue/eslint-config-prettier": "^7.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/test-utils": "^2.2.6",
|
||||
"@vue/tsconfig": "^0.1.3",
|
||||
"cypress": "^12.0.2",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"jsdom": "^20.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"sass": "^1.32.12",
|
||||
"start-server-and-test": "^1.15.2",
|
||||
"typescript": "~4.7.4",
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.25.6",
|
||||
"vue-tsc": "^1.0.12"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
public/icons/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 731 B After Width: | Height: | Size: 731 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
|
@ -1,23 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong
|
||||
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
|
||||
properly without JavaScript enabled. Please enable it to
|
||||
continue.</strong
|
||||
>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBzPSF5NxUZ1G8DKBnJvJPTqCR0Ct2xf58&libraries=places"></script>
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -35,44 +35,43 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineEmits, defineProps } from "vue";
|
||||
import { useDialogPluginComponent } from "quasar";
|
||||
import { useDialogPluginComponent } from 'quasar'
|
||||
|
||||
const props = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: "primary",
|
||||
default: 'primary',
|
||||
},
|
||||
textOk: {
|
||||
type: String,
|
||||
default: "ตกลง",
|
||||
default: 'ตกลง',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "หัวข้อ?",
|
||||
default: 'หัวข้อ?',
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: "ข้อความ",
|
||||
default: 'ข้อความ',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "question_mark",
|
||||
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();
|
||||
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
|
||||
|
|
@ -84,7 +83,7 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
|
|||
function onOKClick() {
|
||||
// on OK, it is REQUIRED to
|
||||
// call onDialogOK (with optional payload)
|
||||
onDialogOK();
|
||||
onDialogOK()
|
||||
// or with payload: onDialogOK({ ... })
|
||||
// ...and it will also hide the dialog automatically
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,225 +1,225 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, defineProps } from "vue";
|
||||
import { useQuasar } from "quasar";
|
||||
import moment, { Moment } from "moment";
|
||||
|
||||
// importStores
|
||||
import { useCounterMixin } from "@/stores/mixin";
|
||||
// importType
|
||||
import type { FormRef } from "@/interface/index/Main";
|
||||
|
||||
const $q = useQuasar();
|
||||
const mixin = useCounterMixin();
|
||||
const { date2Thai, covertDateObject, dialogConfirm } = mixin;
|
||||
|
||||
const props = defineProps({
|
||||
dataById: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
closePopup: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const dataById = ref<any>([]);
|
||||
const date = ref<Date | null>(null);
|
||||
const checkboxIn = ref<boolean>(false);
|
||||
const checkboxOut = ref<boolean>(false);
|
||||
const reason = ref<string>("");
|
||||
const statusAction = ref<boolean>(false);
|
||||
|
||||
onMounted(() => {
|
||||
updateClock();
|
||||
dataById.value = props.dataById;
|
||||
if (dataById.value == null) {
|
||||
statusAction.value = true;
|
||||
}
|
||||
});
|
||||
const dateNow = ref<Date>(new Date());
|
||||
const timeNoew = ref<string>("");
|
||||
|
||||
function updateClock() {
|
||||
const date = Date.now();
|
||||
let hh = moment(date).format("HH");
|
||||
let mm = moment(date).format("mm");
|
||||
timeNoew.value = `${hh}:${mm} น.`;
|
||||
}
|
||||
|
||||
const dateRef = ref<object | null>(null);
|
||||
const reasonRef = ref<object | null>(null);
|
||||
|
||||
const objectRef: FormRef = {
|
||||
date: dateRef,
|
||||
reason: reasonRef,
|
||||
};
|
||||
const checkstatusBox = ref<boolean>(false);
|
||||
function onCkickSave() {
|
||||
const hasError = [];
|
||||
for (const key in objectRef) {
|
||||
if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
|
||||
const property = objectRef[key];
|
||||
if (property.value && typeof property.value.validate === "function") {
|
||||
const isValid = property.value.validate();
|
||||
hasError.push(isValid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (checkboxIn.value === false && checkboxOut.value === false) {
|
||||
checkstatusBox.value = true;
|
||||
}
|
||||
if (
|
||||
hasError.every(
|
||||
(result) => result === true && checkstatusBox.value === false
|
||||
)
|
||||
) {
|
||||
dialogConfirm($q, async () => {
|
||||
console.log("save");
|
||||
props.closePopup();
|
||||
});
|
||||
} else {
|
||||
console.log("ไม่ผ่าน ");
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => checkboxIn.value, () => checkboxOut.value],
|
||||
([newCheckboxIn, newCheckboxOut]) => {
|
||||
if (checkstatusBox.value) {
|
||||
if (newCheckboxIn || newCheckboxOut) {
|
||||
checkstatusBox.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<q-card-section class="col q-pt-none">
|
||||
<div class="row q-pa-sm q-col-q-gutter-sm">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
:class="$q.screen.gt.xs ? 'col-12 bg-grey-1' : 'col-12 '"
|
||||
>
|
||||
<q-card-section class="bg-primary text-white q-pa-sm">
|
||||
<div class="text-center text-bold">เวลาปัจจุบัน</div>
|
||||
</q-card-section>
|
||||
<!-- <div class="q-pa-sm text-primary">เวลาปัจจุบัน</div> -->
|
||||
<q-card-section class="text-center q-pa-sm">
|
||||
<div class="row q-gutter-md">
|
||||
<div class="col">{{ date2Thai(dateNow) }}</div>
|
||||
<div class="col">{{ timeNoew }}</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card flat bordered class="col-12 q-mt-sm" v-if="statusAction">
|
||||
<div class="q-pa-sm">
|
||||
<VueDatePicker
|
||||
v-model="date"
|
||||
:locale="'th'"
|
||||
autoApply
|
||||
:enableTimePicker="false"
|
||||
week-start="0"
|
||||
outlined
|
||||
>
|
||||
<template #year="{ year }">
|
||||
{{ year + 543 }}
|
||||
</template>
|
||||
<template #year-overlay-value="{ value }">
|
||||
{{ parseInt(value + 543) }}
|
||||
</template>
|
||||
<template #trigger>
|
||||
<q-input
|
||||
ref="dateRef"
|
||||
outlined
|
||||
dense
|
||||
:model-value="date !== null ? date2Thai(date) : null"
|
||||
:label="`${'กรอกวันที่'}`"
|
||||
:rules="[(val) => !!val || 'กรุณาเลือกวันที่']"
|
||||
lazy-rules
|
||||
hide-bottom-space
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon
|
||||
name="event"
|
||||
class="cursor-pointer"
|
||||
style="color: var(--q-primary)"
|
||||
>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input> </template
|
||||
></VueDatePicker>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-card flat bordered class="q-pa-sm col-12 bg-grey-1 q-mt-sm" v-else>
|
||||
<div class="row q-gutter-md text-grey-5">
|
||||
<div class="col-1">
|
||||
<q-icon color="grey-5" name="calendar_today" />
|
||||
</div>
|
||||
<div class="col">{{ covertDateObject(dataById.date) }}</div>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
class="q-pa-sm col-12 q-mt-sm"
|
||||
:style="{
|
||||
borderColor: checkstatusBox ? '#B22222' : '',
|
||||
borderWidth: checkstatusBox ? '2px' : '',
|
||||
}"
|
||||
>
|
||||
<div class="row q-gutter-xs">
|
||||
<div class="col-12">
|
||||
<q-checkbox
|
||||
keep-color
|
||||
color="primary"
|
||||
v-model="checkboxIn"
|
||||
label="ขอแก้ไขเวลาเข้างาน"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-checkbox
|
||||
keep-color
|
||||
v-model="checkboxOut"
|
||||
color="primary"
|
||||
label="ขอแก้ไขเวลาออกงาน"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
<div
|
||||
v-if="checkstatusBox"
|
||||
class="text-red-9 q-pa-sm"
|
||||
style="font-size: 10px"
|
||||
>
|
||||
กรุณาเลือก
|
||||
</div>
|
||||
<q-card flat bordered class="q-pa-sm col-12 q-mt-sm">
|
||||
<q-input
|
||||
ref="reasonRef"
|
||||
dense
|
||||
outlined
|
||||
v-model="reason"
|
||||
label="เหตุผล"
|
||||
type="textarea"
|
||||
:rows="$q.screen.gt.xs ? '5' : '1'"
|
||||
label-color="grey-5"
|
||||
:rules="[(val) => !!val || 'กรุณากรอกเหตุผล']"
|
||||
lazy-rules
|
||||
hide-bottom-space
|
||||
class="custom-aqua-border"
|
||||
/>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-actions align="right">
|
||||
<q-btn dense color="secondary" label="บันทึก" @click="onCkickSave" />
|
||||
</q-card-actions>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useQuasar } from 'quasar'
|
||||
import moment from 'moment'
|
||||
|
||||
// importStores
|
||||
import { useCounterMixin } from '@/stores/mixin'
|
||||
// importType
|
||||
import type { FormRef } from '@/interface/index/Main'
|
||||
|
||||
const $q = useQuasar()
|
||||
const mixin = useCounterMixin()
|
||||
const { date2Thai, covertDateObject, dialogConfirm } = mixin
|
||||
|
||||
const props = defineProps({
|
||||
dataById: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
closePopup: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const dataById = ref<any>([])
|
||||
const date = ref<Date | null>(null)
|
||||
const checkboxIn = ref<boolean>(false)
|
||||
const checkboxOut = ref<boolean>(false)
|
||||
const reason = ref<string>('')
|
||||
const statusAction = ref<boolean>(false)
|
||||
|
||||
onMounted(() => {
|
||||
updateClock()
|
||||
dataById.value = props.dataById
|
||||
if (dataById.value == null) {
|
||||
statusAction.value = true
|
||||
}
|
||||
})
|
||||
const dateNow = ref<Date>(new Date())
|
||||
const timeNoew = ref<string>('')
|
||||
|
||||
function updateClock() {
|
||||
const date = Date.now()
|
||||
const hh = moment(date).format('HH')
|
||||
const mm = moment(date).format('mm')
|
||||
timeNoew.value = `${hh}:${mm} น.`
|
||||
}
|
||||
|
||||
const dateRef = ref<object | null>(null)
|
||||
const reasonRef = ref<object | null>(null)
|
||||
|
||||
const objectRef: FormRef = {
|
||||
date: dateRef,
|
||||
reason: reasonRef,
|
||||
}
|
||||
const checkstatusBox = ref<boolean>(false)
|
||||
function onCkickSave() {
|
||||
const hasError = []
|
||||
for (const key in objectRef) {
|
||||
if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
|
||||
const property = objectRef[key]
|
||||
if (property.value && typeof property.value.validate === 'function') {
|
||||
const isValid = property.value.validate()
|
||||
hasError.push(isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (checkboxIn.value === false && checkboxOut.value === false) {
|
||||
checkstatusBox.value = true
|
||||
}
|
||||
if (
|
||||
hasError.every(
|
||||
(result) => result === true && checkstatusBox.value === false
|
||||
)
|
||||
) {
|
||||
dialogConfirm($q, async () => {
|
||||
console.log('save')
|
||||
props.closePopup()
|
||||
})
|
||||
} else {
|
||||
console.log('ไม่ผ่าน ')
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => checkboxIn.value, () => checkboxOut.value],
|
||||
([newCheckboxIn, newCheckboxOut]) => {
|
||||
if (checkstatusBox.value) {
|
||||
if (newCheckboxIn || newCheckboxOut) {
|
||||
checkstatusBox.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<template>
|
||||
<q-card-section class="col q-pt-none">
|
||||
<div class="row q-pa-sm q-col-q-gutter-sm">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
:class="$q.screen.gt.xs ? 'col-12 bg-grey-1' : 'col-12 '"
|
||||
>
|
||||
<q-card-section class="bg-primary text-white q-pa-sm">
|
||||
<div class="text-center text-bold">เวลาปัจจุบัน</div>
|
||||
</q-card-section>
|
||||
<!-- <div class="q-pa-sm text-primary">เวลาปัจจุบัน</div> -->
|
||||
<q-card-section class="text-center q-pa-sm">
|
||||
<div class="row q-gutter-md">
|
||||
<div class="col">{{ date2Thai(dateNow) }}</div>
|
||||
<div class="col">{{ timeNoew }}</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
<q-card flat bordered class="col-12 q-mt-sm" v-if="statusAction">
|
||||
<div class="q-pa-sm">
|
||||
<VueDatePicker
|
||||
v-model="date"
|
||||
:locale="'th'"
|
||||
autoApply
|
||||
:enableTimePicker="false"
|
||||
week-start="0"
|
||||
outlined
|
||||
>
|
||||
<template #year="{ year }">
|
||||
{{ year + 543 }}
|
||||
</template>
|
||||
<template #year-overlay-value="{ value }">
|
||||
{{ parseInt(value + 543) }}
|
||||
</template>
|
||||
<template #trigger>
|
||||
<q-input
|
||||
ref="dateRef"
|
||||
outlined
|
||||
dense
|
||||
:model-value="date !== null ? date2Thai(date) : null"
|
||||
:label="`${'กรอกวันที่'}`"
|
||||
:rules="[(val) => !!val || 'กรุณาเลือกวันที่']"
|
||||
lazy-rules
|
||||
hide-bottom-space
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon
|
||||
name="event"
|
||||
class="cursor-pointer"
|
||||
style="color: var(--q-primary)"
|
||||
>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input> </template
|
||||
></VueDatePicker>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-card flat bordered class="q-pa-sm col-12 bg-grey-1 q-mt-sm" v-else>
|
||||
<div class="row q-gutter-md text-grey-5">
|
||||
<div class="col-1">
|
||||
<q-icon color="grey-5" name="calendar_today" />
|
||||
</div>
|
||||
<div class="col">{{ covertDateObject(dataById.date) }}</div>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
class="q-pa-sm col-12 q-mt-sm"
|
||||
:style="{
|
||||
borderColor: checkstatusBox ? '#B22222' : '',
|
||||
borderWidth: checkstatusBox ? '2px' : '',
|
||||
}"
|
||||
>
|
||||
<div class="row q-gutter-xs">
|
||||
<div class="col-12">
|
||||
<q-checkbox
|
||||
keep-color
|
||||
color="primary"
|
||||
v-model="checkboxIn"
|
||||
label="ขอแก้ไขเวลาเข้างาน"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-checkbox
|
||||
keep-color
|
||||
v-model="checkboxOut"
|
||||
color="primary"
|
||||
label="ขอแก้ไขเวลาออกงาน"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card>
|
||||
<div
|
||||
v-if="checkstatusBox"
|
||||
class="text-red-9 q-pa-sm"
|
||||
style="font-size: 10px"
|
||||
>
|
||||
กรุณาเลือก
|
||||
</div>
|
||||
<q-card flat bordered class="q-pa-sm col-12 q-mt-sm">
|
||||
<q-input
|
||||
ref="reasonRef"
|
||||
dense
|
||||
outlined
|
||||
v-model="reason"
|
||||
label="เหตุผล"
|
||||
type="textarea"
|
||||
:rows="$q.screen.gt.xs ? '5' : '1'"
|
||||
label-color="grey-5"
|
||||
:rules="[(val) => !!val || 'กรุณากรอกเหตุผล']"
|
||||
lazy-rules
|
||||
hide-bottom-space
|
||||
class="custom-aqua-border"
|
||||
/>
|
||||
</q-card>
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<q-card-actions align="right">
|
||||
<q-btn dense color="secondary" label="บันทึก" @click="onCkickSave" />
|
||||
</q-card-actions>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { defineProps } from "vue";
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
clickClose: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
function clickClosePopup() {
|
||||
props.clickClose();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title class="text-subtitle2 text-bold">{{
|
||||
props.title
|
||||
}}</q-toolbar-title>
|
||||
<q-btn
|
||||
icon="close"
|
||||
unelevated
|
||||
round
|
||||
dense
|
||||
@click="clickClosePopup"
|
||||
style="color: #ff8080; background-color: #ffdede"
|
||||
/>
|
||||
</q-toolbar>
|
||||
<q-separator />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
clickClose: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
function clickClosePopup() {
|
||||
props.clickClose()
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title class="text-subtitle2 text-bold">{{
|
||||
props.title
|
||||
}}</q-toolbar-title>
|
||||
<q-btn
|
||||
icon="close"
|
||||
unelevated
|
||||
round
|
||||
dense
|
||||
@click="clickClosePopup"
|
||||
style="color: #ff8080; background-color: #ffdede"
|
||||
/>
|
||||
</q-toolbar>
|
||||
<q-separator />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,101 +1,101 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { GoogleMap, Marker } from "vue3-google-map";
|
||||
|
||||
declare var google: any;
|
||||
const center = ref<any>();
|
||||
const location = ref<string>("");
|
||||
const test = ref();
|
||||
|
||||
// hook
|
||||
onMounted(() => {
|
||||
findNearestPlace();
|
||||
});
|
||||
|
||||
// หาตำแหน่ง
|
||||
function findNearestPlace() {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const userLocation = {
|
||||
lat: position.coords.latitude,
|
||||
lng: position.coords.longitude,
|
||||
};
|
||||
|
||||
// ส่ง Location ไปยัง API เพื่อหาสถานที่ที่ใกล้ที่สุด
|
||||
findNearestPlaceFromAPI(userLocation);
|
||||
center.value = userLocation;
|
||||
console.log(center.value);
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
console.log("erroe");
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.error("เบราว์เซอร์ไม่รองรับการระบุตำแหน่ง");
|
||||
}
|
||||
}
|
||||
function findNearestPlaceFromAPI(userLocation: any) {
|
||||
const placesService = new google.maps.places.PlacesService(
|
||||
document.createElement("div")
|
||||
);
|
||||
|
||||
const request = {
|
||||
location: userLocation,
|
||||
radius: 1000, // รัศมีในเมตร
|
||||
types: ["point_of_interest"], // ประเภทของสถานที่ที่คุณต้องการค้นหา
|
||||
};
|
||||
|
||||
placesService.nearbySearch(request, (results: any, status: any) => {
|
||||
if (status === google.maps.places.PlacesServiceStatus.OK) {
|
||||
console.log("Nearby places:", results[0]);
|
||||
const place = results[0];
|
||||
location.value = place.name;
|
||||
test.value = place.geometry.location;
|
||||
} else {
|
||||
console.error("Error fetching nearby places:", status);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card
|
||||
bordered
|
||||
flat
|
||||
class="col-12 bg-grey-2 shadow-0"
|
||||
:style="$q.screen.gt.xs ? 'height: 350px;' : 'height: 360px;'"
|
||||
>
|
||||
<div style="width: 100%; height: 90%">
|
||||
<GoogleMap
|
||||
api-key="AIzaSyBzPSF5NxUZ1G8DKBnJvJPTqCR0Ct2xf58"
|
||||
style="width: 100%; height: 100%"
|
||||
:center="center"
|
||||
:zoom="17"
|
||||
>
|
||||
<Marker :options="{ position: center }" />
|
||||
<Marker
|
||||
:options="{
|
||||
position: test,
|
||||
icon: 'https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png',
|
||||
}"
|
||||
/>
|
||||
</GoogleMap>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="
|
||||
$q.screen.gt.xs
|
||||
? 'q-pa-sm text-weight-medium text-grey-8'
|
||||
: ' text-weight-medium text-grey-8'
|
||||
"
|
||||
>
|
||||
พื้นที่ใกล้เคียง
|
||||
<span class="q-px-sm">:</span>
|
||||
{{ location }}
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { GoogleMap, Marker } from 'vue3-google-map'
|
||||
|
||||
declare var google: any
|
||||
const center = ref<any>()
|
||||
const location = ref<string>('')
|
||||
const test = ref()
|
||||
|
||||
// hook
|
||||
onMounted(() => {
|
||||
findNearestPlace()
|
||||
})
|
||||
|
||||
// หาตำแหน่ง
|
||||
function findNearestPlace() {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const userLocation = {
|
||||
lat: position.coords.latitude,
|
||||
lng: position.coords.longitude,
|
||||
}
|
||||
|
||||
// ส่ง Location ไปยัง API เพื่อหาสถานที่ที่ใกล้ที่สุด
|
||||
findNearestPlaceFromAPI(userLocation)
|
||||
center.value = userLocation
|
||||
console.log(center.value)
|
||||
},
|
||||
(error) => {
|
||||
console.error(error)
|
||||
console.log('erroe')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
console.error('เบราว์เซอร์ไม่รองรับการระบุตำแหน่ง')
|
||||
}
|
||||
}
|
||||
function findNearestPlaceFromAPI(userLocation: any) {
|
||||
const placesService = new google.maps.places.PlacesService(
|
||||
document.createElement('div')
|
||||
)
|
||||
|
||||
const request = {
|
||||
location: userLocation,
|
||||
radius: 1000, // รัศมีในเมตร
|
||||
types: ['point_of_interest'], // ประเภทของสถานที่ที่คุณต้องการค้นหา
|
||||
}
|
||||
|
||||
placesService.nearbySearch(request, (results: any, status: any) => {
|
||||
if (status === google.maps.places.PlacesServiceStatus.OK) {
|
||||
console.log('Nearby places:', results[0])
|
||||
const place = results[0]
|
||||
location.value = place.name
|
||||
test.value = place.geometry.location
|
||||
} else {
|
||||
console.error('Error fetching nearby places:', status)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card
|
||||
bordered
|
||||
flat
|
||||
class="col-12 bg-grey-2 shadow-0"
|
||||
:style="$q.screen.gt.xs ? 'height: 350px;' : 'height: 360px;'"
|
||||
>
|
||||
<div style="width: 100%; height: 90%">
|
||||
<GoogleMap
|
||||
api-key="AIzaSyBzPSF5NxUZ1G8DKBnJvJPTqCR0Ct2xf58"
|
||||
style="width: 100%; height: 100%"
|
||||
:center="center"
|
||||
:zoom="17"
|
||||
>
|
||||
<Marker :options="{ position: center }" />
|
||||
<Marker
|
||||
:options="{
|
||||
position: test,
|
||||
icon: 'https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png',
|
||||
}"
|
||||
/>
|
||||
</GoogleMap>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="
|
||||
$q.screen.gt.xs
|
||||
? 'q-pa-sm text-weight-medium text-grey-8'
|
||||
: ' text-weight-medium text-grey-8'
|
||||
"
|
||||
>
|
||||
พื้นที่ใกล้เคียง
|
||||
<span class="q-px-sm">:</span>
|
||||
{{ location }}
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,45 +1,45 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, defineProps, watch } from "vue";
|
||||
import HeaderPopup from "@/components/HeaderPopup.vue";
|
||||
import FormTime from "@/components/FormTime.vue";
|
||||
|
||||
const props = defineProps({
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
clickClose: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
dataById: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// -ข้อมูล
|
||||
const data = ref<any>();
|
||||
function clickClosePopup() {
|
||||
props.clickClose();
|
||||
}
|
||||
watch(props, () => {
|
||||
if (props.modal === true) {
|
||||
data.value = props.dataById;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<q-dialog v-model="props.modal" full-height>
|
||||
<q-card class="column full-height" style="width: 300px">
|
||||
<HeaderPopup :title="props.title" :clickClose="clickClosePopup" />
|
||||
<FormTime :dataById="data" :closePopup="clickClosePopup" />
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import HeaderPopup from '@/components/HeaderPopup.vue'
|
||||
import FormTime from '@/components/FormTime.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
clickClose: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
dataById: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
// -ข้อมูล
|
||||
const data = ref<any>()
|
||||
function clickClosePopup() {
|
||||
props.clickClose()
|
||||
}
|
||||
watch(props, () => {
|
||||
if (props.modal === true) {
|
||||
data.value = props.dataById
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<q-dialog v-model="props.modal" full-height>
|
||||
<q-card class="column full-height" style="width: 300px">
|
||||
<HeaderPopup :title="props.title" :clickClose="clickClosePopup" />
|
||||
<FormTime :dataById="data" :closePopup="clickClosePopup" />
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,311 +1,311 @@
|
|||
<script setup lang="ts">
|
||||
import type { QTableProps } from "quasar";
|
||||
import { ref } from "vue";
|
||||
|
||||
// importStores
|
||||
import { useChekIn } from "@/stores/chekin";
|
||||
|
||||
// importComponent
|
||||
import Popup from "@/components/PopUp.vue";
|
||||
|
||||
const stores = useChekIn();
|
||||
|
||||
const columns = ref<QTableProps["columns"]>([
|
||||
{
|
||||
name: "date",
|
||||
align: "left",
|
||||
label: "วัน/เดือน/ปี",
|
||||
sortable: true,
|
||||
field: "date",
|
||||
headerStyle: "font-size: 14px",
|
||||
style: "font-size: 14px; width:15%;",
|
||||
},
|
||||
{
|
||||
name: "in",
|
||||
align: "left",
|
||||
label: "เวลาเข้างาน",
|
||||
sortable: true,
|
||||
field: "in",
|
||||
headerStyle: "font-size: 14px",
|
||||
style: "font-size: 14px; width:15%;",
|
||||
},
|
||||
{
|
||||
name: "out",
|
||||
align: "left",
|
||||
label: "เวลาออกงาน",
|
||||
sortable: true,
|
||||
field: "out",
|
||||
headerStyle: "font-size: 14px",
|
||||
style: "font-size: 14px; width:15%;",
|
||||
},
|
||||
{
|
||||
name: "Morningstatus",
|
||||
align: "left",
|
||||
label: "สถานะช่วงเช้า",
|
||||
sortable: true,
|
||||
field: "Morningstatus",
|
||||
headerStyle: "font-size: 14px",
|
||||
style: "font-size: 14px; width:15%;",
|
||||
},
|
||||
{
|
||||
name: "AfternoonStatus",
|
||||
align: "left",
|
||||
label: "สถานะช่วงบ่าย",
|
||||
sortable: true,
|
||||
field: "AfternoonStatus",
|
||||
headerStyle: "font-size: 14px",
|
||||
style: "font-size: 14px; width:15%;",
|
||||
},
|
||||
// {
|
||||
// name: "loIn",
|
||||
// align: "left",
|
||||
// label: "พิกัด",
|
||||
// sortable: true,
|
||||
// field: "loIn",
|
||||
// headerStyle: "font-size: 14px",
|
||||
// style: "font-size: 14px",
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "loOut",
|
||||
// align: "left",
|
||||
// label: "พิกัด",
|
||||
// sortable: true,
|
||||
// field: "loOut",
|
||||
// headerStyle: "font-size: 14px",
|
||||
// style: "font-size: 14px",
|
||||
// },
|
||||
// {
|
||||
// name: "status",
|
||||
// align: "left",
|
||||
// label: "สถานะ",
|
||||
// sortable: true,
|
||||
// field: "status",
|
||||
// headerStyle: "font-size: 14px",
|
||||
// style: "font-size: 14px; width:10%;",
|
||||
// },
|
||||
]);
|
||||
// popup
|
||||
const modalPopup = ref<boolean>(false);
|
||||
const titlePopup = ref<string>("");
|
||||
const dataRow = ref<any>();
|
||||
function openPopup(data: any) {
|
||||
const title = "แก้ไขลงเวลา";
|
||||
modalPopup.value = true;
|
||||
titlePopup.value = title;
|
||||
dataRow.value = data;
|
||||
}
|
||||
function closePopup() {
|
||||
modalPopup.value = false;
|
||||
}
|
||||
function classStatus(status: string) {
|
||||
switch (status) {
|
||||
case "ขาดราชการ":
|
||||
return "text-red";
|
||||
case "ปกติ":
|
||||
return "text-blue";
|
||||
case "สาย":
|
||||
return "text-yellow-8";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 10,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
class="custom-table2"
|
||||
:columns="columns"
|
||||
:rows="stores.rows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-sticky-size-start="48"
|
||||
:style="$q.screen.gt.xs ? 'max-height: 64vh' : ''"
|
||||
:grid="$q.screen.gt.xs ? false : true"
|
||||
v-model:pagination="pagination"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<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> </q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props" class="cursor-pointer">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<div
|
||||
v-if="col.name === 'Morningstatus'"
|
||||
:class="classStatus(props.row.Morningstatus)"
|
||||
>
|
||||
{{ props.row.Morningstatus }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="col.name === 'AfternoonStatus'"
|
||||
:class="classStatus(props.row.AfternoonStatus)"
|
||||
>
|
||||
{{ props.row.AfternoonStatus }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ col.value }}
|
||||
</div>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
v-if="props.row.statusEdit === 'edit'"
|
||||
outline
|
||||
icon="edit"
|
||||
size="12px"
|
||||
label="ขอแก้ไข"
|
||||
color="cyan-6"
|
||||
@click="openPopup(props.row)"
|
||||
/>
|
||||
<q-chip
|
||||
v-else
|
||||
:color="`${stores.classColorStatus(props.row.statusEdit)}-1`"
|
||||
:text-color="`${stores.classColorStatus(props.row.statusEdit)}-7`"
|
||||
>{{ props.row.statusEditName }}</q-chip
|
||||
>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template #item="props">
|
||||
<div
|
||||
class="q-pa-xs col-xs-12 col-sm-6 col-md-4 col-lg-3 grid-style-transition"
|
||||
>
|
||||
<q-card bordered flat class="q-py-sm shadow-0">
|
||||
<q-list dense>
|
||||
<q-item v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ col.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-item-label>
|
||||
<span
|
||||
v-if="col.name == 'Morningstatus'"
|
||||
:class="classStatus(props.row.Morningstatus)"
|
||||
>{{ col.value }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="col.name == 'AfternoonStatus'"
|
||||
:class="classStatus(props.row.AfternoonStatus)"
|
||||
>{{ col.value }}</span
|
||||
>
|
||||
<span v-else class="text-black">{{ col.value }} </span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-separator />
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col" v-if="props.row.statusEdit === 'edit'">
|
||||
<q-btn
|
||||
class="full-width text-cen"
|
||||
outline
|
||||
rounded
|
||||
icon="edit"
|
||||
label="ขอแก้ไข"
|
||||
color="info"
|
||||
size="sm"
|
||||
@click="openPopup(props.row)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col" v-else>
|
||||
<q-chip
|
||||
:color="`${stores.classColorStatus(props.row.statusEdit)}-1`"
|
||||
:text-color="`${stores.classColorStatus(
|
||||
props.row.statusEdit
|
||||
)}-7`"
|
||||
>
|
||||
{{ props.row.statusEditName }}
|
||||
</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</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>
|
||||
</q-table>
|
||||
|
||||
<Popup
|
||||
:modal="modalPopup"
|
||||
:clickClose="closePopup"
|
||||
:title="titlePopup"
|
||||
:dataById="dataRow"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon-color {
|
||||
color: #4154b3;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-table2 {
|
||||
.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;
|
||||
}
|
||||
|
||||
.q-table td:nth-of-type(2) {
|
||||
z-index: 3 !important;
|
||||
}
|
||||
|
||||
.q-table th:nth-of-type(2),
|
||||
.q-table td:nth-of-type(2) {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
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;
|
||||
}
|
||||
.text-caption {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.q-card-section-last {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
<script setup lang="ts">
|
||||
import type { QTableProps } from 'quasar'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// importStores
|
||||
import { useChekIn } from '@/stores/chekin'
|
||||
|
||||
// importComponent
|
||||
import Popup from '@/components/PopUp.vue'
|
||||
|
||||
const stores = useChekIn()
|
||||
|
||||
const columns = ref<QTableProps['columns']>([
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
label: 'วัน/เดือน/ปี',
|
||||
sortable: true,
|
||||
field: 'date',
|
||||
headerStyle: 'font-size: 14px',
|
||||
style: 'font-size: 14px; width:15%;',
|
||||
},
|
||||
{
|
||||
name: 'in',
|
||||
align: 'left',
|
||||
label: 'เวลาเข้างาน',
|
||||
sortable: true,
|
||||
field: 'in',
|
||||
headerStyle: 'font-size: 14px',
|
||||
style: 'font-size: 14px; width:15%;',
|
||||
},
|
||||
{
|
||||
name: 'out',
|
||||
align: 'left',
|
||||
label: 'เวลาออกงาน',
|
||||
sortable: true,
|
||||
field: 'out',
|
||||
headerStyle: 'font-size: 14px',
|
||||
style: 'font-size: 14px; width:15%;',
|
||||
},
|
||||
{
|
||||
name: 'Morningstatus',
|
||||
align: 'left',
|
||||
label: 'สถานะช่วงเช้า',
|
||||
sortable: true,
|
||||
field: 'Morningstatus',
|
||||
headerStyle: 'font-size: 14px',
|
||||
style: 'font-size: 14px; width:15%;',
|
||||
},
|
||||
{
|
||||
name: 'AfternoonStatus',
|
||||
align: 'left',
|
||||
label: 'สถานะช่วงบ่าย',
|
||||
sortable: true,
|
||||
field: 'AfternoonStatus',
|
||||
headerStyle: 'font-size: 14px',
|
||||
style: 'font-size: 14px; width:15%;',
|
||||
},
|
||||
// {
|
||||
// name: "loIn",
|
||||
// align: "left",
|
||||
// label: "พิกัด",
|
||||
// sortable: true,
|
||||
// field: "loIn",
|
||||
// headerStyle: "font-size: 14px",
|
||||
// style: "font-size: 14px",
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "loOut",
|
||||
// align: "left",
|
||||
// label: "พิกัด",
|
||||
// sortable: true,
|
||||
// field: "loOut",
|
||||
// headerStyle: "font-size: 14px",
|
||||
// style: "font-size: 14px",
|
||||
// },
|
||||
// {
|
||||
// name: "status",
|
||||
// align: "left",
|
||||
// label: "สถานะ",
|
||||
// sortable: true,
|
||||
// field: "status",
|
||||
// headerStyle: "font-size: 14px",
|
||||
// style: "font-size: 14px; width:10%;",
|
||||
// },
|
||||
])
|
||||
// popup
|
||||
const modalPopup = ref<boolean>(false)
|
||||
const titlePopup = ref<string>('')
|
||||
const dataRow = ref<any>()
|
||||
function openPopup(data: any) {
|
||||
const title = 'แก้ไขลงเวลา'
|
||||
modalPopup.value = true
|
||||
titlePopup.value = title
|
||||
dataRow.value = data
|
||||
}
|
||||
function closePopup() {
|
||||
modalPopup.value = false
|
||||
}
|
||||
function classStatus(status: string) {
|
||||
switch (status) {
|
||||
case 'ขาดราชการ':
|
||||
return 'text-red'
|
||||
case 'ปกติ':
|
||||
return 'text-blue'
|
||||
case 'สาย':
|
||||
return 'text-yellow-8'
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
rowsPerPage: 10,
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
class="custom-table2"
|
||||
:columns="columns"
|
||||
:rows="stores.rows"
|
||||
virtual-scroll
|
||||
:virtual-scroll-sticky-size-start="48"
|
||||
:style="$q.screen.gt.xs ? 'max-height: 64vh' : ''"
|
||||
:grid="$q.screen.gt.xs ? false : true"
|
||||
v-model:pagination="pagination"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<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> </q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props" class="cursor-pointer">
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<div
|
||||
v-if="col.name === 'Morningstatus'"
|
||||
:class="classStatus(props.row.Morningstatus)"
|
||||
>
|
||||
{{ props.row.Morningstatus }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="col.name === 'AfternoonStatus'"
|
||||
:class="classStatus(props.row.AfternoonStatus)"
|
||||
>
|
||||
{{ props.row.AfternoonStatus }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ col.value }}
|
||||
</div>
|
||||
</q-td>
|
||||
<q-td>
|
||||
<q-btn
|
||||
v-if="props.row.statusEdit === 'edit'"
|
||||
outline
|
||||
icon="edit"
|
||||
size="12px"
|
||||
label="ขอแก้ไข"
|
||||
color="cyan-6"
|
||||
@click="openPopup(props.row)"
|
||||
/>
|
||||
<q-chip
|
||||
v-else
|
||||
:color="`${stores.classColorStatus(props.row.statusEdit)}-1`"
|
||||
:text-color="`${stores.classColorStatus(props.row.statusEdit)}-7`"
|
||||
>{{ props.row.statusEditName }}</q-chip
|
||||
>
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template #item="props">
|
||||
<div
|
||||
class="q-pa-xs col-xs-12 col-sm-6 col-md-4 col-lg-3 grid-style-transition"
|
||||
>
|
||||
<q-card bordered flat class="q-py-sm shadow-0">
|
||||
<q-list dense>
|
||||
<q-item v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-item-section>
|
||||
<q-item-label caption>{{ col.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-item-label>
|
||||
<span
|
||||
v-if="col.name == 'Morningstatus'"
|
||||
:class="classStatus(props.row.Morningstatus)"
|
||||
>{{ col.value }}</span
|
||||
>
|
||||
<span
|
||||
v-else-if="col.name == 'AfternoonStatus'"
|
||||
:class="classStatus(props.row.AfternoonStatus)"
|
||||
>{{ col.value }}</span
|
||||
>
|
||||
<span v-else class="text-black">{{ col.value }} </span>
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-separator />
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col" v-if="props.row.statusEdit === 'edit'">
|
||||
<q-btn
|
||||
class="full-width text-cen"
|
||||
outline
|
||||
rounded
|
||||
icon="edit"
|
||||
label="ขอแก้ไข"
|
||||
color="info"
|
||||
size="sm"
|
||||
@click="openPopup(props.row)"
|
||||
/>
|
||||
</div>
|
||||
<div class="col" v-else>
|
||||
<q-chip
|
||||
:color="`${stores.classColorStatus(props.row.statusEdit)}-1`"
|
||||
:text-color="`${stores.classColorStatus(
|
||||
props.row.statusEdit
|
||||
)}-7`"
|
||||
>
|
||||
{{ props.row.statusEditName }}
|
||||
</q-chip>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</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>
|
||||
</q-table>
|
||||
|
||||
<Popup
|
||||
:modal="modalPopup"
|
||||
:clickClose="closePopup"
|
||||
:title="titlePopup"
|
||||
:dataById="dataRow"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icon-color {
|
||||
color: #4154b3;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.custom-table2 {
|
||||
.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;
|
||||
}
|
||||
|
||||
.q-table td:nth-of-type(2) {
|
||||
z-index: 3 !important;
|
||||
}
|
||||
|
||||
.q-table th:nth-of-type(2),
|
||||
.q-table td:nth-of-type(2) {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
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;
|
||||
}
|
||||
.text-caption {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.q-card-section-last {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,49 +1,49 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import type { DataOption } from "@/interface/index/Main";
|
||||
import Popup from "@/components/PopUp.vue";
|
||||
// import HeaderPopup from "@/components/HeaderPopup.vue";
|
||||
// import FormTime from "@/components/FormTime.vue";
|
||||
|
||||
const filterYear = ref<string>("");
|
||||
const yearOption = ref<DataOption[]>([{ id: "2566", name: "2566" }]);
|
||||
const titleName = ref<string>("เพิ่มรายการลงเวลากรณีพิเศษ");
|
||||
|
||||
const modalPopup = ref<boolean>(false);
|
||||
function onClickopen() {
|
||||
modalPopup.value = true;
|
||||
}
|
||||
function onClickClose() {
|
||||
modalPopup.value = false;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="q-pb-sm row">
|
||||
<div class="items-center col-12 row q-gutter-sm">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
style="width: 180px"
|
||||
label="ปีประวัติลงเวลา"
|
||||
v-model="filterYear"
|
||||
:options="yearOption"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn
|
||||
unelevated
|
||||
outline
|
||||
icon="add"
|
||||
color="light-blue"
|
||||
:class="$q.screen.gt.xs ? 'q-px-sm bg-blue-1' : ''"
|
||||
:label="$q.screen.gt.xs ? 'เพิ่มรายการลงเวลากรณีพิเศษ' : ''"
|
||||
@click="onClickopen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Popup :modal="modalPopup" :title="titleName" :clickClose="onClickClose" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { DataOption } from '@/interface/index/Main'
|
||||
import Popup from '@/components/PopUp.vue'
|
||||
// import HeaderPopup from "@/components/HeaderPopup.vue";
|
||||
// import FormTime from "@/components/FormTime.vue";
|
||||
|
||||
const filterYear = ref<string>('')
|
||||
const yearOption = ref<DataOption[]>([{ id: '2566', name: '2566' }])
|
||||
const titleName = ref<string>('เพิ่มรายการลงเวลากรณีพิเศษ')
|
||||
|
||||
const modalPopup = ref<boolean>(false)
|
||||
function onClickopen() {
|
||||
modalPopup.value = true
|
||||
}
|
||||
function onClickClose() {
|
||||
modalPopup.value = false
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="q-pb-sm row">
|
||||
<div class="items-center col-12 row q-gutter-sm">
|
||||
<q-select
|
||||
dense
|
||||
outlined
|
||||
style="width: 180px"
|
||||
label="ปีประวัติลงเวลา"
|
||||
v-model="filterYear"
|
||||
:options="yearOption"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
/>
|
||||
<q-space />
|
||||
<q-btn
|
||||
unelevated
|
||||
outline
|
||||
icon="add"
|
||||
color="light-blue"
|
||||
:class="$q.screen.gt.xs ? 'q-px-sm bg-blue-1' : ''"
|
||||
:label="$q.screen.gt.xs ? 'เพิ่มรายการลงเวลากรณีพิเศษ' : ''"
|
||||
@click="onClickopen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Popup :modal="modalPopup" :title="titleName" :clickClose="onClickClose" />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
interface DataOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
interface FormRef {
|
||||
date: object | null;
|
||||
reason: object | null;
|
||||
[key: string]: any;
|
||||
}
|
||||
export type { DataOption, FormRef };
|
||||
interface DataOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
interface FormRef {
|
||||
date: object | null
|
||||
reason: object | null
|
||||
[key: string]: any
|
||||
}
|
||||
export type { DataOption, FormRef }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
interface FormRef {
|
||||
model: object | null;
|
||||
useLocation: object | null;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type { FormRef };
|
||||
interface FormRef {
|
||||
model: object | null
|
||||
useLocation: object | null
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type { FormRef }
|
||||
|
|
|
|||
45
src/main.ts
|
|
@ -1,29 +1,36 @@
|
|||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./registerServiceWorker";
|
||||
import router from "./router";
|
||||
import { createPinia } from "pinia";
|
||||
import { createApp, defineAsyncComponent } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './registerServiceWorker'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import { Quasar, Dialog, Notify, Loading } from "quasar";
|
||||
import '@quasar/extras/material-icons/material-icons.css';
|
||||
import './styles/quasar.sass';
|
||||
import quasarUserOptions from "./quasar-user-options";
|
||||
import VueDatePicker from "@vuepic/vue-datepicker";
|
||||
import "@vuepic/vue-datepicker/dist/main.css";
|
||||
import { Quasar, Dialog, Notify, Loading } from 'quasar'
|
||||
import '@vuepic/vue-datepicker/dist/main.css'
|
||||
import quasarUserOptions from './quasar-user-options'
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
import 'quasar/src/css/index.sass'
|
||||
import th from "quasar/lang/th";
|
||||
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
app.use(pinia)
|
||||
|
||||
app.use(router);
|
||||
app.use(Quasar, quasarUserOptions);
|
||||
app.use(Quasar, {
|
||||
...quasarUserOptions,
|
||||
plugins: {
|
||||
Notify,
|
||||
Dialog,
|
||||
Loading,
|
||||
},
|
||||
});
|
||||
app.use(pinia);
|
||||
app.component("VueDatePicker", VueDatePicker);
|
||||
lang: th,
|
||||
})
|
||||
|
||||
app.mount("#app");
|
||||
app.component(
|
||||
'datepicker',
|
||||
defineAsyncComponent(() => import('@vuepic/vue-datepicker'))
|
||||
)
|
||||
|
||||
app.mount('#app')
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import "./styles/quasar.sass";
|
||||
// import "quasar/dist/quasar.css";
|
||||
import "@quasar/extras/material-icons/material-icons.css";
|
||||
import { Dialog, Notify } from "quasar";
|
||||
// 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: { Dialog, Notify },
|
||||
};
|
||||
plugins: {},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,30 +3,32 @@
|
|||
import { register } from 'register-service-worker'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready () {
|
||||
register('registerSW.js', {
|
||||
ready() {
|
||||
console.log(
|
||||
'App is being served from cache by a service worker.\n' +
|
||||
'For more details, visit https://goo.gl/AFskqB'
|
||||
'For more details, visit https://goo.gl/AFskqB'
|
||||
)
|
||||
},
|
||||
registered () {
|
||||
registered() {
|
||||
console.log('Service worker has been registered.')
|
||||
},
|
||||
cached () {
|
||||
cached() {
|
||||
console.log('Content has been cached for offline use.')
|
||||
},
|
||||
updatefound () {
|
||||
updatefound() {
|
||||
console.log('New content is downloading.')
|
||||
},
|
||||
updated () {
|
||||
updated() {
|
||||
console.log('New content is available; please refresh.')
|
||||
},
|
||||
offline () {
|
||||
console.log('No internet connection found. App is running in offline mode.')
|
||||
offline() {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
)
|
||||
},
|
||||
error (error) {
|
||||
error(error) {
|
||||
console.error('Error during service worker registration:', error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,42 @@
|
|||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||
import HomeView from "../views/HomeView.vue";
|
||||
import HistoryView from "@/views/HistoryView.vue";
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/about",
|
||||
name: "about",
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/history",
|
||||
name: "history",
|
||||
component: HistoryView,
|
||||
},
|
||||
];
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '@/views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
routes,
|
||||
});
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (about.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('@/views/AboutView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/history',
|
||||
name: 'history',
|
||||
component: () => import('@/views/HistoryView.vue'),
|
||||
},
|
||||
// {
|
||||
// path: '/camera',
|
||||
// name: 'camera',
|
||||
// component: () => import('@/views/SampleCamera.vue'),
|
||||
// },
|
||||
/**
|
||||
* 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;
|
||||
export default router
|
||||
|
|
|
|||
2
src/shims-vue.d.ts
vendored
|
|
@ -1 +1 @@
|
|||
declare module '*.vue';
|
||||
declare module '*.vue'
|
||||
|
|
|
|||
|
|
@ -1,61 +1,61 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
export const useChekIn = defineStore("checkin", () => {
|
||||
const rows = ref<any>();
|
||||
|
||||
function fetchHistoryList(data: any) {
|
||||
console.log(data);
|
||||
const datalist = data.map((e: any) => ({
|
||||
no: e.no,
|
||||
date: e.date,
|
||||
in: e.in,
|
||||
loIn: e.loIn,
|
||||
out: e.out,
|
||||
loOut: e.loOut,
|
||||
status: e.status,
|
||||
Morningstatus: convertStatus(e.Morningstatus),
|
||||
AfternoonStatus: convertStatus(e.AfternoonStatus),
|
||||
statusEdit: e.statusEdit,
|
||||
statusEditName: convertStatusEdit(e.statusEdit),
|
||||
}));
|
||||
rows.value = datalist;
|
||||
}
|
||||
|
||||
function convertStatus(status: string) {
|
||||
switch (status) {
|
||||
case "1":
|
||||
return "ขาดราชการ";
|
||||
case "2":
|
||||
return "ปกติ";
|
||||
case "3":
|
||||
return "สาย";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
function convertStatusEdit(val: string) {
|
||||
switch (val) {
|
||||
case "edit":
|
||||
return "ขอแก้ไข";
|
||||
case "wait":
|
||||
return "รออนุมัติ";
|
||||
case "approve":
|
||||
return "อนุมัติ";
|
||||
}
|
||||
}
|
||||
function classColorStatus(val: string) {
|
||||
switch (val) {
|
||||
case "wait":
|
||||
return "orange";
|
||||
case "approve":
|
||||
return "green";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rows,
|
||||
fetchHistoryList,
|
||||
classColorStatus,
|
||||
};
|
||||
});
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useChekIn = defineStore('checkin', () => {
|
||||
const rows = ref<any>()
|
||||
|
||||
function fetchHistoryList(data: any) {
|
||||
console.log(data)
|
||||
const datalist = data.map((e: any) => ({
|
||||
no: e.no,
|
||||
date: e.date,
|
||||
in: e.in,
|
||||
loIn: e.loIn,
|
||||
out: e.out,
|
||||
loOut: e.loOut,
|
||||
status: e.status,
|
||||
Morningstatus: convertStatus(e.Morningstatus),
|
||||
AfternoonStatus: convertStatus(e.AfternoonStatus),
|
||||
statusEdit: e.statusEdit,
|
||||
statusEditName: convertStatusEdit(e.statusEdit),
|
||||
}))
|
||||
rows.value = datalist
|
||||
}
|
||||
|
||||
function convertStatus(status: string) {
|
||||
switch (status) {
|
||||
case '1':
|
||||
return 'ขาดราชการ'
|
||||
case '2':
|
||||
return 'ปกติ'
|
||||
case '3':
|
||||
return 'สาย'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
function convertStatusEdit(val: string) {
|
||||
switch (val) {
|
||||
case 'edit':
|
||||
return 'ขอแก้ไข'
|
||||
case 'wait':
|
||||
return 'รออนุมัติ'
|
||||
case 'approve':
|
||||
return 'อนุมัติ'
|
||||
}
|
||||
}
|
||||
function classColorStatus(val: string) {
|
||||
switch (val) {
|
||||
case 'wait':
|
||||
return 'orange'
|
||||
case 'approve':
|
||||
return 'green'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rows,
|
||||
fetchHistoryList,
|
||||
classColorStatus,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,122 +1,122 @@
|
|||
import { defineStore } from "pinia";
|
||||
import CustomComponent from "@/components/CustomDialog.vue";
|
||||
import { useQuasar } from "quasar";
|
||||
const $q = useQuasar();
|
||||
import { defineStore } from 'pinia'
|
||||
import CustomComponent from '@/components/CustomDialog.vue'
|
||||
|
||||
export const useCounterMixin = defineStore("mixin", () => {
|
||||
export const useCounterMixin = defineStore('mixin', () => {
|
||||
function date2Thai(srcDate: Date, isFullMonth = false, isTime = false) {
|
||||
if (srcDate == null) {
|
||||
return 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 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;
|
||||
'ม.ค.',
|
||||
'ก.พ.',
|
||||
'มี.ค.',
|
||||
'เม.ย.',
|
||||
'พ.ค.',
|
||||
'มิ.ย.',
|
||||
'ก.ค.',
|
||||
'ส.ค.',
|
||||
'ก.ย.',
|
||||
'ต.ค.',
|
||||
'พ.ย.',
|
||||
'ธ.ค.',
|
||||
]
|
||||
let dstYear = 0
|
||||
if (date.getFullYear() > 2500) {
|
||||
dstYear = date.getFullYear();
|
||||
dstYear = date.getFullYear()
|
||||
} else {
|
||||
dstYear = date.getFullYear() + 543;
|
||||
dstYear = date.getFullYear() + 543
|
||||
}
|
||||
let dstMonth = "";
|
||||
let dstMonth = ''
|
||||
if (isFullMonth) {
|
||||
dstMonth = fullMonthThai[date.getMonth()];
|
||||
dstMonth = fullMonthThai[date.getMonth()]
|
||||
} else {
|
||||
dstMonth = abbrMonthThai[date.getMonth()];
|
||||
dstMonth = abbrMonthThai[date.getMonth()]
|
||||
}
|
||||
let dstTime = "";
|
||||
let dstTime = ''
|
||||
if (isTime) {
|
||||
const H = date.getHours().toString().padStart(2, "0");
|
||||
const M = date.getMinutes().toString().padStart(2, "0");
|
||||
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 + " น.";
|
||||
dstTime = ' ' + H + ':' + M + ' น.'
|
||||
}
|
||||
return (
|
||||
date.getDate().toString().padStart(2, "0") +
|
||||
" " +
|
||||
date.getDate().toString().padStart(2, '0') +
|
||||
' ' +
|
||||
dstMonth +
|
||||
" " +
|
||||
' ' +
|
||||
dstYear +
|
||||
dstTime
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function covertDateObject(date: string) {
|
||||
if (date) {
|
||||
const dateParts = date.split("/");
|
||||
const dateParts = date.split('/')
|
||||
// ประกาศตัวแปรเพื่อเก็บค่าวันที่, เดือน, และ ปี
|
||||
const day = parseInt(dateParts[0], 10);
|
||||
const month = parseInt(dateParts[1], 10) - 1;
|
||||
const year = parseInt(dateParts[2], 10) + 2500;
|
||||
const day = parseInt(dateParts[0], 10)
|
||||
const month = parseInt(dateParts[1], 10) - 1
|
||||
const year = parseInt(dateParts[2], 10) + 2500
|
||||
// สร้างอ็อบเจ็กต์ Date ด้วยค่าที่ได้
|
||||
const dateObject = new Date(year, month, day);
|
||||
return date2Thai(dateObject);
|
||||
const dateObject = new Date(year, month, day)
|
||||
return date2Thai(dateObject)
|
||||
}
|
||||
}
|
||||
type OkCallback = () => void;
|
||||
type CancelCallback = () => void;
|
||||
const dialogConfirm = (
|
||||
|
||||
type OkCallback = () => void
|
||||
type CancelCallback = () => void
|
||||
function dialogConfirm(
|
||||
q: any,
|
||||
ok?: OkCallback,
|
||||
title?: string, // ถ้ามี cancel action ใส่เป็น null
|
||||
desc?: string, // ถ้ามี cancel action ใส่เป็น null
|
||||
cancel?: CancelCallback
|
||||
) => {
|
||||
) {
|
||||
q.dialog({
|
||||
component: CustomComponent,
|
||||
componentProps: {
|
||||
title: title && title != null ? title : "ยืนยันการบันทึก",
|
||||
title: title && title != null ? title : 'ยืนยันการบันทึก',
|
||||
message:
|
||||
desc && desc != null
|
||||
? desc
|
||||
: "ต้องการยืนยันการบันทึกข้อมูลนี้ใช่หรือไม่?",
|
||||
icon: "info",
|
||||
color: "public",
|
||||
textOk: "ตกลง",
|
||||
: 'ต้องการยืนยันการบันทึกข้อมูลนี้ใช่หรือไม่?',
|
||||
icon: 'info',
|
||||
color: 'public',
|
||||
textOk: 'ตกลง',
|
||||
onlycancel: false,
|
||||
},
|
||||
})
|
||||
.onOk(() => {
|
||||
if (ok) ok();
|
||||
if (ok) ok()
|
||||
})
|
||||
.onCancel(() => {
|
||||
if (cancel) cancel();
|
||||
});
|
||||
};
|
||||
if (cancel) cancel()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
date2Thai,
|
||||
covertDateObject,
|
||||
dialogConfirm,
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
@import './quasar.variables.sass'
|
||||
@import '~quasar-styl'
|
||||
// @import '~quasar-addon-styl'
|
||||
27
src/views/ErrorNotFoundPage.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Error404NotFound",
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="fullscreen bg-secondary text-white text-center q-pa-md flex flex-center"
|
||||
>
|
||||
<div>
|
||||
<div class="text-h1">ไม่พบหน้าที่ต้องการ</div>
|
||||
<div class="text-h2">(404 Page Not Found)</div>
|
||||
<q-btn
|
||||
class="q-mt-xl"
|
||||
color="white"
|
||||
text-color="secondary"
|
||||
unelevated
|
||||
to="/"
|
||||
label="กลับไปหน้าหลัก"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,103 +1,103 @@
|
|||
<script setup lang="ts">
|
||||
import type { QTableProps } from "quasar";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import Table from "@/components/TableHistory.vue";
|
||||
import ToolBar from "@/components/ToolBar.vue";
|
||||
|
||||
// importStores
|
||||
import { useChekIn } from "@/stores/chekin";
|
||||
|
||||
const router = useRouter();
|
||||
const stores = useChekIn();
|
||||
|
||||
onMounted(() => {
|
||||
fetchlist();
|
||||
});
|
||||
|
||||
function fetchlist() {
|
||||
const listData = [
|
||||
{
|
||||
no: "1",
|
||||
date: "13/08/66",
|
||||
in: "11:20",
|
||||
loIn: "สำนักงาน ก.ก ",
|
||||
out: "",
|
||||
loOut: "",
|
||||
status: "",
|
||||
Morningstatus: "1",
|
||||
AfternoonStatus: "1",
|
||||
statusEdit: "wait",
|
||||
},
|
||||
{
|
||||
no: "2",
|
||||
date: "12/08/66",
|
||||
in: "08:04",
|
||||
loIn: "สำนักงาน ก.ก ",
|
||||
out: "17:01",
|
||||
loOut: "สำนักงาน ก.ก ",
|
||||
status: "ลงเวลาเรียบร้อย",
|
||||
Morningstatus: "2",
|
||||
AfternoonStatus: "2",
|
||||
statusEdit: "edit",
|
||||
},
|
||||
{
|
||||
no: "3",
|
||||
date: "11/08/66",
|
||||
in: "08:34",
|
||||
loIn: "สำนักงาน ก.ก ",
|
||||
out: "17:36",
|
||||
loOut: "สำนักงาน ก.ก ",
|
||||
status: "สาย ทำงานครบ",
|
||||
Morningstatus: "3",
|
||||
AfternoonStatus: "2",
|
||||
statusEdit: "edit",
|
||||
},
|
||||
{
|
||||
no: "4",
|
||||
date: "10/08/66",
|
||||
in: "08:48",
|
||||
loIn: "สำนักงาน ก.ก ",
|
||||
out: "17:00",
|
||||
loOut: "สำนักงาน ก.ก ",
|
||||
status: "สาย ทำงานไม่ครบ",
|
||||
Morningstatus: "3",
|
||||
AfternoonStatus: "3",
|
||||
statusEdit: "approve",
|
||||
},
|
||||
];
|
||||
stores.fetchHistoryList(listData);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="col-12 row justify-center">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12">
|
||||
<q-card flat class="row col-12 cardNone">
|
||||
<div
|
||||
class="bg-secondary text-white col-12 row items-center q-px-md q-py-sm"
|
||||
>
|
||||
<div class="col-2">
|
||||
<q-btn
|
||||
icon="arrow_backt"
|
||||
unelevated
|
||||
round
|
||||
dense
|
||||
flat
|
||||
color="white"
|
||||
@click="router.go(-1)"
|
||||
/>
|
||||
</div>
|
||||
<q-space />
|
||||
<span class="text-body1 text-weight-bold col-8 text-center"
|
||||
>ประวัติการลงเวลา</span
|
||||
>
|
||||
<div class="col-2"></div>
|
||||
</div>
|
||||
<div class="col-12 q-pa-md text-grey-9">
|
||||
<ToolBar />
|
||||
<Table />
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { QTableProps } from 'quasar'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Table from '@/components/TableHistory.vue'
|
||||
import ToolBar from '@/components/ToolBar.vue'
|
||||
|
||||
// importStores
|
||||
import { useChekIn } from '@/stores/chekin'
|
||||
|
||||
const router = useRouter()
|
||||
const stores = useChekIn()
|
||||
|
||||
onMounted(() => {
|
||||
fetchlist()
|
||||
})
|
||||
|
||||
function fetchlist() {
|
||||
const listData = [
|
||||
{
|
||||
no: '1',
|
||||
date: '13/08/66',
|
||||
in: '11:20',
|
||||
loIn: 'สำนักงาน ก.ก ',
|
||||
out: '',
|
||||
loOut: '',
|
||||
status: '',
|
||||
Morningstatus: '1',
|
||||
AfternoonStatus: '1',
|
||||
statusEdit: 'wait',
|
||||
},
|
||||
{
|
||||
no: '2',
|
||||
date: '12/08/66',
|
||||
in: '08:04',
|
||||
loIn: 'สำนักงาน ก.ก ',
|
||||
out: '17:01',
|
||||
loOut: 'สำนักงาน ก.ก ',
|
||||
status: 'ลงเวลาเรียบร้อย',
|
||||
Morningstatus: '2',
|
||||
AfternoonStatus: '2',
|
||||
statusEdit: 'edit',
|
||||
},
|
||||
{
|
||||
no: '3',
|
||||
date: '11/08/66',
|
||||
in: '08:34',
|
||||
loIn: 'สำนักงาน ก.ก ',
|
||||
out: '17:36',
|
||||
loOut: 'สำนักงาน ก.ก ',
|
||||
status: 'สาย ทำงานครบ',
|
||||
Morningstatus: '3',
|
||||
AfternoonStatus: '2',
|
||||
statusEdit: 'edit',
|
||||
},
|
||||
{
|
||||
no: '4',
|
||||
date: '10/08/66',
|
||||
in: '08:48',
|
||||
loIn: 'สำนักงาน ก.ก ',
|
||||
out: '17:00',
|
||||
loOut: 'สำนักงาน ก.ก ',
|
||||
status: 'สาย ทำงานไม่ครบ',
|
||||
Morningstatus: '3',
|
||||
AfternoonStatus: '3',
|
||||
statusEdit: 'approve',
|
||||
},
|
||||
]
|
||||
stores.fetchHistoryList(listData)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="col-12 row justify-center">
|
||||
<div class="col-xs-12 col-sm-12 col-md-12">
|
||||
<q-card flat class="row col-12 cardNone">
|
||||
<div
|
||||
class="bg-secondary text-white col-12 row items-center q-px-md q-py-sm"
|
||||
>
|
||||
<div class="col-2">
|
||||
<q-btn
|
||||
icon="arrow_backt"
|
||||
unelevated
|
||||
round
|
||||
dense
|
||||
flat
|
||||
color="white"
|
||||
@click="router.go(-1)"
|
||||
/>
|
||||
</div>
|
||||
<q-space />
|
||||
<span class="text-body1 text-weight-bold col-8 text-center"
|
||||
>ประวัติการลงเวลา</span
|
||||
>
|
||||
<div class="col-2"></div>
|
||||
</div>
|
||||
<div class="col-12 q-pa-md text-grey-9">
|
||||
<ToolBar />
|
||||
<Table />
|
||||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,202 +1,134 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useQuasar } from "quasar";
|
||||
import moment, { Moment } from "moment";
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useQuasar } from 'quasar'
|
||||
import moment from 'moment'
|
||||
import Camera from 'simple-vue-camera'
|
||||
|
||||
// import Type
|
||||
import type { FormRef } from "@/interface/response/checkin";
|
||||
import type { FormRef } from '@/interface/response/checkin'
|
||||
// import components
|
||||
import MapCheck from "@/components/MapCheckin.vue";
|
||||
import MapCheck from '@/components/MapCheckin.vue'
|
||||
// import Stores
|
||||
import { useCounterMixin } from "@/stores/mixin";
|
||||
import { useCounterMixin } from '@/stores/mixin'
|
||||
|
||||
const mixin = useCounterMixin();
|
||||
const { date2Thai, dialogConfirm } = mixin;
|
||||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
const mixin = useCounterMixin()
|
||||
const { date2Thai, dialogConfirm } = mixin
|
||||
const router = useRouter()
|
||||
const $q = useQuasar()
|
||||
|
||||
const stetusCheckin = ref(true);
|
||||
const stetusCheckin = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
updateClock();
|
||||
});
|
||||
updateClock()
|
||||
})
|
||||
|
||||
//time
|
||||
const dateNow = ref<Date>(new Date());
|
||||
const Thai = ref<Date>(dateNow.value);
|
||||
const formattedS = ref();
|
||||
const formattedM = ref();
|
||||
const formattedH = ref();
|
||||
const dateNow = ref<Date>(new Date())
|
||||
const Thai = ref<Date>(dateNow.value)
|
||||
const formattedS = ref()
|
||||
const formattedM = ref()
|
||||
const formattedH = ref()
|
||||
|
||||
function updateClock() {
|
||||
const date = Date.now();
|
||||
let hh = moment(date).format("HH");
|
||||
let mm = moment(date).format("mm");
|
||||
let ss = moment(date).format("ss");
|
||||
formattedS.value = ss;
|
||||
formattedM.value = mm;
|
||||
formattedH.value = hh;
|
||||
const date = Date.now()
|
||||
const hh = moment(date).format('HH')
|
||||
const mm = moment(date).format('mm')
|
||||
const ss = moment(date).format('ss')
|
||||
formattedS.value = ss
|
||||
formattedM.value = mm
|
||||
formattedH.value = hh
|
||||
}
|
||||
setInterval(updateClock, 1000);
|
||||
setInterval(updateClock, 1000)
|
||||
|
||||
//location
|
||||
const location = ref<string>("");
|
||||
const coordinates = ref<string>("13° 43’ 45” N 100° 31’ 26” E");
|
||||
const workplace = ref<string>("in-place");
|
||||
const useLocation = ref<string | null>("");
|
||||
const model = ref<string | null>("");
|
||||
const location = ref<string>('')
|
||||
const coordinates = ref<string>('13° 43’ 45” N 100° 31’ 26” E')
|
||||
const workplace = ref<string>('in-place')
|
||||
const useLocation = ref<string | null>('')
|
||||
const model = ref<string | null>('')
|
||||
const options = ref<string[]>([
|
||||
"ปฏิบัติงานที่บ้าน",
|
||||
"ลืมลงเวลาปฏิบัติงาน",
|
||||
"ไปประชุม/อบรม/สัมมนา/ปฏิบัติงานที่บ้านนอกสถานที่",
|
||||
"ขออนุญาตออกนอกสถานที่",
|
||||
"อื่นๆ",
|
||||
]);
|
||||
'ปฏิบัติงานที่บ้าน',
|
||||
'ลืมลงเวลาปฏิบัติงาน',
|
||||
'ไปประชุม/อบรม/สัมมนา/ปฏิบัติงานที่บ้านนอกสถานที่',
|
||||
'ขออนุญาตออกนอกสถานที่',
|
||||
'อื่นๆ',
|
||||
])
|
||||
function selectLocation() {
|
||||
if (model.value === "อื่นๆ") {
|
||||
useLocation.value = "";
|
||||
if (model.value === 'อื่นๆ') {
|
||||
useLocation.value = ''
|
||||
} else {
|
||||
useLocation.value = model.value;
|
||||
useLocation.value = model.value
|
||||
}
|
||||
}
|
||||
//camera
|
||||
const camera = ref(false);
|
||||
const hasPhoto = ref<boolean>(true);
|
||||
const mediaStream = ref<MediaStream | null>(null);
|
||||
const video = ref<HTMLVideoElement | null>(null);
|
||||
const canvas = ref<HTMLCanvasElement | null>(null);
|
||||
const img = ref<any>(null);
|
||||
const camera = ref<InstanceType<typeof Camera>>()
|
||||
const cameraIsOn = ref<boolean>(false)
|
||||
const img = ref<any>(undefined)
|
||||
const photoWidth = ref<number>(350)
|
||||
const photoHeight = ref<number>(350)
|
||||
|
||||
const openCamera = () => {
|
||||
camera.value = true;
|
||||
camera.value && setupCamera();
|
||||
};
|
||||
const setupCamera = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
if (video.value) {
|
||||
video.value.srcObject = stream;
|
||||
}
|
||||
mediaStream.value = stream;
|
||||
} catch (error) {
|
||||
console.error("Error accessing camera:", error);
|
||||
}
|
||||
};
|
||||
function capturePhoto() {
|
||||
const videoElement = video.value;
|
||||
const canvasElement = canvas.value;
|
||||
if (!videoElement || !canvasElement) {
|
||||
console.error("Video or Canvas element not available");
|
||||
return;
|
||||
}
|
||||
const context = canvasElement.getContext("2d");
|
||||
if (!context) {
|
||||
console.error("Canvas context not available");
|
||||
return;
|
||||
}
|
||||
const desiredWidth = 150;
|
||||
const desiredHeight = 200;
|
||||
const zoomFactor = 10;
|
||||
cameraIsOn.value ? camera.value?.stop() : camera.value?.start()
|
||||
cameraIsOn.value = !cameraIsOn.value
|
||||
}
|
||||
|
||||
const videoAspectRatio = videoElement.videoWidth / videoElement.videoHeight;
|
||||
const canvasAspectRatio = desiredWidth / desiredHeight;
|
||||
|
||||
let drawWidth, drawHeight;
|
||||
|
||||
if (videoAspectRatio > canvasAspectRatio) {
|
||||
drawWidth = desiredWidth * zoomFactor;
|
||||
drawHeight = (desiredWidth * zoomFactor) / videoAspectRatio;
|
||||
} else {
|
||||
drawWidth = desiredHeight * zoomFactor * videoAspectRatio;
|
||||
drawHeight = desiredHeight * zoomFactor;
|
||||
}
|
||||
|
||||
canvasElement.width = drawWidth;
|
||||
canvasElement.height = drawHeight;
|
||||
if (context) {
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.imageSmoothingQuality = "low";
|
||||
}
|
||||
|
||||
// context.drawImage(
|
||||
// videoElement,
|
||||
// 0,
|
||||
// 0,
|
||||
// canvasElement.width,
|
||||
// canvasElement.height
|
||||
// );
|
||||
context.drawImage(
|
||||
videoElement,
|
||||
0,
|
||||
0,
|
||||
videoElement.videoWidth,
|
||||
videoElement.videoHeight,
|
||||
0,
|
||||
0,
|
||||
drawWidth,
|
||||
drawHeight
|
||||
);
|
||||
async function capturePhoto() {
|
||||
const imageBlob: any = await camera.value?.snapshot(
|
||||
{ width: photoWidth.value, height: photoHeight.value },
|
||||
'image/png',
|
||||
0.5
|
||||
)
|
||||
|
||||
//ไฟล์รูป
|
||||
const dataURL = canvasElement.toDataURL(".image/.png");
|
||||
img.value = dataURL;
|
||||
console.log(img.value);
|
||||
camera.value?.stop()
|
||||
const url = URL.createObjectURL(imageBlob)
|
||||
img.value = url
|
||||
}
|
||||
|
||||
if (mediaStream.value) {
|
||||
mediaStream.value.getTracks().forEach((track) => track.stop());
|
||||
videoElement.srcObject = null;
|
||||
hasPhoto.value = false;
|
||||
}
|
||||
}
|
||||
function refreshPhoto() {
|
||||
hasPhoto.value = true;
|
||||
img.value = "";
|
||||
img.value = undefined
|
||||
camera.value?.start()
|
||||
}
|
||||
|
||||
// validate
|
||||
const useLocationRef = ref<object | null>(null);
|
||||
const modelRef = ref<object | null>(null);
|
||||
const useLocationRef = ref<object | null>(null)
|
||||
const modelRef = ref<object | null>(null)
|
||||
const objectRef: FormRef = {
|
||||
model: modelRef,
|
||||
useLocation: useLocationRef,
|
||||
};
|
||||
}
|
||||
function validateForm() {
|
||||
const hasError = [];
|
||||
const hasError = []
|
||||
for (const key in objectRef) {
|
||||
if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
|
||||
const property = objectRef[key];
|
||||
if (property.value && typeof property.value.validate === "function") {
|
||||
const isValid = property.value.validate();
|
||||
hasError.push(isValid);
|
||||
const property = objectRef[key]
|
||||
if (property.value && typeof property.value.validate === 'function') {
|
||||
const isValid = property.value.validate()
|
||||
hasError.push(isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasError.every((result) => result === true)) {
|
||||
confirm();
|
||||
confirm()
|
||||
} else {
|
||||
console.log("ไม่ผ่าน ");
|
||||
console.log('ไม่ผ่าน ')
|
||||
}
|
||||
}
|
||||
// ยืนยันการลงเวลา
|
||||
const dialogTime = ref<boolean>(false);
|
||||
const dialogTime = ref<boolean>(false)
|
||||
const confirm = () => {
|
||||
dialogConfirm(
|
||||
$q,
|
||||
async () => {
|
||||
dialogTime.value = true;
|
||||
},
|
||||
"ยืนยันการบันทึกเวลา ?",
|
||||
"ต้องการยืนยันการบันทึกการลงเวลานี้ใช่หรือไม่"
|
||||
);
|
||||
};
|
||||
// ยิงไปที่ api แล้วแสดง popup
|
||||
dialogTime.value = true
|
||||
}
|
||||
|
||||
// class
|
||||
const getClass = (val: boolean) => {
|
||||
return {
|
||||
"bg-primary text-white col-12 row items-center q-px-md q-py-sm": val,
|
||||
"bg-red-9 text-white col-12 row items-center q-px-md q-py-sm": !val,
|
||||
};
|
||||
};
|
||||
'bg-primary text-white col-12 row items-center q-px-md q-py-sm': val,
|
||||
'bg-red-9 text-white col-12 row items-center q-px-md q-py-sm': !val,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -263,13 +195,12 @@ const getClass = (val: boolean) => {
|
|||
<MapCheck />
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<q-card
|
||||
flat
|
||||
bordered
|
||||
class="card-container"
|
||||
@click="openCamera()"
|
||||
>
|
||||
<div v-if="!camera" class="preview-placeholder">
|
||||
<q-card flat bordered class="card-container">
|
||||
<div
|
||||
v-if="!cameraIsOn && img == null"
|
||||
class="preview-placeholder"
|
||||
@click="openCamera()"
|
||||
>
|
||||
<div class="text-center">
|
||||
<q-icon
|
||||
name="photo_camera"
|
||||
|
|
@ -279,18 +210,26 @@ const getClass = (val: boolean) => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="hasPhoto" class="video-container">
|
||||
<video ref="video" autoplay class="video-element"></video>
|
||||
<canvas ref="canvas" class="canvas-element"></canvas>
|
||||
</div>
|
||||
<div v-else class="image-container">
|
||||
<div>
|
||||
<!-- แสดงกล้องตอนกดถ่ายภาพ -->
|
||||
<Camera
|
||||
:resolution="{ width: photoWidth, height: photoHeight }"
|
||||
ref="camera"
|
||||
:autoplay="false"
|
||||
:style="!img ? 'display: block' : 'display: none'"
|
||||
/>
|
||||
|
||||
<!-- แสดงรูปเมื่อกด capture -->
|
||||
<div v-if="img" class="image-container">
|
||||
<q-img :src="img" class="image-element"></q-img>
|
||||
<canvas ref="canvas" class="canvas-element"></canvas>
|
||||
</div>
|
||||
<div class="absolute-bottom-right q-ma-md">
|
||||
|
||||
<div
|
||||
v-if="cameraIsOn"
|
||||
class="absolute-bottom-right q-ma-md"
|
||||
>
|
||||
<q-btn
|
||||
v-if="hasPhoto"
|
||||
v-if="img == null"
|
||||
round
|
||||
push
|
||||
icon="photo_camera"
|
||||
|
|
@ -311,6 +250,7 @@ const getClass = (val: boolean) => {
|
|||
</div>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
<div class="col-12 q-mb-md">
|
||||
<q-card
|
||||
bordered
|
||||
|
|
@ -377,6 +317,7 @@ const getClass = (val: boolean) => {
|
|||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
|
||||
<div class="col-12 text-right">
|
||||
<q-separator />
|
||||
<div class="col-12 q-pa-md">
|
||||
|
|
@ -471,8 +412,6 @@ const getClass = (val: boolean) => {
|
|||
height: 350px; /* Adjust as needed */
|
||||
background: #f6f5f5;
|
||||
}
|
||||
|
||||
.video-container,
|
||||
.image-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -481,7 +420,6 @@ const getClass = (val: boolean) => {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.video-element,
|
||||
.image-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -489,7 +427,8 @@ const getClass = (val: boolean) => {
|
|||
border-radius: 5px; /* Adjust as needed */
|
||||
}
|
||||
|
||||
.canvas-element {
|
||||
display: none; /* Adjust as needed */
|
||||
.preview-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
42
src/views/SampleCamera.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<script setup lang="ts">
|
||||
import Camera from 'simple-vue-camera'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const camera = ref<InstanceType<typeof Camera>>();
|
||||
|
||||
const cameraIsOn = ref<boolean>(false)
|
||||
|
||||
const cameraOff = () => {
|
||||
cameraIsOn.value ? camera.value?.stop() : camera.value?.start()
|
||||
cameraIsOn.value = !cameraIsOn.value
|
||||
}
|
||||
|
||||
// Use camera reference to call functions
|
||||
const takePicture = async () => {
|
||||
const imageBlob = await camera.value?.snapshot(
|
||||
{ width: 640, height: 480 },
|
||||
'image/png',
|
||||
0.5
|
||||
)
|
||||
|
||||
console.log('imageBlob', imageBlob)
|
||||
camera.value?.stop()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row col-12">
|
||||
<div class="col-3">
|
||||
<Camera
|
||||
:resolution="{ width: 500, height: 500 }"
|
||||
ref="camera"
|
||||
:autoplay="false"
|
||||
>
|
||||
<q-btn color="primary" @click="cameraOff"
|
||||
>I'm on top of the video</q-btn
|
||||
>
|
||||
<q-btn color="primary" @click="takePicture">Take Picture</q-btn>
|
||||
</Camera>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
16
tsconfig.app.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"ignoreDeprecations": "5.0",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": ["dom", "es2015", "es2018", "es2018.promise"]
|
||||
}
|
||||
}
|
||||
14
tsconfig.config.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"ignoreDeprecations": "5.0",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["webpack-env"],
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.config.json"
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
9
tsconfig.vitest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
||||
60
vite.config.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: { transformAssetUrls },
|
||||
}),
|
||||
quasar({
|
||||
sassVariables: 'src/style/quasar-variables.sass',
|
||||
}),
|
||||
vueJsx(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
injectRegister: 'auto',
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
globPatterns: ['**/*.*'],
|
||||
},
|
||||
includeAssets: ['icons/safari-pinned-tab.svg'],
|
||||
manifest: {
|
||||
name: 'BMA-Checkin',
|
||||
short_name: 'EHR Checkin',
|
||||
theme_color: '#ffffff',
|
||||
icons: [
|
||||
{
|
||||
src: 'icons/android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'icons/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'icons/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: ['any', 'maskable'],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3008,
|
||||
},
|
||||
})
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: [
|
||||
'quasar'
|
||||
],
|
||||
|
||||
pluginOptions: {
|
||||
quasar: {
|
||||
importStrategy: 'kebab',
|
||||
rtlSupport: false
|
||||
}
|
||||
}
|
||||
})
|
||||