From 22592affd1e6c6c231494dc9c710882e93650065 Mon Sep 17 00:00:00 2001 From: Rynare Date: Sat, 17 May 2025 11:12:05 +0700 Subject: [PATCH] api integration for update and delete product, integration api for create category and delete category --- .vscode/settings.json | 4 + app.vue | 11 + assets/css/shad-tailwind.css | 82 -------- assets/css/sidebar.tw.css | 47 +++++ components.json | 20 -- .../dataset/product-category-input.vue | 24 +++ .../dashboard/dataset/product/modal-add.vue | 62 ++++++ .../dataset/product/modal-category.vue | 78 +++++++ .../dataset/product/modal-delete.vue | 48 +++++ .../dataset/product/modal-update.vue | 73 +++++++ components/dashboard/dataset/products.vue | 79 +++++++ .../dataset/supplier/modal-add-supplier.vue | 49 +++++ .../supplier/modal-delete-supplier.vue | 47 +++++ .../supplier/modal-update-supplier.vue | 53 +++++ components/dashboard/dataset/suppliers.vue | 76 +++++++ components/dashboard/sidebar/index.vue | 188 ----------------- .../landing/demo/modalMakePrediction.vue | 109 ++++------ .../my/dashboard/sidebar/sidebar-group.vue | 42 ++++ .../my/dashboard/sidebar/sidebar-link.vue | 31 +++ components/my/dashboard/sidebar/sidebar.vue | 28 +++ components/{ => my}/loader/clipboard.vue | 0 .../{ => my}/loader/flipping-book-page.vue | 0 components/{ => my}/loader/flipping-card.vue | 0 components/{ => my}/loader/padlock.vue | 0 components/{ => my}/loader/pulse-ring.vue | 0 components/{ => my}/loader/text.vue | 0 .../{ => my}/loader/trailing-letter.vue | 0 components/my/sidebar/index.vue | 41 ++++ components/ui/button/Button.vue | 26 --- components/ui/button/index.ts | 35 ---- components/ui/card/Card.vue | 21 -- components/ui/card/CardContent.vue | 14 -- components/ui/card/CardDescription.vue | 14 -- components/ui/card/CardFooter.vue | 14 -- components/ui/card/CardHeader.vue | 14 -- components/ui/card/CardTitle.vue | 18 -- components/ui/card/index.ts | 6 - components/ui/input/Input.vue | 24 --- components/ui/input/index.ts | 1 - components/ui/label/Label.vue | 27 --- components/ui/label/index.ts | 1 - components/ui/separator/Separator.vue | 38 ---- components/ui/separator/index.ts | 1 - components/ui/sheet/Sheet.vue | 14 -- components/ui/sheet/SheetClose.vue | 11 - components/ui/sheet/SheetContent.vue | 56 ----- components/ui/sheet/SheetDescription.vue | 22 -- components/ui/sheet/SheetFooter.vue | 19 -- components/ui/sheet/SheetHeader.vue | 16 -- components/ui/sheet/SheetTitle.vue | 22 -- components/ui/sheet/SheetTrigger.vue | 11 - components/ui/sheet/index.ts | 31 --- components/ui/sidebar/Sidebar.vue | 85 -------- components/ui/sidebar/SidebarContent.vue | 17 -- components/ui/sidebar/SidebarFooter.vue | 17 -- components/ui/sidebar/SidebarGroup.vue | 17 -- components/ui/sidebar/SidebarGroupAction.vue | 26 --- components/ui/sidebar/SidebarGroupContent.vue | 17 -- components/ui/sidebar/SidebarGroupLabel.vue | 24 --- components/ui/sidebar/SidebarHeader.vue | 17 -- components/ui/sidebar/SidebarInput.vue | 21 -- components/ui/sidebar/SidebarInset.vue | 20 -- components/ui/sidebar/SidebarMenu.vue | 17 -- components/ui/sidebar/SidebarMenuAction.vue | 33 --- components/ui/sidebar/SidebarMenuBadge.vue | 25 --- components/ui/sidebar/SidebarMenuButton.vue | 49 ----- .../ui/sidebar/SidebarMenuButtonChild.vue | 33 --- components/ui/sidebar/SidebarMenuItem.vue | 17 -- components/ui/sidebar/SidebarMenuSkeleton.vue | 33 --- components/ui/sidebar/SidebarMenuSub.vue | 21 -- .../ui/sidebar/SidebarMenuSubButton.vue | 35 ---- components/ui/sidebar/SidebarMenuSubItem.vue | 9 - components/ui/sidebar/SidebarProvider.vue | 80 -------- components/ui/sidebar/SidebarRail.vue | 32 --- components/ui/sidebar/SidebarSeparator.vue | 18 -- components/ui/sidebar/SidebarTrigger.vue | 26 --- components/ui/sidebar/index.ts | 60 ------ components/ui/sidebar/utils.ts | 19 -- components/ui/skeleton/Skeleton.vue | 14 -- components/ui/skeleton/index.ts | 1 - components/ui/tooltip/Tooltip.vue | 14 -- components/ui/tooltip/TooltipContent.vue | 31 --- components/ui/tooltip/TooltipProvider.vue | 11 - components/ui/tooltip/TooltipTrigger.vue | 11 - components/ui/tooltip/index.ts | 4 - composables/autoRefreshAccessToken.ts | 43 ++++ composables/core.ts | 1 + composables/productCategoryFetch.ts | 109 ++++++++++ composables/productFetch.ts | 106 ++++++++++ composables/supplierFetch.ts | 106 ++++++++++ .../{useAuth$fetch.ts => use$fetchAuto.ts} | 61 ++---- composables/useAuth.ts | 2 +- .../{useAuthFetch.ts => useFetchAuto.ts} | 56 ++--- composables/usePredictionFetch.ts | 112 ++++++++++ composables/usePredictionTable.ts | 179 +++++----------- composables/useSpreadsheet.ts | 115 ++++++----- constants/dashboard-menu.ts | 35 ++++ env.d.ts | 1 + layouts/main-copy.vue | 116 +++++++++++ layouts/main.vue | 104 ++++++++-- middleware/guest.ts | 2 +- nuxt.config.ts | 19 +- package-lock.json | 192 +----------------- package.json | 3 +- pages/auth/forgot-password/[token].vue | 2 +- pages/auth/index.vue | 5 + pages/auth/verify/[token].vue | 2 +- pages/dashboard/{ => cashier}/index.vue | 0 pages/dashboard/dataset/index.vue | 15 ++ pages/dashboard/dataset/products/index.vue | 9 + pages/dashboard/dataset/suppliers/index.vue | 15 ++ pages/dashboard/home/index.vue | 9 + pages/dashboard/restock/index.vue | 9 + pages/demo.vue | 166 ++++++--------- shad-tailwind.config.ts | 22 -- types/api-response/basicResponse.ts | 16 +- types/api-response/product.ts | 13 ++ types/api-response/product_category.ts | 5 + types/api-response/py-prediction.ts | 11 + types/api-response/supplier.ts | 7 + utils/spreadsheet/fileReader.ts | 40 ++++ utils/spreadsheet/sheetsToJSON.ts | 13 -- 122 files changed, 1918 insertions(+), 2305 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 assets/css/shad-tailwind.css create mode 100644 assets/css/sidebar.tw.css delete mode 100644 components.json create mode 100644 components/dashboard/dataset/product-category-input.vue create mode 100644 components/dashboard/dataset/product/modal-add.vue create mode 100644 components/dashboard/dataset/product/modal-category.vue create mode 100644 components/dashboard/dataset/product/modal-delete.vue create mode 100644 components/dashboard/dataset/product/modal-update.vue create mode 100644 components/dashboard/dataset/products.vue create mode 100644 components/dashboard/dataset/supplier/modal-add-supplier.vue create mode 100644 components/dashboard/dataset/supplier/modal-delete-supplier.vue create mode 100644 components/dashboard/dataset/supplier/modal-update-supplier.vue create mode 100644 components/dashboard/dataset/suppliers.vue delete mode 100644 components/dashboard/sidebar/index.vue create mode 100644 components/my/dashboard/sidebar/sidebar-group.vue create mode 100644 components/my/dashboard/sidebar/sidebar-link.vue create mode 100644 components/my/dashboard/sidebar/sidebar.vue rename components/{ => my}/loader/clipboard.vue (100%) rename components/{ => my}/loader/flipping-book-page.vue (100%) rename components/{ => my}/loader/flipping-card.vue (100%) rename components/{ => my}/loader/padlock.vue (100%) rename components/{ => my}/loader/pulse-ring.vue (100%) rename components/{ => my}/loader/text.vue (100%) rename components/{ => my}/loader/trailing-letter.vue (100%) create mode 100644 components/my/sidebar/index.vue delete mode 100644 components/ui/button/Button.vue delete mode 100644 components/ui/button/index.ts delete mode 100644 components/ui/card/Card.vue delete mode 100644 components/ui/card/CardContent.vue delete mode 100644 components/ui/card/CardDescription.vue delete mode 100644 components/ui/card/CardFooter.vue delete mode 100644 components/ui/card/CardHeader.vue delete mode 100644 components/ui/card/CardTitle.vue delete mode 100644 components/ui/card/index.ts delete mode 100644 components/ui/input/Input.vue delete mode 100644 components/ui/input/index.ts delete mode 100644 components/ui/label/Label.vue delete mode 100644 components/ui/label/index.ts delete mode 100644 components/ui/separator/Separator.vue delete mode 100644 components/ui/separator/index.ts delete mode 100644 components/ui/sheet/Sheet.vue delete mode 100644 components/ui/sheet/SheetClose.vue delete mode 100644 components/ui/sheet/SheetContent.vue delete mode 100644 components/ui/sheet/SheetDescription.vue delete mode 100644 components/ui/sheet/SheetFooter.vue delete mode 100644 components/ui/sheet/SheetHeader.vue delete mode 100644 components/ui/sheet/SheetTitle.vue delete mode 100644 components/ui/sheet/SheetTrigger.vue delete mode 100644 components/ui/sheet/index.ts delete mode 100644 components/ui/sidebar/Sidebar.vue delete mode 100644 components/ui/sidebar/SidebarContent.vue delete mode 100644 components/ui/sidebar/SidebarFooter.vue delete mode 100644 components/ui/sidebar/SidebarGroup.vue delete mode 100644 components/ui/sidebar/SidebarGroupAction.vue delete mode 100644 components/ui/sidebar/SidebarGroupContent.vue delete mode 100644 components/ui/sidebar/SidebarGroupLabel.vue delete mode 100644 components/ui/sidebar/SidebarHeader.vue delete mode 100644 components/ui/sidebar/SidebarInput.vue delete mode 100644 components/ui/sidebar/SidebarInset.vue delete mode 100644 components/ui/sidebar/SidebarMenu.vue delete mode 100644 components/ui/sidebar/SidebarMenuAction.vue delete mode 100644 components/ui/sidebar/SidebarMenuBadge.vue delete mode 100644 components/ui/sidebar/SidebarMenuButton.vue delete mode 100644 components/ui/sidebar/SidebarMenuButtonChild.vue delete mode 100644 components/ui/sidebar/SidebarMenuItem.vue delete mode 100644 components/ui/sidebar/SidebarMenuSkeleton.vue delete mode 100644 components/ui/sidebar/SidebarMenuSub.vue delete mode 100644 components/ui/sidebar/SidebarMenuSubButton.vue delete mode 100644 components/ui/sidebar/SidebarMenuSubItem.vue delete mode 100644 components/ui/sidebar/SidebarProvider.vue delete mode 100644 components/ui/sidebar/SidebarRail.vue delete mode 100644 components/ui/sidebar/SidebarSeparator.vue delete mode 100644 components/ui/sidebar/SidebarTrigger.vue delete mode 100644 components/ui/sidebar/index.ts delete mode 100644 components/ui/sidebar/utils.ts delete mode 100644 components/ui/skeleton/Skeleton.vue delete mode 100644 components/ui/skeleton/index.ts delete mode 100644 components/ui/tooltip/Tooltip.vue delete mode 100644 components/ui/tooltip/TooltipContent.vue delete mode 100644 components/ui/tooltip/TooltipProvider.vue delete mode 100644 components/ui/tooltip/TooltipTrigger.vue delete mode 100644 components/ui/tooltip/index.ts create mode 100644 composables/autoRefreshAccessToken.ts create mode 100644 composables/productCategoryFetch.ts create mode 100644 composables/productFetch.ts create mode 100644 composables/supplierFetch.ts rename composables/{useAuth$fetch.ts => use$fetchAuto.ts} (60%) rename composables/{useAuthFetch.ts => useFetchAuto.ts} (51%) create mode 100644 composables/usePredictionFetch.ts create mode 100644 constants/dashboard-menu.ts create mode 100644 layouts/main-copy.vue rename pages/dashboard/{ => cashier}/index.vue (100%) create mode 100644 pages/dashboard/dataset/index.vue create mode 100644 pages/dashboard/dataset/products/index.vue create mode 100644 pages/dashboard/dataset/suppliers/index.vue create mode 100644 pages/dashboard/home/index.vue create mode 100644 pages/dashboard/restock/index.vue delete mode 100644 shad-tailwind.config.ts create mode 100644 types/api-response/product.ts create mode 100644 types/api-response/product_category.ts create mode 100644 types/api-response/py-prediction.ts create mode 100644 types/api-response/supplier.ts create mode 100644 utils/spreadsheet/fileReader.ts delete mode 100644 utils/spreadsheet/sheetsToJSON.ts 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 @@ + + 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 @@ + + \ 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 @@ + + \ 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 @@ + + + + \ 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 @@ + + \ 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 @@ + + \ 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 @@ + + \ 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 @@ + + + + \ 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 @@ + + \ 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 @@ + + \ 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 @@ - - \ 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." /> - - - - -
- -
- - - +
+ +
+ + + - - - + + + - - - + + + +
- 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 @@ + + + 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 @@ + + + + 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 @@ + + + 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 @@ - - - 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 @@ - - - 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 @@ - - - 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 @@ \ 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 @@ @@ -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