diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..79d57ec
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "iconify.annotations": false,
+ "iconify.inplace": false
+}
\ No newline at end of file
diff --git a/app.vue b/app.vue
index ec8256b..f1097ec 100644
--- a/app.vue
+++ b/app.vue
@@ -1,13 +1,24 @@
+
\ No newline at end of file
diff --git a/assets/css/shad-tailwind.css b/assets/css/shad-tailwind.css
deleted file mode 100644
index 67a1728..0000000
--- a/assets/css/shad-tailwind.css
+++ /dev/null
@@ -1,82 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-@layer base {
- :root {
- --background: 0 0% 100%;
- --foreground: 240 10% 3.9%;
- --card: 0 0% 100%;
- --card-foreground: 240 10% 3.9%;
- --popover: 0 0% 100%;
- --popover-foreground: 240 10% 3.9%;
- --primary: 240 5.9% 10%;
- --primary-foreground: 0 0% 98%;
- --secondary: 240 4.8% 95.9%;
- --secondary-foreground: 240 5.9% 10%;
- --muted: 240 4.8% 95.9%;
- --muted-foreground: 240 3.8% 46.1%;
- --accent: 240 4.8% 95.9%;
- --accent-foreground: 240 5.9% 10%;
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 0 0% 98%;
- --border: 240 5.9% 90%;
- --input: 240 5.9% 90%;
- --ring: 240 10% 3.9%;
- --chart-1: 12 76% 61%;
- --chart-2: 173 58% 39%;
- --chart-3: 197 37% 24%;
- --chart-4: 43 74% 66%;
- --chart-5: 27 87% 67%;
- --radius: 0.5rem
- ;
- --sidebar-background: 0 0% 98%;
- --sidebar-foreground: 240 5.3% 26.1%;
- --sidebar-primary: 240 5.9% 10%;
- --sidebar-primary-foreground: 0 0% 98%;
- --sidebar-accent: 240 4.8% 95.9%;
- --sidebar-accent-foreground: 240 5.9% 10%;
- --sidebar-border: 220 13% 91%;
- --sidebar-ring: 217.2 91.2% 59.8%}
- .dark {
- --background: 240 10% 3.9%;
- --foreground: 0 0% 98%;
- --card: 240 10% 3.9%;
- --card-foreground: 0 0% 98%;
- --popover: 240 10% 3.9%;
- --popover-foreground: 0 0% 98%;
- --primary: 0 0% 98%;
- --primary-foreground: 240 5.9% 10%;
- --secondary: 240 3.7% 15.9%;
- --secondary-foreground: 0 0% 98%;
- --muted: 240 3.7% 15.9%;
- --muted-foreground: 240 5% 64.9%;
- --accent: 240 3.7% 15.9%;
- --accent-foreground: 0 0% 98%;
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 0 0% 98%;
- --border: 240 3.7% 15.9%;
- --input: 240 3.7% 15.9%;
- --ring: 240 4.9% 83.9%;
- --chart-1: 220 70% 50%;
- --chart-2: 160 60% 45%;
- --chart-3: 30 80% 55%;
- --chart-4: 280 65% 60%;
- --chart-5: 340 75% 55%
- ;
- --sidebar-background: 240 5.9% 10%;
- --sidebar-foreground: 240 4.8% 95.9%;
- --sidebar-primary: 224.3 76.3% 48%;
- --sidebar-primary-foreground: 0 0% 100%;
- --sidebar-accent: 240 3.7% 15.9%;
- --sidebar-accent-foreground: 240 4.8% 95.9%;
- --sidebar-border: 240 3.7% 15.9%;
- --sidebar-ring: 217.2 91.2% 59.8%}
-}
-@layer base {
- * {
- @apply border-border;
- }
- body {
- @apply bg-background text-foreground;
- }
-}
diff --git a/assets/css/sidebar.tw.css b/assets/css/sidebar.tw.css
new file mode 100644
index 0000000..ea71604
--- /dev/null
+++ b/assets/css/sidebar.tw.css
@@ -0,0 +1,47 @@
+/* Sidebar styles - supports Tailwind and dark mode */
+
+.sidebar-header h2 {
+ @apply text-xl font-semibold m-0;
+}
+
+.sidebar-content {
+ @apply flex-1 overflow-y-auto py-3;
+}
+
+.nav-list {
+ @apply list-none p-0 m-0;
+}
+
+.nav-item {
+ @apply mb-1;
+}
+
+.sub-item {
+ @apply pl-6;
+}
+
+.nav-icon {
+ @apply mr-3 flex items-center justify-center w-6 h-6;
+}
+
+.nav-text {
+ @apply text-sm flex-1 truncate;
+}
+
+.chevron {
+ @apply transition-transform duration-200;
+}
+
+.chevron-rotated {
+ @apply rotate-180;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+ @apply transition-all duration-200 ease-in-out;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ @apply opacity-0 -translate-y-1;
+}
diff --git a/components.json b/components.json
deleted file mode 100644
index 29d6896..0000000
--- a/components.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "$schema": "https://shadcn-vue.com/schema.json",
- "style": "new-york",
- "typescript": true,
- "tailwind": {
- "config": "shad-tailwind.config.ts",
- "css": "assets/css/shad-tailwind.css",
- "baseColor": "zinc",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/components",
- "composables": "@/composables",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib"
- },
- "iconLibrary": "lucide"
-}
\ No newline at end of file
diff --git a/components/dashboard/dataset/product-category-input.vue b/components/dashboard/dataset/product-category-input.vue
new file mode 100644
index 0000000..a0d76b6
--- /dev/null
+++ b/components/dashboard/dataset/product-category-input.vue
@@ -0,0 +1,24 @@
+
+ {{ selectedId }}
+
+
+
+
diff --git a/components/dashboard/dataset/product/modal-add.vue b/components/dashboard/dataset/product/modal-add.vue
new file mode 100644
index 0000000..047d223
--- /dev/null
+++ b/components/dashboard/dataset/product/modal-add.vue
@@ -0,0 +1,62 @@
+
+
+
modalShown = true" />
+
+
+
+
+ Form Add Product
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ modalShown = false">Cancel
+
+ Save
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/dashboard/dataset/product/modal-category.vue b/components/dashboard/dataset/product/modal-category.vue
new file mode 100644
index 0000000..b0d4ca8
--- /dev/null
+++ b/components/dashboard/dataset/product/modal-category.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ createNow()" :loading="createStatus === 'loading'"
+ label="Simpan" type="submit" />
+
+
+
+
+
+
+
+
+
+ Are you sure delete this category?
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/dashboard/dataset/product/modal-delete.vue b/components/dashboard/dataset/product/modal-delete.vue
new file mode 100644
index 0000000..50fbf31
--- /dev/null
+++ b/components/dashboard/dataset/product/modal-delete.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+ Confirm Deletion
+
+
+ Are you sure you want to delete this product? This action cannot be undone.
+
+
+
+
+
+
+ Cancel
+
+ execute()" :loading="status === 'pending'">
+ Delete
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/dashboard/dataset/product/modal-update.vue b/components/dashboard/dataset/product/modal-update.vue
new file mode 100644
index 0000000..9f57444
--- /dev/null
+++ b/components/dashboard/dataset/product/modal-update.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ Form Update Supplier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ modalShown = false">Cancel
+
+ Save
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/dashboard/dataset/products.vue b/components/dashboard/dataset/products.vue
new file mode 100644
index 0000000..dba5060
--- /dev/null
+++ b/components/dashboard/dataset/products.vue
@@ -0,0 +1,79 @@
+
+
+
+
+ refresh()" />
+
+
+ {{ modal.update.data }}
+
+
+
+
+
+
+
+
+
+
+
+
+ Nothing here.
+
+
+
+
+ Show {{ data?.data?.data.length }} data from {{ data?.data?.meta.total }} data
+
+
+
+
+
+
+
+
+ refresh()" v-model:data="modal.update.data"
+ v-if="modal.update.data?.id" :key="modal.update.data?.id" />
+ refresh()" v-model:shown="modal.delete.shown"
+ v-model:data="modal.delete.data" v-if="modal.delete.data?.id" :key="modal.delete.data?.id" />
+
+
\ No newline at end of file
diff --git a/components/dashboard/dataset/supplier/modal-add-supplier.vue b/components/dashboard/dataset/supplier/modal-add-supplier.vue
new file mode 100644
index 0000000..1f62911
--- /dev/null
+++ b/components/dashboard/dataset/supplier/modal-add-supplier.vue
@@ -0,0 +1,49 @@
+
+
+
modalShown = true" />
+
+
+
+
+ Form Add Supplier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ modalShown = false">Cancel
+
+ Save
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/dashboard/dataset/supplier/modal-delete-supplier.vue b/components/dashboard/dataset/supplier/modal-delete-supplier.vue
new file mode 100644
index 0000000..0613cde
--- /dev/null
+++ b/components/dashboard/dataset/supplier/modal-delete-supplier.vue
@@ -0,0 +1,47 @@
+
+
+
+
+
+ Confirm Deletion
+
+
+ Are you sure you want to delete this supplier? This action cannot be undone.
+
+
+
+
+
+
+ Cancel
+
+ execute()" :loading="status === 'pending'">
+ Delete
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/dashboard/dataset/supplier/modal-update-supplier.vue b/components/dashboard/dataset/supplier/modal-update-supplier.vue
new file mode 100644
index 0000000..5afef5d
--- /dev/null
+++ b/components/dashboard/dataset/supplier/modal-update-supplier.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ Form Update Supplier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ modalShown = false">Cancel
+
+ Save
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/dashboard/dataset/suppliers.vue b/components/dashboard/dataset/suppliers.vue
new file mode 100644
index 0000000..53df125
--- /dev/null
+++ b/components/dashboard/dataset/suppliers.vue
@@ -0,0 +1,76 @@
+
+
+
+
+ refresh()" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nothing here.
+
+
+
+
+ Show {{ data?.data?.data.length }} data from {{ data?.data?.meta.total }} data
+
+
+
+
+
+
+
+
+ refresh()" v-model:shown="modal.update.shown"
+ v-model:data="modal.update.data" v-if="modal.update.data?.id" :key="modal.update.data?.id" />
+ refresh()" v-model:shown="modal.delete.shown"
+ v-model:data="modal.delete.data" v-if="modal.delete.data?.id" :key="modal.delete.data?.id" />
+
+
\ No newline at end of file
diff --git a/components/dashboard/sidebar/index.vue b/components/dashboard/sidebar/index.vue
deleted file mode 100644
index e3d0f8e..0000000
--- a/components/dashboard/sidebar/index.vue
+++ /dev/null
@@ -1,188 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Documentation
- v1.0.0
-
-
-
-
-
-
-
-
-
-
-
-
- {{ item.title }}
-
-
-
-
-
- {{ childItem.title }}
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/components/landing/demo/modalMakePrediction.vue b/components/landing/demo/modalMakePrediction.vue
index b336c61..863fca4 100644
--- a/components/landing/demo/modalMakePrediction.vue
+++ b/components/landing/demo/modalMakePrediction.vue
@@ -13,44 +13,41 @@
description="Please fill form honestly for better prediction." />
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
+
- {
- emit('prepared')
- modalShown = false
- }">Analyze Now!
+ Analyze Now!
@@ -58,65 +55,39 @@
\ No newline at end of file
diff --git a/components/my/dashboard/sidebar/sidebar-group.vue b/components/my/dashboard/sidebar/sidebar-group.vue
new file mode 100644
index 0000000..c3960b1
--- /dev/null
+++ b/components/my/dashboard/sidebar/sidebar-group.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
diff --git a/components/my/dashboard/sidebar/sidebar-link.vue b/components/my/dashboard/sidebar/sidebar-link.vue
new file mode 100644
index 0000000..f7f89ef
--- /dev/null
+++ b/components/my/dashboard/sidebar/sidebar-link.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ {{ label }}
+
+
+
+
+
+
diff --git a/components/my/dashboard/sidebar/sidebar.vue b/components/my/dashboard/sidebar/sidebar.vue
new file mode 100644
index 0000000..ab4f1a4
--- /dev/null
+++ b/components/my/dashboard/sidebar/sidebar.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/loader/clipboard.vue b/components/my/loader/clipboard.vue
similarity index 100%
rename from components/loader/clipboard.vue
rename to components/my/loader/clipboard.vue
diff --git a/components/loader/flipping-book-page.vue b/components/my/loader/flipping-book-page.vue
similarity index 100%
rename from components/loader/flipping-book-page.vue
rename to components/my/loader/flipping-book-page.vue
diff --git a/components/loader/flipping-card.vue b/components/my/loader/flipping-card.vue
similarity index 100%
rename from components/loader/flipping-card.vue
rename to components/my/loader/flipping-card.vue
diff --git a/components/loader/padlock.vue b/components/my/loader/padlock.vue
similarity index 100%
rename from components/loader/padlock.vue
rename to components/my/loader/padlock.vue
diff --git a/components/loader/pulse-ring.vue b/components/my/loader/pulse-ring.vue
similarity index 100%
rename from components/loader/pulse-ring.vue
rename to components/my/loader/pulse-ring.vue
diff --git a/components/loader/text.vue b/components/my/loader/text.vue
similarity index 100%
rename from components/loader/text.vue
rename to components/my/loader/text.vue
diff --git a/components/loader/trailing-letter.vue b/components/my/loader/trailing-letter.vue
similarity index 100%
rename from components/loader/trailing-letter.vue
rename to components/my/loader/trailing-letter.vue
diff --git a/components/my/sidebar/index.vue b/components/my/sidebar/index.vue
new file mode 100644
index 0000000..8a1dc7a
--- /dev/null
+++ b/components/my/sidebar/index.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+ MyApp
+
+ Home
+ About
+ Contact
+
+
+
+
+
+ MyApp
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+ About
+ Contact
+
+
+
+
+
diff --git a/components/ui/button/Button.vue b/components/ui/button/Button.vue
deleted file mode 100644
index 17dc84d..0000000
--- a/components/ui/button/Button.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/button/index.ts b/components/ui/button/index.ts
deleted file mode 100644
index aa6014c..0000000
--- a/components/ui/button/index.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { cva, type VariantProps } from 'class-variance-authority'
-
-export { default as Button } from './Button.vue'
-
-export const buttonVariants = cva(
- 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
- {
- variants: {
- variant: {
- default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
- destructive:
- 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
- outline:
- 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
- secondary:
- 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
- ghost: 'hover:bg-accent hover:text-accent-foreground',
- link: 'text-primary underline-offset-4 hover:underline',
- },
- size: {
- default: 'h-9 px-4 py-2',
- xs: 'h-7 rounded px-2',
- sm: 'h-8 rounded-md px-3 text-xs',
- lg: 'h-10 rounded-md px-8',
- icon: 'h-9 w-9',
- },
- },
- defaultVariants: {
- variant: 'default',
- size: 'default',
- },
- },
-)
-
-export type ButtonVariants = VariantProps
diff --git a/components/ui/card/Card.vue b/components/ui/card/Card.vue
deleted file mode 100644
index 94b6903..0000000
--- a/components/ui/card/Card.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/card/CardContent.vue b/components/ui/card/CardContent.vue
deleted file mode 100644
index 785913a..0000000
--- a/components/ui/card/CardContent.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/card/CardDescription.vue b/components/ui/card/CardDescription.vue
deleted file mode 100644
index d5faedd..0000000
--- a/components/ui/card/CardDescription.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/card/CardFooter.vue b/components/ui/card/CardFooter.vue
deleted file mode 100644
index 1ed2efe..0000000
--- a/components/ui/card/CardFooter.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/card/CardHeader.vue b/components/ui/card/CardHeader.vue
deleted file mode 100644
index 951d227..0000000
--- a/components/ui/card/CardHeader.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/card/CardTitle.vue b/components/ui/card/CardTitle.vue
deleted file mode 100644
index fc302e2..0000000
--- a/components/ui/card/CardTitle.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/card/index.ts b/components/ui/card/index.ts
deleted file mode 100644
index 9ff6d5e..0000000
--- a/components/ui/card/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export { default as Card } from './Card.vue'
-export { default as CardContent } from './CardContent.vue'
-export { default as CardDescription } from './CardDescription.vue'
-export { default as CardFooter } from './CardFooter.vue'
-export { default as CardHeader } from './CardHeader.vue'
-export { default as CardTitle } from './CardTitle.vue'
diff --git a/components/ui/input/Input.vue b/components/ui/input/Input.vue
deleted file mode 100644
index 1165ea5..0000000
--- a/components/ui/input/Input.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
diff --git a/components/ui/input/index.ts b/components/ui/input/index.ts
deleted file mode 100644
index a691dd6..0000000
--- a/components/ui/input/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Input } from './Input.vue'
diff --git a/components/ui/label/Label.vue b/components/ui/label/Label.vue
deleted file mode 100644
index b4991db..0000000
--- a/components/ui/label/Label.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/label/index.ts b/components/ui/label/index.ts
deleted file mode 100644
index 572c2f0..0000000
--- a/components/ui/label/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Label } from './Label.vue'
diff --git a/components/ui/separator/Separator.vue b/components/ui/separator/Separator.vue
deleted file mode 100644
index b7a3942..0000000
--- a/components/ui/separator/Separator.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
- {{ props.label }}
-
-
diff --git a/components/ui/separator/index.ts b/components/ui/separator/index.ts
deleted file mode 100644
index 2287bcb..0000000
--- a/components/ui/separator/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Separator } from './Separator.vue'
diff --git a/components/ui/sheet/Sheet.vue b/components/ui/sheet/Sheet.vue
deleted file mode 100644
index 9fc9c7d..0000000
--- a/components/ui/sheet/Sheet.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sheet/SheetClose.vue b/components/ui/sheet/SheetClose.vue
deleted file mode 100644
index ba036b5..0000000
--- a/components/ui/sheet/SheetClose.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sheet/SheetContent.vue b/components/ui/sheet/SheetContent.vue
deleted file mode 100644
index 1c3e07e..0000000
--- a/components/ui/sheet/SheetContent.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/components/ui/sheet/SheetDescription.vue b/components/ui/sheet/SheetDescription.vue
deleted file mode 100644
index ebbc5c8..0000000
--- a/components/ui/sheet/SheetDescription.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sheet/SheetFooter.vue b/components/ui/sheet/SheetFooter.vue
deleted file mode 100644
index ac2d0c1..0000000
--- a/components/ui/sheet/SheetFooter.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sheet/SheetHeader.vue b/components/ui/sheet/SheetHeader.vue
deleted file mode 100644
index 541f48f..0000000
--- a/components/ui/sheet/SheetHeader.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sheet/SheetTitle.vue b/components/ui/sheet/SheetTitle.vue
deleted file mode 100644
index 72f6a97..0000000
--- a/components/ui/sheet/SheetTitle.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sheet/SheetTrigger.vue b/components/ui/sheet/SheetTrigger.vue
deleted file mode 100644
index 2984f37..0000000
--- a/components/ui/sheet/SheetTrigger.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sheet/index.ts b/components/ui/sheet/index.ts
deleted file mode 100644
index 4c4e77a..0000000
--- a/components/ui/sheet/index.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { cva, type VariantProps } from 'class-variance-authority'
-
-export { default as Sheet } from './Sheet.vue'
-export { default as SheetClose } from './SheetClose.vue'
-export { default as SheetContent } from './SheetContent.vue'
-export { default as SheetDescription } from './SheetDescription.vue'
-export { default as SheetFooter } from './SheetFooter.vue'
-export { default as SheetHeader } from './SheetHeader.vue'
-export { default as SheetTitle } from './SheetTitle.vue'
-export { default as SheetTrigger } from './SheetTrigger.vue'
-
-export const sheetVariants = cva(
- 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
- {
- variants: {
- side: {
- top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
- bottom:
- 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
- left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
- right:
- 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
- },
- },
- defaultVariants: {
- side: 'right',
- },
- },
-)
-
-export type SheetVariants = VariantProps
diff --git a/components/ui/sidebar/Sidebar.vue b/components/ui/sidebar/Sidebar.vue
deleted file mode 100644
index f101566..0000000
--- a/components/ui/sidebar/Sidebar.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarContent.vue b/components/ui/sidebar/SidebarContent.vue
deleted file mode 100644
index 4b6244a..0000000
--- a/components/ui/sidebar/SidebarContent.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarFooter.vue b/components/ui/sidebar/SidebarFooter.vue
deleted file mode 100644
index 9d145c0..0000000
--- a/components/ui/sidebar/SidebarFooter.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarGroup.vue b/components/ui/sidebar/SidebarGroup.vue
deleted file mode 100644
index adc6843..0000000
--- a/components/ui/sidebar/SidebarGroup.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarGroupAction.vue b/components/ui/sidebar/SidebarGroupAction.vue
deleted file mode 100644
index 92d8d8f..0000000
--- a/components/ui/sidebar/SidebarGroupAction.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarGroupContent.vue b/components/ui/sidebar/SidebarGroupContent.vue
deleted file mode 100644
index 37390c9..0000000
--- a/components/ui/sidebar/SidebarGroupContent.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarGroupLabel.vue b/components/ui/sidebar/SidebarGroupLabel.vue
deleted file mode 100644
index c119a8c..0000000
--- a/components/ui/sidebar/SidebarGroupLabel.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarHeader.vue b/components/ui/sidebar/SidebarHeader.vue
deleted file mode 100644
index eecaddb..0000000
--- a/components/ui/sidebar/SidebarHeader.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarInput.vue b/components/ui/sidebar/SidebarInput.vue
deleted file mode 100644
index fe696d3..0000000
--- a/components/ui/sidebar/SidebarInput.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarInset.vue b/components/ui/sidebar/SidebarInset.vue
deleted file mode 100644
index 27d1db5..0000000
--- a/components/ui/sidebar/SidebarInset.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenu.vue b/components/ui/sidebar/SidebarMenu.vue
deleted file mode 100644
index 3bfd73e..0000000
--- a/components/ui/sidebar/SidebarMenu.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuAction.vue b/components/ui/sidebar/SidebarMenuAction.vue
deleted file mode 100644
index 64ad356..0000000
--- a/components/ui/sidebar/SidebarMenuAction.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuBadge.vue b/components/ui/sidebar/SidebarMenuBadge.vue
deleted file mode 100644
index f878968..0000000
--- a/components/ui/sidebar/SidebarMenuBadge.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuButton.vue b/components/ui/sidebar/SidebarMenuButton.vue
deleted file mode 100644
index ac6926b..0000000
--- a/components/ui/sidebar/SidebarMenuButton.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ tooltip }}
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuButtonChild.vue b/components/ui/sidebar/SidebarMenuButtonChild.vue
deleted file mode 100644
index c37fc69..0000000
--- a/components/ui/sidebar/SidebarMenuButtonChild.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuItem.vue b/components/ui/sidebar/SidebarMenuItem.vue
deleted file mode 100644
index b600073..0000000
--- a/components/ui/sidebar/SidebarMenuItem.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuSkeleton.vue b/components/ui/sidebar/SidebarMenuSkeleton.vue
deleted file mode 100644
index 22bea3f..0000000
--- a/components/ui/sidebar/SidebarMenuSkeleton.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuSub.vue b/components/ui/sidebar/SidebarMenuSub.vue
deleted file mode 100644
index 0bb5af7..0000000
--- a/components/ui/sidebar/SidebarMenuSub.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuSubButton.vue b/components/ui/sidebar/SidebarMenuSubButton.vue
deleted file mode 100644
index 2419f28..0000000
--- a/components/ui/sidebar/SidebarMenuSubButton.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarMenuSubItem.vue b/components/ui/sidebar/SidebarMenuSubItem.vue
deleted file mode 100644
index b04030b..0000000
--- a/components/ui/sidebar/SidebarMenuSubItem.vue
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarProvider.vue b/components/ui/sidebar/SidebarProvider.vue
deleted file mode 100644
index edff8df..0000000
--- a/components/ui/sidebar/SidebarProvider.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarRail.vue b/components/ui/sidebar/SidebarRail.vue
deleted file mode 100644
index 9b644cd..0000000
--- a/components/ui/sidebar/SidebarRail.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarSeparator.vue b/components/ui/sidebar/SidebarSeparator.vue
deleted file mode 100644
index fa49c01..0000000
--- a/components/ui/sidebar/SidebarSeparator.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/sidebar/SidebarTrigger.vue b/components/ui/sidebar/SidebarTrigger.vue
deleted file mode 100644
index 7c3185a..0000000
--- a/components/ui/sidebar/SidebarTrigger.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
- Toggle Sidebar
-
-
diff --git a/components/ui/sidebar/index.ts b/components/ui/sidebar/index.ts
deleted file mode 100644
index e238e44..0000000
--- a/components/ui/sidebar/index.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import type { VariantProps } from 'class-variance-authority'
-import type { HTMLAttributes } from 'vue'
-import { cva } from 'class-variance-authority'
-
-export interface SidebarProps {
- side?: 'left' | 'right'
- variant?: 'sidebar' | 'floating' | 'inset'
- collapsible?: 'offcanvas' | 'icon' | 'none'
- class?: HTMLAttributes['class']
-}
-
-export { default as Sidebar } from './Sidebar.vue'
-export { default as SidebarContent } from './SidebarContent.vue'
-export { default as SidebarFooter } from './SidebarFooter.vue'
-export { default as SidebarGroup } from './SidebarGroup.vue'
-export { default as SidebarGroupAction } from './SidebarGroupAction.vue'
-export { default as SidebarGroupContent } from './SidebarGroupContent.vue'
-export { default as SidebarGroupLabel } from './SidebarGroupLabel.vue'
-export { default as SidebarHeader } from './SidebarHeader.vue'
-export { default as SidebarInput } from './SidebarInput.vue'
-export { default as SidebarInset } from './SidebarInset.vue'
-export { default as SidebarMenu } from './SidebarMenu.vue'
-export { default as SidebarMenuAction } from './SidebarMenuAction.vue'
-export { default as SidebarMenuBadge } from './SidebarMenuBadge.vue'
-export { default as SidebarMenuButton } from './SidebarMenuButton.vue'
-export { default as SidebarMenuItem } from './SidebarMenuItem.vue'
-export { default as SidebarMenuSkeleton } from './SidebarMenuSkeleton.vue'
-export { default as SidebarMenuSub } from './SidebarMenuSub.vue'
-export { default as SidebarMenuSubButton } from './SidebarMenuSubButton.vue'
-export { default as SidebarMenuSubItem } from './SidebarMenuSubItem.vue'
-export { default as SidebarProvider } from './SidebarProvider.vue'
-export { default as SidebarRail } from './SidebarRail.vue'
-export { default as SidebarSeparator } from './SidebarSeparator.vue'
-export { default as SidebarTrigger } from './SidebarTrigger.vue'
-
-export { useSidebar } from './utils'
-
-export const sidebarMenuButtonVariants = cva(
- 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
- {
- variants: {
- variant: {
- default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
- outline:
- 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
- },
- size: {
- default: 'h-8 text-sm',
- sm: 'h-7 text-xs',
- lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
- },
- },
- defaultVariants: {
- variant: 'default',
- size: 'default',
- },
- },
-)
-
-export type SidebarMenuButtonVariants = VariantProps
diff --git a/components/ui/sidebar/utils.ts b/components/ui/sidebar/utils.ts
deleted file mode 100644
index 9d6f866..0000000
--- a/components/ui/sidebar/utils.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { ComputedRef, Ref } from 'vue'
-import { createContext } from 'reka-ui'
-
-export const SIDEBAR_COOKIE_NAME = 'sidebar:state'
-export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
-export const SIDEBAR_WIDTH = '16rem'
-export const SIDEBAR_WIDTH_MOBILE = '18rem'
-export const SIDEBAR_WIDTH_ICON = '3rem'
-export const SIDEBAR_KEYBOARD_SHORTCUT = 'b'
-
-export const [useSidebar, provideSidebarContext] = createContext<{
- state: ComputedRef<'expanded' | 'collapsed'>
- open: Ref
- setOpen: (value: boolean) => void
- isMobile: Ref
- openMobile: Ref
- setOpenMobile: (value: boolean) => void
- toggleSidebar: () => void
-}>('Sidebar')
diff --git a/components/ui/skeleton/Skeleton.vue b/components/ui/skeleton/Skeleton.vue
deleted file mode 100644
index 94bc183..0000000
--- a/components/ui/skeleton/Skeleton.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
diff --git a/components/ui/skeleton/index.ts b/components/ui/skeleton/index.ts
deleted file mode 100644
index be21fad..0000000
--- a/components/ui/skeleton/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default as Skeleton } from './Skeleton.vue'
diff --git a/components/ui/tooltip/Tooltip.vue b/components/ui/tooltip/Tooltip.vue
deleted file mode 100644
index 90741e3..0000000
--- a/components/ui/tooltip/Tooltip.vue
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/tooltip/TooltipContent.vue b/components/ui/tooltip/TooltipContent.vue
deleted file mode 100644
index 2416268..0000000
--- a/components/ui/tooltip/TooltipContent.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/components/ui/tooltip/TooltipProvider.vue b/components/ui/tooltip/TooltipProvider.vue
deleted file mode 100644
index abf42d8..0000000
--- a/components/ui/tooltip/TooltipProvider.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/tooltip/TooltipTrigger.vue b/components/ui/tooltip/TooltipTrigger.vue
deleted file mode 100644
index 9255272..0000000
--- a/components/ui/tooltip/TooltipTrigger.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
diff --git a/components/ui/tooltip/index.ts b/components/ui/tooltip/index.ts
deleted file mode 100644
index 5ab9653..0000000
--- a/components/ui/tooltip/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export { default as Tooltip } from './Tooltip.vue'
-export { default as TooltipContent } from './TooltipContent.vue'
-export { default as TooltipProvider } from './TooltipProvider.vue'
-export { default as TooltipTrigger } from './TooltipTrigger.vue'
diff --git a/composables/autoRefreshAccessToken.ts b/composables/autoRefreshAccessToken.ts
new file mode 100644
index 0000000..a6ac7c2
--- /dev/null
+++ b/composables/autoRefreshAccessToken.ts
@@ -0,0 +1,43 @@
+import type { TAPIResponse } from "~/types/api-response/basicResponse";
+
+export function useAutoRefreshAccessToken() {
+ const config = useRuntimeConfig()
+ const { apiAccessToken, apiAccessTokenStatus, authState } = useMyAppState()
+
+ async function refreshAccessToken() {
+ try {
+ await $fetch>(`/auth/refresh-token`, {
+ baseURL: config.public.API_HOST,
+ method: 'GET',
+ credentials: 'include',
+ headers: {
+ Accept: 'application/json'
+ },
+ onResponse: async (ctx) => {
+ if (ctx.response.ok) {
+ apiAccessToken.value = ctx.response._data.data.accessToken;
+ apiAccessTokenStatus.value = 'valid'
+ }
+ },
+ onResponseError: async (ctx) => {
+ const statusCode = ctx.response?.status;
+
+ if ([401, 403].includes(statusCode)) {
+ authState.value = 'logged-out'
+ apiAccessToken.value = null;
+ }
+ },
+ });
+ } catch (e) {
+ console.error("🔄 Failed to refresh token:", e);
+ }
+ }
+
+ watch(apiAccessTokenStatus, newVal => {
+ if (newVal === 'expired') {
+ refreshAccessToken()
+ }
+ }, { immediate: true })
+}
\ No newline at end of file
diff --git a/composables/core.ts b/composables/core.ts
index aebfd33..8b679bd 100644
--- a/composables/core.ts
+++ b/composables/core.ts
@@ -9,5 +9,6 @@ export function useMyAppState() {
apiAccessToken: useCookie("myAppState-accessToken", {
default: () => null,
}),
+ apiAccessTokenStatus: useState<'idle' | 'expired' | 'valid' | 'waiting'>("myAppState-accessTokenStatus", () => 'idle')
}
}
\ No newline at end of file
diff --git a/composables/productCategoryFetch.ts b/composables/productCategoryFetch.ts
new file mode 100644
index 0000000..561c5aa
--- /dev/null
+++ b/composables/productCategoryFetch.ts
@@ -0,0 +1,109 @@
+import type { TAPIResponse } from "~/types/api-response/basicResponse"
+import type { TProductCategoryResponse } from "~/types/api-response/product_category"
+
+export function useShowProductCategory() {
+ const { data, clear, refresh, status, error } = useFetchWithAutoReNew>(computed(() => `/product-categories`))
+ return {
+ refresh, data, clear, status, error
+ }
+}
+
+export function useAddProductCategory() {
+ const toast = useToast()
+ const status = ref<'idle' | 'success' | 'error' | 'loading'>('idle')
+ const formState = reactive<{
+ category_name?: string
+ }>({
+ category_name: undefined
+ })
+ async function createNow() {
+ status.value = 'loading'
+ let id = undefined
+ const { execute, data } = use$fetchWithAutoReNew>(`/product-category`, {
+ method: 'post',
+ body: formState,
+ onResponse() {
+ status.value = 'success'
+ formState.category_name = undefined
+ toast.add({
+ color: 'green',
+ title: 'Created',
+ description: 'Product category created successfully.'
+ })
+ },
+ onResponseError() {
+ status.value = 'error'
+ }
+ })
+ await execute()
+ return data.value?.data?.productCategoryId
+ }
+ return { formState, createNow, status }
+}
+
+export function useUpdateProductCategory(id: number) {
+ const productCategoryId = ref()
+ const toast = useToast()
+ const { execute, data, error, status } = use$fetchWithAutoReNew(`/product-category/${id}`, {
+ method: 'patch',
+ onResponse() {
+ toast.add({
+ color: 'green',
+ title: 'Deleted',
+ description: 'Product category deleted successfully.'
+ })
+ }
+ })
+
+ return { productCategoryId, execute, data, error, status }
+}
+
+export function useDeleteProductCategory() {
+ const productCategoryId = ref()
+ const toast = useToast()
+ const { execute, data, error, status } = use$fetchWithAutoReNew(computed(() => `/product-category/${productCategoryId.value}`), {
+ method: 'delete',
+ onResponse() {
+ toast.add({
+ color: 'green',
+ title: 'Deleted',
+ description: 'Product category deleted successfully.'
+ })
+ }
+ })
+
+ return { productCategoryId, execute, data, error, status }
+}
+
+export function useProductCategoryListOptions() {
+ const { data, refresh, status } = useShowProductCategory()
+ const options = ref()
+
+ watch(status, newVal => {
+ if (newVal === 'success')
+ options.value = data.value?.data
+ })
+ const selectedReal = ref()
+
+ const selected = computed({
+ get: () => selectedReal.value,
+ set: async (newVal) => {
+ if (Number(newVal) >= 0) {
+ selectedReal.value = newVal
+ return newVal
+ }
+ const {
+ createNow, formState
+ } = useAddProductCategory()
+ formState.category_name = newVal?.toString()
+ const id = await createNow()
+ selectedReal.value = id
+ refresh()
+ return id
+ }
+ })
+
+ return { selected, options, status }
+}
\ No newline at end of file
diff --git a/composables/productFetch.ts b/composables/productFetch.ts
new file mode 100644
index 0000000..bc52524
--- /dev/null
+++ b/composables/productFetch.ts
@@ -0,0 +1,106 @@
+import { z } from "zod"
+import type { TPaginatedResponse } from "~/types/api-response/basicResponse"
+import type { TProductResponse } from "~/types/api-response/product"
+
+export function useProductList() {
+ const page = ref(1)
+ const limit = ref(10)
+ const { data, clear, refresh, status, error } = useFetchWithAutoReNew>(computed(() => `/products?page=${page.value}&limit=${limit.value}`))
+ return {
+ page, limit, refresh, data, clear, status, error
+ }
+}
+
+export function useAddProduct() {
+ const toast = useToast()
+ const formState = reactive({
+ product_code: undefined,
+ product_name: undefined,
+ stock: undefined,
+ buying_price: undefined,
+ selling_price: undefined,
+ product_category_id: undefined,
+ })
+ const productSchema = z.object({
+ product_code: z.string().min(1, 'Product name is required'),
+ product_name: z.string().min(1, 'Product name is required'),
+ stock: z.number().optional(),
+ buying_price: z.number().optional(),
+ selling_price: z.number().optional(),
+ product_category_id: z.number().optional(),
+ });
+
+ const { execute, data, error, status } = use$fetchWithAutoReNew('/product', {
+ method: 'post',
+ body: formState,
+ onResponse() {
+ toast.add({
+ color: 'green',
+ title: 'Created',
+ description: 'New product added successfully.'
+ })
+ }
+ })
+
+ watch(status, newVal => {
+ if (newVal === 'success') {
+ formState.product_code = undefined
+ formState.buying_price = undefined
+ formState.product_category_id = undefined
+ formState.product_name = undefined
+ formState.selling_price = undefined
+ formState.stock = undefined
+ }
+ })
+ return { formState, execute, data, error, status, productSchema }
+}
+
+export function useUpdateProduct(productData: TProductResponse) {
+ const toast = useToast()
+ const formState = reactive>({
+ product_code: productData.product_code,
+ product_name: productData.product_name,
+ product_category_id: productData.product_category_id || undefined,
+ buying_price: productData.buying_price || undefined,
+ selling_price: productData.selling_price || undefined,
+ stock: productData.stock || undefined,
+ })
+ const productSchema = z.object({
+ product_code: z.string().min(1, 'Product name is required'),
+ product_name: z.string().min(1, 'Product name is required'),
+ stock: z.number().optional(),
+ buying_price: z.number().optional(),
+ selling_price: z.number().optional(),
+ product_category_id: z.number().optional(),
+ });
+
+ const { execute, data, error, status } = use$fetchWithAutoReNew(`/product/${productData.id}`, {
+ method: 'patch',
+ body: formState,
+ onResponse() {
+ toast.add({
+ color: 'green',
+ title: 'Updated',
+ description: 'Data product updated successfully.'
+ })
+ }
+ })
+
+ return { formState, execute, data, error, status, productSchema }
+}
+
+export function useDeleteProduct(id: number) {
+ const toast = useToast()
+ const { execute, data, error, status } = use$fetchWithAutoReNew(`/product/${id}`, {
+ method: 'delete',
+ onResponse() {
+ toast.add({
+ color: 'green',
+ title: 'Deleted',
+ description: 'Product deleted successfully.'
+ })
+ }
+ })
+
+ return { execute, data, error, status }
+}
\ No newline at end of file
diff --git a/composables/supplierFetch.ts b/composables/supplierFetch.ts
new file mode 100644
index 0000000..32a057a
--- /dev/null
+++ b/composables/supplierFetch.ts
@@ -0,0 +1,106 @@
+import { promise, z } from "zod"
+import type { TPaginatedResponse } from "~/types/api-response/basicResponse"
+import { parsePhoneNumberFromString } from 'libphonenumber-js';
+import type { TSupplierResponse } from "~/types/api-response/supplier";
+
+export function useSupplierList() {
+ const page = ref(1)
+ const limit = ref(10)
+ const { data, clear, refresh, status, error } = useFetchWithAutoReNew>(computed(() => `/suppliers?page=${page.value}&limit=${limit.value}`))
+ return {
+ page, limit, refresh, data, clear, status, error
+ }
+}
+
+export function useAddSupplier() {
+ const toast = useToast()
+ const formState = reactive({
+ supplier_name: undefined,
+ address: undefined,
+ contact: undefined
+ })
+ const internationalPhoneSchema = z.string().optional().refine((val) => {
+ if (!val) return true
+ const phone = parsePhoneNumberFromString(`+${val}`);
+ return phone?.isValid() ?? false;
+ }, {
+ message: 'Invalid phone number format'
+ });
+ const supplierSchema = z.object({
+ supplier_name: z.string().min(1, 'Supplier name is required'),
+ address: z.string().optional(),
+ contact: internationalPhoneSchema
+ });
+
+ const { execute, data, error, status } = use$fetchWithAutoReNew('/supplier', {
+ method: 'post',
+ body: formState,
+ onResponse() {
+ toast.add({
+ color: 'green',
+ title: 'Created',
+ description: 'New supplier added successfully.'
+ })
+ }
+ })
+
+ watch(status, newVal => {
+ if (newVal === 'success') {
+ formState.address = undefined
+ formState.supplier_name = undefined
+ formState.contact = undefined
+ }
+ })
+ return { formState, execute, data, error, status, supplierSchema }
+}
+
+export function useUpdateSupplier(supplierData: TSupplierResponse) {
+ const toast = useToast()
+ const formState = reactive>({
+ supplier_name: supplierData.supplier_name,
+ address: supplierData.address,
+ contact: supplierData.contact || undefined
+ })
+ const internationalPhoneSchema = z.string().optional().refine((val) => {
+ if (!val) return true
+ const phone = parsePhoneNumberFromString(`+${val}`);
+ return phone?.isValid() ?? false;
+ }, {
+ message: 'Invalid phone number format'
+ });
+ const supplierSchema = z.object({
+ supplier_name: z.string().min(1, 'Supplier name is required'),
+ address: z.string().optional(),
+ contact: internationalPhoneSchema
+ });
+
+ const { execute, data, error, status } = use$fetchWithAutoReNew(`/supplier/${supplierData.id}`, {
+ method: 'patch',
+ body: formState,
+ onResponse() {
+ toast.add({
+ color: 'green',
+ title: 'Updated',
+ description: 'Data supplier updated successfully.'
+ })
+ }
+ })
+
+ return { formState, execute, data, error, status, supplierSchema }
+}
+
+export function useDeleteSupplier(id: number) {
+ const toast = useToast()
+ const { execute, data, error, status } = use$fetchWithAutoReNew(`/supplier/${id}`, {
+ method: 'delete',
+ onResponse() {
+ toast.add({
+ color: 'green',
+ title: 'Deleted',
+ description: 'Supplier deleted successfully.'
+ })
+ }
+ })
+
+ return { execute, data, error, status }
+}
\ No newline at end of file
diff --git a/composables/useAuth$fetch.ts b/composables/use$fetchAuto.ts
similarity index 60%
rename from composables/useAuth$fetch.ts
rename to composables/use$fetchAuto.ts
index 6c09ba8..1e54182 100644
--- a/composables/useAuth$fetch.ts
+++ b/composables/use$fetchAuto.ts
@@ -4,11 +4,12 @@ import type { TAPIResponse } from '~/types/api-response/basicResponse';
import type { NitroFetchOptions, NitroFetchRequest } from 'nitropack';
export function use$fetchWithAutoReNew(
- url: string,
+ url: string | MaybeRefOrGetter,
options?: NitroFetchOptions
) {
+ const isWaiting = ref(false)
const config = useRuntimeConfig();
- const { authState, apiAccessToken } = useMyAppState();
+ const { apiAccessToken, apiAccessTokenStatus } = useMyAppState();
const headers = computed(() => {
let h = {}
@@ -30,49 +31,13 @@ export function use$fetchWithAutoReNew(
const status = ref('idle');
const error = ref(null);
- async function refreshAccessToken(): Promise {
- try {
- let newAccessToken = null
- const res = await $fetch>('/auth/refresh-token', {
- baseURL: config.public.API_HOST,
- method: 'GET',
- credentials: 'include',
- headers: {
- Accept: 'application/json',
- },
- onResponse: async (ctx) => {
- if (ctx.response.ok) {
- newAccessToken = ctx.response._data.data.accessToken;
- }
- },
- onResponseError: async (ctx) => {
- const statusCode = ctx.response?.status;
-
- if ([401, 403].includes(statusCode)) {
- authState.value = 'logged-out'
- apiAccessToken.value = null;
- }
- },
- });
-
- if (!!newAccessToken) {
- apiAccessToken.value = newAccessToken;
- return true;
- }
-
- throw new Error('No accessToken received');
- } catch (e) {
- console.error('🔄 Failed to refresh token', e);
- return false;
- }
- }
-
async function execute() {
status.value = 'pending';
error.value = null;
+ const resolvedUrl = toValue(url)
try {
- await $fetch(url, {
+ await $fetch(resolvedUrl, {
...options,
headers: headers.value,
baseURL: config.public.API_HOST,
@@ -91,11 +56,8 @@ export function use$fetchWithAutoReNew(
const statusCode = ctx.response?.status;
if ([401, 403].includes(statusCode)) {
- const refreshed = await refreshAccessToken();
- if (refreshed) {
- await execute();
- return;
- }
+ isWaiting.value = true
+ apiAccessTokenStatus.value = 'expired'
}
if (typeof options?.onResponseError === 'function') {
@@ -111,5 +73,12 @@ export function use$fetchWithAutoReNew(
}
}
+ watch(apiAccessTokenStatus, newVal => {
+ if (newVal === 'valid' && isWaiting.value) {
+ execute()
+ isWaiting.value = false
+ }
+ })
+
return { data, status, error, execute };
-}
+}
\ No newline at end of file
diff --git a/composables/useAuth.ts b/composables/useAuth.ts
index cd892af..7ae7513 100644
--- a/composables/useAuth.ts
+++ b/composables/useAuth.ts
@@ -25,7 +25,7 @@ export function useAuthLogin() {
onResponse(ctx) {
authState.value = 'logged-in'
apiAccessToken.value = ctx.response._data.data.accessToken
- navigateTo('/dashboard')
+ navigateTo('/dashboard/home')
}
})
diff --git a/composables/useAuthFetch.ts b/composables/useFetchAuto.ts
similarity index 51%
rename from composables/useAuthFetch.ts
rename to composables/useFetchAuto.ts
index 12597a8..62df2b8 100644
--- a/composables/useAuthFetch.ts
+++ b/composables/useFetchAuto.ts
@@ -1,4 +1,3 @@
-import { useLocalStorage, useNetwork } from '@vueuse/core';
import type { UseFetchOptions } from 'nuxt/app';
import type { TAPIResponse } from '~/types/api-response/basicResponse';
@@ -6,8 +5,9 @@ export function useFetchWithAutoReNew(
url: string | Request | Ref | (() => string | Request),
options?: UseFetchOptions
) {
+ const isWaiting = ref(false)
const config = useRuntimeConfig();
- const { authState, apiAccessToken } = useMyAppState();
+ const { apiAccessToken, apiAccessTokenStatus } = useMyAppState();
const originalHeadersAsObject = () => {
if (options?.headers) {
@@ -41,56 +41,26 @@ export function useFetchWithAutoReNew(
async onResponseError(ctx) {
const status = ctx.response.status;
if ([401, 403].includes(status)) {
- await refreshAccessToken()
+ isWaiting.value = true
+ apiAccessTokenStatus.value = 'expired'
}
if (typeof options?.onResponseError === "function") {
options.onResponseError(ctx);
}
},
+ immediate: false,
};
const { data, status, error, refresh, clear } = useFetch(url, mergedOptions)
- async function refreshAccessToken(): Promise {
- try {
- let newAccessToken = null
- await $fetch>(`/auth/refresh-token`, {
- baseURL: config.public.API_HOST,
- method: 'GET',
- credentials: 'include',
- headers: {
- Accept: 'application/json'
- },
- onResponse: async (ctx) => {
- if (ctx.response.ok) {
- newAccessToken = ctx.response._data.data.accessToken;
- }
- },
- onResponseError: async (ctx) => {
- error.value = ctx?.error ?? ctx.response._data ?? null;
- const statusCode = ctx.response?.status;
-
- if ([401, 403].includes(statusCode)) {
- authState.value = 'logged-out'
- apiAccessToken.value = null;
- }
- },
- });
- if (!!newAccessToken) {
- apiAccessToken.value = newAccessToken;
- return true;
- }
-
- throw new Error('No accessToken received');
- } catch (e) {
- console.error("🔄 Failed to refresh token:", e);
- return false;
- } finally {
-
+ watch(apiAccessTokenStatus, newVal => {
+ if (newVal === 'valid' && isWaiting.value) {
+ refresh()
+ isWaiting.value = false
}
- }
+ })
+ refresh()
return { data, status, error, refresh, clear }
-}
\ No newline at end of file
+}
+
diff --git a/composables/usePredictionFetch.ts b/composables/usePredictionFetch.ts
new file mode 100644
index 0000000..e7afb51
--- /dev/null
+++ b/composables/usePredictionFetch.ts
@@ -0,0 +1,112 @@
+import type { AsyncDataRequestStatus } from 'nuxt/app';
+import type { TDurationType, TPredictionMode } from '~/types/landing-page/demo/modalMakePrediction';
+import { z } from 'zod';
+import type { TPyPrediction } from '~/types/api-response/py-prediction';
+
+export function usePredictionFetch() {
+ const config = useRuntimeConfig();
+
+ const recordPeriodOptions: TDurationType[] = ['daily', 'weekly', 'monthly']
+ const predictionModeOptions: {
+ value: TPredictionMode,
+ label: string
+ }[] = [
+ { value: 'optimal', label: 'Optimal (2,1,2)' },
+ { value: 'auto', label: 'Auto (Flexible)' },
+ { value: 'custom', label: 'Custom' },
+ ]
+ const predictionPeriodOptions = computed(() => {
+ switch (formData.recordPeriod) {
+ case 'daily':
+ case 'weekly':
+ return ['weekly', 'monthly']
+ case 'monthly':
+ return ['monthly']
+ default:
+ break;
+ }
+ })
+
+ const ARIMASchema = z.object({
+ modelAR: z.string().min(1, { message: 'Model AR wajib diisi' }).regex(/^\d+$/, { message: 'Model AR harus angka' }),
+ differencing: z.string().min(1, { message: 'Differencing wajib diisi' }).regex(/^\d+$/, { message: 'Differencing harus angka' }).max(2),
+ modelMA: z.string().min(1, { message: 'Model MA wajib diisi' }).regex(/^\d+$/, { message: 'Model MA harus angka' }),
+ })
+
+ const formData = reactive<{
+ sheet: File | null
+ recordPeriod: TDurationType,
+ predictionPeriod: 'weekly' | 'monthly'
+ predictionMode: TPredictionMode,
+ modelAR: number,
+ differencing: 0 | 1,
+ modelMA: number,
+ }>({
+ sheet: null,
+ recordPeriod: 'daily',
+ predictionPeriod: 'weekly',
+ predictionMode: 'optimal',
+ modelAR: 2,
+ differencing: 1,
+ modelMA: 2,
+ })
+
+ const ARIMAModel = computed(() => {
+ switch (formData.predictionMode) {
+ case 'auto':
+ return []
+ case 'optimal':
+ return [2, 1, 2]
+ case 'custom':
+ return [
+ formData.modelAR,
+ formData.differencing,
+ formData.modelMA
+ ]
+ default:
+ return [2, 1, 2]
+ }
+ })
+
+ const result = ref();
+ const status = ref('idle');
+ const error = ref(null);
+ async function execute() {
+ status.value = 'pending';
+ error.value = null;
+ const fd = new FormData()
+ try {
+ if (!formData.sheet) throw new Error('Sheet not found!')
+ fd.append('sheet', formData.sheet)
+ fd.append('recordPeriod', formData.recordPeriod)
+ fd.append('predictionPeriod', formData.predictionPeriod)
+ fd.append('predictionMode', formData.predictionMode)
+ fd.append('arimaModel', ARIMAModel.value.join(','))
+ await $fetch('/predict-file', {
+ method: 'post',
+ baseURL: config.public.PYTHON_API_HOST,
+ onResponse: async (ctx) => {
+ result.value = ctx.response._data;
+ if (ctx.response.ok) {
+ status.value = 'success';
+ }
+ },
+ onResponseError: async (ctx) => {
+ error.value = ctx?.error ?? ctx.response._data ?? null;
+ const statusCode = ctx.response?.status;
+ status.value = 'error';
+ },
+ body: fd
+ });
+ } catch (err) {
+ error.value = err as Error;
+ status.value = 'error';
+ console.error('❌ Fetch failed:', err);
+ }
+ }
+
+ return {
+ result, status, error, execute, formData, ARIMASchema,
+ recordPeriodOptions, predictionPeriodOptions, predictionModeOptions
+ };
+}
diff --git a/composables/usePredictionTable.ts b/composables/usePredictionTable.ts
index f8e6dfc..6437310 100644
--- a/composables/usePredictionTable.ts
+++ b/composables/usePredictionTable.ts
@@ -1,150 +1,71 @@
import dayjs from '#build/dayjs.imports.mjs'
import type { TableColumn } from '#ui/types'
-import type { TProduct } from '~/types/landing-page/demo/modalMakePrediction'
const requiredColumn = [
{ label: 'Date', key: 'date', sortable: true, },
- { label: 'Product Code', key: 'product code', sortable: true, },
- { label: 'Product Name', key: 'product name', sortable: true, },
+ { label: 'Product Code', key: 'product_code', sortable: true, },
+ { label: 'Product Name', key: 'product_name', sortable: true, },
{ label: 'Sold(qty)', key: 'sold(qty)', sortable: true, }
]
export function usePredictionTable() {
- const records = ref[]>([])
- const processedRecords = ref[]>([])
+ const {
+ inputFile, result, status: sheetReaderStatus
+ } = useSpreadSheet()
const status = ref<'idle' | 'loading' | 'loaded'>('idle')
const loadingDetail = ref();
- const mismatchDetail = ref([])
-
- const columns = ref([])
+ const columns = ref(requiredColumn)
const missingColumns = ref([])
+ const mismatchDetail = ref([])
+ const records = ref[]>([])
+ const products = ref([])
+
+ watch(sheetReaderStatus, newVal => {
+ if (newVal === 'idle' || newVal === 'error')
+ status.value = 'idle'
+ if (newVal === 'loading')
+ status.value = 'loading'
+ if (newVal === 'success') {
+ status.value = 'loading'
+
+ columns.value = result.jsonHeaders.value!.map(v => ({ ...v, sortable: true }))
+ records.value = result.json.value as any
+
+ const jsonHeadersKeys = result.jsonHeaders.value!.map(v => v.key)
+ missingColumns.value = requiredColumn.filter(v => !jsonHeadersKeys.includes(v.key)).map(v => v.label)
+
+ let dateInvalidCounter = 0
+ mismatchDetail.value = []
+ const productsSet = new Set()
+ result.json.value?.forEach((v, i) => {
+ if (!dayjs(v.date).isValid())
+ dateInvalidCounter += 1
+
+ if (jsonHeadersKeys.includes('product_code')) {
+ productsSet.add(v.product_code)
+ } else {
+ productsSet.add(v.product_name)
+ }
+ })
+ if (dateInvalidCounter >= 1)
+ mismatchDetail.value.push(`${dateInvalidCounter} invalid date ${dateInvalidCounter === 1 ? 'value was' : 'values were'} found in the 'Date' column.`);
+
+ products.value = productsSet.values().toArray() as string[]
+
+ status.value = 'loaded'
+ }
+ })
const page = ref(1)
const pageCount = ref(10)
-
- const products = ref[]>([])
-
const rows = computed(() => {
- return processedRecords.value.slice((page.value - 1) * pageCount.value, (page.value) * pageCount.value)
- })
-
- watch(records, newVal => {
- status.value = 'loading'
- mismatchDetail.value = []
-
- loadingDetail.value = 'Collecting and validating record'
- processedRecords.value = normalizeRecord(newVal, requiredColumn.map(v => v.key))
- })
-
- watch(processedRecords, newVal => {
- console.log('processed')
- loadingDetail.value = 'Load all column keys from record'
- columns.value = getColumnsFromRecord(newVal[0], requiredColumn)
-
- loadingDetail.value = 'Checking missing column'
- missingColumns.value = getMissingColumns(
- Object.keys(newVal[0]),
- requiredColumn.map(v => v.key)
- )
-
- loadingDetail.value = 'Collecting product list'
- const hasProductCode = Object.keys(newVal[0]).includes('product code')
- const listedProduct = new Set()
- let dateInvalidCounter: number = 0
- newVal.forEach((v, i) => {
- if (hasProductCode) {
- const cv = v['product code']
- const cl = v['product name']
- if (!listedProduct.has(cv)) {
- listedProduct.add(cv)
- products.value?.push({
- label: cv,
- value: cl
- })
- }
- } else {
- const cl = v['product name']
- if (!listedProduct.has(cl)) {
- listedProduct.add(cl)
- products.value?.push({
- label: cl,
- value: cl
- })
- }
- }
-
- if (!dayjs(v.date).isValid()) {
- dateInvalidCounter += 1
- }
- })
-
- if (dateInvalidCounter >= 1) {
- mismatchDetail.value.push(`There is ${dateInvalidCounter} invalid date inside column 'date'.`)
- }
-
- loadingDetail.value = undefined
- status.value = 'loaded'
+ return records.value.slice((page.value - 1) * pageCount.value, (page.value) * pageCount.value)
})
return {
- records, processedRecords, status, loadingDetail, mismatchDetail,
- columns, missingColumns,
- page, pageCount,
- products, rows
+ inputFile, status, loadingDetail, result,
+ columns, missingColumns, mismatchDetail,
+ records, products,
+ page, pageCount, rows
}
-}
-
-function getProductList(records: Record[]) {
- return
-}
-
-function normalizeRecord(records: Record[], requiredColumnKey: string[]) {
- return records.map(record => {
- const normalized: Record = {}
-
- Object.entries(record).forEach(([key, value]) => {
- const lowerKey = key.toLowerCase()
- if (requiredColumnKey.includes(lowerKey)) {
- normalized[lowerKey] = value
- } else {
- normalized[key] = value
- }
- })
-
- return normalized
- })
-}
-
-function getColumnsFromRecord(
- records: Record = {},
- requiredColumn: TableColumn[]
-) {
- const columnKeys: string[] | Record[] = Object.keys(records || {})
- const requiredColumnKey = requiredColumn.map(v => v.key)
- const finalColumn = [
- // {
- // key: 'actions',
- // sortable: true,
- // label: 'Actions'
- // },
- ...requiredColumn,
- ]
- if (columnKeys.length >= 1) {
- const candidateCol = columnKeys.map(v => ({
- key: v,
- label: v,
- sortable: true,
- }))
- finalColumn.push(
- ...candidateCol.filter(v => !requiredColumnKey.includes(v.key))
- )
- }
- return finalColumn
-}
-
-function getMissingColumns(
- columnsKey: string[],
- requiredColumnKey: string[]
-) {
- return requiredColumnKey.filter(v => !columnsKey.includes(v))
}
\ No newline at end of file
diff --git a/composables/useSpreadsheet.ts b/composables/useSpreadsheet.ts
index 1fca8fd..2004553 100644
--- a/composables/useSpreadsheet.ts
+++ b/composables/useSpreadsheet.ts
@@ -1,69 +1,72 @@
-import { sheetToJSON } from "~/utils/spreadsheet/sheetsToJSON"
+import { headerNRow2Sheet, sheet2CSV, sheet2HeaderNRow, sheet2JSON, spreadsheetReader } from "~/utils/spreadsheet/fileReader"
+import * as XLSX from 'xlsx'
-export function useFileToJSON() {
+export function useSpreadSheet() {
const toast = useToast()
- const file = ref(null)
+ const inputFile = ref()
const status = ref<'idle' | 'loading' | 'error' | 'success'>('idle')
- const result = ref[]>([])
- const error = ref(null)
- watch(file, async (newVal) => {
- if (!newVal)
- return
- status.value = 'loading'
- error.value = null
+ const error = ref()
+ const result = {
+ jsonHeaders: ref<{
+ key: string,
+ label: string
+ }[]>(),
+ csv: ref(),
+ json: ref[]>(),
+ }
+
+ watch(inputFile, async (newVal) => {
try {
- const json = await sheetToJSON(newVal);
- if (json) {
- const newJSON = json.map((jsonObj) => {
- const entries = Object.entries(
- jsonObj as Record
- ).map(([key, value]) => {
- switch (key.toLowerCase().trim()) {
- case 'date':
- key = 'date'
- break;
- case 'product code':
- key = 'product code'
- break;
- case 'product name':
- key = 'product name'
- break;
- case 'sold(qty)':
- key = 'sold(qty)'
- break;
- default:
- break;
- }
- return [key, value];
- });
- return Object.fromEntries(entries);
- })
- result.value = json as Record[]
- }
- } catch (e: unknown) {
- status.value = 'error'
- if (e instanceof Error) {
- error.value = e
- }
- toast.add({
- title: 'Error',
- icon: 'i-heroicons-x-circle',
- color: 'red',
- description: error.value?.message
+ if (status.value === 'loading')
+ throw new Error('Please wait until the current file is fully loaded before uploading a new one.');
+ if (!newVal)
+ return
+
+ status.value = 'loading'
+ error.value = undefined
+
+ const ws = await spreadsheetReader(newVal)
+ const { headers, rows } = sheet2HeaderNRow(ws)
+ result.jsonHeaders.value = []
+ const validHeaders = headers.map((v: string) => {
+ const label = v.replaceAll(/\s+/g, ' ')
+ const key = v.toLowerCase().replaceAll(/\s+/g, '_').trim()
+ result.jsonHeaders.value?.push({ label, key })
+ return key
})
+ const newWs = headerNRow2Sheet(validHeaders, rows)
+ result.json.value = sheet2JSON[]>(newWs)
+ result.csv.value = sheet2CSV(newWs)
+ } catch (e: unknown) {
+ setError(error.value?.message || 'Unknown Error', e as Error)
} finally {
if (status.value !== 'error') {
- status.value = 'success'
- toast.add({
- title: 'Success',
- icon: 'i-heroicons-document-check',
- color: 'green',
- description: 'File Imported Successfully.'
- })
+ setSuccess()
}
}
})
+
+ function setError(msg: string, err?: Error) {
+ status.value = 'error'
+ error.value = err || new Error(msg)
+ toast.add({
+ title: 'Error',
+ icon: 'i-heroicons-x-circle',
+ color: 'red',
+ description: msg
+ })
+ }
+ function setSuccess() {
+ status.value = 'success'
+ toast.add({
+ title: 'Success',
+ icon: 'i-heroicons-document-check',
+ color: 'green',
+ description: 'File Imported Successfully.'
+ })
+ }
+
return {
- file, status, result, error
+ inputFile, status, result, error,
}
}
\ No newline at end of file
diff --git a/constants/dashboard-menu.ts b/constants/dashboard-menu.ts
new file mode 100644
index 0000000..3dbbb21
--- /dev/null
+++ b/constants/dashboard-menu.ts
@@ -0,0 +1,35 @@
+// SidebarItems.js
+export const sidebarItems = [
+ {
+ label: 'Dashboard',
+ to: '/dashboard/home',
+ icon: 'i-heroicons-chart-bar-20-solid',
+ },
+ {
+ label: 'Cashier',
+ to: '/dashboard/cashier',
+ icon: 'i-heroicons-shopping-bag-20-solid',
+ },
+ {
+ label: 'Restock',
+ to: '/dashboard/restock',
+ icon: 'i-heroicons-arrow-path-20-solid',
+ },
+ {
+ 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',
+ // },
+ // ],
+ },
+]
diff --git a/env.d.ts b/env.d.ts
index e90f51a..982df11 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -2,5 +2,6 @@ declare namespace NodeJS {
interface ProcessEnv {
HOST: string
API_HOST: string
+ PYTHON_API_HOST: string
}
}
\ No newline at end of file
diff --git a/layouts/main-copy.vue b/layouts/main-copy.vue
new file mode 100644
index 0000000..fb6d573
--- /dev/null
+++ b/layouts/main-copy.vue
@@ -0,0 +1,116 @@
+
+
+
+
\ No newline at end of file
diff --git a/layouts/main.vue b/layouts/main.vue
index eea7444..b155cf6 100644
--- a/layouts/main.vue
+++ b/layouts/main.vue
@@ -1,22 +1,88 @@
-
-
-
-
-
-
-
+
+
+
+
sidebarShownToggle()" />
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file
diff --git a/middleware/guest.ts b/middleware/guest.ts
index ed2cbdb..a44cee7 100644
--- a/middleware/guest.ts
+++ b/middleware/guest.ts
@@ -10,6 +10,6 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
})
await execute()
if (gotoDashboard)
- return navigateTo('/dashboard')
+ return navigateTo('/dashboard/home')
}
});
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 0c79c8d..ca50585 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -12,32 +12,23 @@ export default defineNuxtConfig({
},
compatibilityDate: '2024-11-01',
devtools: { enabled: true },
- modules: ['@nuxt/image', '@nuxt/ui', 'shadcn-nuxt', 'dayjs-nuxt'],
+ modules: ['@nuxt/image', '@nuxt/ui', 'dayjs-nuxt'],
ui: {
prefix: 'NuxtUi'
},
runtimeConfig: {
public: {
HOST: process.env.HOST,
- API_HOST: process.env.API_HOST
+ API_HOST: process.env.API_HOST,
+ PYTHON_API_HOST: process.env.PYTHON_API_HOST,
}
},
- shadcn: {
- /**
- * Prefix for all the imported component
- */
- prefix: 'Shad',
- /**
- * Directory that the component lives in.
- * @default "./components/ui"
- */
- componentDir: './components/ui'
- },
image: {
format: ['webp'],
quality: 80,
},
css: [
- '@/assets/css/main.tw.css'
+ '@/assets/css/main.tw.css',
+ '~/assets/css/sidebar.tw.css'
]
})
diff --git a/package-lock.json b/package-lock.json
index 10c4aa9..6c8d760 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,10 +14,9 @@
"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",
"nuxt": "^3.16.1",
- "reka-ui": "^2.2.0",
- "shadcn-nuxt": "^1.0.3",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"v-calendar": "^3.1.2",
@@ -995,68 +994,6 @@
"node": ">=14"
}
},
- "node_modules/@floating-ui/core": {
- "version": "1.6.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
- "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/utils": "^0.2.9"
- }
- },
- "node_modules/@floating-ui/dom": {
- "version": "1.6.13",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
- "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/core": "^1.6.0",
- "@floating-ui/utils": "^0.2.9"
- }
- },
- "node_modules/@floating-ui/utils": {
- "version": "0.2.9",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
- "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
- "license": "MIT"
- },
- "node_modules/@floating-ui/vue": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.6.tgz",
- "integrity": "sha512-XFlUzGHGv12zbgHNk5FN2mUB7ROul3oG2ENdTpWdE+qMFxyNxWSRmsoyhiEnpmabNm6WnUvR1OvJfUfN4ojC1A==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/dom": "^1.0.0",
- "@floating-ui/utils": "^0.2.9",
- "vue-demi": ">=0.13.0"
- }
- },
- "node_modules/@floating-ui/vue/node_modules/vue-demi": {
- "version": "0.14.10",
- "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
- "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "vue-demi-fix": "bin/vue-demi-fix.js",
- "vue-demi-switch": "bin/vue-demi-switch.js"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- },
- "peerDependencies": {
- "@vue/composition-api": "^1.0.0-rc.1",
- "vue": "^3.0.0-0 || ^2.6.0"
- },
- "peerDependenciesMeta": {
- "@vue/composition-api": {
- "optional": true
- }
- }
- },
"node_modules/@headlessui/tailwindcss": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.2.2.tgz",
@@ -1151,24 +1088,6 @@
"vue": ">=3"
}
},
- "node_modules/@internationalized/date": {
- "version": "3.7.0",
- "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.7.0.tgz",
- "integrity": "sha512-VJ5WS3fcVx0bejE/YHfbDKR/yawZgKqn/if+oEeLqNwBtPzVB06olkfcnojTmEMX+gTpH+FlQ69SHNitJ8/erQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@swc/helpers": "^0.5.0"
- }
- },
- "node_modules/@internationalized/number": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.0.tgz",
- "integrity": "sha512-PtrRcJVy7nw++wn4W2OuePQQfTqDzfusSuY1QTtui4wa7r+rGVtR75pO8CyKvHvzyQYi3Q1uO5sY0AsB4e65Bw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@swc/helpers": "^0.5.0"
- }
- },
"node_modules/@ioredis/commands": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
@@ -2854,15 +2773,6 @@
"integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==",
"license": "CC0-1.0"
},
- "node_modules/@swc/helpers": {
- "version": "0.5.15",
- "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
- "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.8.0"
- }
- },
"node_modules/@tailwindcss/aspect-ratio": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz",
@@ -3669,18 +3579,6 @@
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"license": "MIT"
},
- "node_modules/aria-hidden": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
- "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
- "license": "MIT",
- "dependencies": {
- "tslib": "^2.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/ast-kit": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.4.2.tgz",
@@ -6807,6 +6705,12 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/libphonenumber-js": {
+ "version": "1.12.8",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.8.tgz",
+ "integrity": "sha512-f1KakiQJa9tdc7w1phC2ST+DyxWimy9c3g3yeF+84QtEanJr2K77wAmBPP22riU05xldniHsvXuflnLZ4oysqA==",
+ "license": "MIT"
+ },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -8992,51 +8896,6 @@
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
- "node_modules/reka-ui": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/reka-ui/-/reka-ui-2.2.0.tgz",
- "integrity": "sha512-eeRrLI4LwJ6dkdwks6KFNKGs0+beqZlHO3JMHen7THDTh+yJ5Z0KNwONmOhhV/0hZC2uJCEExgG60QPzGstkQg==",
- "license": "MIT",
- "dependencies": {
- "@floating-ui/dom": "^1.6.13",
- "@floating-ui/vue": "^1.1.6",
- "@internationalized/date": "^3.5.0",
- "@internationalized/number": "^3.5.0",
- "@tanstack/vue-virtual": "^3.12.0",
- "@vueuse/core": "^12.5.0",
- "@vueuse/shared": "^12.5.0",
- "aria-hidden": "^1.2.4",
- "defu": "^6.1.4",
- "ohash": "^2.0.11"
- },
- "peerDependencies": {
- "vue": ">= 3.2.0"
- }
- },
- "node_modules/reka-ui/node_modules/@vueuse/core": {
- "version": "12.8.2",
- "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz",
- "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==",
- "license": "MIT",
- "dependencies": {
- "@types/web-bluetooth": "^0.0.21",
- "@vueuse/metadata": "12.8.2",
- "@vueuse/shared": "12.8.2",
- "vue": "^3.5.13"
- },
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/reka-ui/node_modules/@vueuse/metadata": {
- "version": "12.8.2",
- "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz",
- "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
"node_modules/replace-in-file": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.5.tgz",
@@ -9482,38 +9341,6 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
- "node_modules/shadcn-nuxt": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/shadcn-nuxt/-/shadcn-nuxt-1.0.3.tgz",
- "integrity": "sha512-Jf2yK5P7dtQHKUVWlMPByKvHBjgld76P1yCmYgx3XpmOK0mRpePxasHNWoau3wR1ReTWRNQzYVO9+A3sg5aNEA==",
- "license": "MIT",
- "dependencies": {
- "@nuxt/kit": "^3.15.4",
- "@oxc-parser/wasm": "^0.50.0",
- "typescript": "5.6.3"
- }
- },
- "node_modules/shadcn-nuxt/node_modules/@oxc-parser/wasm": {
- "version": "0.50.0",
- "resolved": "https://registry.npmjs.org/@oxc-parser/wasm/-/wasm-0.50.0.tgz",
- "integrity": "sha512-be/QsKqtXQbKhnIRzezPrV385L6EVaX1GNAGeaQaT+HX6Lny/SfmFetCrEZCRqn2/cAqo+P5rGDNJzv06dY/vw==",
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "^0.50.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/shadcn-nuxt/node_modules/@oxc-project/types": {
- "version": "0.50.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.50.0.tgz",
- "integrity": "sha512-VGV87PmDCGv1D+57iEmIuDoso3ig8d8D4VIK9AS0H7h2aNiRwFoCpVyp01+3jvIuHjPcEM2wbo3NG5EKyfx6Jw==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
"node_modules/sharp": {
"version": "0.32.6",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
@@ -10491,7 +10318,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
+ "license": "0BSD",
+ "optional": true
},
"node_modules/tsscmp": {
"version": "1.0.6",
@@ -10545,6 +10373,8 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/package.json b/package.json
index f118617..515279b 100644
--- a/package.json
+++ b/package.json
@@ -17,10 +17,9 @@
"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",
"nuxt": "^3.16.1",
- "reka-ui": "^2.2.0",
- "shadcn-nuxt": "^1.0.3",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"v-calendar": "^3.1.2",
diff --git a/pages/auth/forgot-password/[token].vue b/pages/auth/forgot-password/[token].vue
index 2a6f13b..d8a55c9 100644
--- a/pages/auth/forgot-password/[token].vue
+++ b/pages/auth/forgot-password/[token].vue
@@ -2,7 +2,7 @@
import { z } from 'zod'
import type { FormSubmitEvent } from '#ui/types'
definePageMeta({
- middleware: ['forgot-password-confirmation'],
+ middleware: ['guest', 'forgot-password-confirmation'],
});
const forgotPasswordState = useState<'valid' | 'invalid' | 'unset'>('forgot-password-state', () => 'unset')
diff --git a/pages/auth/index.vue b/pages/auth/index.vue
index bff9b36..d80e460 100644
--- a/pages/auth/index.vue
+++ b/pages/auth/index.vue
@@ -7,3 +7,8 @@
+
diff --git a/pages/auth/verify/[token].vue b/pages/auth/verify/[token].vue
index 8a3a40e..3f8368f 100644
--- a/pages/auth/verify/[token].vue
+++ b/pages/auth/verify/[token].vue
@@ -130,7 +130,7 @@
import type { TAPIResponse } from '~/types/api-response/basicResponse';
definePageMeta({
- middleware: 'account-activation'
+ middleware: ['guest', 'account-activation']
})
const route = useRoute();
diff --git a/pages/dashboard/index.vue b/pages/dashboard/cashier/index.vue
similarity index 100%
rename from pages/dashboard/index.vue
rename to pages/dashboard/cashier/index.vue
diff --git a/pages/dashboard/dataset/index.vue b/pages/dashboard/dataset/index.vue
new file mode 100644
index 0000000..ef3daa7
--- /dev/null
+++ b/pages/dashboard/dataset/index.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/dashboard/dataset/products/index.vue b/pages/dashboard/dataset/products/index.vue
new file mode 100644
index 0000000..6ae1beb
--- /dev/null
+++ b/pages/dashboard/dataset/products/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/dashboard/dataset/suppliers/index.vue b/pages/dashboard/dataset/suppliers/index.vue
new file mode 100644
index 0000000..ef3daa7
--- /dev/null
+++ b/pages/dashboard/dataset/suppliers/index.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/dashboard/home/index.vue b/pages/dashboard/home/index.vue
new file mode 100644
index 0000000..6ae1beb
--- /dev/null
+++ b/pages/dashboard/home/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/dashboard/restock/index.vue b/pages/dashboard/restock/index.vue
new file mode 100644
index 0000000..6ae1beb
--- /dev/null
+++ b/pages/dashboard/restock/index.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/demo.vue b/pages/demo.vue
index c7181f3..4036f54 100644
--- a/pages/demo.vue
+++ b/pages/demo.vue
@@ -1,10 +1,6 @@
- {
- if (convertStatus !== 'loading') {
- handleDragFile(e)
- }
- }">
+
@@ -13,6 +9,7 @@
-
-
-
-
+
+
+
@@ -66,20 +47,15 @@
Nothing here. Please import your spreadsheet or drag your spreadsheet file here.
- Show {{ rows.length }} data from {{ processedRecords.length }} data
+ Show {{ rows.length }} data from {{ records.length }} data
-
-
-
@@ -87,17 +63,33 @@
definePageMeta({
middleware: 'guest'
})
-import type { DropdownItem } from '#ui/types'
+import type { TPyPrediction } from '~/types/api-response/py-prediction';
import type { TModalMakePredictionModel } from '~/types/landing-page/demo/modalMakePrediction'
-const { file, result: convertResult, status: convertStatus } = useFileToJSON()
+function handleDragFile(e: DragEvent) {
+ e.preventDefault();
+ if (status.value === 'loading') return
+ const files = e.dataTransfer?.files;
+ if (files && files.length > 0) {
+ inputFile.value = files[0]
+ }
+}
+
+function handleFileInput(e: Event) {
+ if (status.value === 'loading') return
+ const target = e.target as HTMLInputElement
+ if (target?.files && target.files.length > 0) {
+ const uploaded = target.files[0];
+ inputFile.value = uploaded;
+ }
+}
+
const {
- records, processedRecords, status, loadingDetail, mismatchDetail,
- columns, missingColumns,
- page, pageCount,
- products, rows
+ inputFile, status, loadingDetail, result,
+ columns, missingColumns, mismatchDetail,
+ records, products,
+ page, pageCount, rows
} = usePredictionTable()
-watch(convertResult, newVal => records.value = newVal)
const analyzeBtnDisabled = computed(() => {
const notHaveAnyProduct = products.value.length < 1
const hasMissingColumn = missingColumns.value.length >= 1
@@ -118,64 +110,36 @@ const modalMakePredictionModel = reactive
({
arimaModel: undefined,
predictionMode: 'optimal'
})
-
-const modal = reactive({
- view: {
- shown: false,
- data: {}
- },
- update: {
- shown: false,
- data: {}
- },
- delete: {
- shown: false
+const predictionResult = ref<{
+ status: 'idle' | 'pending' | 'success' | 'error'
+ result?: TPyPrediction
+}>({
+ status: 'idle',
+ result: undefined,
+})
+const predictionResultHeader = computed(() => {
+ const period = predictionResult.value.result?.data[0].predictionPeriod === 'monthly' ? 'Month' : 'Week'
+ return ([
+ { key: "product", label: "#", sortable: true },
+ { key: "phase1", label: `${period} 1`, sortable: true },
+ { key: "phase2", label: `${period} 2`, sortable: true },
+ { key: "phase3", label: `${period} 3`, sortable: true },
+ ])
+})
+const selectedTab = ref(0)
+watch(() => predictionResult.value.status, newVal => {
+ if (newVal === 'success') {
+ selectedTab.value = 1
}
})
-
-function handleDragFile(e: DragEvent) {
- e.preventDefault();
- const files = e.dataTransfer?.files;
- if (files && files.length > 0) {
- file.value = files[0]
- }
-}
-
-function handleFileInput(e: Event) {
- const target = e.target as HTMLInputElement
- if (target?.files && target.files.length > 0) {
- const uploaded = target.files[0];
- file.value = uploaded;
- }
-}
-
-type TPredictionTableDropdown = DropdownItem & {
- data?: Ref>,
- modalShown?: Ref
-}
-const items:
- TPredictionTableDropdown[][] = [
- [{
- label: 'View',
- icon: 'i-heroicons-eye-20-solid',
- shortcuts: ['V'],
- iconClass: '',
- data: ref({}),
- modalShown: ref(false)
- }, {
- label: 'Edit',
- icon: 'i-heroicons-pencil-square-20-solid',
- shortcuts: ['E'],
- iconClass: '',
- data: ref({}),
- modalShown: ref(false)
- }], [{
- label: 'Delete',
- icon: 'i-heroicons-trash-20-solid',
- shortcuts: ['D'],
- iconClass: '',
- data: ref({}),
- modalShown: ref(false)
- }]
- ]
+const tabItems = [
+ {
+ label: 'Table',
+ icon: 'i-heroicons-table-cells',
+ },
+ {
+ label: 'Result',
+ icon: 'i-heroicons-chart-bar',
+ },
+];
diff --git a/shad-tailwind.config.ts b/shad-tailwind.config.ts
deleted file mode 100644
index fbefb05..0000000
--- a/shad-tailwind.config.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-export default {
- darkMode: ["class"],
- content: [],
- theme: {
- extend: {
- colors: {
- sidebar: {
- DEFAULT: 'hsl(var(--sidebar-background))',
- foreground: 'hsl(var(--sidebar-foreground))',
- primary: 'hsl(var(--sidebar-primary))',
- 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
- accent: 'hsl(var(--sidebar-accent))',
- 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
- border: 'hsl(var(--sidebar-border))',
- ring: 'hsl(var(--sidebar-ring))'
- }
- }
- }
- },
- plugins: [],
-}
\ No newline at end of file
diff --git a/types/api-response/basicResponse.ts b/types/api-response/basicResponse.ts
index c13b7b7..06c1bd3 100644
--- a/types/api-response/basicResponse.ts
+++ b/types/api-response/basicResponse.ts
@@ -5,11 +5,15 @@ export type TAPIResponse> = {
data?: T;
};
-export type TPaginatedResponse> = TAPIResponse & {
- pagination?: {
- total: number;
+interface TPaginatedResult {
+ data: T[];
+ meta: {
page: number;
- pageSize: number;
+ limit: number;
+ total: number;
totalPages: number;
- }
-};
\ No newline at end of file
+ };
+}
+
+export type TPaginatedResponse> =
+ TAPIResponse>
diff --git a/types/api-response/product.ts b/types/api-response/product.ts
new file mode 100644
index 0000000..a732cc2
--- /dev/null
+++ b/types/api-response/product.ts
@@ -0,0 +1,13 @@
+import type { TProductCategoryResponse } from "./product_category"
+
+export type TProductResponse = {
+ id: number
+ product_name: string
+ product_code: string
+ stock: number
+ selling_price: number
+ buying_price: number
+ user_id: number
+ product_category_id?: number
+ category_name: string
+}
\ No newline at end of file
diff --git a/types/api-response/product_category.ts b/types/api-response/product_category.ts
new file mode 100644
index 0000000..2e806e3
--- /dev/null
+++ b/types/api-response/product_category.ts
@@ -0,0 +1,5 @@
+export type TProductCategoryResponse = {
+ id: number
+ category_name: string
+ user_id: number
+}
\ No newline at end of file
diff --git a/types/api-response/py-prediction.ts b/types/api-response/py-prediction.ts
new file mode 100644
index 0000000..1a7b291
--- /dev/null
+++ b/types/api-response/py-prediction.ts
@@ -0,0 +1,11 @@
+export type TPyPrediction = {
+ status: "success" | 'error',
+ data: {
+ predictionPeriod: "weekly" | "monthly",
+ product: string,
+ order: string,
+ phase1: number,
+ phase2: number,
+ phase3: number
+ }[]
+}
\ No newline at end of file
diff --git a/types/api-response/supplier.ts b/types/api-response/supplier.ts
new file mode 100644
index 0000000..63ec297
--- /dev/null
+++ b/types/api-response/supplier.ts
@@ -0,0 +1,7 @@
+export type TSupplierResponse = {
+ address: string
+ contact: string
+ id: number
+ supplier_name: string
+ user_id: number
+}
\ No newline at end of file
diff --git a/utils/spreadsheet/fileReader.ts b/utils/spreadsheet/fileReader.ts
new file mode 100644
index 0000000..45df364
--- /dev/null
+++ b/utils/spreadsheet/fileReader.ts
@@ -0,0 +1,40 @@
+import * as XLSX from 'xlsx';
+
+export async function spreadsheetReader(file: File) {
+ try {
+ const fileBuffer = await file.arrayBuffer();
+ const workbook = XLSX.read(fileBuffer, {
+ cellDates: true,
+ type: 'array',
+ });
+ const worksheet = workbook.Sheets[workbook.SheetNames[0]];
+ return worksheet;
+ } catch (error: unknown) {
+ throw error;
+ }
+}
+
+export function sheet2HeaderNRow(worksheet: XLSX.WorkSheet) {
+ const [headers, ...rows] = XLSX.utils.sheet_to_json(worksheet, {
+ raw: false,
+ header: 1,
+ });
+ return {
+ headers,
+ rows
+ };
+}
+
+export function headerNRow2Sheet(headers: any[], rows: any[][]) {
+ return XLSX.utils.aoa_to_sheet([headers, ...rows]);
+}
+
+export function sheet2JSON(worksheet: XLSX.WorkSheet) {
+ return XLSX.utils.sheet_to_json(worksheet, { defval: "" });
+}
+
+export function sheet2CSV(worksheet: XLSX.WorkSheet) {
+ const csv = XLSX.utils.sheet_to_csv(worksheet);
+ const csvBlob = new Blob([csv], { type: 'text/csv' });
+ return new File([csvBlob], 'converted.csv', { type: 'text/csv' });
+}
diff --git a/utils/spreadsheet/sheetsToJSON.ts b/utils/spreadsheet/sheetsToJSON.ts
deleted file mode 100644
index fa658f2..0000000
--- a/utils/spreadsheet/sheetsToJSON.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as XLSX from 'xlsx';
-
-export async function sheetToJSON(file: File) {
- try {
- const fileBuffer = await file.arrayBuffer();
- const workbook = XLSX.read(fileBuffer);
- const sheet = workbook.Sheets[workbook.SheetNames[0]]
- const json = XLSX.utils.sheet_to_json(sheet)
- return json
- } catch (error: unknown) {
- throw error
- }
-}
\ No newline at end of file