From 959785d0ce01120777aaf1fba3c7a40f44bb978b Mon Sep 17 00:00:00 2001 From: fhm Date: Sat, 24 May 2025 13:50:10 +0700 Subject: [PATCH] after exam registration --- .../dataset/product-category-input.vue | 1 - .../dashboard/dataset/product/modal-new.vue | 34 +- components/landing/header.vue | 23 +- components/landing/introduction/cta-sct.vue | 10 +- components/landing/introduction/hero-sct.vue | 7 +- components/my/barcodeScanner.vue | 175 ++++++-- .../my/dashboard/sidebar/sidebar-group.vue | 10 + components/my/ui/casier/newProduct.vue | 74 ++++ components/my/ui/casier/setSellingPrice.vue | 37 ++ components/my/ui/restock/setBuyingPrice.vue | 37 ++ composables/barcodeScanner.ts | 404 ++++++++++++++---- composables/fileHandler.ts | 18 + composables/use$fetchAuto.ts | 2 +- composables/useFetchAuto.ts | 1 + composables/usePredictionTable.ts | 6 +- composables/useSpreadsheet.ts | 4 +- constants/dashboard-menu.ts | 46 +- nuxt.config.ts | 2 +- package-lock.json | 39 +- package.json | 5 +- pages/dashboard/cashier/index.vue | 364 ++++++++++++++++ pages/dashboard/dataset/products/index.vue | 1 + pages/dashboard/dataset/suppliers/index.vue | 1 - pages/dashboard/file-operation.vue | 135 ++++++ pages/dashboard/history/restock-history.vue | 49 +++ pages/dashboard/history/sales-history.vue | 50 +++ pages/dashboard/home/index.vue | 294 +++++++++++++ pages/dashboard/restock/index.vue | 403 +++++++++++++---- pages/demo.vue | 6 +- types/api-response/trx.ts | 3 + 30 files changed, 1988 insertions(+), 253 deletions(-) create mode 100644 components/my/ui/casier/newProduct.vue create mode 100644 components/my/ui/casier/setSellingPrice.vue create mode 100644 components/my/ui/restock/setBuyingPrice.vue create mode 100644 composables/fileHandler.ts create mode 100644 pages/dashboard/file-operation.vue create mode 100644 pages/dashboard/history/restock-history.vue create mode 100644 pages/dashboard/history/sales-history.vue create mode 100644 types/api-response/trx.ts diff --git a/components/dashboard/dataset/product-category-input.vue b/components/dashboard/dataset/product-category-input.vue index a0d76b6..454bd74 100644 --- a/components/dashboard/dataset/product-category-input.vue +++ b/components/dashboard/dataset/product-category-input.vue @@ -1,5 +1,4 @@ \ No newline at end of file diff --git a/components/landing/header.vue b/components/landing/header.vue index c440504..87febef 100644 --- a/components/landing/header.vue +++ b/components/landing/header.vue @@ -14,29 +14,33 @@ color="white" variant="link" /> -
+
-
+
- How It Works + How It Works? Features + + Dashboard + + @click="isOpen = false" v-if="authState !== 'logged-in'"> Demo
-
+
+ class="text-sm font-medium text-primary transition-colors hover:text-primary/80" + v-if="authState !== 'logged-in'"> Demo + + }" v-else> Log In
@@ -93,4 +99,7 @@ const isOpen = ref(false) const authModalIsOpen = ref(false) const route = useRoute(); const authSection = useState<'login' | 'register' | 'forgot-password'>('auth-section', () => 'login') +const { + authState +} = useMyAppState() \ No newline at end of file diff --git a/components/landing/introduction/cta-sct.vue b/components/landing/introduction/cta-sct.vue index cd49267..d4bf69f 100644 --- a/components/landing/introduction/cta-sct.vue +++ b/components/landing/introduction/cta-sct.vue @@ -13,10 +13,16 @@ - Try Demo + {{ authState === 'logged-in' ? 'Dashboard' : 'Try Demo' }} +
- \ No newline at end of file + + \ No newline at end of file diff --git a/components/landing/introduction/hero-sct.vue b/components/landing/introduction/hero-sct.vue index 95974a2..5ab7afb 100644 --- a/components/landing/introduction/hero-sct.vue +++ b/components/landing/introduction/hero-sct.vue @@ -13,7 +13,7 @@ - Try Demo + {{ authState === 'logged-in' ? 'Dashboard' : 'Try Demo' }} + \ No newline at end of file diff --git a/components/my/dashboard/sidebar/sidebar-group.vue b/components/my/dashboard/sidebar/sidebar-group.vue index c3960b1..8c61455 100644 --- a/components/my/dashboard/sidebar/sidebar-group.vue +++ b/components/my/dashboard/sidebar/sidebar-group.vue @@ -40,3 +40,13 @@ const toggle = () => { isOpen.value = !isOpen.value }; + + diff --git a/components/my/ui/casier/newProduct.vue b/components/my/ui/casier/newProduct.vue new file mode 100644 index 0000000..38e24ca --- /dev/null +++ b/components/my/ui/casier/newProduct.vue @@ -0,0 +1,74 @@ + + \ No newline at end of file diff --git a/components/my/ui/casier/setSellingPrice.vue b/components/my/ui/casier/setSellingPrice.vue new file mode 100644 index 0000000..62a65b8 --- /dev/null +++ b/components/my/ui/casier/setSellingPrice.vue @@ -0,0 +1,37 @@ + + \ No newline at end of file diff --git a/components/my/ui/restock/setBuyingPrice.vue b/components/my/ui/restock/setBuyingPrice.vue new file mode 100644 index 0000000..4bc9d0a --- /dev/null +++ b/components/my/ui/restock/setBuyingPrice.vue @@ -0,0 +1,37 @@ + + \ No newline at end of file diff --git a/composables/barcodeScanner.ts b/composables/barcodeScanner.ts index e9fc57a..0b5c27d 100644 --- a/composables/barcodeScanner.ts +++ b/composables/barcodeScanner.ts @@ -1,149 +1,367 @@ -import { ref, onMounted, onBeforeUnmount, type Ref } from 'vue' -import { BrowserMultiFormatReader, type IScannerControls } from '@zxing/browser' +import { ref, onMounted, onBeforeUnmount, type Ref, watch } from 'vue' +import { BrowserMultiFormatReader, type IScannerControls, BarcodeFormat } from '@zxing/browser' -export function useBarcodeScanner(videoElement: Ref) { - const barcode = ref(null) - const isSupported = ref(false) - const error = ref(null) +export interface BarcodeScannerOptions { + formats?: BarcodeFormat[]; + tryHarder?: boolean; + autostart?: boolean; + preferBackCamera?: boolean; + resetAfterScan?: boolean; + resetDelay?: number; +} - const status = ref<'idle' | 'changing' | 'error' | 'scanning' | 'scanned'>('idle') - const permissionStatus = ref<'loading' | 'allowed' | 'denied'>('loading') +export function useBarcodeScanner( + videoElement: Ref, + options: BarcodeScannerOptions = {} +) { + // Default options + const { + formats = [BarcodeFormat.QR_CODE, BarcodeFormat.EAN_13, BarcodeFormat.CODE_128], + tryHarder = true, + autostart = true, + preferBackCamera = true, + resetAfterScan = true, + resetDelay = 1500 + } = options; - let codeReader: BrowserMultiFormatReader - let controls: IScannerControls - let devices: MediaDeviceInfo[] = [] - const currentDeviceIndex = ref(0) + // Reactive state + const barcode = ref(null); + const isSupported = ref(false); + const error = ref(null); + const status = ref<'idle' | 'changing' | 'error' | 'scanning' | 'scanned'>('idle'); + const permissionStatus = ref<'loading' | 'allowed' | 'denied'>('loading'); + const currentDeviceIndex = ref(0); + const devices = ref([]); + // Internal state + let codeReader: BrowserMultiFormatReader | null = null; + let controls: IScannerControls | null = null; + let resetTimer: number | null = null; + + /** + * Check if the camera is supported by the browser + */ const checkCameraSupport = (): boolean => { - return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia) - } + return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); + }; + /** + * Request camera permission from the user + */ const requestCameraPermission = async (): Promise => { try { - const stream = await navigator.mediaDevices.getUserMedia({ video: true }) - stream.getTracks().forEach(track => track.stop()) - permissionStatus.value = 'allowed' - return true + permissionStatus.value = 'loading'; + await navigator.mediaDevices.getUserMedia({ video: true }); + 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 + error.value = 'Camera permission denied or failed.'; + permissionStatus.value = 'denied'; + console.error('[BarcodeScanner] Permission error:', err); + return false; } - } + }; - const startScanner = async (deviceId?: string) => { + /** + * Get available video devices + */ + const getVideoDevices = async (): Promise => { + try { + const availableDevices = await BrowserMultiFormatReader.listVideoInputDevices(); + devices.value = availableDevices; + return availableDevices; + } catch (err) { + console.error('[BarcodeScanner] Failed to list devices:', err); + error.value = 'Failed to get camera devices.'; + return []; + } + }; + + /** + * Find the best camera device to use based on options + */ + const findBestDevice = (deviceList: MediaDeviceInfo[]): MediaDeviceInfo | null => { + if (!deviceList.length) return null; + + // Try to find back camera if preferred + if (preferBackCamera) { + const backCamera = deviceList.find(device => + /back|rear|environment/i.test(device.label) + ); + + if (backCamera) { + currentDeviceIndex.value = deviceList.findIndex(d => d.deviceId === backCamera.deviceId); + return backCamera; + } + } + + // Fallback to first camera + currentDeviceIndex.value = 0; + return deviceList[0]; + }; + + /** + * Initialize the barcode reader with hints + */ + const initializeReader = async () => { + if (codeReader) { + // If we already have a reader, stop it first + stopScanner(); + } + + const hints = new Map(); + if (formats.length) { + hints.set(2, formats); // DecodeHintType.POSSIBLE_FORMATS = 2 + } + if (tryHarder) { + hints.set(3, true); // DecodeHintType.TRY_HARDER = 3 + } + + codeReader = new BrowserMultiFormatReader(hints); + }; + + /** + * Start the barcode scanner + */ + const startScanner = async (deviceId?: string): Promise => { if (!checkCameraSupport()) { - error.value = 'Perangkat tidak mendukung akses kamera.' - status.value = 'error' - console.warn('[BarcodeScanner] getUserMedia tidak didukung.') - return + error.value = 'Device does not support camera access.'; + status.value = 'error'; + console.warn('[BarcodeScanner] getUserMedia is not supported.'); + return false; } + // Clear any previous errors + error.value = null; + + // Check permissions if (permissionStatus.value !== 'allowed') { - const granted = await requestCameraPermission() - if (!granted) return + const granted = await requestCameraPermission(); + if (!granted) return false; } - codeReader = new BrowserMultiFormatReader() + // Initialize reader if needed + if (!codeReader) { + await initializeReader(); + } try { - devices = await BrowserMultiFormatReader.listVideoInputDevices() - if (!devices.length) throw new Error('Kamera tidak ditemukan') + status.value = 'changing'; - // 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 + // Get devices if we don't have them yet + if (!devices.value.length) { + const availableDevices = await getVideoDevices(); + if (!availableDevices.length) { + throw new Error('No cameras found'); } } - controls = await codeReader.decodeFromVideoDevice( - deviceId, - videoElement.value!, + // Find device to use + let selectedDevice: MediaDeviceInfo | null = devices.value[0]; + + if (deviceId) { + // Use specified device + selectedDevice = devices.value.find(d => d.deviceId === deviceId) || null; + if (selectedDevice) { + currentDeviceIndex.value = devices.value.findIndex(d => d.deviceId === deviceId); + } + } else { + // Find best device + selectedDevice = findBestDevice(devices.value); + } + + if (!selectedDevice) { + throw new Error('No suitable camera found'); + } + + // Ensure video element exists + if (!videoElement.value) { + throw new Error('Video element not available'); + } + + // Start decoding + controls = await codeReader!.decodeFromVideoDevice( + selectedDevice.deviceId, + videoElement.value, (result, err) => { - status.value = 'scanning' + if (err) { + // Just a failure to read - this is normal during scanning + if (!/NotFoundException/.test(err.toString())) { + console.debug('[BarcodeScanner] Decode error:', err); + } + return; + } + if (result) { - barcode.value = result.getText() - status.value = 'scanned' + barcode.value = result.getText(); + status.value = 'scanned'; + + // Reset after scan if enabled + if (resetAfterScan) { + // Clear any existing timer + if (resetTimer !== null) { + window.clearTimeout(resetTimer); + } + + resetTimer = window.setTimeout(() => { + barcode.value = null; + status.value = 'scanning'; + resetTimer = null; + }, resetDelay); + } } } - ) + ); + + status.value = 'scanning'; + return true; } catch (err) { - console.error('[BarcodeScanner] Gagal inisialisasi:', err) - error.value = (err as Error).message - status.value = 'error' - throw new Error('[BarcodeScanner] Gagal inisialisasi') + console.error('[BarcodeScanner] Initialization failed:', err); + error.value = err instanceof Error ? err.message : 'Scanner initialization failed'; + status.value = 'error'; + return false; } - } - + }; + /** + * Stop the barcode scanner + */ const stopScanner = () => { - if (codeReader && controls) { - controls.stop() + if (resetTimer !== null) { + window.clearTimeout(resetTimer); + resetTimer = null; } - } - const switchCamera = async () => { + if (codeReader && controls) { + try { + controls.stop(); + controls = null; + } catch (err) { + console.warn('[BarcodeScanner] Error stopping scanner:', err); + } + } + + status.value = 'idle'; + }; + + /** + * Switch to the next available camera + */ + const switchCamera = async (): Promise => { try { - status.value = 'changing' - if (devices.length <= 1) { - console.warn('[BarcodeScanner] Hanya ada satu kamera, tidak bisa switch.') - status.value = 'idle' - return + if (devices.value.length <= 1) { + console.warn('[BarcodeScanner] Only one camera available, cannot switch.'); + return false; } - stopScanner() - currentDeviceIndex.value = (currentDeviceIndex.value + 1) % devices.length - await startScanner(devices[currentDeviceIndex.value].deviceId) + status.value = 'changing'; + stopScanner(); - status.value = 'idle' - } catch (error) { - console.error('[BarcodeScanner] Gagal switch kamera:', error) - status.value = 'error' + // ⏳ Delay sedikit biar kamera bener-bener "released" + await new Promise((res) => setTimeout(res, 500)); + + currentDeviceIndex.value = (currentDeviceIndex.value + 1) % devices.value.length; + const nextDevice = devices.value[currentDeviceIndex.value]; + + return await startScanner(nextDevice.deviceId); + } catch (err) { + console.error('[BarcodeScanner] Failed to switch camera:', err); + status.value = 'error'; + error.value = 'Failed to switch camera'; + return false; } - } + }; + /** + * Reset the scanner after a successful scan + */ + const resetScanner = () => { + barcode.value = null; + if (status.value === 'scanned') { + status.value = 'scanning'; + } + }; + + /** + * Get the current device name + */ + const getCurrentDeviceName = (): string => { + if (devices.value.length && currentDeviceIndex.value < devices.value.length) { + return devices.value[currentDeviceIndex.value].label || `Camera ${currentDeviceIndex.value + 1}`; + } + return 'Unknown'; + }; + + // Watch for video element changes + watch(videoElement, async (newEl) => { + if (newEl && autostart) { + // Tunggu DOM benar-benar render + await nextTick(); + + // Pastikan permission sudah granted + if (permissionStatus.value !== 'allowed') { + const granted = await requestCameraPermission(); + if (!granted) return; + } + + // Ambil ulang device list dan pilih kamera terbaik + const deviceList = await getVideoDevices(); + const bestDevice = findBestDevice(deviceList); + if (bestDevice) { + await startScanner(bestDevice.deviceId); // Gunakan deviceId eksplisit + } + } + }); + + + // Setup on mount onMounted(async () => { - isSupported.value = checkCameraSupport() + isSupported.value = checkCameraSupport(); if (!isSupported.value) { - console.warn('[BarcodeScanner] Kamera tidak didukung di browser ini.') - return + console.warn('[BarcodeScanner] Camera is not supported in this browser.'); + return; } - await requestCameraPermission() - if (permissionStatus.value !== 'allowed') return + const hasPermission = await requestCameraPermission(); + if (!hasPermission) return; - devices = await BrowserMultiFormatReader.listVideoInputDevices() - if (devices.length === 0) { - error.value = 'Tidak ada kamera yang tersedia.' - return + const availableDevices = await getVideoDevices(); + if (availableDevices.length === 0) { + error.value = 'No cameras available.'; + return; } - await startScanner() - }) + // Delay start until devices are fully ready + const bestDevice = findBestDevice(availableDevices); + if (autostart && videoElement.value && bestDevice) { + await startScanner(bestDevice.deviceId); + } + }); + // Cleanup on unmount onBeforeUnmount(() => { - stopScanner() - }) + stopScanner(); + }); return { + // State barcode, error, isSupported, + status, + permissionStatus, + devices, + currentDeviceIndex, + + // Methods startScanner, stopScanner, switchCamera, - devices, - status, - permissionStatus - } -} + resetScanner, + getCurrentDeviceName, + + // Computed + currentDevice: computed(() => devices.value[currentDeviceIndex.value] || null), + hasMultipleCameras: computed(() => devices.value.length > 1) + }; +} \ No newline at end of file diff --git a/composables/fileHandler.ts b/composables/fileHandler.ts new file mode 100644 index 0000000..769ce1b --- /dev/null +++ b/composables/fileHandler.ts @@ -0,0 +1,18 @@ +export function useFileHandler() { + const file = ref() + function onDragHandler(e: DragEvent) { + e.preventDefault(); + const files = e.dataTransfer?.files; + if (files && files.length > 0) { + file.value = files[0] + } + } + function onInputHandler(e: Event) { + const target = e.target as HTMLInputElement + if (target?.files && target.files.length > 0) { + const uploaded = target.files[0]; + file.value = uploaded; + } + } + return { file, onDragHandler, onInputHandler } +} \ No newline at end of file diff --git a/composables/use$fetchAuto.ts b/composables/use$fetchAuto.ts index 1e54182..6d2ad23 100644 --- a/composables/use$fetchAuto.ts +++ b/composables/use$fetchAuto.ts @@ -41,7 +41,7 @@ export function use$fetchWithAutoReNew( ...options, headers: headers.value, baseURL: config.public.API_HOST, - // credentials: 'include', + credentials: 'include', onResponse: async (ctx) => { data.value = ctx.response._data; if (ctx.response.ok) { diff --git a/composables/useFetchAuto.ts b/composables/useFetchAuto.ts index 62df2b8..dde902f 100644 --- a/composables/useFetchAuto.ts +++ b/composables/useFetchAuto.ts @@ -31,6 +31,7 @@ export function useFetchWithAutoReNew( ...options, headers, baseURL: config.public.API_HOST, + credentials: 'include', async onResponse(ctx) { if (ctx.response.ok) { if (typeof options?.onResponse === "function") { diff --git a/composables/usePredictionTable.ts b/composables/usePredictionTable.ts index 6437310..2fa8edd 100644 --- a/composables/usePredictionTable.ts +++ b/composables/usePredictionTable.ts @@ -8,10 +8,10 @@ const requiredColumn = [ { label: 'Sold(qty)', key: 'sold(qty)', sortable: true, } ] -export function usePredictionTable() { +export function usePredictionTable(inputFile: Ref) { const { - inputFile, result, status: sheetReaderStatus - } = useSpreadSheet() + result, status: sheetReaderStatus + } = useSpreadSheet(inputFile) const status = ref<'idle' | 'loading' | 'loaded'>('idle') const loadingDetail = ref(); const columns = ref(requiredColumn) diff --git a/composables/useSpreadsheet.ts b/composables/useSpreadsheet.ts index 2004553..3cb07a9 100644 --- a/composables/useSpreadsheet.ts +++ b/composables/useSpreadsheet.ts @@ -1,9 +1,9 @@ import { headerNRow2Sheet, sheet2CSV, sheet2HeaderNRow, sheet2JSON, spreadsheetReader } from "~/utils/spreadsheet/fileReader" import * as XLSX from 'xlsx' -export function useSpreadSheet() { +export function useSpreadSheet(inputFile: Ref) { const toast = useToast() - const inputFile = ref() + // const inputFile = ref() const status = ref<'idle' | 'loading' | 'error' | 'success'>('idle') const error = ref() const result = { diff --git a/constants/dashboard-menu.ts b/constants/dashboard-menu.ts index 3dbbb21..e343b17 100644 --- a/constants/dashboard-menu.ts +++ b/constants/dashboard-menu.ts @@ -19,17 +19,39 @@ export const sidebarItems = [ label: 'Dataset', icon: 'i-heroicons-folder-20-solid', to: '/dashboard/dataset', - // children: [ - // { - // label: 'Suppliers', - // to: '/dashboard/dataset/suppliers', - // icon: 'i-heroicons-building-storefront-20-solid', - // }, - // { - // label: 'Products', - // to: '/dashboard/dataset/products', - // icon: 'i-heroicons-cube-20-solid', - // }, - // ], + children: [ + { + label: 'Suppliers', + to: '/dashboard/dataset/suppliers', + icon: 'i-heroicons-building-storefront-20-solid', + }, + { + label: 'Products', + to: '/dashboard/dataset/products', + icon: 'i-heroicons-cube-20-solid', + }, + ], }, + { + label: 'Logs', + to: '/dashboard/history', + icon: 'i-heroicons-clipboard-document-list-20-solid', + children: [ + { + label: 'Restock History', + to: '/dashboard/history/restock-history', + icon: 'i-heroicons-truck-20-solid', + }, + { + label: 'Sales History', + to: '/dashboard/history/sales-history', + icon: 'i-heroicons-banknotes-20-solid', + }, + ], + }, + { + label: 'File Operation', + to: '/dashboard/file-operation', + icon: 'i-heroicons-arrow-up-tray-20-solid', + } ] diff --git a/nuxt.config.ts b/nuxt.config.ts index ca50585..8bfc825 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -11,7 +11,7 @@ export default defineNuxtConfig({ } }, compatibilityDate: '2024-11-01', - devtools: { enabled: true }, + devtools: { enabled: false }, modules: ['@nuxt/image', '@nuxt/ui', 'dayjs-nuxt'], ui: { prefix: 'NuxtUi' diff --git a/package-lock.json b/package-lock.json index 34fa961..368b6f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,14 @@ "@nuxt/ui": "^2.21.1", "@vueuse/core": "^13.0.0", "@zxing/browser": "^0.1.5", + "chart.js": "^4.4.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^2.30.0", "dayjs-nuxt": "^2.1.11", "libphonenumber-js": "^1.12.8", "lucide-vue-next": "^0.485.0", + "numeral": "^2.0.6", "nuxt": "^3.16.1", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", @@ -28,7 +30,8 @@ "zod": "^3.24.2" }, "devDependencies": { - "@types/node": "^22.15.12" + "@types/node": "^22.15.12", + "@types/numeral": "^2.0.5" } }, "node_modules/@alloc/quick-lru": { @@ -1199,6 +1202,12 @@ "node": ">= 12" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@kwsites/file-exists": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", @@ -3129,6 +3138,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/numeral": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/numeral/-/numeral-2.0.5.tgz", + "integrity": "sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/parse-path": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/parse-path/-/parse-path-7.0.3.tgz", @@ -4394,6 +4410,18 @@ "node": ">=8" } }, + "node_modules/chart.js": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -7711,6 +7739,15 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/numeral": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", + "integrity": "sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/nuxt": { "version": "3.16.1", "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-3.16.1.tgz", diff --git a/package.json b/package.json index 0b60ea2..7ec7142 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,14 @@ "@nuxt/ui": "^2.21.1", "@vueuse/core": "^13.0.0", "@zxing/browser": "^0.1.5", + "chart.js": "^4.4.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^2.30.0", "dayjs-nuxt": "^2.1.11", "libphonenumber-js": "^1.12.8", "lucide-vue-next": "^0.485.0", + "numeral": "^2.0.6", "nuxt": "^3.16.1", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", @@ -31,6 +33,7 @@ "zod": "^3.24.2" }, "devDependencies": { - "@types/node": "^22.15.12" + "@types/node": "^22.15.12", + "@types/numeral": "^2.0.5" } } diff --git a/pages/dashboard/cashier/index.vue b/pages/dashboard/cashier/index.vue index 6ae1beb..d98bce9 100644 --- a/pages/dashboard/cashier/index.vue +++ b/pages/dashboard/cashier/index.vue @@ -1,9 +1,373 @@ + \ No newline at end of file diff --git a/pages/dashboard/dataset/products/index.vue b/pages/dashboard/dataset/products/index.vue index 6ae1beb..5ccaddf 100644 --- a/pages/dashboard/dataset/products/index.vue +++ b/pages/dashboard/dataset/products/index.vue @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/pages/dashboard/history/restock-history.vue b/pages/dashboard/history/restock-history.vue new file mode 100644 index 0000000..98b27dd --- /dev/null +++ b/pages/dashboard/history/restock-history.vue @@ -0,0 +1,49 @@ + + \ No newline at end of file diff --git a/pages/dashboard/history/sales-history.vue b/pages/dashboard/history/sales-history.vue new file mode 100644 index 0000000..163b088 --- /dev/null +++ b/pages/dashboard/history/sales-history.vue @@ -0,0 +1,50 @@ + + \ No newline at end of file diff --git a/pages/dashboard/home/index.vue b/pages/dashboard/home/index.vue index 6ae1beb..4abfaa4 100644 --- a/pages/dashboard/home/index.vue +++ b/pages/dashboard/home/index.vue @@ -1,9 +1,303 @@ \ No newline at end of file diff --git a/pages/dashboard/restock/index.vue b/pages/dashboard/restock/index.vue index 9ce5ce8..edcd1fb 100644 --- a/pages/dashboard/restock/index.vue +++ b/pages/dashboard/restock/index.vue @@ -1,54 +1,127 @@ + + + +// Set QR shown state based on screen size +onMounted(() => { + qrShown.value = windowWidth.value < 988; +}); + \ No newline at end of file diff --git a/pages/demo.vue b/pages/demo.vue index 4036f54..4787494 100644 --- a/pages/demo.vue +++ b/pages/demo.vue @@ -66,6 +66,8 @@ definePageMeta({ import type { TPyPrediction } from '~/types/api-response/py-prediction'; import type { TModalMakePredictionModel } from '~/types/landing-page/demo/modalMakePrediction' +const inputFile = ref(null) + function handleDragFile(e: DragEvent) { e.preventDefault(); if (status.value === 'loading') return @@ -85,11 +87,11 @@ function handleFileInput(e: Event) { } const { - inputFile, status, loadingDetail, result, + status, loadingDetail, result, columns, missingColumns, mismatchDetail, records, products, page, pageCount, rows -} = usePredictionTable() +} = usePredictionTable(inputFile) const analyzeBtnDisabled = computed(() => { const notHaveAnyProduct = products.value.length < 1 const hasMissingColumn = missingColumns.value.length >= 1 diff --git a/types/api-response/trx.ts b/types/api-response/trx.ts new file mode 100644 index 0000000..934633b --- /dev/null +++ b/types/api-response/trx.ts @@ -0,0 +1,3 @@ +export type TRestocksHistoryResponse = { + +} \ No newline at end of file