backup
This commit is contained in:
parent
22592affd1
commit
1cc52cc025
|
@ -12,7 +12,7 @@
|
|||
<div v-if="authSectionIsLoading"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 z-30 flex backdrop-brightness-75">
|
||||
<div class="flex flex-col w-1/2 m-auto items-center justify-center">
|
||||
<LoaderPadlock class="scale-50" />
|
||||
<MyLoaderPadlock class="scale-50" />
|
||||
<p>Please Wait...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,21 @@
|
|||
|
||||
<NuxtUiForm @submit="execute" :schema="productSchema" :state="formState">
|
||||
<NuxtUiFormGroup label="Product Code" name="product_code" required>
|
||||
<NuxtUiInput v-model="formState.product_code" placeholder="Enter product code" />
|
||||
<div v-if="scanMode || !formState.product_code" class="mb-2">
|
||||
<MyBarcodeScanner @scanned="e => {
|
||||
formState.product_code = e
|
||||
scanMode = false
|
||||
}" />
|
||||
</div>
|
||||
<NuxtUiInput v-model="formState.product_code" placeholder="Enter product code"
|
||||
:ui="{ icon: { trailing: { pointer: '', padding: { md: 'px-0.5' } } } }" size="md">
|
||||
<template #trailing>
|
||||
<NuxtUiButton icon="i-heroicons-qr-code-20-solid" @click="() => {
|
||||
scanMode = true
|
||||
console.log('clicked')
|
||||
}" label="QR" />
|
||||
</template>
|
||||
</NuxtUiInput>
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Product Name" name="product_name" required>
|
||||
|
@ -49,6 +63,7 @@
|
|||
</template>
|
||||
<script lang="ts" setup>
|
||||
const modalShown = ref<boolean>(false)
|
||||
const scanMode = ref(false)
|
||||
const emit = defineEmits(['created'])
|
||||
const {
|
||||
data, error, execute, formState, status, productSchema
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<NuxtUiModal v-model="modalShown" :prevent-close="true">
|
||||
<NuxtUiCard>
|
||||
<template #header>
|
||||
<div class="text-xl font-semibold">
|
||||
Form Add Product
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<NuxtUiForm @submit="execute" :schema="productSchema" :state="formState">
|
||||
<div v-if="scanMode || !formState.product_code">
|
||||
<MyBarcodeScanner @scanned="e => {
|
||||
formState.product_code = e
|
||||
scanMode = false
|
||||
}" />
|
||||
</div>
|
||||
<NuxtUiFormGroup label="Product Code" name="product_code" required v-else>
|
||||
<NuxtUiInput v-model="formState.product_code" placeholder="Enter product code">
|
||||
<template #trailing>
|
||||
<NuxtUiButton icon="i-heroicons-qr-code-20-solid" @click="scanMode = true" />
|
||||
</template>
|
||||
</NuxtUiInput>
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Product Name" name="product_name" required>
|
||||
<NuxtUiInput v-model="formState.product_name" placeholder="Enter product name" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Product Category" name="product_category_id">
|
||||
<DashboardDatasetProductCategoryInput v-model="formState.product_category_id" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Stock" name="stock">
|
||||
<NuxtUiInput v-model="formState.stock" type="number" placeholder="Enter stock amount" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Buying Price" name="buying_price">
|
||||
<NuxtUiInput v-model="formState.buying_price" type="number" placeholder="Enter buying price" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<NuxtUiFormGroup label="Selling Price" name="selling_price">
|
||||
<NuxtUiInput v-model="formState.selling_price" type="number" placeholder="Enter selling price" />
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<div class="flex justify-end mt-4 space-x-2">
|
||||
<NuxtUiButton type="button" color="red" :loading="status === 'pending'"
|
||||
@click="() => modalShown = false">Cancel
|
||||
</NuxtUiButton>
|
||||
<NuxtUiButton type="submit" color="primary" :loading="status === 'pending'">Save</NuxtUiButton>
|
||||
</div>
|
||||
</NuxtUiForm>
|
||||
</NuxtUiCard>
|
||||
</NuxtUiModal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const modalProductId = defineModel<string>('id')
|
||||
const modalShown = ref<boolean>(false)
|
||||
const scanMode = ref(false)
|
||||
const emit = defineEmits(['created'])
|
||||
const {
|
||||
data, error, execute, formState, status, productSchema
|
||||
} = useAddProduct()
|
||||
watch(status, newVal => {
|
||||
if (newVal === 'success') {
|
||||
emit('created', {
|
||||
id: data!.value!.data!.productId,
|
||||
...formState
|
||||
})
|
||||
modalShown.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div v-if="permissionStatus === 'allowed'">
|
||||
<div class="relative">
|
||||
<div class="aspect-w-16 aspect-h-9">
|
||||
<video ref="video" autoplay playsinline class="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0 me-2 mb-2">
|
||||
<NuxtUiButton @click="switchCamera" icon="i-heroicons-arrow-path-20-solid" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <p class="mt-2">Barcode: {{ barcode }}</p> -->
|
||||
</div>
|
||||
<div v-else-if="permissionStatus === 'denied'">
|
||||
You already denied camera permission.
|
||||
</div>
|
||||
<div v-else class="flex">
|
||||
<div class="m-auto max-w-[300px] w-full px-3">
|
||||
<p class="text-end">Loading...</p>
|
||||
<NuxtUiProgress animation="carousel" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['scanned'])
|
||||
const video = ref<HTMLVideoElement | null>(null)
|
||||
const { barcode, error, switchCamera, status, permissionStatus, devices } = useBarcodeScanner(video)
|
||||
watch(status, newVal => {
|
||||
if (newVal === 'scanned') {
|
||||
emit('scanned', barcode.value)
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<div v-if="inputFrom === 'camera'">
|
||||
<MyBarcodeScanner @scanned="(e) => {
|
||||
console.log(e)
|
||||
model = e
|
||||
inputFrom = 'manual'
|
||||
}" />
|
||||
<NuxtUiButton icon="i-heroicons-pencil-solid" @click="inputFrom = 'manual'" label="Manual" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<NuxtUiInput v-model="model" />
|
||||
<NuxtUiButton icon="i-heroicons-qr-code-20-solid" @click="inputFrom = 'camera'" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
const model = defineModel<string>()
|
||||
const inputFrom = ref<'camera' | 'manual'>('camera')
|
||||
</script>
|
|
@ -0,0 +1,149 @@
|
|||
import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue'
|
||||
import { BrowserMultiFormatReader, type IScannerControls } from '@zxing/browser'
|
||||
|
||||
export function useBarcodeScanner(videoElement: Ref<HTMLVideoElement | null>) {
|
||||
const barcode = ref<string | null>(null)
|
||||
const isSupported = ref<boolean>(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const status = ref<'idle' | 'changing' | 'error' | 'scanning' | 'scanned'>('idle')
|
||||
const permissionStatus = ref<'loading' | 'allowed' | 'denied'>('loading')
|
||||
|
||||
let codeReader: BrowserMultiFormatReader
|
||||
let controls: IScannerControls
|
||||
let devices: MediaDeviceInfo[] = []
|
||||
const currentDeviceIndex = ref(0)
|
||||
|
||||
const checkCameraSupport = (): boolean => {
|
||||
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
|
||||
}
|
||||
|
||||
const requestCameraPermission = async (): Promise<boolean> => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
|
||||
stream.getTracks().forEach(track => track.stop())
|
||||
permissionStatus.value = 'allowed'
|
||||
return true
|
||||
} catch (err) {
|
||||
error.value = 'Izin kamera ditolak atau gagal.'
|
||||
permissionStatus.value = 'denied'
|
||||
console.error('[BarcodeScanner] Permission error:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const startScanner = async (deviceId?: string) => {
|
||||
if (!checkCameraSupport()) {
|
||||
error.value = 'Perangkat tidak mendukung akses kamera.'
|
||||
status.value = 'error'
|
||||
console.warn('[BarcodeScanner] getUserMedia tidak didukung.')
|
||||
return
|
||||
}
|
||||
|
||||
if (permissionStatus.value !== 'allowed') {
|
||||
const granted = await requestCameraPermission()
|
||||
if (!granted) return
|
||||
}
|
||||
|
||||
codeReader = new BrowserMultiFormatReader()
|
||||
|
||||
try {
|
||||
devices = await BrowserMultiFormatReader.listVideoInputDevices()
|
||||
if (!devices.length) throw new Error('Kamera tidak ditemukan')
|
||||
|
||||
// Otomatis pakai kamera belakang kalau ada
|
||||
if (!deviceId) {
|
||||
const backCam = devices.find(device =>
|
||||
/back|rear/i.test(device.label)
|
||||
)
|
||||
if (backCam) {
|
||||
currentDeviceIndex.value = devices.findIndex(d => d.deviceId === backCam.deviceId)
|
||||
deviceId = backCam.deviceId
|
||||
} else {
|
||||
// Fallback ke kamera pertama
|
||||
currentDeviceIndex.value = 0
|
||||
deviceId = devices[0].deviceId
|
||||
}
|
||||
}
|
||||
|
||||
controls = await codeReader.decodeFromVideoDevice(
|
||||
deviceId,
|
||||
videoElement.value!,
|
||||
(result, err) => {
|
||||
status.value = 'scanning'
|
||||
if (result) {
|
||||
barcode.value = result.getText()
|
||||
status.value = 'scanned'
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (err) {
|
||||
console.error('[BarcodeScanner] Gagal inisialisasi:', err)
|
||||
error.value = (err as Error).message
|
||||
status.value = 'error'
|
||||
throw new Error('[BarcodeScanner] Gagal inisialisasi')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const stopScanner = () => {
|
||||
if (codeReader && controls) {
|
||||
controls.stop()
|
||||
}
|
||||
}
|
||||
|
||||
const switchCamera = async () => {
|
||||
try {
|
||||
status.value = 'changing'
|
||||
if (devices.length <= 1) {
|
||||
console.warn('[BarcodeScanner] Hanya ada satu kamera, tidak bisa switch.')
|
||||
status.value = 'idle'
|
||||
return
|
||||
}
|
||||
|
||||
stopScanner()
|
||||
currentDeviceIndex.value = (currentDeviceIndex.value + 1) % devices.length
|
||||
await startScanner(devices[currentDeviceIndex.value].deviceId)
|
||||
|
||||
status.value = 'idle'
|
||||
} catch (error) {
|
||||
console.error('[BarcodeScanner] Gagal switch kamera:', error)
|
||||
status.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isSupported.value = checkCameraSupport()
|
||||
if (!isSupported.value) {
|
||||
console.warn('[BarcodeScanner] Kamera tidak didukung di browser ini.')
|
||||
return
|
||||
}
|
||||
|
||||
await requestCameraPermission()
|
||||
if (permissionStatus.value !== 'allowed') return
|
||||
|
||||
devices = await BrowserMultiFormatReader.listVideoInputDevices()
|
||||
if (devices.length === 0) {
|
||||
error.value = 'Tidak ada kamera yang tersedia.'
|
||||
return
|
||||
}
|
||||
|
||||
await startScanner()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopScanner()
|
||||
})
|
||||
|
||||
return {
|
||||
barcode,
|
||||
error,
|
||||
isSupported,
|
||||
startScanner,
|
||||
stopScanner,
|
||||
switchCamera,
|
||||
devices,
|
||||
status,
|
||||
permissionStatus
|
||||
}
|
||||
}
|
|
@ -13,7 +13,14 @@ export function useProductList() {
|
|||
|
||||
export function useAddProduct() {
|
||||
const toast = useToast()
|
||||
const formState = reactive({
|
||||
const formState = reactive<Partial<{
|
||||
product_code: string,
|
||||
product_name: string,
|
||||
stock: number,
|
||||
buying_price: number,
|
||||
selling_price: number,
|
||||
product_category_id: number,
|
||||
}>>({
|
||||
product_code: undefined,
|
||||
product_name: undefined,
|
||||
stock: undefined,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="w-full h-full min-h-screen">
|
||||
<div class="w-full h-full">
|
||||
<header class="fixed top-0 left-0 right-0 z-50" ref="header">
|
||||
<div
|
||||
class="m-3 md:mx-10 p-2 md:px-6 flex gap-2 shadow-md rounded-r-full rounded-l-full items-center bg-[#f9fafb]/70 text-gray-800 dark:bg-[#1f2937]/70 backdrop-blur-sm dark:text-white">
|
||||
|
@ -41,6 +41,21 @@
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<NuxtUiModal v-model="logoutConfirmation">
|
||||
<NuxtUiCard class="p-6">
|
||||
<div class="text-center space-y-2">
|
||||
<h2 class="text-lg font-semibold text-gray-800">Confirm Logout</h2>
|
||||
<p class="text-sm text-gray-600">Are you sure you want to log out of your account?</p>
|
||||
</div>
|
||||
<div class="flex justify-center gap-2 mt-10">
|
||||
<NuxtUiButton label="Cancel" variant="ghost" @click="logoutConfirmation = false"
|
||||
:disabled="logoutStatus === 'pending'" />
|
||||
<NuxtUiButton color="red" label="Log out" @click="logoutNow()"
|
||||
:loading="logoutStatus === 'pending'" />
|
||||
</div>
|
||||
</NuxtUiCard>
|
||||
</NuxtUiModal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
@ -73,6 +88,13 @@ function sidebarShownToggle() {
|
|||
}
|
||||
sidebarShown.value = !sidebarShown.value
|
||||
}
|
||||
|
||||
const logoutConfirmation = ref(false)
|
||||
|
||||
const {
|
||||
logoutNow,
|
||||
logoutStatus
|
||||
} = useAuthLogout()
|
||||
const items: DropdownItem[][] = [
|
||||
[{
|
||||
label: 'MyProfile',
|
||||
|
@ -81,7 +103,7 @@ const items: DropdownItem[][] = [
|
|||
}],
|
||||
[{
|
||||
label: 'Logout',
|
||||
click() { console.log('logout confirm') },
|
||||
click() { logoutConfirmation.value = true },
|
||||
icon: 'i-heroicons-arrow-right-on-rectangle-20-solid'
|
||||
}]
|
||||
]
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
"name": "nuxt-app",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@ngrok/ngrok": "^1.5.1",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxt/ui": "^2.21.1",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@zxing/browser": "^0.1.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^2.30.0",
|
||||
|
@ -1275,6 +1277,235 @@
|
|||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok/-/ngrok-1.5.1.tgz",
|
||||
"integrity": "sha512-sfcgdpiAJHqmuO3e6QjQGbavIrR3E72do/NAsnGhm+7SGstLj1aM3Sd8mkfTORb2Hj7ATMuoBYuED5ylKuRQCg==",
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ngrok/ngrok-android-arm64": "1.5.1",
|
||||
"@ngrok/ngrok-darwin-arm64": "1.5.1",
|
||||
"@ngrok/ngrok-darwin-universal": "1.5.1",
|
||||
"@ngrok/ngrok-darwin-x64": "1.5.1",
|
||||
"@ngrok/ngrok-freebsd-x64": "1.5.1",
|
||||
"@ngrok/ngrok-linux-arm-gnueabihf": "1.5.1",
|
||||
"@ngrok/ngrok-linux-arm64-gnu": "1.5.1",
|
||||
"@ngrok/ngrok-linux-arm64-musl": "1.5.1",
|
||||
"@ngrok/ngrok-linux-x64-gnu": "1.5.1",
|
||||
"@ngrok/ngrok-linux-x64-musl": "1.5.1",
|
||||
"@ngrok/ngrok-win32-arm64-msvc": "1.5.1",
|
||||
"@ngrok/ngrok-win32-ia32-msvc": "1.5.1",
|
||||
"@ngrok/ngrok-win32-x64-msvc": "1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-android-arm64": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-android-arm64/-/ngrok-android-arm64-1.5.1.tgz",
|
||||
"integrity": "sha512-2Tokwi5GVWNLw3JEoM0Ieb/ypALniZu6fciUTgpuByutbKxOjvahD4fYOKwW3KMdV9bCb3XGGtWJCZXfRPPq1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-darwin-arm64": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-arm64/-/ngrok-darwin-arm64-1.5.1.tgz",
|
||||
"integrity": "sha512-HNOhrPDP+nJJY7Bh45DOeh6jmcGASWINGbUuseZM0C8psQMp7crPywjRh0inkRegUrb4K8y06sfmgt2fmsF6jQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-darwin-universal": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-universal/-/ngrok-darwin-universal-1.5.1.tgz",
|
||||
"integrity": "sha512-EsMxYC/tY+ZqhjbeZtVq5MFIuD8SEPgAlHINEszsHd8ZRICc2U9Xl15CbDrew3pcfEg/ZVFrOH9CyC4aZ/V/cA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-darwin-x64": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-darwin-x64/-/ngrok-darwin-x64-1.5.1.tgz",
|
||||
"integrity": "sha512-H/x1BsYpAoTMhOtv4oYvwY6WHqbY0MsJ1XFcJQgrpAIjgmYqlwsnsUMHvEdBB/KY9kXF9DPgKUdRMfJwUIpwGA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-freebsd-x64": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-freebsd-x64/-/ngrok-freebsd-x64-1.5.1.tgz",
|
||||
"integrity": "sha512-dY2W6HUv7e2xkpdfVj7fIk+5qmvrC7kVu6PJWJ8/rshW1FrU7qMcpnU53JvoQJRZzUf5k8xMNdx30zai/8mqYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-arm-gnueabihf": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm-gnueabihf/-/ngrok-linux-arm-gnueabihf-1.5.1.tgz",
|
||||
"integrity": "sha512-JvbI/IIycw4Qq02ysyOBsSK5E0bZDgRqXSslHLTwuDAfw14lmrq2U0QkBeEOL8qwJ7wCwCH1PEOJacUyrqa9bg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-arm64-gnu": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-gnu/-/ngrok-linux-arm64-gnu-1.5.1.tgz",
|
||||
"integrity": "sha512-yLFAlqTYYvH7QRg589HJarQGw1QrKQZcHiw0gm175eCqc+jpUG/Zcf8wohCTIJVLylMIzjDzVFSUsXC7UtMJdQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-arm64-musl": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-arm64-musl/-/ngrok-linux-arm64-musl-1.5.1.tgz",
|
||||
"integrity": "sha512-momB/ZjjrxaGYOZ3YPAw1kT4DAfWT1x3dAHL0YoSVfNCpc8Fw0189ZAcxGn0hUFqkGDmSARS9o8b7hYd1b41oA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-x64-gnu": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-gnu/-/ngrok-linux-x64-gnu-1.5.1.tgz",
|
||||
"integrity": "sha512-fmMaz0b1Ry2CDLLn0mV8b9nLxqm0taQ2jYyn+C9OrazYNMT4XYYDKRQSm4UEaNoakdnoH+f2FsrWi/712GFxAQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-linux-x64-musl": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-linux-x64-musl/-/ngrok-linux-x64-musl-1.5.1.tgz",
|
||||
"integrity": "sha512-6Ajl9wpJSlvukl4WrkIw+WxVwAr7WTGnE35Voec6CERWtKMsO/F+BOSu3pfAa6iwxGK//JBpsTT1IwLLw7b2xQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-win32-arm64-msvc": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-arm64-msvc/-/ngrok-win32-arm64-msvc-1.5.1.tgz",
|
||||
"integrity": "sha512-JUH2yZxDPQGmQNT1d2KIu64u2k/R6uG1kEIXjcbsoff37v9aI6nUlzldRWB/wFSYkpZ4W/EuovM4Epar+fQOxQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-win32-ia32-msvc": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-ia32-msvc/-/ngrok-win32-ia32-msvc-1.5.1.tgz",
|
||||
"integrity": "sha512-zS1JsMTJHnY+lPJFUwKnB5fzPm4GZCKeeZLehHrXP0LpQaKN8Y/vywqDGhuC0WtymvWE88+oreMV/6hQdviLSA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngrok/ngrok-win32-x64-msvc": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ngrok/ngrok-win32-x64-msvc/-/ngrok-win32-x64-msvc-1.5.1.tgz",
|
||||
"integrity": "sha512-HegRwV9Gchh4p7K7sC6SPpWmFRwDEgwPByrb8tkuWDyP+EWNgpt3GKp8OAIK2xdWWHnN5VIwMa9u3COE/e5S8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -3397,6 +3628,41 @@
|
|||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@zxing/browser": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.5.tgz",
|
||||
"integrity": "sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"@zxing/text-encoding": "^0.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@zxing/library": "^0.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zxing/library": {
|
||||
"version": "0.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
|
||||
"integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ts-custom-error": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@zxing/text-encoding": "~0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@zxing/text-encoding": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
|
||||
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
|
||||
"license": "(Unlicense OR Apache-2.0)",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.0.tgz",
|
||||
|
@ -10308,6 +10574,16 @@
|
|||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-custom-error": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
|
||||
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-interface-checker": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||
|
|
|
@ -10,9 +10,11 @@
|
|||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ngrok/ngrok": "^1.5.1",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxt/ui": "^2.21.1",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@zxing/browser": "^0.1.5",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^2.30.0",
|
||||
|
|
|
@ -1,9 +1,140 @@
|
|||
<template>
|
||||
<NuxtLayout name="main">
|
||||
<div class="space-y-6 p-4 md:flex">
|
||||
<!-- Scanner -->
|
||||
<div class="md:w-1/2 max-w-[360px] h-full">
|
||||
<MyBarcodeScanner @scanned="(e) => handleScan(e)" class="sticky top-[84px] " />
|
||||
</div>
|
||||
|
||||
<!-- Product List -->
|
||||
<div class="space-y-4" ref="productsContainer">
|
||||
<div v-for="(item, index) in productsFormState" :key="index"
|
||||
class="p-4 rounded-lg border shadow-sm space-y-3" :class="[
|
||||
productAlreadyExist === item.product_code ?
|
||||
'bg-green-500/50 dark:bg-green-400/50 animate-pulse' : ''
|
||||
]" @mouseenter="productExistMouseEnterHandle" :id="`id-${item.product_code}`">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-semibold text-lg">{{ item.product_name || 'Unnamed Product' }}</h3>
|
||||
<NuxtUiButton color="red" size="sm" @click="deleteModalId = index">
|
||||
Delete
|
||||
</NuxtUiButton>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-center">
|
||||
<!-- Qty Input -->
|
||||
<NuxtUiFormGroup label="Quantity" class="col-span-1">
|
||||
<NuxtUiInput v-model="item.qty" type="number">
|
||||
<template #leading>
|
||||
<NuxtUiButton color="gray" variant="link" icon="i-heroicons-minus-small-20-solid"
|
||||
:padded="false" @click="item.qty = Math.max(1, item.qty - 1)" />
|
||||
</template>
|
||||
<template #trailing>
|
||||
<NuxtUiButton color="gray" variant="link" icon="i-heroicons-plus-small-20-solid"
|
||||
:padded="false" @click="item.qty += 1" />
|
||||
</template>
|
||||
</NuxtUiInput>
|
||||
</NuxtUiFormGroup>
|
||||
|
||||
<!-- Price Display -->
|
||||
<div class="col-span-1">
|
||||
<p class="text-sm text-gray-500">Price</p>
|
||||
<p class="font-medium text-gray-800">Rp{{ item.price.toLocaleString() }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Subtotal -->
|
||||
<div class="col-span-1">
|
||||
<p class="text-sm text-gray-500">Subtotal</p>
|
||||
<p class="font-medium text-gray-800">
|
||||
Rp{{ (item.price * item.qty).toLocaleString() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<NuxtUiModal v-model="deleteModalShown">
|
||||
<NuxtUiCard>
|
||||
<p>Are you sure you want to delete this product from transaction?</p>
|
||||
<template #footer>
|
||||
<NuxtUiButton label="Cancel" variant="ghost" color="gray" @click="deleteModalShown = false" />
|
||||
<NuxtUiButton label="Delete" color="red" @click="handleDelete(deleteModalId)" />
|
||||
</template>
|
||||
</NuxtUiCard>
|
||||
</NuxtUiModal>
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
middleware: 'authentication'
|
||||
})
|
||||
|
||||
const productsContainer = ref<HTMLDivElement>()
|
||||
|
||||
const productsFormState = ref<
|
||||
{
|
||||
product_code: string
|
||||
product_name: string
|
||||
price: number
|
||||
qty: number
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const newProduct = ref()
|
||||
const newProductModalShown = computed({
|
||||
get() { return !!newProduct.value },
|
||||
set(newVal) {
|
||||
if (!newVal) newProduct.value = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const handleScan = (code: string) => {
|
||||
const existing = productsContainer.value?.querySelector(`#id-${code}`)
|
||||
|
||||
if (existing) {
|
||||
productAlreadyExist.value = code
|
||||
existing.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'nearest'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { execute } = use$fetchWithAutoReNew(`/product/${code}`, {
|
||||
onResponse(ctx) {
|
||||
const data = ctx.response._data.data
|
||||
productsFormState.value.push({
|
||||
product_code: code,
|
||||
product_name: data.product_name,
|
||||
price: data.buying_price,
|
||||
qty: 1,
|
||||
})
|
||||
},
|
||||
onResponseError(ctx) {
|
||||
if (ctx.response.status === 404)
|
||||
newProduct.value = code
|
||||
}
|
||||
})
|
||||
execute()
|
||||
}
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
productsFormState.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const deleteModalId = ref()
|
||||
const deleteModalShown = computed({
|
||||
get() { return !!deleteModalId.value },
|
||||
set(newVal) {
|
||||
if (!newVal) deleteModalId.value = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const productAlreadyExist = ref<string>()
|
||||
const productExistMouseEnterHandle = () => {
|
||||
productAlreadyExist.value = undefined
|
||||
}
|
||||
</script>
|
Loading…
Reference in New Issue