-
+
- How It Works
+ How It Works?
Features
+
+ Dashboard
+
+ @click="isOpen = false" v-if="authState !== 'logged-in'">
Demo
-
+
{
if (route.path.startsWith('/auth/forgot-password')) {
navigateTo('/auth')
@@ -60,15 +64,17 @@
Features
+ class="text-sm font-medium text-primary transition-colors hover:text-primary/80"
+ v-if="authState !== 'logged-in'">
Demo
+
{
if (route.path.startsWith('/auth/forgot-password')) {
navigateTo('/auth')
} else {
authModalIsOpen = true
}
- }">
+ }" 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 @@
+
+
+
+
+
+ Form Add Product
+
+
+
+
+
+ {
+ formState.product_code = e
+ scanMode = false
+ }" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+ Save
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+ {{ item.product_name }}
+
+
+
+
+
+
+ Price:
+ {{ numeral(Number(item.price)).format('0,0') }}
+
+
+
Qty:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sub Total:
+
+ {{ numeral(calculateSubtotal(item)).format('0,0') }}
+
+
+
+
+
+
+
+
+
+
+
+
Scan a product to add it to the cart
+
+
+
+
+
+
+
+
+ Scan here
+
+
+
+
+
+
+ Total:
+
+ {{ numeral(priceTotal).format('0,0') }}
+
+
+
+ Items:
+ {{ totalItems }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total: {{ numeral(priceTotal).format('0,0')
+ }}
+
+
Items: {{ totalItems }}
+
+
+
+
+
+
+
+
+
+
+ Are you sure you want to delete this product from transaction?
+
+
+
+
+
+
+
+
+ {
+ productsFormState.push({
+ amount: 1,
+ product_code: e.product_code,
+ product_name: e.product_name,
+ price: e.selling_price
+ })
+ }" />
+
\ 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 @@
+
+
+
+
+ refresh()" />
+
+
+
+
+
+
+
+ Nothing here.
+
+
+
+
+ Show {{ data?.data?.data.length }} data from {{ data?.data?.meta.total }} data
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+ refresh()" />
+
+
+
+
+
+
+
+
+ Nothing here.
+
+
+
+
+ Show {{ data?.data?.data.length }} data from {{ data?.data?.meta.total }} data
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+ Dashboard Overview
+
+
+
+
+
+
+
1,248
+
+
+
+ 12%
+
+ from last month
+
+
+
+
+
+
+
+
Low Stock Items
+
+
+
+
+
+
24
+
+
+
+ 8%
+
+ from last week
+
+
+
+
+
+
+
+
Rp 125.4M
+
+
+
+ 18%
+
+ from last month
+
+
+
+
+
+
+
+
Monthly Purchases
+
+
+
+
+
+
Rp 78.2M
+
+
+
+ 5%
+
+ from last month
+
+
+
+
+
+
+
+
+
+
+
Sales & Purchases Trend
+
+
+
+
+
+
+
+
+
+
+
+
Stock Forecast
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Low Stock Products
+ View All
+
+
+
+
+
+
+ Product |
+
+ Category |
+
+ Current Stock |
+
+ Threshold |
+
+ Status |
+
+ Action |
+
+
+
+
+
+
+
+
+
+ {{ product.name }}
+
+ |
+ {{ product.category }} |
+ {{ product.stock }} |
+ {{ product.threshold }} |
+
+
+ {{ product.stock === 0 ? 'Out of Stock' : 'Low Stock' }}
+
+ |
+
+ Restock
+ |
+
+
+
+
+
+
+
+
+
+
+
+
Next Week Prediction
+
+
+
+
+
+
+
+
{{ prediction.name }}
+
{{
+ prediction.category }}
+
+
+
+
{{ prediction.predicted }} units
+
+ {{ prediction.change > 0 ? '+' : '' }}{{ prediction.change }}%
+
+
+
+
+
View Detailed Forecast
+
+
+
+
+
+
+
Next Month Prediction
+
+
+
+
+
+
+
+
{{ prediction.name }}
+
{{
+ prediction.category }}
+
+
+
+
{{ prediction.predicted }} units
+
+ {{ prediction.change > 0 ? '+' : '' }}{{ prediction.change }}%
+
+
+
+
+
View Detailed Forecast
+
+
+
+
+
\ 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 @@
-
-
-
-
handleScan(e)" class="sticky top-[84px] " />
+
+
+
+
+
+ {{ item.product_name }}
+
+
+
+
+
+
+ Price:
+ {{ numeral(Number(item.price)).format('0,0') }}
+
+
+
Qty:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sub Total:
+
+ {{ numeral(calculateSubtotal(item)).format('0,0') }}
+
+
+
+
+
+
+
+
+
+
+
+
Scan a product to add it to the cart
+
-
-
-
-
-
{{ item.product_name || 'Unnamed Product' }}
-
- Delete
-
+
+
+
+
+
+ Scan here
+
+
+
+
+
+
+ Total:
+
+ {{ numeral(priceTotal).format('0,0') }}
+
+
+
+ Items:
+ {{ totalItems }}
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Price
-
Rp{{ item.price.toLocaleString() }}
-
-
-
-
-
Subtotal
-
- Rp{{ (item.price * item.qty).toLocaleString() }}
+
+
+
+
+
+
+
+
+ Total: {{ numeral(priceTotal).format('0,0')
+ }}
+
Items: {{ totalItems }}
+
@@ -63,78 +136,238 @@
+
+
{
+ productsFormState.push({
+ amount: 1,
+ product_code: e.product_code,
+ product_name: e.product_name,
+ price: e.buying_price
+ })
+ }" />
+
+// 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