- 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,16 +1,16 @@
<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: {
@ -21,53 +21,53 @@ const props = defineProps({
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(
@ -75,11 +75,11 @@ function onCkickSave() {
) )
) { ) {
dialogConfirm($q, async () => { dialogConfirm($q, async () => {
console.log("save"); console.log('save')
props.closePopup(); props.closePopup()
}); })
} else { } else {
console.log("ไม่ผ่าน "); console.log('ไม่ผ่าน ')
} }
} }
@ -88,11 +88,11 @@ watch(
([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">

View file

@ -1,18 +1,17 @@
<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>

View file

@ -1,16 +1,16 @@
<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() {
@ -20,43 +20,43 @@ function findNearestPlace() {
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>

View file

@ -1,7 +1,7 @@
<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: {
@ -10,7 +10,7 @@ const props = defineProps({
}, },
title: { title: {
type: String, type: String,
default: "", default: '',
}, },
clickClose: { clickClose: {
type: Function, type: Function,
@ -20,18 +20,18 @@ const props = defineProps({
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>

View file

@ -1,60 +1,60 @@
<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",
@ -84,36 +84,36 @@ const columns = ref<QTableProps["columns"]>([
// 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

View file

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

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,7 +3,7 @@
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' +
@ -23,10 +23,12 @@ if (process.env.NODE_ENV === 'production') {
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> = [ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ {
path: "/", path: '/',
name: "home", name: 'home',
component: HomeView, component: HomeView,
}, },
{ {
path: "/about", path: '/about',
name: "about", name: 'about',
// route level code-splitting // route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route // this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
component: () => component: () => import('@/views/AboutView.vue'),
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
}, },
{ {
path: "/history", path: '/history',
name: "history", name: 'history',
component: HistoryView, 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'),
},
],
})
const router = createRouter({ export default router
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;

2
src/shims-vue.d.ts vendored
View file

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

View file

@ -1,11 +1,11 @@
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,
@ -18,38 +18,38 @@ export const useChekIn = defineStore("checkin", () => {
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'
} }
} }
@ -57,5 +57,5 @@ export const useChekIn = defineStore("checkin", () => {
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,72 +1,72 @@
<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>

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;
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; async function capturePhoto() {
canvasElement.height = drawHeight; const imageBlob: any = await camera.value?.snapshot(
if (context) { { width: photoWidth.value, height: photoHeight.value },
context.imageSmoothingEnabled = true; 'image/png',
context.imageSmoothingQuality = "low"; 0.5
} )
// 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", "path": "./tsconfig.vitest.json"
"src/**/*.tsx", }
"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
}
}
})