- run by vite

- change camera
This commit is contained in:
Warunee Tamkoo 2023-11-14 17:47:43 +07:00
parent 782fa7f59f
commit 85d163fb64
57 changed files with 1494 additions and 1375 deletions

View file

@ -1,20 +1,19 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true node: true,
es2022: true,
}, },
'extends': [ extends: [
'plugin:vue/vue3-essential', 'plugin:vue/vue3-essential',
'eslint:recommended', 'eslint:recommended',
'@vue/typescript/recommended' '@vue/typescript/recommended',
], ],
parserOptions: {
ecmaVersion: 2020
},
rules: { rules: {
// 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// 'no-debugger': 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
View file

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": true
}

View file

@ -1,24 +1,30 @@
# maptest # BMA ระบบลงเวลาเข้า-ออกงาน
## Project setup ## Project setup
``` ```
npm install npm install
``` ```
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
``` ```
npm run dev npm run dev
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
``` ```
npm run build npm run build
npm run preview
``` ```
### Lints and fixes files ### Format and fixes files
``` ```
npm run lint npm run format
``` ```
### Customize configuration ### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/). See [Configuration Reference](https://cli.vuejs.org/config/).

View file

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

8
cypress.config.ts Normal file
View 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
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View 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>

View file

@ -3,39 +3,60 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vue-cli-service serve", "dev": "vite",
"build": "vue-cli-service build", "build": "run-p type-check build-only",
"lint": "vue-cli-service lint" "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": { "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", "@googlemaps/js-api-loader": "^1.16.2",
"@quasar/cli": "^2.3.0", "@quasar/extras": "^1.15.8",
"@quasar/extras": "^1.0.0", "@vuepic/vue-datepicker": "^5.2.1",
"@vuepic/vue-datepicker": "^7.2.1",
"core-js": "^3.8.3",
"moment": "^2.29.4", "moment": "^2.29.4",
"pinia": "^2.1.7", "pinia": "^2.1.4",
"quasar": "^2.0.0", "quasar": "^2.11.1",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"vue": "^3.2.13", "simple-vue-camera": "^1.1.3",
"vue-router": "^4.0.3", "vite-plugin-pwa": "^0.16.7",
"vue": "^3.2.45",
"vue-router": "^4.1.6",
"vue3-google-map": "^0.18.0" "vue3-google-map": "^0.18.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.4.0", "@quasar/vite-plugin": "^1.3.0",
"@typescript-eslint/parser": "^5.4.0", "@rushstack/eslint-patch": "^1.1.4",
"@vue/cli-plugin-babel": "~5.0.0", "@types/jsdom": "^20.0.1",
"@vue/cli-plugin-eslint": "~5.0.0", "@types/node": "^18.11.12",
"@vue/cli-plugin-pwa": "~5.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vue/cli-plugin-router": "~5.0.0", "@vitejs/plugin-vue-jsx": "^3.0.0",
"@vue/cli-plugin-typescript": "~5.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/cli-service": "~5.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/eslint-config-typescript": "^9.1.0", "@vue/test-utils": "^2.2.6",
"eslint": "^8.53.0", "@vue/tsconfig": "^0.1.3",
"eslint-plugin-prettier": "^5.0.1", "cypress": "^12.0.2",
"sass": "1.32.12", "eslint": "^8.22.0",
"sass-loader": "^12.0.0", "eslint-plugin-cypress": "^2.12.1",
"typescript": "~4.5.5", "eslint-plugin-vue": "^9.3.0",
"vue-cli-plugin-quasar": "~5.0.2" "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"
} }
} }

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 731 B

After

Width:  |  Height:  |  Size: 731 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View file

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

View file

@ -35,44 +35,43 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineEmits, defineProps } from "vue"; import { useDialogPluginComponent } from 'quasar'
import { useDialogPluginComponent } from "quasar";
const props = defineProps({ const props = defineProps({
color: { color: {
type: String, type: String,
default: "primary", default: 'primary',
}, },
textOk: { textOk: {
type: String, type: String,
default: "ตกลง", default: 'ตกลง',
}, },
title: { title: {
type: String, type: String,
default: "หัวข้อ?", default: 'หัวข้อ?',
}, },
message: { message: {
type: String, type: String,
default: "ข้อความ", default: 'ข้อความ',
}, },
icon: { icon: {
type: String, type: String,
default: "question_mark", default: 'question_mark',
}, },
onlycancel: { onlycancel: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}); })
defineEmits([ defineEmits([
// REQUIRED; need to specify some events that your // REQUIRED; need to specify some events that your
// component will emit through useDialogPluginComponent() // component will emit through useDialogPluginComponent()
...useDialogPluginComponent.emits, ...useDialogPluginComponent.emits,
]); ])
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent(); useDialogPluginComponent()
// dialogRef - Vue ref to be applied to QDialog // dialogRef - Vue ref to be applied to QDialog
// onDialogHide - Function to be used as handler for @hide on QDialog // onDialogHide - Function to be used as handler for @hide on QDialog
// onDialogOK - Function to call to settle dialog with "ok" outcome // onDialogOK - Function to call to settle dialog with "ok" outcome
@ -84,7 +83,7 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
function onOKClick() { function onOKClick() {
// on OK, it is REQUIRED to // on OK, it is REQUIRED to
// call onDialogOK (with optional payload) // call onDialogOK (with optional payload)
onDialogOK(); onDialogOK()
// or with payload: onDialogOK({ ... }) // or with payload: onDialogOK({ ... })
// ...and it will also hide the dialog automatically // ...and it will also hide the dialog automatically
} }

View file

@ -1,225 +1,225 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, onMounted, defineProps } from "vue"; import { ref, watch, onMounted } from 'vue'
import { useQuasar } from "quasar"; import { useQuasar } from 'quasar'
import moment, { Moment } from "moment"; import moment from 'moment'
// importStores // importStores
import { useCounterMixin } from "@/stores/mixin"; import { useCounterMixin } from '@/stores/mixin'
// importType // importType
import type { FormRef } from "@/interface/index/Main"; import type { FormRef } from '@/interface/index/Main'
const $q = useQuasar(); const $q = useQuasar()
const mixin = useCounterMixin(); const mixin = useCounterMixin()
const { date2Thai, covertDateObject, dialogConfirm } = mixin; const { date2Thai, covertDateObject, dialogConfirm } = mixin
const props = defineProps({ const props = defineProps({
dataById: { dataById: {
type: Object, type: Object,
default: null, default: null,
}, },
closePopup: { closePopup: {
type: Function, type: Function,
required: true, required: true,
}, },
}); })
const dataById = ref<any>([]); const dataById = ref<any>([])
const date = ref<Date | null>(null); const date = ref<Date | null>(null)
const checkboxIn = ref<boolean>(false); const checkboxIn = ref<boolean>(false)
const checkboxOut = ref<boolean>(false); const checkboxOut = ref<boolean>(false)
const reason = ref<string>(""); const reason = ref<string>('')
const statusAction = ref<boolean>(false); const statusAction = ref<boolean>(false)
onMounted(() => { onMounted(() => {
updateClock(); updateClock()
dataById.value = props.dataById; dataById.value = props.dataById
if (dataById.value == null) { if (dataById.value == null) {
statusAction.value = true; statusAction.value = true
} }
}); })
const dateNow = ref<Date>(new Date()); const dateNow = ref<Date>(new Date())
const timeNoew = ref<string>(""); const timeNoew = ref<string>('')
function updateClock() { function updateClock() {
const date = Date.now(); const date = Date.now()
let hh = moment(date).format("HH"); const hh = moment(date).format('HH')
let mm = moment(date).format("mm"); const mm = moment(date).format('mm')
timeNoew.value = `${hh}:${mm} น.`; timeNoew.value = `${hh}:${mm} น.`
} }
const dateRef = ref<object | null>(null); const dateRef = ref<object | null>(null)
const reasonRef = ref<object | null>(null); const reasonRef = ref<object | null>(null)
const objectRef: FormRef = { const objectRef: FormRef = {
date: dateRef, date: dateRef,
reason: reasonRef, reason: reasonRef,
}; }
const checkstatusBox = ref<boolean>(false); const checkstatusBox = ref<boolean>(false)
function onCkickSave() { function onCkickSave() {
const hasError = []; const hasError = []
for (const key in objectRef) { for (const key in objectRef) {
if (Object.prototype.hasOwnProperty.call(objectRef, key)) { if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
const property = objectRef[key]; const property = objectRef[key]
if (property.value && typeof property.value.validate === "function") { if (property.value && typeof property.value.validate === 'function') {
const isValid = property.value.validate(); const isValid = property.value.validate()
hasError.push(isValid); hasError.push(isValid)
} }
} }
} }
if (checkboxIn.value === false && checkboxOut.value === false) { if (checkboxIn.value === false && checkboxOut.value === false) {
checkstatusBox.value = true; checkstatusBox.value = true
} }
if ( if (
hasError.every( hasError.every(
(result) => result === true && checkstatusBox.value === false (result) => result === true && checkstatusBox.value === false
) )
) { ) {
dialogConfirm($q, async () => { dialogConfirm($q, async () => {
console.log("save"); console.log('save')
props.closePopup(); props.closePopup()
}); })
} else { } else {
console.log("ไม่ผ่าน "); console.log('ไม่ผ่าน ')
} }
} }
watch( watch(
[() => checkboxIn.value, () => checkboxOut.value], [() => checkboxIn.value, () => checkboxOut.value],
([newCheckboxIn, newCheckboxOut]) => { ([newCheckboxIn, newCheckboxOut]) => {
if (checkstatusBox.value) { if (checkstatusBox.value) {
if (newCheckboxIn || newCheckboxOut) { if (newCheckboxIn || newCheckboxOut) {
checkstatusBox.value = false; checkstatusBox.value = false
} }
} }
} }
); )
</script> </script>
<template> <template>
<q-card-section class="col q-pt-none"> <q-card-section class="col q-pt-none">
<div class="row q-pa-sm q-col-q-gutter-sm"> <div class="row q-pa-sm q-col-q-gutter-sm">
<q-card <q-card
flat flat
bordered bordered
:class="$q.screen.gt.xs ? 'col-12 bg-grey-1' : 'col-12 '" :class="$q.screen.gt.xs ? 'col-12 bg-grey-1' : 'col-12 '"
> >
<q-card-section class="bg-primary text-white q-pa-sm"> <q-card-section class="bg-primary text-white q-pa-sm">
<div class="text-center text-bold">เวลาปจจ</div> <div class="text-center text-bold">เวลาปจจ</div>
</q-card-section> </q-card-section>
<!-- <div class="q-pa-sm text-primary">เวลาปจจ</div> --> <!-- <div class="q-pa-sm text-primary">เวลาปจจ</div> -->
<q-card-section class="text-center q-pa-sm"> <q-card-section class="text-center q-pa-sm">
<div class="row q-gutter-md"> <div class="row q-gutter-md">
<div class="col">{{ date2Thai(dateNow) }}</div> <div class="col">{{ date2Thai(dateNow) }}</div>
<div class="col">{{ timeNoew }}</div> <div class="col">{{ timeNoew }}</div>
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
<q-card flat bordered class="col-12 q-mt-sm" v-if="statusAction"> <q-card flat bordered class="col-12 q-mt-sm" v-if="statusAction">
<div class="q-pa-sm"> <div class="q-pa-sm">
<VueDatePicker <VueDatePicker
v-model="date" v-model="date"
:locale="'th'" :locale="'th'"
autoApply autoApply
:enableTimePicker="false" :enableTimePicker="false"
week-start="0" week-start="0"
outlined outlined
> >
<template #year="{ year }"> <template #year="{ year }">
{{ year + 543 }} {{ year + 543 }}
</template> </template>
<template #year-overlay-value="{ value }"> <template #year-overlay-value="{ value }">
{{ parseInt(value + 543) }} {{ parseInt(value + 543) }}
</template> </template>
<template #trigger> <template #trigger>
<q-input <q-input
ref="dateRef" ref="dateRef"
outlined outlined
dense dense
:model-value="date !== null ? date2Thai(date) : null" :model-value="date !== null ? date2Thai(date) : null"
:label="`${'กรอกวันที่'}`" :label="`${'กรอกวันที่'}`"
:rules="[(val) => !!val || 'กรุณาเลือกวันที่']" :rules="[(val) => !!val || 'กรุณาเลือกวันที่']"
lazy-rules lazy-rules
hide-bottom-space hide-bottom-space
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon <q-icon
name="event" name="event"
class="cursor-pointer" class="cursor-pointer"
style="color: var(--q-primary)" style="color: var(--q-primary)"
> >
</q-icon> </q-icon>
</template> </template>
</q-input> </template </q-input> </template
></VueDatePicker> ></VueDatePicker>
</div> </div>
</q-card> </q-card>
<q-card flat bordered class="q-pa-sm col-12 bg-grey-1 q-mt-sm" v-else> <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="row q-gutter-md text-grey-5">
<div class="col-1"> <div class="col-1">
<q-icon color="grey-5" name="calendar_today" /> <q-icon color="grey-5" name="calendar_today" />
</div> </div>
<div class="col">{{ covertDateObject(dataById.date) }}</div> <div class="col">{{ covertDateObject(dataById.date) }}</div>
</div> </div>
</q-card> </q-card>
<q-card <q-card
flat flat
bordered bordered
class="q-pa-sm col-12 q-mt-sm" class="q-pa-sm col-12 q-mt-sm"
:style="{ :style="{
borderColor: checkstatusBox ? '#B22222' : '', borderColor: checkstatusBox ? '#B22222' : '',
borderWidth: checkstatusBox ? '2px' : '', borderWidth: checkstatusBox ? '2px' : '',
}" }"
> >
<div class="row q-gutter-xs"> <div class="row q-gutter-xs">
<div class="col-12"> <div class="col-12">
<q-checkbox <q-checkbox
keep-color keep-color
color="primary" color="primary"
v-model="checkboxIn" v-model="checkboxIn"
label="ขอแก้ไขเวลาเข้างาน" label="ขอแก้ไขเวลาเข้างาน"
/> />
</div> </div>
<div class="col-12"> <div class="col-12">
<q-checkbox <q-checkbox
keep-color keep-color
v-model="checkboxOut" v-model="checkboxOut"
color="primary" color="primary"
label="ขอแก้ไขเวลาออกงาน" label="ขอแก้ไขเวลาออกงาน"
/> />
</div> </div>
</div> </div>
</q-card> </q-card>
<div <div
v-if="checkstatusBox" v-if="checkstatusBox"
class="text-red-9 q-pa-sm" class="text-red-9 q-pa-sm"
style="font-size: 10px" style="font-size: 10px"
> >
กรณาเลอก กรณาเลอก
</div> </div>
<q-card flat bordered class="q-pa-sm col-12 q-mt-sm"> <q-card flat bordered class="q-pa-sm col-12 q-mt-sm">
<q-input <q-input
ref="reasonRef" ref="reasonRef"
dense dense
outlined outlined
v-model="reason" v-model="reason"
label="เหตุผล" label="เหตุผล"
type="textarea" type="textarea"
:rows="$q.screen.gt.xs ? '5' : '1'" :rows="$q.screen.gt.xs ? '5' : '1'"
label-color="grey-5" label-color="grey-5"
:rules="[(val) => !!val || 'กรุณากรอกเหตุผล']" :rules="[(val) => !!val || 'กรุณากรอกเหตุผล']"
lazy-rules lazy-rules
hide-bottom-space hide-bottom-space
class="custom-aqua-border" class="custom-aqua-border"
/> />
</q-card> </q-card>
</div> </div>
</q-card-section> </q-card-section>
<q-separator /> <q-separator />
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn dense color="secondary" label="บันทึก" @click="onCkickSave" /> <q-btn dense color="secondary" label="บันทึก" @click="onCkickSave" />
</q-card-actions> </q-card-actions>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -1,35 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from "vue"; const props = defineProps({
const props = defineProps({ title: {
title: { type: String,
type: String, default: '',
default: "", },
}, clickClose: {
clickClose: { type: Function,
type: Function, default: null,
default: null, },
}, })
});
function clickClosePopup() {
function clickClosePopup() { props.clickClose()
props.clickClose(); }
} </script>
</script> <template>
<template> <q-toolbar>
<q-toolbar> <q-toolbar-title class="text-subtitle2 text-bold">{{
<q-toolbar-title class="text-subtitle2 text-bold">{{ props.title
props.title }}</q-toolbar-title>
}}</q-toolbar-title> <q-btn
<q-btn icon="close"
icon="close" unelevated
unelevated round
round dense
dense @click="clickClosePopup"
@click="clickClosePopup" style="color: #ff8080; background-color: #ffdede"
style="color: #ff8080; background-color: #ffdede" />
/> </q-toolbar>
</q-toolbar> <q-separator />
<q-separator /> </template>
</template>
<style scoped></style>
<style scoped></style>

View file

@ -1,101 +1,101 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted } from 'vue'
import { GoogleMap, Marker } from "vue3-google-map"; import { GoogleMap, Marker } from 'vue3-google-map'
declare var google: any; declare var google: any
const center = ref<any>(); const center = ref<any>()
const location = ref<string>(""); const location = ref<string>('')
const test = ref(); const test = ref()
// hook // hook
onMounted(() => { onMounted(() => {
findNearestPlace(); findNearestPlace()
}); })
// //
function findNearestPlace() { function findNearestPlace() {
if (navigator.geolocation) { if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(position) => { (position) => {
const userLocation = { const userLocation = {
lat: position.coords.latitude, lat: position.coords.latitude,
lng: position.coords.longitude, lng: position.coords.longitude,
}; }
// Location API // Location API
findNearestPlaceFromAPI(userLocation); findNearestPlaceFromAPI(userLocation)
center.value = userLocation; center.value = userLocation
console.log(center.value); console.log(center.value)
}, },
(error) => { (error) => {
console.error(error); console.error(error)
console.log("erroe"); console.log('erroe')
} }
); )
} else { } else {
console.error("เบราว์เซอร์ไม่รองรับการระบุตำแหน่ง"); console.error('เบราว์เซอร์ไม่รองรับการระบุตำแหน่ง')
} }
} }
function findNearestPlaceFromAPI(userLocation: any) { function findNearestPlaceFromAPI(userLocation: any) {
const placesService = new google.maps.places.PlacesService( const placesService = new google.maps.places.PlacesService(
document.createElement("div") document.createElement('div')
); )
const request = { const request = {
location: userLocation, location: userLocation,
radius: 1000, // radius: 1000, //
types: ["point_of_interest"], // types: ['point_of_interest'], //
}; }
placesService.nearbySearch(request, (results: any, status: any) => { placesService.nearbySearch(request, (results: any, status: any) => {
if (status === google.maps.places.PlacesServiceStatus.OK) { if (status === google.maps.places.PlacesServiceStatus.OK) {
console.log("Nearby places:", results[0]); console.log('Nearby places:', results[0])
const place = results[0]; const place = results[0]
location.value = place.name; location.value = place.name
test.value = place.geometry.location; test.value = place.geometry.location
} else { } else {
console.error("Error fetching nearby places:", status); console.error('Error fetching nearby places:', status)
} }
}); })
} }
</script> </script>
<template> <template>
<q-card <q-card
bordered bordered
flat flat
class="col-12 bg-grey-2 shadow-0" class="col-12 bg-grey-2 shadow-0"
:style="$q.screen.gt.xs ? 'height: 350px;' : 'height: 360px;'" :style="$q.screen.gt.xs ? 'height: 350px;' : 'height: 360px;'"
> >
<div style="width: 100%; height: 90%"> <div style="width: 100%; height: 90%">
<GoogleMap <GoogleMap
api-key="AIzaSyBzPSF5NxUZ1G8DKBnJvJPTqCR0Ct2xf58" api-key="AIzaSyBzPSF5NxUZ1G8DKBnJvJPTqCR0Ct2xf58"
style="width: 100%; height: 100%" style="width: 100%; height: 100%"
:center="center" :center="center"
:zoom="17" :zoom="17"
> >
<Marker :options="{ position: center }" /> <Marker :options="{ position: center }" />
<Marker <Marker
:options="{ :options="{
position: test, position: test,
icon: 'https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png', icon: 'https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png',
}" }"
/> />
</GoogleMap> </GoogleMap>
</div> </div>
<div <div
:class=" :class="
$q.screen.gt.xs $q.screen.gt.xs
? 'q-pa-sm text-weight-medium text-grey-8' ? 'q-pa-sm text-weight-medium text-grey-8'
: ' text-weight-medium text-grey-8' : ' text-weight-medium text-grey-8'
" "
> >
นทใกลเคยง นทใกลเคยง
<span class="q-px-sm">:</span> <span class="q-px-sm">:</span>
{{ location }} {{ location }}
</div> </div>
</q-card> </q-card>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -1,45 +1,45 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineProps, watch } from "vue"; import { ref, watch } from 'vue'
import HeaderPopup from "@/components/HeaderPopup.vue"; import HeaderPopup from '@/components/HeaderPopup.vue'
import FormTime from "@/components/FormTime.vue"; import FormTime from '@/components/FormTime.vue'
const props = defineProps({ const props = defineProps({
modal: { modal: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
title: { title: {
type: String, type: String,
default: "", default: '',
}, },
clickClose: { clickClose: {
type: Function, type: Function,
default: null, default: null,
}, },
dataById: { dataById: {
type: Object, type: Object,
default: null, default: null,
}, },
}); })
// - // -
const data = ref<any>(); const data = ref<any>()
function clickClosePopup() { function clickClosePopup() {
props.clickClose(); props.clickClose()
} }
watch(props, () => { watch(props, () => {
if (props.modal === true) { if (props.modal === true) {
data.value = props.dataById; data.value = props.dataById
} }
}); })
</script> </script>
<template> <template>
<q-dialog v-model="props.modal" full-height> <q-dialog v-model="props.modal" full-height>
<q-card class="column full-height" style="width: 300px"> <q-card class="column full-height" style="width: 300px">
<HeaderPopup :title="props.title" :clickClose="clickClosePopup" /> <HeaderPopup :title="props.title" :clickClose="clickClosePopup" />
<FormTime :dataById="data" :closePopup="clickClosePopup" /> <FormTime :dataById="data" :closePopup="clickClosePopup" />
</q-card> </q-card>
</q-dialog> </q-dialog>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -1,311 +1,311 @@
<script setup lang="ts"> <script setup lang="ts">
import type { QTableProps } from "quasar"; import type { QTableProps } from 'quasar'
import { ref } from "vue"; import { ref } from 'vue'
// importStores // importStores
import { useChekIn } from "@/stores/chekin"; import { useChekIn } from '@/stores/chekin'
// importComponent // importComponent
import Popup from "@/components/PopUp.vue"; import Popup from '@/components/PopUp.vue'
const stores = useChekIn(); const stores = useChekIn()
const columns = ref<QTableProps["columns"]>([ const columns = ref<QTableProps['columns']>([
{ {
name: "date", name: 'date',
align: "left", align: 'left',
label: "วัน/เดือน/ปี", label: 'วัน/เดือน/ปี',
sortable: true, sortable: true,
field: "date", field: 'date',
headerStyle: "font-size: 14px", headerStyle: 'font-size: 14px',
style: "font-size: 14px; width:15%;", style: 'font-size: 14px; width:15%;',
}, },
{ {
name: "in", name: 'in',
align: "left", align: 'left',
label: "เวลาเข้างาน", label: 'เวลาเข้างาน',
sortable: true, sortable: true,
field: "in", field: 'in',
headerStyle: "font-size: 14px", headerStyle: 'font-size: 14px',
style: "font-size: 14px; width:15%;", style: 'font-size: 14px; width:15%;',
}, },
{ {
name: "out", name: 'out',
align: "left", align: 'left',
label: "เวลาออกงาน", label: 'เวลาออกงาน',
sortable: true, sortable: true,
field: "out", field: 'out',
headerStyle: "font-size: 14px", headerStyle: 'font-size: 14px',
style: "font-size: 14px; width:15%;", style: 'font-size: 14px; width:15%;',
}, },
{ {
name: "Morningstatus", name: 'Morningstatus',
align: "left", align: 'left',
label: "สถานะช่วงเช้า", label: 'สถานะช่วงเช้า',
sortable: true, sortable: true,
field: "Morningstatus", field: 'Morningstatus',
headerStyle: "font-size: 14px", headerStyle: 'font-size: 14px',
style: "font-size: 14px; width:15%;", style: 'font-size: 14px; width:15%;',
}, },
{ {
name: "AfternoonStatus", name: 'AfternoonStatus',
align: "left", align: 'left',
label: "สถานะช่วงบ่าย", label: 'สถานะช่วงบ่าย',
sortable: true, sortable: true,
field: "AfternoonStatus", field: 'AfternoonStatus',
headerStyle: "font-size: 14px", headerStyle: 'font-size: 14px',
style: "font-size: 14px; width:15%;", style: 'font-size: 14px; width:15%;',
}, },
// { // {
// name: "loIn", // name: "loIn",
// align: "left", // align: "left",
// label: "", // label: "",
// sortable: true, // sortable: true,
// field: "loIn", // field: "loIn",
// headerStyle: "font-size: 14px", // headerStyle: "font-size: 14px",
// style: "font-size: 14px", // style: "font-size: 14px",
// }, // },
// { // {
// name: "loOut", // name: "loOut",
// align: "left", // align: "left",
// label: "", // label: "",
// sortable: true, // sortable: true,
// field: "loOut", // field: "loOut",
// headerStyle: "font-size: 14px", // headerStyle: "font-size: 14px",
// style: "font-size: 14px", // style: "font-size: 14px",
// }, // },
// { // {
// name: "status", // name: "status",
// align: "left", // align: "left",
// label: "", // label: "",
// sortable: true, // sortable: true,
// field: "status", // field: "status",
// headerStyle: "font-size: 14px", // headerStyle: "font-size: 14px",
// style: "font-size: 14px; width:10%;", // style: "font-size: 14px; width:10%;",
// }, // },
]); ])
// popup // popup
const modalPopup = ref<boolean>(false); const modalPopup = ref<boolean>(false)
const titlePopup = ref<string>(""); const titlePopup = ref<string>('')
const dataRow = ref<any>(); const dataRow = ref<any>()
function openPopup(data: any) { function openPopup(data: any) {
const title = "แก้ไขลงเวลา"; const title = 'แก้ไขลงเวลา'
modalPopup.value = true; modalPopup.value = true
titlePopup.value = title; titlePopup.value = title
dataRow.value = data; dataRow.value = data
} }
function closePopup() { function closePopup() {
modalPopup.value = false; modalPopup.value = false
} }
function classStatus(status: string) { function classStatus(status: string) {
switch (status) { switch (status) {
case "ขาดราชการ": case 'ขาดราชการ':
return "text-red"; return 'text-red'
case "ปกติ": case 'ปกติ':
return "text-blue"; return 'text-blue'
case "สาย": case 'สาย':
return "text-yellow-8"; return 'text-yellow-8'
default: default:
break; break
} }
} }
const pagination = ref({ const pagination = ref({
page: 1, page: 1,
rowsPerPage: 10, rowsPerPage: 10,
}); })
</script> </script>
<template> <template>
<q-table <q-table
flat flat
bordered bordered
class="custom-table2" class="custom-table2"
:columns="columns" :columns="columns"
:rows="stores.rows" :rows="stores.rows"
virtual-scroll virtual-scroll
:virtual-scroll-sticky-size-start="48" :virtual-scroll-sticky-size-start="48"
:style="$q.screen.gt.xs ? 'max-height: 64vh' : ''" :style="$q.screen.gt.xs ? 'max-height: 64vh' : ''"
:grid="$q.screen.gt.xs ? false : true" :grid="$q.screen.gt.xs ? false : true"
v-model:pagination="pagination" v-model:pagination="pagination"
> >
<template v-slot:header="props"> <template v-slot:header="props">
<q-tr :props="props"> <q-tr :props="props">
<q-th v-for="col in props.cols" :key="col.name" :props="props"> <q-th v-for="col in props.cols" :key="col.name" :props="props">
<span class="text-weight-medium">{{ col.label }}</span> <span class="text-weight-medium">{{ col.label }}</span>
</q-th> </q-th>
<q-th auto-width> </q-th> <q-th auto-width> </q-th>
</q-tr> </q-tr>
</template> </template>
<template v-slot:body="props"> <template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer"> <q-tr :props="props" class="cursor-pointer">
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
<div <div
v-if="col.name === 'Morningstatus'" v-if="col.name === 'Morningstatus'"
:class="classStatus(props.row.Morningstatus)" :class="classStatus(props.row.Morningstatus)"
> >
{{ props.row.Morningstatus }} {{ props.row.Morningstatus }}
</div> </div>
<div <div
v-else-if="col.name === 'AfternoonStatus'" v-else-if="col.name === 'AfternoonStatus'"
:class="classStatus(props.row.AfternoonStatus)" :class="classStatus(props.row.AfternoonStatus)"
> >
{{ props.row.AfternoonStatus }} {{ props.row.AfternoonStatus }}
</div> </div>
<div v-else> <div v-else>
{{ col.value }} {{ col.value }}
</div> </div>
</q-td> </q-td>
<q-td> <q-td>
<q-btn <q-btn
v-if="props.row.statusEdit === 'edit'" v-if="props.row.statusEdit === 'edit'"
outline outline
icon="edit" icon="edit"
size="12px" size="12px"
label="ขอแก้ไข" label="ขอแก้ไข"
color="cyan-6" color="cyan-6"
@click="openPopup(props.row)" @click="openPopup(props.row)"
/> />
<q-chip <q-chip
v-else v-else
:color="`${stores.classColorStatus(props.row.statusEdit)}-1`" :color="`${stores.classColorStatus(props.row.statusEdit)}-1`"
:text-color="`${stores.classColorStatus(props.row.statusEdit)}-7`" :text-color="`${stores.classColorStatus(props.row.statusEdit)}-7`"
>{{ props.row.statusEditName }}</q-chip >{{ props.row.statusEditName }}</q-chip
> >
</q-td> </q-td>
</q-tr> </q-tr>
</template> </template>
<template #item="props"> <template #item="props">
<div <div
class="q-pa-xs col-xs-12 col-sm-6 col-md-4 col-lg-3 grid-style-transition" 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-card bordered flat class="q-py-sm shadow-0">
<q-list dense> <q-list dense>
<q-item v-for="col in props.cols" :key="col.name" :props="props"> <q-item v-for="col in props.cols" :key="col.name" :props="props">
<q-item-section> <q-item-section>
<q-item-label caption>{{ col.label }}</q-item-label> <q-item-label caption>{{ col.label }}</q-item-label>
</q-item-section> </q-item-section>
<q-item-section side> <q-item-section side>
<q-item-label> <q-item-label>
<span <span
v-if="col.name == 'Morningstatus'" v-if="col.name == 'Morningstatus'"
:class="classStatus(props.row.Morningstatus)" :class="classStatus(props.row.Morningstatus)"
>{{ col.value }}</span >{{ col.value }}</span
> >
<span <span
v-else-if="col.name == 'AfternoonStatus'" v-else-if="col.name == 'AfternoonStatus'"
:class="classStatus(props.row.AfternoonStatus)" :class="classStatus(props.row.AfternoonStatus)"
>{{ col.value }}</span >{{ col.value }}</span
> >
<span v-else class="text-black">{{ col.value }} </span> <span v-else class="text-black">{{ col.value }} </span>
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
<q-separator /> <q-separator />
</q-item> </q-item>
</q-list> </q-list>
<q-card-section> <q-card-section>
<div class="row"> <div class="row">
<div class="col" v-if="props.row.statusEdit === 'edit'"> <div class="col" v-if="props.row.statusEdit === 'edit'">
<q-btn <q-btn
class="full-width text-cen" class="full-width text-cen"
outline outline
rounded rounded
icon="edit" icon="edit"
label="ขอแก้ไข" label="ขอแก้ไข"
color="info" color="info"
size="sm" size="sm"
@click="openPopup(props.row)" @click="openPopup(props.row)"
/> />
</div> </div>
<div class="col" v-else> <div class="col" v-else>
<q-chip <q-chip
:color="`${stores.classColorStatus(props.row.statusEdit)}-1`" :color="`${stores.classColorStatus(props.row.statusEdit)}-1`"
:text-color="`${stores.classColorStatus( :text-color="`${stores.classColorStatus(
props.row.statusEdit props.row.statusEdit
)}-7`" )}-7`"
> >
{{ props.row.statusEditName }} {{ props.row.statusEditName }}
</q-chip> </q-chip>
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>
</template> </template>
<template v-slot:pagination="scope"> <template v-slot:pagination="scope">
<q-pagination <q-pagination
v-model="pagination.page" v-model="pagination.page"
active-color="primary" active-color="primary"
color="dark" color="dark"
:max="scope.pagesNumber" :max="scope.pagesNumber"
:max-pages="5" :max-pages="5"
size="sm" size="sm"
boundary-links boundary-links
direction-links direction-links
></q-pagination> ></q-pagination>
</template> </template>
</q-table> </q-table>
<Popup <Popup
:modal="modalPopup" :modal="modalPopup"
:clickClose="closePopup" :clickClose="closePopup"
:title="titlePopup" :title="titlePopup"
:dataById="dataRow" :dataById="dataRow"
/> />
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.icon-color { .icon-color {
color: #4154b3; color: #4154b3;
} }
.items-center { .items-center {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.custom-table2 { .custom-table2 {
.q-table tr:nth-child(odd) td { .q-table tr:nth-child(odd) td {
background: white; background: white;
} }
.q-table tr:nth-child(even) td { .q-table tr:nth-child(even) td {
background: #f8f8f8; background: #f8f8f8;
} }
.q-table thead tr { .q-table thead tr {
background: #ecebeb; background: #ecebeb;
} }
.q-table thead tr th { .q-table thead tr th {
position: sticky; position: sticky;
} }
.q-table td:nth-of-type(2) { .q-table td:nth-of-type(2) {
z-index: 3 !important; z-index: 3 !important;
} }
.q-table th:nth-of-type(2), .q-table th:nth-of-type(2),
.q-table td:nth-of-type(2) { .q-table td:nth-of-type(2) {
position: sticky; position: sticky;
left: 0; left: 0;
z-index: 1; z-index: 1;
} }
/* this will be the loading indicator */ /* this will be the loading indicator */
.q-table thead tr:last-child th { .q-table thead tr:last-child th {
/* height of all previous header rows */ /* height of all previous header rows */
top: 48px; top: 48px;
} }
.q-table thead tr:first-child th { .q-table thead tr:first-child th {
top: 0; top: 0;
} }
.text-caption { .text-caption {
text-align: left; text-align: left;
} }
} }
.q-card-section-last { .q-card-section-last {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
</style> </style>

View file

@ -1,49 +1,49 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from 'vue'
import type { DataOption } from "@/interface/index/Main"; import type { DataOption } from '@/interface/index/Main'
import Popup from "@/components/PopUp.vue"; import Popup from '@/components/PopUp.vue'
// import HeaderPopup from "@/components/HeaderPopup.vue"; // import HeaderPopup from "@/components/HeaderPopup.vue";
// import FormTime from "@/components/FormTime.vue"; // import FormTime from "@/components/FormTime.vue";
const filterYear = ref<string>(""); const filterYear = ref<string>('')
const yearOption = ref<DataOption[]>([{ id: "2566", name: "2566" }]); const yearOption = ref<DataOption[]>([{ id: '2566', name: '2566' }])
const titleName = ref<string>("เพิ่มรายการลงเวลากรณีพิเศษ"); const titleName = ref<string>('เพิ่มรายการลงเวลากรณีพิเศษ')
const modalPopup = ref<boolean>(false); const modalPopup = ref<boolean>(false)
function onClickopen() { function onClickopen() {
modalPopup.value = true; modalPopup.value = true
} }
function onClickClose() { function onClickClose() {
modalPopup.value = false; modalPopup.value = false
} }
</script> </script>
<template> <template>
<div class="q-pb-sm row"> <div class="q-pb-sm row">
<div class="items-center col-12 row q-gutter-sm"> <div class="items-center col-12 row q-gutter-sm">
<q-select <q-select
dense dense
outlined outlined
style="width: 180px" style="width: 180px"
label="ปีประวัติลงเวลา" label="ปีประวัติลงเวลา"
v-model="filterYear" v-model="filterYear"
:options="yearOption" :options="yearOption"
option-label="name" option-label="name"
option-value="id" option-value="id"
/> />
<q-space /> <q-space />
<q-btn <q-btn
unelevated unelevated
outline outline
icon="add" icon="add"
color="light-blue" color="light-blue"
:class="$q.screen.gt.xs ? 'q-px-sm bg-blue-1' : ''" :class="$q.screen.gt.xs ? 'q-px-sm bg-blue-1' : ''"
:label="$q.screen.gt.xs ? 'เพิ่มรายการลงเวลากรณีพิเศษ' : ''" :label="$q.screen.gt.xs ? 'เพิ่มรายการลงเวลากรณีพิเศษ' : ''"
@click="onClickopen" @click="onClickopen"
/> />
</div> </div>
</div> </div>
<Popup :modal="modalPopup" :title="titleName" :clickClose="onClickClose" /> <Popup :modal="modalPopup" :title="titleName" :clickClose="onClickClose" />
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -1,10 +1,10 @@
interface DataOption { interface DataOption {
id: string; id: string
name: string; name: string
} }
interface FormRef { interface FormRef {
date: object | null; date: object | null
reason: object | null; reason: object | null
[key: string]: any; [key: string]: any
} }
export type { DataOption, FormRef }; export type { DataOption, FormRef }

View file

@ -1,7 +1,7 @@
interface FormRef { interface FormRef {
model: object | null; model: object | null
useLocation: object | null; useLocation: object | null
[key: string]: any; [key: string]: any
} }
export type { FormRef }; export type { FormRef }

View file

@ -1,29 +1,36 @@
import { createApp } from "vue"; import { createApp, defineAsyncComponent } from 'vue'
import App from "./App.vue"; import App from './App.vue'
import "./registerServiceWorker"; import './registerServiceWorker'
import router from "./router"; import router from './router'
import { createPinia } from "pinia"; import { createPinia } from 'pinia'
import { Quasar, Dialog, Notify, Loading } from "quasar"; import { Quasar, Dialog, Notify, Loading } from 'quasar'
import '@quasar/extras/material-icons/material-icons.css'; import '@vuepic/vue-datepicker/dist/main.css'
import './styles/quasar.sass'; import quasarUserOptions from './quasar-user-options'
import quasarUserOptions from "./quasar-user-options";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
const app = createApp(App); import 'quasar/src/css/index.sass'
const pinia = createPinia(); 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, { app.use(Quasar, {
...quasarUserOptions,
plugins: { plugins: {
Notify, Notify,
Dialog, Dialog,
Loading, Loading,
}, },
}); lang: th,
app.use(pinia); })
app.component("VueDatePicker", VueDatePicker);
app.mount("#app"); app.component(
'datepicker',
defineAsyncComponent(() => import('@vuepic/vue-datepicker'))
)
app.mount('#app')

View file

@ -1,10 +1,11 @@
import "./styles/quasar.sass"; // import "./styles/quasar.scss"
// import "quasar/dist/quasar.css"; import '@quasar/extras/material-icons/material-icons.css'
import "@quasar/extras/material-icons/material-icons.css"; import '@quasar/extras/material-icons-outlined/material-icons-outlined.css'
import { Dialog, Notify } from "quasar"; import '@quasar/extras/fontawesome-v5/fontawesome-v5.css'
import '@quasar/extras/mdi-v4/mdi-v4.css'
// To be used on app.use(Quasar, { ... }) // To be used on app.use(Quasar, { ... })
export default { export default {
config: {}, config: {},
plugins: { Dialog, Notify }, plugins: {},
}; }

View file

@ -3,30 +3,32 @@
import { register } from 'register-service-worker' import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, { register('registerSW.js', {
ready () { ready() {
console.log( console.log(
'App is being served from cache by a service worker.\n' + '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.') console.log('Service worker has been registered.')
}, },
cached () { cached() {
console.log('Content has been cached for offline use.') console.log('Content has been cached for offline use.')
}, },
updatefound () { updatefound() {
console.log('New content is downloading.') console.log('New content is downloading.')
}, },
updated () { updated() {
console.log('New content is available; please refresh.') console.log('New content is available; please refresh.')
}, },
offline () { offline() {
console.log('No internet connection found. App is running in offline mode.') console.log(
'No internet connection found. App is running in offline mode.'
)
}, },
error (error) { error(error) {
console.error('Error during service worker registration:', error) console.error('Error during service worker registration:', error)
} },
}) })
} }

View file

@ -1,32 +1,42 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { createRouter, createWebHistory } from 'vue-router'
import HomeView from "../views/HomeView.vue"; 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,
},
];
const router = createRouter({ const router = createRouter({
history: createWebHistory(process.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes, 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
View file

@ -1 +1 @@
declare module '*.vue'; declare module '*.vue'

View file

@ -1,61 +1,61 @@
import { defineStore } from "pinia"; import { defineStore } from 'pinia'
import { ref } from "vue"; import { ref } from 'vue'
export const useChekIn = defineStore("checkin", () => { export const useChekIn = defineStore('checkin', () => {
const rows = ref<any>(); const rows = ref<any>()
function fetchHistoryList(data: any) { function fetchHistoryList(data: any) {
console.log(data); console.log(data)
const datalist = data.map((e: any) => ({ const datalist = data.map((e: any) => ({
no: e.no, no: e.no,
date: e.date, date: e.date,
in: e.in, in: e.in,
loIn: e.loIn, loIn: e.loIn,
out: e.out, out: e.out,
loOut: e.loOut, loOut: e.loOut,
status: e.status, status: e.status,
Morningstatus: convertStatus(e.Morningstatus), Morningstatus: convertStatus(e.Morningstatus),
AfternoonStatus: convertStatus(e.AfternoonStatus), AfternoonStatus: convertStatus(e.AfternoonStatus),
statusEdit: e.statusEdit, statusEdit: e.statusEdit,
statusEditName: convertStatusEdit(e.statusEdit), statusEditName: convertStatusEdit(e.statusEdit),
})); }))
rows.value = datalist; rows.value = datalist
} }
function convertStatus(status: string) { function convertStatus(status: string) {
switch (status) { switch (status) {
case "1": case '1':
return "ขาดราชการ"; return 'ขาดราชการ'
case "2": case '2':
return "ปกติ"; return 'ปกติ'
case "3": case '3':
return "สาย"; return 'สาย'
default: default:
return ""; return ''
} }
} }
function convertStatusEdit(val: string) { function convertStatusEdit(val: string) {
switch (val) { switch (val) {
case "edit": case 'edit':
return "ขอแก้ไข"; return 'ขอแก้ไข'
case "wait": case 'wait':
return "รออนุมัติ"; return 'รออนุมัติ'
case "approve": case 'approve':
return "อนุมัติ"; return 'อนุมัติ'
} }
} }
function classColorStatus(val: string) { function classColorStatus(val: string) {
switch (val) { switch (val) {
case "wait": case 'wait':
return "orange"; return 'orange'
case "approve": case 'approve':
return "green"; return 'green'
} }
} }
return { return {
rows, rows,
fetchHistoryList, fetchHistoryList,
classColorStatus, classColorStatus,
}; }
}); })

View file

@ -1,122 +1,122 @@
import { defineStore } from "pinia"; import { defineStore } from 'pinia'
import CustomComponent from "@/components/CustomDialog.vue"; import CustomComponent from '@/components/CustomDialog.vue'
import { useQuasar } from "quasar";
const $q = useQuasar();
export const useCounterMixin = defineStore("mixin", () => { export const useCounterMixin = defineStore('mixin', () => {
function date2Thai(srcDate: Date, isFullMonth = false, isTime = false) { function date2Thai(srcDate: Date, isFullMonth = false, isTime = false) {
if (srcDate == null) { if (srcDate == null) {
return null; return null
` ;`
`; `
} }
const date = new Date(srcDate); const date = new Date(srcDate)
const isValidDate = Boolean(+date); const isValidDate = Boolean(+date)
if (!isValidDate) return srcDate.toString(); if (!isValidDate) return srcDate.toString()
if (isValidDate && date.getFullYear() < 1000) return srcDate.toString(); if (isValidDate && date.getFullYear() < 1000) return srcDate.toString()
const fullMonthThai = [ const fullMonthThai = [
"มกราคม", 'มกราคม',
"กุมภาพันธ์", 'กุมภาพันธ์',
"มีนาคม", 'มีนาคม',
"เมษายน", 'เมษายน',
"พฤษภาคม", 'พฤษภาคม',
"มิถุนายน", 'มิถุนายน',
"กรกฎาคม", 'กรกฎาคม',
"สิงหาคม", 'สิงหาคม',
"กันยายน", 'กันยายน',
"ตุลาคม", 'ตุลาคม',
"พฤศจิกายน", 'พฤศจิกายน',
"ธันวาคม", 'ธันวาคม',
]; ]
const abbrMonthThai = [ const abbrMonthThai = [
"ม.ค.", 'ม.ค.',
"ก.พ.", 'ก.พ.',
"มี.ค.", 'มี.ค.',
"เม.ย.", 'เม.ย.',
"พ.ค.", 'พ.ค.',
"มิ.ย.", 'มิ.ย.',
"ก.ค.", 'ก.ค.',
"ส.ค.", 'ส.ค.',
"ก.ย.", 'ก.ย.',
"ต.ค.", 'ต.ค.',
"พ.ย.", 'พ.ย.',
"ธ.ค.", 'ธ.ค.',
]; ]
let dstYear = 0; let dstYear = 0
if (date.getFullYear() > 2500) { if (date.getFullYear() > 2500) {
dstYear = date.getFullYear(); dstYear = date.getFullYear()
} else { } else {
dstYear = date.getFullYear() + 543; dstYear = date.getFullYear() + 543
} }
let dstMonth = ""; let dstMonth = ''
if (isFullMonth) { if (isFullMonth) {
dstMonth = fullMonthThai[date.getMonth()]; dstMonth = fullMonthThai[date.getMonth()]
} else { } else {
dstMonth = abbrMonthThai[date.getMonth()]; dstMonth = abbrMonthThai[date.getMonth()]
} }
let dstTime = ""; let dstTime = ''
if (isTime) { if (isTime) {
const H = date.getHours().toString().padStart(2, "0"); const H = date.getHours().toString().padStart(2, '0')
const M = date.getMinutes().toString().padStart(2, "0"); const M = date.getMinutes().toString().padStart(2, '0')
// const S = date.getSeconds().toString().padStart(2, "0") // const S = date.getSeconds().toString().padStart(2, "0")
// dstTime = " " + H + ":" + M + ":" + S + " น." // dstTime = " " + H + ":" + M + ":" + S + " น."
dstTime = " " + H + ":" + M + " น."; dstTime = ' ' + H + ':' + M + ' น.'
} }
return ( return (
date.getDate().toString().padStart(2, "0") + date.getDate().toString().padStart(2, '0') +
" " + ' ' +
dstMonth + dstMonth +
" " + ' ' +
dstYear + dstYear +
dstTime dstTime
); )
} }
function covertDateObject(date: string) { function covertDateObject(date: string) {
if (date) { if (date) {
const dateParts = date.split("/"); const dateParts = date.split('/')
// ประกาศตัวแปรเพื่อเก็บค่าวันที่, เดือน, และ ปี // ประกาศตัวแปรเพื่อเก็บค่าวันที่, เดือน, และ ปี
const day = parseInt(dateParts[0], 10); const day = parseInt(dateParts[0], 10)
const month = parseInt(dateParts[1], 10) - 1; const month = parseInt(dateParts[1], 10) - 1
const year = parseInt(dateParts[2], 10) + 2500; const year = parseInt(dateParts[2], 10) + 2500
// สร้างอ็อบเจ็กต์ Date ด้วยค่าที่ได้ // สร้างอ็อบเจ็กต์ Date ด้วยค่าที่ได้
const dateObject = new Date(year, month, day); const dateObject = new Date(year, month, day)
return date2Thai(dateObject); return date2Thai(dateObject)
} }
} }
type OkCallback = () => void;
type CancelCallback = () => void; type OkCallback = () => void
const dialogConfirm = ( type CancelCallback = () => void
function dialogConfirm(
q: any, q: any,
ok?: OkCallback, ok?: OkCallback,
title?: string, // ถ้ามี cancel action ใส่เป็น null title?: string, // ถ้ามี cancel action ใส่เป็น null
desc?: string, // ถ้ามี cancel action ใส่เป็น null desc?: string, // ถ้ามี cancel action ใส่เป็น null
cancel?: CancelCallback cancel?: CancelCallback
) => { ) {
q.dialog({ q.dialog({
component: CustomComponent, component: CustomComponent,
componentProps: { componentProps: {
title: title && title != null ? title : "ยืนยันการบันทึก", title: title && title != null ? title : 'ยืนยันการบันทึก',
message: message:
desc && desc != null desc && desc != null
? desc ? desc
: "ต้องการยืนยันการบันทึกข้อมูลนี้ใช่หรือไม่?", : 'ต้องการยืนยันการบันทึกข้อมูลนี้ใช่หรือไม่?',
icon: "info", icon: 'info',
color: "public", color: 'public',
textOk: "ตกลง", textOk: 'ตกลง',
onlycancel: false, onlycancel: false,
}, },
}) })
.onOk(() => { .onOk(() => {
if (ok) ok(); if (ok) ok()
}) })
.onCancel(() => { .onCancel(() => {
if (cancel) cancel(); if (cancel) cancel()
}); })
}; }
return { return {
date2Thai, date2Thai,
covertDateObject, covertDateObject,
dialogConfirm, dialogConfirm,
}; }
}); })

View file

@ -1,3 +0,0 @@
@import './quasar.variables.sass'
@import '~quasar-styl'
// @import '~quasar-addon-styl'

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Error404NotFound",
});
</script>
<template>
<div
class="fullscreen bg-secondary text-white text-center q-pa-md flex flex-center"
>
<div>
<div class="text-h1">ไมพบหนาทองการ</div>
<div class="text-h2">(404 Page Not Found)</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="secondary"
unelevated
to="/"
label="กลับไปหน้าหลัก"
no-caps
/>
</div>
</div>
</template>

View file

@ -1,103 +1,103 @@
<script setup lang="ts"> <script setup lang="ts">
import type { QTableProps } from "quasar"; import type { QTableProps } from 'quasar'
import { ref, onMounted } from "vue"; import { ref, onMounted } from 'vue'
import { useRouter } from "vue-router"; import { useRouter } from 'vue-router'
import Table from "@/components/TableHistory.vue"; import Table from '@/components/TableHistory.vue'
import ToolBar from "@/components/ToolBar.vue"; import ToolBar from '@/components/ToolBar.vue'
// importStores // importStores
import { useChekIn } from "@/stores/chekin"; import { useChekIn } from '@/stores/chekin'
const router = useRouter(); const router = useRouter()
const stores = useChekIn(); const stores = useChekIn()
onMounted(() => { onMounted(() => {
fetchlist(); fetchlist()
}); })
function fetchlist() { function fetchlist() {
const listData = [ const listData = [
{ {
no: "1", no: '1',
date: "13/08/66", date: '13/08/66',
in: "11:20", in: '11:20',
loIn: "สำนักงาน ก.ก ", loIn: 'สำนักงาน ก.ก ',
out: "", out: '',
loOut: "", loOut: '',
status: "", status: '',
Morningstatus: "1", Morningstatus: '1',
AfternoonStatus: "1", AfternoonStatus: '1',
statusEdit: "wait", statusEdit: 'wait',
}, },
{ {
no: "2", no: '2',
date: "12/08/66", date: '12/08/66',
in: "08:04", in: '08:04',
loIn: "สำนักงาน ก.ก ", loIn: 'สำนักงาน ก.ก ',
out: "17:01", out: '17:01',
loOut: "สำนักงาน ก.ก ", loOut: 'สำนักงาน ก.ก ',
status: "ลงเวลาเรียบร้อย", status: 'ลงเวลาเรียบร้อย',
Morningstatus: "2", Morningstatus: '2',
AfternoonStatus: "2", AfternoonStatus: '2',
statusEdit: "edit", statusEdit: 'edit',
}, },
{ {
no: "3", no: '3',
date: "11/08/66", date: '11/08/66',
in: "08:34", in: '08:34',
loIn: "สำนักงาน ก.ก ", loIn: 'สำนักงาน ก.ก ',
out: "17:36", out: '17:36',
loOut: "สำนักงาน ก.ก ", loOut: 'สำนักงาน ก.ก ',
status: "สาย ทำงานครบ", status: 'สาย ทำงานครบ',
Morningstatus: "3", Morningstatus: '3',
AfternoonStatus: "2", AfternoonStatus: '2',
statusEdit: "edit", statusEdit: 'edit',
}, },
{ {
no: "4", no: '4',
date: "10/08/66", date: '10/08/66',
in: "08:48", in: '08:48',
loIn: "สำนักงาน ก.ก ", loIn: 'สำนักงาน ก.ก ',
out: "17:00", out: '17:00',
loOut: "สำนักงาน ก.ก ", loOut: 'สำนักงาน ก.ก ',
status: "สาย ทำงานไม่ครบ", status: 'สาย ทำงานไม่ครบ',
Morningstatus: "3", Morningstatus: '3',
AfternoonStatus: "3", AfternoonStatus: '3',
statusEdit: "approve", statusEdit: 'approve',
}, },
]; ]
stores.fetchHistoryList(listData); stores.fetchHistoryList(listData)
} }
</script> </script>
<template> <template>
<div class="col-12 row justify-center"> <div class="col-12 row justify-center">
<div class="col-xs-12 col-sm-12 col-md-12"> <div class="col-xs-12 col-sm-12 col-md-12">
<q-card flat class="row col-12 cardNone"> <q-card flat class="row col-12 cardNone">
<div <div
class="bg-secondary text-white col-12 row items-center q-px-md q-py-sm" class="bg-secondary text-white col-12 row items-center q-px-md q-py-sm"
> >
<div class="col-2"> <div class="col-2">
<q-btn <q-btn
icon="arrow_backt" icon="arrow_backt"
unelevated unelevated
round round
dense dense
flat flat
color="white" color="white"
@click="router.go(-1)" @click="router.go(-1)"
/> />
</div> </div>
<q-space /> <q-space />
<span class="text-body1 text-weight-bold col-8 text-center" <span class="text-body1 text-weight-bold col-8 text-center"
>ประวการลงเวลา</span >ประวการลงเวลา</span
> >
<div class="col-2"></div> <div class="col-2"></div>
</div> </div>
<div class="col-12 q-pa-md text-grey-9"> <div class="col-12 q-pa-md text-grey-9">
<ToolBar /> <ToolBar />
<Table /> <Table />
</div> </div>
</q-card> </q-card>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,202 +1,134 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted } from 'vue'
import { useRouter } from "vue-router"; import { useRouter } from 'vue-router'
import { useQuasar } from "quasar"; import { useQuasar } from 'quasar'
import moment, { Moment } from "moment"; import moment from 'moment'
import Camera from 'simple-vue-camera'
// import Type // import Type
import type { FormRef } from "@/interface/response/checkin"; import type { FormRef } from '@/interface/response/checkin'
// import components // import components
import MapCheck from "@/components/MapCheckin.vue"; import MapCheck from '@/components/MapCheckin.vue'
// import Stores // import Stores
import { useCounterMixin } from "@/stores/mixin"; import { useCounterMixin } from '@/stores/mixin'
const mixin = useCounterMixin(); const mixin = useCounterMixin()
const { date2Thai, dialogConfirm } = mixin; const { date2Thai, dialogConfirm } = mixin
const router = useRouter(); const router = useRouter()
const $q = useQuasar(); const $q = useQuasar()
const stetusCheckin = ref(true); const stetusCheckin = ref(true)
onMounted(() => { onMounted(() => {
updateClock(); updateClock()
}); })
//time //time
const dateNow = ref<Date>(new Date()); const dateNow = ref<Date>(new Date())
const Thai = ref<Date>(dateNow.value); const Thai = ref<Date>(dateNow.value)
const formattedS = ref(); const formattedS = ref()
const formattedM = ref(); const formattedM = ref()
const formattedH = ref(); const formattedH = ref()
function updateClock() { function updateClock() {
const date = Date.now(); const date = Date.now()
let hh = moment(date).format("HH"); const hh = moment(date).format('HH')
let mm = moment(date).format("mm"); const mm = moment(date).format('mm')
let ss = moment(date).format("ss"); const ss = moment(date).format('ss')
formattedS.value = ss; formattedS.value = ss
formattedM.value = mm; formattedM.value = mm
formattedH.value = hh; formattedH.value = hh
} }
setInterval(updateClock, 1000); setInterval(updateClock, 1000)
//location //location
const location = ref<string>(""); const location = ref<string>('')
const coordinates = ref<string>("13° 43 45” N 100° 31 26” E"); const coordinates = ref<string>('13° 43 45” N 100° 31 26” E')
const workplace = ref<string>("in-place"); const workplace = ref<string>('in-place')
const useLocation = ref<string | null>(""); const useLocation = ref<string | null>('')
const model = ref<string | null>(""); const model = ref<string | null>('')
const options = ref<string[]>([ const options = ref<string[]>([
"ปฏิบัติงานที่บ้าน", 'ปฏิบัติงานที่บ้าน',
"ลืมลงเวลาปฏิบัติงาน", 'ลืมลงเวลาปฏิบัติงาน',
"ไปประชุม/อบรม/สัมมนา/ปฏิบัติงานที่บ้านนอกสถานที่", 'ไปประชุม/อบรม/สัมมนา/ปฏิบัติงานที่บ้านนอกสถานที่',
"ขออนุญาตออกนอกสถานที่", 'ขออนุญาตออกนอกสถานที่',
"อื่นๆ", 'อื่นๆ',
]); ])
function selectLocation() { function selectLocation() {
if (model.value === "อื่นๆ") { if (model.value === 'อื่นๆ') {
useLocation.value = ""; useLocation.value = ''
} else { } else {
useLocation.value = model.value; useLocation.value = model.value
} }
} }
//camera //camera
const camera = ref(false); const camera = ref<InstanceType<typeof Camera>>()
const hasPhoto = ref<boolean>(true); const cameraIsOn = ref<boolean>(false)
const mediaStream = ref<MediaStream | null>(null); const img = ref<any>(undefined)
const video = ref<HTMLVideoElement | null>(null); const photoWidth = ref<number>(350)
const canvas = ref<HTMLCanvasElement | null>(null); const photoHeight = ref<number>(350)
const img = ref<any>(null);
const openCamera = () => { const openCamera = () => {
camera.value = true; cameraIsOn.value ? camera.value?.stop() : camera.value?.start()
camera.value && setupCamera(); cameraIsOn.value = !cameraIsOn.value
}; }
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;
const videoAspectRatio = videoElement.videoWidth / videoElement.videoHeight; async function capturePhoto() {
const canvasAspectRatio = desiredWidth / desiredHeight; const imageBlob: any = await camera.value?.snapshot(
{ width: photoWidth.value, height: photoHeight.value },
let drawWidth, drawHeight; 'image/png',
0.5
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
);
// //
const dataURL = canvasElement.toDataURL(".image/.png"); camera.value?.stop()
img.value = dataURL; const url = URL.createObjectURL(imageBlob)
console.log(img.value); img.value = url
}
if (mediaStream.value) {
mediaStream.value.getTracks().forEach((track) => track.stop());
videoElement.srcObject = null;
hasPhoto.value = false;
}
}
function refreshPhoto() { function refreshPhoto() {
hasPhoto.value = true; img.value = undefined
img.value = ""; camera.value?.start()
} }
// validate // validate
const useLocationRef = ref<object | null>(null); const useLocationRef = ref<object | null>(null)
const modelRef = ref<object | null>(null); const modelRef = ref<object | null>(null)
const objectRef: FormRef = { const objectRef: FormRef = {
model: modelRef, model: modelRef,
useLocation: useLocationRef, useLocation: useLocationRef,
}; }
function validateForm() { function validateForm() {
const hasError = []; const hasError = []
for (const key in objectRef) { for (const key in objectRef) {
if (Object.prototype.hasOwnProperty.call(objectRef, key)) { if (Object.prototype.hasOwnProperty.call(objectRef, key)) {
const property = objectRef[key]; const property = objectRef[key]
if (property.value && typeof property.value.validate === "function") { if (property.value && typeof property.value.validate === 'function') {
const isValid = property.value.validate(); const isValid = property.value.validate()
hasError.push(isValid); hasError.push(isValid)
} }
} }
} }
if (hasError.every((result) => result === true)) { if (hasError.every((result) => result === true)) {
confirm(); confirm()
} else { } else {
console.log("ไม่ผ่าน "); console.log('ไม่ผ่าน ')
} }
} }
// //
const dialogTime = ref<boolean>(false); const dialogTime = ref<boolean>(false)
const confirm = () => { const confirm = () => {
dialogConfirm( // api popup
$q, dialogTime.value = true
async () => { }
dialogTime.value = true;
},
"ยืนยันการบันทึกเวลา ?",
"ต้องการยืนยันการบันทึกการลงเวลานี้ใช่หรือไม่"
);
};
// class // class
const getClass = (val: boolean) => { const getClass = (val: boolean) => {
return { return {
"bg-primary 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, 'bg-red-9 text-white col-12 row items-center q-px-md q-py-sm': !val,
}; }
}; }
</script> </script>
<template> <template>
@ -263,13 +195,12 @@ const getClass = (val: boolean) => {
<MapCheck /> <MapCheck />
</div> </div>
<div class="col-12 col-sm-4"> <div class="col-12 col-sm-4">
<q-card <q-card flat bordered class="card-container">
flat <div
bordered v-if="!cameraIsOn && img == null"
class="card-container" class="preview-placeholder"
@click="openCamera()" @click="openCamera()"
> >
<div v-if="!camera" class="preview-placeholder">
<div class="text-center"> <div class="text-center">
<q-icon <q-icon
name="photo_camera" name="photo_camera"
@ -279,18 +210,26 @@ const getClass = (val: boolean) => {
/> />
</div> </div>
</div> </div>
<div v-else> <div>
<div v-if="hasPhoto" class="video-container"> <!-- แสดงกลองตอนกดถายภาพ -->
<video ref="video" autoplay class="video-element"></video> <Camera
<canvas ref="canvas" class="canvas-element"></canvas> :resolution="{ width: photoWidth, height: photoHeight }"
</div> ref="camera"
<div v-else class="image-container"> :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> <q-img :src="img" class="image-element"></q-img>
<canvas ref="canvas" class="canvas-element"></canvas>
</div> </div>
<div class="absolute-bottom-right q-ma-md">
<div
v-if="cameraIsOn"
class="absolute-bottom-right q-ma-md"
>
<q-btn <q-btn
v-if="hasPhoto" v-if="img == null"
round round
push push
icon="photo_camera" icon="photo_camera"
@ -311,6 +250,7 @@ const getClass = (val: boolean) => {
</div> </div>
</q-card> </q-card>
</div> </div>
<div class="col-12 q-mb-md"> <div class="col-12 q-mb-md">
<q-card <q-card
bordered bordered
@ -377,6 +317,7 @@ const getClass = (val: boolean) => {
/> />
</div> </div>
</q-card> </q-card>
<div class="col-12 text-right"> <div class="col-12 text-right">
<q-separator /> <q-separator />
<div class="col-12 q-pa-md"> <div class="col-12 q-pa-md">
@ -471,8 +412,6 @@ const getClass = (val: boolean) => {
height: 350px; /* Adjust as needed */ height: 350px; /* Adjust as needed */
background: #f6f5f5; background: #f6f5f5;
} }
.video-container,
.image-container { .image-container {
position: absolute; position: absolute;
top: 0; top: 0;
@ -481,7 +420,6 @@ const getClass = (val: boolean) => {
height: 100%; height: 100%;
} }
.video-element,
.image-element { .image-element {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -489,7 +427,8 @@ const getClass = (val: boolean) => {
border-radius: 5px; /* Adjust as needed */ border-radius: 5px; /* Adjust as needed */
} }
.canvas-element { .preview-placeholder {
display: none; /* Adjust as needed */ width: 100%;
height: 100%;
} }
</style> </style>

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

View file

@ -1,29 +1,14 @@
{ {
"compilerOptions": { "files": [],
"target": "es5", "references": [
"module": "esnext", {
"strict": true, "path": "./tsconfig.config.json"
"jsx": "preserve",
"moduleResolution": "node",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": ["webpack-env"],
"paths": {
"@/*": ["src/*"]
}, },
"lib": ["esnext", "dom", "dom.iterable", "scripthost"] {
}, "path": "./tsconfig.app.json"
"include": [ },
"src/**/*.ts", {
"src/**/*.tsx", "path": "./tsconfig.vitest.json"
"src/**/*.vue", }
"tests/**/*.ts", ]
"tests/**/*.tsx"
],
"exclude": ["node_modules"]
} }

9
tsconfig.vitest.json Normal file
View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.app.json",
"exclude": [],
"compilerOptions": {
"composite": true,
"lib": [],
"types": ["node", "jsdom"]
}
}

60
vite.config.js Normal file
View 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,
},
})

View file

@ -1,13 +0,0 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: [
'quasar'
],
pluginOptions: {
quasar: {
importStrategy: 'kebab',
rtlSupport: false
}
}
})