mishrabp commited on
Commit
b86d7f0
·
verified ·
1 Parent(s): 1c2ff02

Upload folder using huggingface_hub

Browse files
Files changed (48) hide show
  1. .gitattributes +1 -0
  2. .gitignore +30 -0
  3. .npmrc +2 -0
  4. Dockerfile +31 -0
  5. README-bkp.md +115 -0
  6. README.md +47 -6
  7. env.d.ts +21 -0
  8. index.html +13 -0
  9. nginx.conf +25 -0
  10. package-lock.json +0 -0
  11. package.json +34 -0
  12. public/favicon.ico +0 -0
  13. src/App.vue +10 -0
  14. src/assets/logo.png +3 -0
  15. src/assets/main.css +1 -0
  16. src/assets/profile.png +0 -0
  17. src/components/cards/CustomerCard.vue +73 -0
  18. src/components/cards/KPICard.vue +35 -0
  19. src/components/charts/BarChart.vue +61 -0
  20. src/components/charts/LineChart.vue +66 -0
  21. src/components/charts/PieChart.vue +39 -0
  22. src/components/charts/StackedBarChart.vue +43 -0
  23. src/components/forms/OrderForm.vue +119 -0
  24. src/components/grids/CategoryList.vue +222 -0
  25. src/components/grids/CustomerList.vue +29 -0
  26. src/components/grids/OrderList.vue +147 -0
  27. src/components/grids/ProductList.vue +305 -0
  28. src/components/grids/ShipperList.vue +175 -0
  29. src/components/grids/SupplierList.vue +235 -0
  30. src/components/grids/TopCustomerList.vue +34 -0
  31. src/components/nav/Navbar.vue +137 -0
  32. src/main.ts +13 -0
  33. src/router/index.ts +60 -0
  34. src/services/orderService.js +45 -0
  35. src/views/AboutView.vue +15 -0
  36. src/views/HomeView.vue +8 -0
  37. src/views/category/CategoryView.vue +7 -0
  38. src/views/customer/CustomerView.vue +39 -0
  39. src/views/dashboard/DashboardView.vue +238 -0
  40. src/views/order/OrderView.vue +7 -0
  41. src/views/product/ProductView.vue +7 -0
  42. src/views/shipper/ShipperView.vue +7 -0
  43. src/views/supplier/SupplierView.vue +7 -0
  44. tsconfig.app.json.txt +12 -0
  45. tsconfig.json +31 -0
  46. tsconfig.json.txt +11 -0
  47. tsconfig.node.json.txt +19 -0
  48. vite.config.ts +43 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ src/assets/logo.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ .DS_Store
12
+ dist
13
+ dist-ssr
14
+ coverage
15
+ *.local
16
+
17
+ /cypress/videos/
18
+ /cypress/screenshots/
19
+
20
+ # Editor directories and files
21
+ .vscode/*
22
+ !.vscode/extensions.json
23
+ .idea
24
+ *.suo
25
+ *.ntvs*
26
+ *.njsproj
27
+ *.sln
28
+ *.sw?
29
+
30
+ *.tsbuildinfo
.npmrc ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ @bibhu2020:registry=https://npm.pkg.github.com
2
+ //npm.pkg.github.com/:_authToken=${GH_TOKEN}
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Step 1: Build the app
2
+ #FROM node:20 AS build #Commented this line to remediate 1es warnings
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy package.json and package-lock.json
7
+ COPY package*.json ./
8
+
9
+ # Install dependencies
10
+ RUN npm install
11
+
12
+ # Copy the rest of the application
13
+ COPY . .
14
+
15
+ # Build the app for production
16
+ RUN npm run build-only
17
+
18
+ # Step 2: Serve the app
19
+ #FROM nginx:alpine #Commented this line to remediate 1es warnings
20
+
21
+ # Copy custom Nginx config
22
+ COPY nginx.conf /etc/nginx/nginx.conf
23
+
24
+ # Copy the built app from the build stage to the nginx HTML directory
25
+ #COPY --from=build /app/dist /usr/share/nginx/html #Commented this line to remediate 1es warnings
26
+
27
+ # Expose the port the app will be running on
28
+ EXPOSE 80
29
+
30
+ # Start nginx
31
+ CMD ["nginx", "-g", "daemon off;"]
README-bkp.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Vue.js App on Hugging Face Spaces (Northwind)
2
+
3
+ This repository hosts a **Vue.js application** deployed on **Hugging Face Spaces** using the **Static SDK**.
4
+ It allows you to serve any frontend built with Vue, React, or plain HTML/CSS/JS directly on Spaces.
5
+
6
+ ---
7
+
8
+ ## 📦 Project Structure
9
+
10
+ ```
11
+ .
12
+ ├── public/ # Static assets
13
+ ├── src/ # Vue app source code
14
+ ├── dist/ # Production build output (after `npm run build`)
15
+ ├── package.json
16
+ ├── .gitignore
17
+ └── README.md
18
+ ```
19
+
20
+ ---
21
+
22
+ ## ⚙️ Setup Instructions
23
+
24
+ ### 1. Install Dependencies
25
+ ```bash
26
+ npm install
27
+ ```
28
+
29
+ ### 2. Build the App
30
+ ```bash
31
+ npm run build
32
+ ```
33
+
34
+ This will create a production-ready `dist/` folder.
35
+
36
+ ### 3. Add a Hugging Face `space.yaml`
37
+
38
+ Create a file named `space.yaml` in the root directory:
39
+
40
+ ```yaml
41
+ sdk: static
42
+ app_file: dist
43
+ title: "Vue.js App"
44
+ emoji: 🚀
45
+ colorFrom: green
46
+ colorTo: blue
47
+ pinned: false
48
+ license: mit
49
+ ```
50
+
51
+ ### 4. Commit and Push
52
+
53
+ Push your code to a Hugging Face Space repository:
54
+
55
+ ```bash
56
+ git add .
57
+ git commit -m "Deploy Vue.js app to Hugging Face Spaces"
58
+ git push
59
+ ```
60
+
61
+ The app will automatically build and deploy!
62
+
63
+ ---
64
+
65
+ ## 🌐 Customization
66
+
67
+ - Update your app name and metadata in `space.yaml`.
68
+ - You can add a favicon in `public/`.
69
+ - Use environment variables by prefixing them with `VITE_` (e.g., `VITE_API_URL`).
70
+
71
+ ---
72
+
73
+ ## 🧠 Notes
74
+
75
+ - Hugging Face automatically hosts the contents of `dist/` as a static site.
76
+ - Make sure to commit the built folder (`dist/`) or ensure your build process runs automatically in CI.
77
+
78
+ ---
79
+
80
+ ## 📄 License
81
+ MIT © 2025 Bibhu Mishra
82
+
83
+ ## northwind
84
+
85
+ This template should help get you started developing with Vue 3 in Vite.
86
+
87
+ ## Recommended IDE Setup
88
+
89
+ [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
90
+
91
+ ### Type Support for `.vue` Imports in TS
92
+
93
+ TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
94
+
95
+ ### Customize configuration
96
+
97
+ See [Vite Configuration Reference](https://vite.dev/config/).
98
+
99
+ ### Project Setup
100
+
101
+ ```sh
102
+ npm install
103
+ ```
104
+
105
+ #### Compile and Hot-Reload for Development
106
+
107
+ ```sh
108
+ npm run dev
109
+ ```
110
+
111
+ #### Type-Check, Compile and Minify for Production
112
+
113
+ ```sh
114
+ npm run build
115
+ ```
README.md CHANGED
@@ -1,11 +1,52 @@
1
  ---
2
  title: Northwind
3
- emoji: 📊
4
- colorFrom: gray
5
- colorTo: red
6
- sdk: docker
7
  pinned: false
8
- short_description: A web application built using vuejs. (connects to northwind)
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Northwind
3
+ emoji: 🐢
4
+ colorFrom: purple
5
+ colorTo: indigo
6
+ sdk: static
7
  pinned: false
8
+ app_build_command: npm run build
9
+ app_file: dist/index.html
10
+ license: mit
11
+ short_description: Its a Vuejs Web App built on Northwind dB.
12
  ---
13
 
14
+ # vue
15
+
16
+ This template should help get you started developing with Vue 3 in Vite.
17
+
18
+ ## Recommended IDE Setup
19
+
20
+ [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
21
+
22
+ ## Type Support for `.vue` Imports in TS
23
+
24
+ TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
25
+
26
+ ## Customize configuration
27
+
28
+ See [Vite Configuration Reference](https://vite.dev/config/).
29
+
30
+ ## Project Setup
31
+
32
+ ```sh
33
+ npm install
34
+ ```
35
+
36
+ ### Compile and Hot-Reload for Development
37
+
38
+ ```sh
39
+ npm run dev
40
+ ```
41
+
42
+ ### Type-Check, Compile and Minify for Production
43
+
44
+ ```sh
45
+ npm run build
46
+ ```
47
+
48
+ ### Lint with [ESLint](https://eslint.org/)
49
+
50
+ ```sh
51
+ npm run lint
52
+ ```
env.d.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ // This allows importing `.vue` files without TypeScript errors
4
+ declare module '*.vue' {
5
+ import { DefineComponent } from 'vue';
6
+ const component: DefineComponent<{}, {}, any>;
7
+ export default component;
8
+ }
9
+
10
+ // Extend the Vite environment variables (optional but recommended)
11
+ interface ImportMetaEnv {
12
+ readonly VITE_API_URL: string;
13
+ readonly VITE_APP_PORT: string;
14
+ // Add more VITE_ prefixed env variables here as needed
15
+ }
16
+
17
+ interface ImportMeta {
18
+ readonly env: ImportMetaEnv;
19
+ }
20
+
21
+
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" href="/favicon.ico">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Northwind</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
nginx.conf ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ worker_processes auto;
2
+
3
+ events {
4
+ worker_connections 1024;
5
+ }
6
+
7
+ http {
8
+ include mime.types;
9
+ default_type application/octet-stream;
10
+
11
+ sendfile on;
12
+ keepalive_timeout 65;
13
+
14
+ server {
15
+ listen 3011;
16
+ server_name localhost;
17
+
18
+ root /usr/share/nginx/html;
19
+ index index.html;
20
+
21
+ location / {
22
+ try_files $uri $uri/ /index.html;
23
+ }
24
+ }
25
+ }
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "northwind",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "start-dev": "vite --mode development",
8
+ "build": "run-p type-check \"build-only {@}\" --",
9
+ "preview": "vite preview --port 3000",
10
+ "build-only": "vite build --mode production",
11
+ "type-check": "vue-tsc --build"
12
+ },
13
+ "dependencies": {
14
+ "axios": "^1.9.0",
15
+ "chart.js": "^4.4.9",
16
+ "primeicons": "^7.0.0",
17
+ "vue": "^3.5.13",
18
+ "vue-router": "^4.5.0",
19
+ "vue-spinner": "^1.0.4"
20
+ },
21
+ "devDependencies": {
22
+ "@tailwindcss/vite": "^4.1.4",
23
+ "@tsconfig/node22": "^22.0.1",
24
+ "@types/node": "^22.14.0",
25
+ "@vitejs/plugin-vue": "^5.2.3",
26
+ "@vue/tsconfig": "^0.7.0",
27
+ "npm-run-all2": "^7.0.2",
28
+ "tailwindcss": "^4.1.4",
29
+ "typescript": "~5.8.0",
30
+ "vite": "^6.2.4",
31
+ "vite-plugin-vue-devtools": "^7.7.2",
32
+ "vue-tsc": "^2.2.8"
33
+ }
34
+ }
public/favicon.ico ADDED
src/App.vue ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { RouterLink, RouterView } from 'vue-router'
3
+ import Navbar from '@/components/nav/Navbar.vue'
4
+ </script>
5
+
6
+ <template>
7
+ <Navbar/>
8
+
9
+ <RouterView />
10
+ </template>
src/assets/logo.png ADDED

Git LFS Details

  • SHA256: b1cffa6a5b54576016b1ac7405144b0d708b8004f23c6efb4351d9fd0af67598
  • Pointer size: 132 Bytes
  • Size of remote file: 1.12 MB
src/assets/main.css ADDED
@@ -0,0 +1 @@
 
 
1
+ @import "tailwindcss";
src/assets/profile.png ADDED
src/components/cards/CustomerCard.vue ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ customer: {
4
+ CustomerID: string;
5
+ CompanyName: string;
6
+ ContactName: string;
7
+ ContactTitle: string;
8
+ Address: string;
9
+ City: string;
10
+ Region: string | null;
11
+ PostalCode: string;
12
+ Country: string;
13
+ Phone: string;
14
+ Fax: string | null;
15
+ };
16
+ }>();
17
+ </script>
18
+
19
+ <template>
20
+ <div class="bg-white rounded-2xl shadow-md p-6 transition-transform duration-200 hover:-translate-y-1 hover:shadow-lg">
21
+ <!-- Header Section -->
22
+ <div class="flex items-center gap-4">
23
+ <!-- Profile Picture -->
24
+ <div class="w-16 h-16 aspect-square rounded-full overflow-hidden shrink-0 flex items-center justify-center bg-blue-600 text-white text-xl font-bold">
25
+ <!-- Commented out until PhotoURL is added to the customer type
26
+ <img
27
+ v-if="customer.PhotoURL"
28
+ :src="customer.PhotoURL"
29
+ alt="Profile Picture"
30
+ class="w-full h-full object-cover"
31
+ />
32
+ <span v-else>
33
+ {{ customer.ContactName.charAt(0).toUpperCase() }}
34
+ </span>
35
+ -->
36
+ <span>
37
+ {{ customer.ContactName.charAt(0).toUpperCase() }}
38
+ </span>
39
+ </div>
40
+
41
+
42
+
43
+ <!-- Title and Contact -->
44
+ <div>
45
+ <h2 class="text-lg font-semibold text-gray-900">{{ customer.CompanyName }}</h2>
46
+ <span class="inline-block mt-1 px-2 py-0.5 text-xs font-medium text-blue-700 bg-blue-100 rounded">
47
+ {{ customer.ContactTitle }}
48
+ </span>
49
+ <p class="text-sm text-gray-600 mt-1">{{ customer.ContactName }}</p>
50
+ </div>
51
+ </div>
52
+
53
+ <!-- Info Section -->
54
+ <div class="mt-4 space-y-2 text-sm text-gray-700">
55
+ <p class="flex items-center gap-2">
56
+ <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
57
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
58
+ d="M17.657 16.657L13 21.314l-4.657-4.657A8 8 0 1117.657 16.657z" />
59
+ </svg>
60
+ {{ customer.Address }}, {{ customer.City }}
61
+ <span v-if="customer.Region">, {{ customer.Region }}</span>,
62
+ {{ customer.Country }} {{ customer.PostalCode }}
63
+ </p>
64
+ <p class="flex items-center gap-2">
65
+ <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
66
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
67
+ d="M3 10l1.664-1.333a1.5 1.5 0 011.865 0L12 12.5l5.471-3.833a1.5 1.5 0 011.865 0L21 10m-9 2v4m0 0l-3-3m3 3l3-3" />
68
+ </svg>
69
+ {{ customer.Phone }}
70
+ </p>
71
+ </div>
72
+ </div>
73
+ </template>
src/components/cards/KPICard.vue ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts" setup>
2
+ import { defineProps, computed } from 'vue';
3
+
4
+ const props = defineProps({
5
+ totalOrders: {
6
+ type: String,
7
+ default: '0'
8
+ },
9
+ color: {
10
+ type: String,
11
+ default: 'blue'
12
+ },
13
+ name: {
14
+ type: String,
15
+ default: ''
16
+ }
17
+ });
18
+
19
+ // Computed style object based on the color prop
20
+ const dynamicStyle = computed(() => ({
21
+ iconColor: "pi pi-shopping-cart text-4xl text-" + props.color + "-500",
22
+ fontColor: "text-3xl font-bold mt-2 text-" + props.color + "-700"
23
+ }));
24
+
25
+ </script>
26
+
27
+ <template>
28
+ <div class="bg-white p-6 rounded-2xl shadow-md flex items-center space-x-4 hover:shadow-lg transition">
29
+ <i :class="dynamicStyle.iconColor"></i>
30
+ <div>
31
+ <h2 class="text-gray-500 text-sm">{{ name }}</h2>
32
+ <p :class="dynamicStyle.fontColor">{{ totalOrders }}</p>
33
+ </div>
34
+ </div>
35
+ </template>
src/components/charts/BarChart.vue ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="bg-white p-6 rounded-2xl shadow-md">
3
+ <h2 class="text-xl font-semibold mb-4">{{title}}</h2>
4
+ <div class="h-72">
5
+ <div class="w-full h-64 relative">
6
+ <canvas ref="canvas" class="absolute inset-0 w-full h-full"></canvas>
7
+ </div>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup>
13
+ import { onMounted, ref, watch, onUnmounted } from 'vue'
14
+ import { Chart } from 'chart.js/auto'
15
+
16
+ const props = defineProps({
17
+ chartData: Object,
18
+ title: String
19
+ })
20
+
21
+ const canvas = ref(null)
22
+ let chartInstance = null
23
+
24
+ const createChart = () => {
25
+ if (chartInstance) {
26
+ chartInstance.destroy() // destroy old instance before creating a new one
27
+ }
28
+
29
+ chartInstance = new Chart(canvas.value, {
30
+ type: 'bar',
31
+ data: props.chartData,
32
+ options: {
33
+ responsive: true,
34
+ maintainAspectRatio: false,
35
+ plugins: {
36
+ legend: {
37
+ position: 'bottom'
38
+ }
39
+ },
40
+ scales: {
41
+ x: { ticks: { color: '#6b7280' } },
42
+ y: { ticks: { color: '#6b7280' } }
43
+ }
44
+ }
45
+ })
46
+ }
47
+
48
+ onMounted(() => {
49
+ createChart()
50
+ })
51
+
52
+ watch(() => props.chartData, (newData) => {
53
+ createChart()
54
+ })
55
+
56
+ onUnmounted(() => {
57
+ if (chartInstance) {
58
+ chartInstance.destroy()
59
+ }
60
+ })
61
+ </script>
src/components/charts/LineChart.vue ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="bg-white p-6 rounded-2xl shadow-md">
3
+ <h2 class="text-xl font-semibold mb-4">{{ title }}</h2>
4
+ <div class="h-72">
5
+ <div class="w-full h-64 relative">
6
+ <canvas ref="canvas" class="absolute inset-0 w-full h-full"></canvas>
7
+ </div>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup>
13
+ import { onMounted, ref, watch, onUnmounted, defineProps } from 'vue'
14
+ import { Chart } from 'chart.js/auto'
15
+
16
+ const props = defineProps({
17
+ chartData: Object,
18
+ title: String
19
+ })
20
+
21
+ const canvas = ref(null)
22
+ let chartInstance = null
23
+
24
+ const createChart = () => {
25
+ if (chartInstance) {
26
+ chartInstance.destroy()
27
+ }
28
+
29
+ chartInstance = new Chart(canvas.value, {
30
+ type: 'line',
31
+ data: props.chartData,
32
+ options: {
33
+ responsive: true,
34
+ maintainAspectRatio: false,
35
+ plugins: {
36
+ legend: {
37
+ position: 'bottom'
38
+ }
39
+ },
40
+ scales: {
41
+ x: { ticks: { color: '#6b7280' } },
42
+ y: { ticks: { color: '#6b7280' } }
43
+ },
44
+ elements: {
45
+ line: {
46
+ tension: 0.4 // smooth curves
47
+ }
48
+ }
49
+ }
50
+ })
51
+ }
52
+
53
+ onMounted(() => {
54
+ createChart()
55
+ })
56
+
57
+ watch(() => props.chartData, () => {
58
+ createChart()
59
+ })
60
+
61
+ onUnmounted(() => {
62
+ if (chartInstance) {
63
+ chartInstance.destroy()
64
+ }
65
+ })
66
+ </script>
src/components/charts/PieChart.vue ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="bg-white p-6 rounded-2xl shadow-md">
3
+ <h2 class="text-xl font-semibold mb-4">{{ title }}</h2>
4
+ <div class="h-72">
5
+ <div class="w-full h-64 relative">
6
+ <canvas ref="canvas" class="absolute inset-0 w-full h-full"></canvas>
7
+ </div>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup>
13
+ import { onMounted, ref } from 'vue';
14
+ import { Chart, PieController, ArcElement, Tooltip, Legend } from 'chart.js';
15
+
16
+ Chart.register(PieController, ArcElement, Tooltip, Legend);
17
+
18
+ const props = defineProps({
19
+ chartData: Object,
20
+ title: String
21
+ });
22
+
23
+ const canvas = ref(null);
24
+
25
+ onMounted(() => {
26
+ new Chart(canvas.value, {
27
+ type: 'pie',
28
+ data: props.chartData,
29
+ options: {
30
+ responsive: true,
31
+ maintainAspectRatio: false,
32
+ plugins: {
33
+ legend: { position: 'bottom' }
34
+ }
35
+ }
36
+ });
37
+ });
38
+ </script>
39
+
src/components/charts/StackedBarChart.vue ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="bg-white p-6 rounded-2xl shadow-md">
3
+ <h2 class="text-xl font-semibold mb-4">{{ title }}</h2>
4
+ <div class="h-72">
5
+ <div class="w-full h-64 relative">
6
+ <canvas ref="canvas" class="absolute inset-0 w-full h-full"></canvas>
7
+ </div>
8
+ </div>
9
+ </div>
10
+ </template>
11
+
12
+ <script setup>
13
+ import { onMounted, ref } from 'vue';
14
+ import { Chart, BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';
15
+
16
+ Chart.register(BarController, BarElement, CategoryScale, LinearScale, Tooltip, Legend);
17
+
18
+ const props = defineProps({
19
+ chartData: Object,
20
+ title: String
21
+ });
22
+
23
+ const canvas = ref(null);
24
+
25
+ onMounted(() => {
26
+ new Chart(canvas.value, {
27
+ type: 'bar',
28
+ data: props.chartData,
29
+ options: {
30
+ responsive: true,
31
+ maintainAspectRatio: false,
32
+ scales: {
33
+ x: { stacked: true },
34
+ y: { stacked: true }
35
+ },
36
+ plugins: {
37
+ legend: { position: 'bottom' }
38
+ }
39
+ }
40
+ });
41
+ });
42
+ </script>
43
+
src/components/forms/OrderForm.vue ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- src/components/OrderForm.vue -->
2
+ <template>
3
+ <div class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center">
4
+ <div class="bg-white rounded-lg w-full max-w-2xl p-6 space-y-4">
5
+ <h2 class="text-2xl font-bold">{{ order?.id ? 'Edit Order' : 'New Order' }}</h2>
6
+
7
+ <div class="grid grid-cols-2 gap-4">
8
+ <div>
9
+ <label class="block mb-1">Customer</label>
10
+ <select v-model="form.customer" class="w-full p-2 border rounded">
11
+ <option v-for="c in dropdowns.customers" :key="c" :value="c">{{ c }}</option>
12
+ </select>
13
+ </div>
14
+
15
+ <div>
16
+ <label class="block mb-1">Employee</label>
17
+ <select v-model="form.employee" class="w-full p-2 border rounded">
18
+ <option v-for="e in dropdowns.employees" :key="e" :value="e">{{ e }}</option>
19
+ </select>
20
+ </div>
21
+
22
+ <div>
23
+ <label class="block mb-1">Shipper</label>
24
+ <select v-model="form.shipper" class="w-full p-2 border rounded">
25
+ <option v-for="s in dropdowns.shippers" :key="s" :value="s">{{ s }}</option>
26
+ </select>
27
+ </div>
28
+
29
+ <div>
30
+ <label class="block mb-1">Ship City</label>
31
+ <input v-model="form.shipCity" type="text" class="w-full p-2 border rounded" />
32
+ </div>
33
+
34
+ <div>
35
+ <label class="block mb-1">Order Date</label>
36
+ <input v-model="form.orderDate" type="date" class="w-full p-2 border rounded" />
37
+ </div>
38
+
39
+ <div>
40
+ <label class="block mb-1">Shipped Date</label>
41
+ <input v-model="form.shippedDate" type="date" class="w-full p-2 border rounded" />
42
+ </div>
43
+ </div>
44
+
45
+ <div>
46
+ <h3 class="text-lg font-semibold mt-4">Order Items</h3>
47
+ <div v-for="(item, index) in form.items" :key="index" class="flex gap-2 items-center mt-2">
48
+ <select v-model="item.product" class="flex-1 p-2 border rounded">
49
+ <option v-for="p in dropdowns.products" :key="p" :value="p">{{ p }}</option>
50
+ </select>
51
+ <input v-model.number="item.quantity" type="number" placeholder="Qty" class="w-20 p-2 border rounded" />
52
+ <input v-model.number="item.price" type="number" placeholder="Price" class="w-24 p-2 border rounded" />
53
+ <button @click="removeItem(index)" class="text-red-600">🗑️</button>
54
+ </div>
55
+
56
+ <button @click="addItem" class="mt-2 bg-green-500 text-white px-4 py-1 rounded hover:bg-green-600">
57
+ ➕ Add Item
58
+ </button>
59
+ </div>
60
+
61
+ <div class="flex justify-end gap-4 mt-6">
62
+ <button @click="$emit('close')" class="bg-gray-400 text-white px-4 py-2 rounded hover:bg-gray-500">
63
+ Cancel
64
+ </button>
65
+ <button @click="saveOrder" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
66
+ Save
67
+ </button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </template>
72
+
73
+ <script setup>
74
+ import { ref, onMounted } from 'vue'
75
+ import orderService from '@/services/orderService'
76
+
77
+ const props = defineProps(['order'])
78
+ const emit = defineEmits(['close', 'saved'])
79
+
80
+ const form = ref({
81
+ customer: '',
82
+ employee: '',
83
+ shipper: '',
84
+ shipCity: '',
85
+ orderDate: '',
86
+ shippedDate: '',
87
+ freight: 0,
88
+ items: []
89
+ })
90
+
91
+ const dropdowns = ref({
92
+ customers: [],
93
+ employees: [],
94
+ shippers: [],
95
+ products: []
96
+ })
97
+
98
+ onMounted(async () => {
99
+ dropdowns.value = await orderService.getDropdownData()
100
+ if (props.order) {
101
+ form.value = JSON.parse(JSON.stringify(props.order))
102
+ }
103
+ })
104
+
105
+ const addItem = () => {
106
+ form.value.items.push({ product: '', quantity: 1, price: 0 })
107
+ }
108
+
109
+ const removeItem = (index) => {
110
+ form.value.items.splice(index, 1)
111
+ }
112
+
113
+ const saveOrder = async () => {
114
+ await orderService.saveOrder(form.value)
115
+ emit('saved')
116
+ emit('close')
117
+ }
118
+ </script>
119
+
src/components/grids/CategoryList.vue ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="p-8 bg-gray-100 min-h-screen">
3
+ <div class="flex justify-between items-center mb-6">
4
+ <h1 class="text-3xl font-bold text-gray-800">📋 Categories</h1>
5
+ <button
6
+ class="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-full shadow hover:bg-blue-700 transition"
7
+ @click="openCreateModal"
8
+ >
9
+ ➕ New Category
10
+ </button>
11
+ </div>
12
+
13
+ <div v-if="!state.isLoading">
14
+ <div class="overflow-x-auto bg-white rounded-2xl shadow">
15
+ <table class="min-w-full divide-y divide-gray-200">
16
+ <thead class="bg-gray-50 sticky top-0">
17
+ <tr>
18
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">ID</th>
19
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Name</th>
20
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Description</th>
21
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Actions</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody class="bg-white divide-y divide-gray-200">
25
+ <tr
26
+ v-for="category in state.categories"
27
+ :key="category.CategoryID"
28
+ class="hover:bg-gray-50 transition"
29
+ >
30
+ <td class="px-6 py-4 text-sm font-medium text-gray-800">{{ category.CategoryID }}</td>
31
+ <td class="px-6 py-4 text-sm font-medium text-gray-800">{{ category.CategoryName }}</td>
32
+ <td class="px-6 py-4 text-sm text-gray-700">{{ category.Description }}</td>
33
+ <td class="px-6 py-4 text-sm font-medium text-gray-500">
34
+ <div class="flex items-center space-x-2">
35
+ <button
36
+ class="flex items-center text-blue-600 hover:text-blue-700"
37
+ @click.stop="startEdit(category)"
38
+ >
39
+ <span class="mr-1">📝</span>
40
+ <span>Edit</span>
41
+ </button>
42
+ <button
43
+ class="flex items-center text-red-600 hover:text-red-700"
44
+ @click.stop="deleteCategory(category.CategoryID)"
45
+ >
46
+ <span class="mr-1">🗑️</span>
47
+ <span>Delete</span>
48
+ </button>
49
+ </div>
50
+ </td>
51
+ </tr>
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+ </div>
56
+
57
+ <div v-else class="flex justify-center py-20">
58
+ <RotateLoader color="#102841" size="15px" />
59
+ </div>
60
+
61
+ <!-- Create Modal -->
62
+ <div v-if="showCreateModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
63
+ <div class="bg-white p-6 rounded-lg w-full max-w-md shadow-lg">
64
+ <h2 class="text-xl font-bold mb-4">Create New Category</h2>
65
+
66
+ <div class="space-y-4">
67
+ <div>
68
+ <label class="block text-gray-700">Name</label>
69
+ <input
70
+ v-model="newName"
71
+ class="border rounded p-2 w-full"
72
+ placeholder="Enter category name"
73
+ />
74
+ </div>
75
+ <div>
76
+ <label class="block text-gray-700">Description</label>
77
+ <input
78
+ v-model="newDescription"
79
+ class="border rounded p-2 w-full"
80
+ placeholder="Enter description"
81
+ />
82
+ </div>
83
+ </div>
84
+
85
+ <div class="flex justify-end space-x-2 mt-6">
86
+ <button
87
+ @click="showCreateModal = false"
88
+ class="px-4 py-2 border rounded hover:bg-gray-100"
89
+ >
90
+ Cancel
91
+ </button>
92
+ <button
93
+ @click="createCategory"
94
+ class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
95
+ >
96
+ Save
97
+ </button>
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- Edit Modal -->
103
+ <div v-if="showEditModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
104
+ <div class="bg-white p-6 rounded-lg w-full max-w-md shadow-lg">
105
+ <h2 class="text-xl font-bold mb-4">Edit Category</h2>
106
+
107
+ <div class="space-y-4">
108
+ <div>
109
+ <label class="block text-gray-700">Name</label>
110
+ <input
111
+ v-model="editName"
112
+ class="border rounded p-2 w-full"
113
+ />
114
+ </div>
115
+ <div>
116
+ <label class="block text-gray-700">Description</label>
117
+ <input
118
+ v-model="editDescription"
119
+ class="border rounded p-2 w-full"
120
+ />
121
+ </div>
122
+ </div>
123
+
124
+ <div class="flex justify-end space-x-2 mt-6">
125
+ <button
126
+ @click="showEditModal = false"
127
+ class="px-4 py-2 border rounded hover:bg-gray-100"
128
+ >
129
+ Cancel
130
+ </button>
131
+ <button
132
+ @click="saveEdit"
133
+ class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
134
+ >
135
+ Save
136
+ </button>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ </div>
142
+ </template>
143
+
144
+ <script setup>
145
+ import { ref, reactive, onMounted } from 'vue'
146
+ import axios from 'axios'
147
+ import RotateLoader from 'vue-spinner/src/RotateLoader.vue'
148
+
149
+ const state = reactive({
150
+ categories: [],
151
+ isLoading: true
152
+ })
153
+
154
+ const showCreateModal = ref(false)
155
+ const showEditModal = ref(false)
156
+ const newName = ref('')
157
+ const newDescription = ref('')
158
+ const editId = ref(null)
159
+ const editName = ref('')
160
+ const editDescription = ref('')
161
+
162
+ async function fetchCategories() {
163
+ try {
164
+ const { data } = await axios.get(`${import.meta.env.VITE_API_URL}/categories`)
165
+ state.categories = data
166
+ } finally {
167
+ state.isLoading = false
168
+ }
169
+ }
170
+
171
+ function startEdit(category) {
172
+ editId.value = category.CategoryID
173
+ editName.value = category.CategoryName
174
+ editDescription.value = category.Description
175
+ showEditModal.value = true
176
+ }
177
+
178
+ async function saveEdit() {
179
+ try {
180
+ await axios.put(`/api/categories/${editId.value}`, {
181
+ CategoryName: editName.value,
182
+ Description: editDescription.value
183
+ })
184
+ const category = state.categories.find(c => c.CategoryID === editId.value)
185
+ category.CategoryName = editName.value
186
+ category.Description = editDescription.value
187
+ showEditModal.value = false
188
+ } catch (error) {
189
+ console.error(error)
190
+ }
191
+ }
192
+
193
+ async function deleteCategory(id) {
194
+ try {
195
+ await axios.delete(`/api/categories/${id}`)
196
+ state.categories = state.categories.filter(c => c.CategoryID !== id)
197
+ } catch (error) {
198
+ console.error(error)
199
+ }
200
+ }
201
+
202
+ function openCreateModal() {
203
+ newName.value = ''
204
+ newDescription.value = ''
205
+ showCreateModal.value = true
206
+ }
207
+
208
+ async function createCategory() {
209
+ try {
210
+ const { data } = await axios.post('/api/categories', {
211
+ CategoryName: newName.value,
212
+ Description: newDescription.value
213
+ })
214
+ state.categories.push(data)
215
+ showCreateModal.value = false
216
+ } catch (error) {
217
+ console.error(error)
218
+ }
219
+ }
220
+
221
+ onMounted(fetchCategories)
222
+ </script>
src/components/grids/CustomerList.vue ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import CustomerCard from '@/components/cards/CustomerCard.vue';
3
+
4
+ defineProps<{
5
+ customers: Array<{
6
+ CustomerID: string;
7
+ CompanyName: string;
8
+ ContactName: string;
9
+ ContactTitle: string;
10
+ Address: string;
11
+ City: string;
12
+ Region: string | null;
13
+ PostalCode: string;
14
+ Country: string;
15
+ Phone: string;
16
+ Fax: string | null;
17
+ }>;
18
+ }>();
19
+ </script>
20
+
21
+ <template>
22
+ <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
23
+ <CustomerCard
24
+ v-for="customer in customers"
25
+ :key="customer.CustomerID"
26
+ :customer="customer"
27
+ />
28
+ </div>
29
+ </template>
src/components/grids/OrderList.vue ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="p-8 bg-gray-100 min-h-screen">
3
+ <div class="flex justify-between items-center mb-6">
4
+ <h1 class="text-3xl font-bold text-gray-800"><svg class="w-8 h-8 text-gray-800 inline-block mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
5
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 3h18l-1.2 8H4.2L3 3zm0 0L1 9l1 11h16l1-11-2.2-6H3z"/>
6
+ </svg>
7
+ Orders</h1>
8
+ <button
9
+ class="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-full shadow hover:bg-blue-700 transition"
10
+ @click="openForm()"
11
+ >
12
+ ➕ Add Order
13
+ </button>
14
+ </div>
15
+
16
+ <div v-if="!state.isLoading">
17
+ <div class="overflow-x-auto bg-white rounded-2xl shadow">
18
+ <table class="min-w-full divide-y divide-gray-200">
19
+ <thead class="bg-gray-50 sticky top-0">
20
+ <tr>
21
+ <th class="px-6 py-3">Expand</th>
22
+ <th class="px-6 py-3">Customer</th>
23
+ <th class="px-6 py-3">Order Date</th>
24
+ <th class="px-6 py-3">Shipped Date</th>
25
+ <th class="px-6 py-3">City</th>
26
+ <th class="px-6 py-3">Actions</th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ <template v-for="order in state.orders" :key="order.id">
31
+ <tr class="hover:bg-gray-50 transition">
32
+ <td class="px-6 py-4 text-center">
33
+ <button
34
+ @click.stop="toggleExpand(order.id)"
35
+ class="text-gray-500 hover:text-blue-600 focus:outline-none"
36
+ >
37
+ <span v-if="expandedOrderId === order.id">🔽</span>
38
+ <span v-else>▶️</span>
39
+ </button>
40
+ </td>
41
+ <td class="px-6 py-4">{{ order.Customer.CompanyName }}</td>
42
+ <td class="px-6 py-4">{{ order.OrderDate }}</td>
43
+ <td class="px-6 py-4">
44
+ <span :class="order.ShippedDate ? 'text-green-600' : 'text-yellow-500'">
45
+ {{ order.ShippedDate || 'Pending' }}
46
+ </span>
47
+ </td>
48
+ <td class="px-6 py-4">{{ order.ShipCity }}</td>
49
+ <td class="px-6 py-4 flex gap-2">
50
+ <button
51
+ class="text-blue-600 hover:underline"
52
+ @click.stop="openForm(order)"
53
+ >
54
+ 📝 Edit
55
+ </button>
56
+ <button
57
+ class="text-red-600 hover:underline"
58
+ @click.stop="deleteOrder(order.id)"
59
+ >
60
+ 🗑️ Delete
61
+ </button>
62
+ </td>
63
+ </tr>
64
+
65
+ <!-- Expanded Row for Items -->
66
+ <tr v-if="expandedOrderId === order.id">
67
+ <td colspan="6" class="bg-gray-50 px-6 py-4">
68
+ <div v-if="order.items?.length" class="space-y-2">
69
+ <div
70
+ v-for="item in order.items"
71
+ :key="item.id"
72
+ class="flex justify-between text-sm text-gray-600"
73
+ >
74
+ <span>{{ item.product }}</span>
75
+ <div class="flex gap-2">
76
+ <span>Qty: {{ item.quantity }}</span>
77
+ <span>💲{{ item.price }}</span>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ <div v-else class="text-gray-400 text-sm">
82
+ No items found for this order.
83
+ </div>
84
+ </td>
85
+ </tr>
86
+ </template>
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+ </div>
91
+
92
+ <div v-else class="flex justify-center py-20">
93
+ <RotateLoader color="#102841" size="15px" />
94
+ </div>
95
+
96
+ <OrderForm v-if="showForm" :order="editingOrder" @close="closeForm" @saved="loadOrders" />
97
+ </div>
98
+ </template>
99
+
100
+ <script setup>
101
+ import { ref, reactive, onMounted } from 'vue'
102
+ import OrderForm from '@/components/forms/OrderForm.vue'
103
+ import orderService from '@/services/orderService'
104
+ import axios from 'axios'
105
+ import RotateLoader from 'vue-spinner/src/RotateLoader.vue'
106
+
107
+ const state = reactive({
108
+ orders: [],
109
+ isLoading: true
110
+ });
111
+ const expandedOrderId = ref(null) // <-- single expanded order
112
+ const showForm = ref(false)
113
+ const editingOrder = ref(null)
114
+
115
+ const loadOrders = async () => {
116
+ try {
117
+ const response = await axios.get(`${import.meta.env.VITE_API_URL}/orders`)
118
+ state.orders = response.data
119
+ } catch (err) {
120
+ console.log(err)
121
+ } finally {
122
+ state.isLoading = false;
123
+ }
124
+ }
125
+
126
+ const toggleExpand = (orderId) => {
127
+ // If clicked order is already expanded, collapse it, else expand it
128
+ expandedOrderId.value = expandedOrderId.value === orderId ? null : orderId
129
+ }
130
+
131
+ const openForm = (order = null) => {
132
+ editingOrder.value = order
133
+ showForm.value = true
134
+ }
135
+
136
+ const closeForm = () => {
137
+ editingOrder.value = null
138
+ showForm.value = false
139
+ }
140
+
141
+ const deleteOrder = async (orderId) => {
142
+ await orderService.deleteOrder(orderId)
143
+ loadOrders()
144
+ }
145
+
146
+ onMounted(loadOrders)
147
+ </script>
src/components/grids/ProductList.vue ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="p-8 bg-gray-100 min-h-screen">
3
+ <div class="flex justify-between items-center mb-6">
4
+ <h1 class="text-3xl font-bold text-gray-800">📦 Products</h1>
5
+ <button
6
+ class="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-full shadow hover:bg-blue-700 transition"
7
+ @click="openCreateModal"
8
+ >
9
+ ➕ New Product
10
+ </button>
11
+ </div>
12
+
13
+ <div v-if="!state.isLoading">
14
+ <div class="overflow-x-auto bg-white rounded-2xl shadow">
15
+ <table class="min-w-full divide-y divide-gray-200">
16
+ <thead class="bg-gray-50 sticky top-0">
17
+ <tr>
18
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">ID</th>
19
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Product Name</th>
20
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Quantity Per Unit</th>
21
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Unit Price</th>
22
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Units In Stock</th>
23
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Discontinued</th>
24
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Actions</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody class="bg-white divide-y divide-gray-200">
28
+ <tr
29
+ v-for="product in state.products"
30
+ :key="product.ProductID"
31
+ class="hover:bg-gray-50 transition"
32
+ >
33
+ <td class="px-6 py-4 text-sm font-medium text-gray-800">{{ product.ProductID }}</td>
34
+ <td class="px-6 py-4 text-sm font-medium text-gray-800">{{ product.ProductName }}</td>
35
+ <td class="px-6 py-4 text-sm text-gray-700">{{ product.QuantityPerUnit }}</td>
36
+ <td class="px-6 py-4 text-sm text-gray-700">${{ product.UnitPrice }}</td>
37
+ <td class="px-6 py-4 text-sm text-gray-700">{{ product.UnitsInStock }}</td>
38
+ <td class="px-6 py-4 text-sm text-gray-700">
39
+ <span :class="{'text-red-600': product.Discontinued, 'text-green-600': !product.Discontinued}">
40
+ {{ product.Discontinued ? 'Yes' : 'No' }}
41
+ </span>
42
+ </td>
43
+ <td class="px-6 py-4 text-sm font-medium text-gray-500">
44
+ <div class="flex items-center space-x-2">
45
+ <button
46
+ class="flex items-center text-blue-600 hover:text-blue-700"
47
+ @click.stop="openEditModal(product)"
48
+ >
49
+ <span class="mr-1">📝</span>
50
+ <span>Edit</span>
51
+ </button>
52
+ <button
53
+ class="flex items-center text-red-600 hover:text-red-700"
54
+ @click.stop="deleteProduct(product.ProductID)"
55
+ >
56
+ <span class="mr-1">🗑️</span>
57
+ <span>Delete</span>
58
+ </button>
59
+ </div>
60
+ </td>
61
+ </tr>
62
+ </tbody>
63
+ </table>
64
+ </div>
65
+ </div>
66
+
67
+ <div v-else class="flex justify-center py-20">
68
+ <RotateLoader color="#102841" size="15px" />
69
+ </div>
70
+
71
+ <!-- Create Modal -->
72
+ <div v-if="showCreateModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
73
+ <div class="bg-white p-6 rounded-lg w-full max-w-md shadow-lg">
74
+ <h2 class="text-xl font-bold mb-4">Create New Product</h2>
75
+
76
+ <div class="space-y-4">
77
+ <div>
78
+ <label class="block text-gray-700">Product Name</label>
79
+ <input
80
+ v-model="newProductName"
81
+ class="border rounded p-2 w-full"
82
+ placeholder="Enter product name"
83
+ />
84
+ </div>
85
+ <div>
86
+ <label class="block text-gray-700">Quantity Per Unit</label>
87
+ <input
88
+ v-model="newQuantityPerUnit"
89
+ class="border rounded p-2 w-full"
90
+ placeholder="Enter quantity per unit"
91
+ />
92
+ </div>
93
+ <div>
94
+ <label class="block text-gray-700">Unit Price</label>
95
+ <input
96
+ v-model="newUnitPrice"
97
+ type="number"
98
+ class="border rounded p-2 w-full"
99
+ placeholder="Enter unit price"
100
+ />
101
+ </div>
102
+ <div>
103
+ <label class="block text-gray-700">Units In Stock</label>
104
+ <input
105
+ v-model="newUnitsInStock"
106
+ type="number"
107
+ class="border rounded p-2 w-full"
108
+ placeholder="Enter units in stock"
109
+ />
110
+ </div>
111
+ <div>
112
+ <label class="block text-gray-700">Discontinued</label>
113
+ <input
114
+ v-model="newDiscontinued"
115
+ type="checkbox"
116
+ class="border rounded p-2 w-full"
117
+ />
118
+ </div>
119
+ </div>
120
+
121
+ <div class="flex justify-end space-x-2 mt-6">
122
+ <button
123
+ @click="showCreateModal = false"
124
+ class="px-4 py-2 border rounded hover:bg-gray-100"
125
+ >
126
+ Cancel
127
+ </button>
128
+ <button
129
+ @click="createProduct"
130
+ class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
131
+ >
132
+ Save
133
+ </button>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <!-- Edit Modal -->
139
+ <div v-if="showEditModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
140
+ <div class="bg-white p-6 rounded-lg w-full max-w-md shadow-lg">
141
+ <h2 class="text-xl font-bold mb-4">Edit Product</h2>
142
+
143
+ <div class="space-y-4">
144
+ <div>
145
+ <label class="block text-gray-700">Product Name</label>
146
+ <input
147
+ v-model="editProductName"
148
+ class="border rounded p-2 w-full"
149
+ />
150
+ </div>
151
+ <div>
152
+ <label class="block text-gray-700">Quantity Per Unit</label>
153
+ <input
154
+ v-model="editQuantityPerUnit"
155
+ class="border rounded p-2 w-full"
156
+ />
157
+ </div>
158
+ <div>
159
+ <label class="block text-gray-700">Unit Price</label>
160
+ <input
161
+ v-model="editUnitPrice"
162
+ type="number"
163
+ class="border rounded p-2 w-full"
164
+ />
165
+ </div>
166
+ <div>
167
+ <label class="block text-gray-700">Units In Stock</label>
168
+ <input
169
+ v-model="editUnitsInStock"
170
+ type="number"
171
+ class="border rounded p-2 w-full"
172
+ />
173
+ </div>
174
+ <div>
175
+ <label class="block text-gray-700">Discontinued</label>
176
+ <input
177
+ v-model="editDiscontinued"
178
+ type="checkbox"
179
+ class="border rounded p-2 w-full"
180
+ />
181
+ </div>
182
+ </div>
183
+
184
+ <div class="flex justify-end space-x-2 mt-6">
185
+ <button
186
+ @click="showEditModal = false"
187
+ class="px-4 py-2 border rounded hover:bg-gray-100"
188
+ >
189
+ Cancel
190
+ </button>
191
+ <button
192
+ @click="saveEdit"
193
+ class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
194
+ >
195
+ Save
196
+ </button>
197
+ </div>
198
+ </div>
199
+ </div>
200
+
201
+ </div>
202
+ </template>
203
+
204
+ <script setup>
205
+ import { ref, reactive, onMounted } from 'vue'
206
+ import axios from 'axios'
207
+ import RotateLoader from 'vue-spinner/src/RotateLoader.vue'
208
+
209
+ const state = reactive({
210
+ products: [],
211
+ isLoading: true
212
+ })
213
+
214
+ const showCreateModal = ref(false)
215
+ const showEditModal = ref(false)
216
+
217
+ const newProductName = ref('')
218
+ const newQuantityPerUnit = ref('')
219
+ const newUnitPrice = ref(0)
220
+ const newUnitsInStock = ref(0)
221
+ const newDiscontinued = ref(false)
222
+
223
+ const editProductID = ref(null)
224
+ const editProductName = ref('')
225
+ const editQuantityPerUnit = ref('')
226
+ const editUnitPrice = ref(0)
227
+ const editUnitsInStock = ref(0)
228
+ const editDiscontinued = ref(false)
229
+
230
+ async function fetchProducts() {
231
+ try {
232
+ const { data } = await axios.get(`${import.meta.env.VITE_API_URL}/products`)
233
+ state.products = data
234
+ } finally {
235
+ state.isLoading = false
236
+ }
237
+ }
238
+
239
+ function openCreateModal() {
240
+ newProductName.value = ''
241
+ newQuantityPerUnit.value = ''
242
+ newUnitPrice.value = 0
243
+ newUnitsInStock.value = 0
244
+ newDiscontinued.value = false
245
+ showCreateModal.value = true
246
+ }
247
+
248
+ async function createProduct() {
249
+ try {
250
+ const { data } = await axios.post('/api/products', {
251
+ ProductName: newProductName.value,
252
+ QuantityPerUnit: newQuantityPerUnit.value,
253
+ UnitPrice: newUnitPrice.value,
254
+ UnitsInStock: newUnitsInStock.value,
255
+ Discontinued: newDiscontinued.value
256
+ })
257
+ state.products.push(data)
258
+ showCreateModal.value = false
259
+ } catch (error) {
260
+ console.error(error)
261
+ }
262
+ }
263
+
264
+ function openEditModal(product) {
265
+ editProductID.value = product.ProductID
266
+ editProductName.value = product.ProductName
267
+ editQuantityPerUnit.value = product.QuantityPerUnit
268
+ editUnitPrice.value = product.UnitPrice
269
+ editUnitsInStock.value = product.UnitsInStock
270
+ editDiscontinued.value = product.Discontinued
271
+ showEditModal.value = true
272
+ }
273
+
274
+ async function saveEdit() {
275
+ try {
276
+ await axios.put(`/api/products/${editProductID.value}`, {
277
+ ProductName: editProductName.value,
278
+ QuantityPerUnit: editQuantityPerUnit.value,
279
+ UnitPrice: editUnitPrice.value,
280
+ UnitsInStock: editUnitsInStock.value,
281
+ Discontinued: editDiscontinued.value
282
+ })
283
+ const product = state.products.find(p => p.ProductID === editProductID.value)
284
+ product.ProductName = editProductName.value
285
+ product.QuantityPerUnit = editQuantityPerUnit.value
286
+ product.UnitPrice = editUnitPrice.value
287
+ product.UnitsInStock = editUnitsInStock.value
288
+ product.Discontinued = editDiscontinued.value
289
+ showEditModal.value = false
290
+ } catch (error) {
291
+ console.error(error)
292
+ }
293
+ }
294
+
295
+ async function deleteProduct(id) {
296
+ try {
297
+ await axios.delete(`/api/products/${id}`)
298
+ state.products = state.products.filter(p => p.ProductID !== id)
299
+ } catch (error) {
300
+ console.error(error)
301
+ }
302
+ }
303
+
304
+ onMounted(fetchProducts)
305
+ </script>
src/components/grids/ShipperList.vue ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="p-8 bg-gray-100 min-h-screen">
3
+ <div class="flex justify-between items-center mb-6">
4
+ <h1 class="text-3xl font-bold text-gray-800">🚚 Shippers</h1>
5
+ <button
6
+ @click="openCreateModal"
7
+ class="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-full shadow hover:bg-blue-700 transition"
8
+ >
9
+ ➕ New Shipper
10
+ </button>
11
+ </div>
12
+
13
+ <!-- Shippers Table -->
14
+ <div v-if="!state.isLoading">
15
+ <div class="overflow-x-auto bg-white rounded-2xl shadow">
16
+ <table class="min-w-full divide-y divide-gray-200">
17
+ <thead class="bg-gray-50 sticky top-0">
18
+ <tr>
19
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">ID</th>
20
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Company Name</th>
21
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Phone</th>
22
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Actions</th>
23
+ </tr>
24
+ </thead>
25
+ <tbody class="bg-white divide-y divide-gray-200">
26
+ <tr
27
+ v-for="shipper in state.shippers"
28
+ :key="shipper.ShipperID"
29
+ class="hover:bg-gray-50 transition"
30
+ >
31
+ <td class="px-6 py-4 text-sm font-medium text-gray-800">{{ shipper.ShipperID }}</td>
32
+ <td class="px-6 py-4 text-sm font-medium text-gray-800">{{ shipper.CompanyName }}</td>
33
+ <td class="px-6 py-4 text-sm text-gray-700">{{ shipper.Phone }}</td>
34
+ <td class="px-6 py-4 text-sm font-medium text-gray-500">
35
+ <div class="flex items-center space-x-2">
36
+ <button
37
+ class="flex items-center text-blue-600 hover:text-blue-700"
38
+ @click="openEditModal(shipper)"
39
+ >
40
+ <span class="mr-1">📝</span>
41
+ <span>Edit</span>
42
+ </button>
43
+ <button
44
+ class="flex items-center text-red-600 hover:text-red-700"
45
+ @click="deleteShipper(shipper.ShipperID)"
46
+ >
47
+ <span class="mr-1">🗑️</span>
48
+ <span>Delete</span>
49
+ </button>
50
+ </div>
51
+ </td>
52
+ </tr>
53
+ </tbody>
54
+ </table>
55
+ </div>
56
+ </div>
57
+
58
+ <div v-else class="flex justify-center py-20">
59
+ <RotateLoader color="#102841" size="15px" />
60
+ </div>
61
+
62
+ <!-- Create or Edit Shipper Modal -->
63
+ <div v-if="showModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
64
+ <div class="bg-white p-6 rounded-lg w-full max-w-md shadow-lg">
65
+ <h2 class="text-xl font-bold mb-4">{{ isEditing ? 'Edit Shipper' : 'Create New Shipper' }}</h2>
66
+
67
+ <div class="space-y-4">
68
+ <div>
69
+ <label class="block text-gray-700">Company Name</label>
70
+ <input v-model="form.CompanyName" class="border rounded p-2 w-full" />
71
+ </div>
72
+ <div>
73
+ <label class="block text-gray-700">Phone</label>
74
+ <input v-model="form.Phone" class="border rounded p-2 w-full" />
75
+ </div>
76
+ </div>
77
+
78
+ <div class="flex justify-end space-x-2 mt-6">
79
+ <button
80
+ @click="showModal = false"
81
+ class="px-4 py-2 border rounded hover:bg-gray-100"
82
+ >
83
+ Cancel
84
+ </button>
85
+ <button
86
+ @click="isEditing ? updateShipper() : createShipper()"
87
+ class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
88
+ >
89
+ {{ isEditing ? 'Save Changes' : 'Create Shipper' }}
90
+ </button>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </template>
96
+
97
+ <script setup>
98
+ import { ref, reactive, onMounted } from 'vue'
99
+ import axios from 'axios'
100
+ import RotateLoader from 'vue-spinner/src/RotateLoader.vue'
101
+
102
+ const state = reactive({
103
+ shippers: [],
104
+ isLoading: true
105
+ })
106
+ const showModal = ref(false)
107
+ const isEditing = ref(false)
108
+ const selectedShipperId = ref(null)
109
+
110
+ const form = ref({
111
+ CompanyName: '',
112
+ Phone: ''
113
+ })
114
+
115
+ async function fetchShippers() {
116
+ try {
117
+ const response = await axios.get(`${import.meta.env.VITE_API_URL}/shippers`)
118
+ state.shippers = response.data
119
+ } finally {
120
+ state.isLoading = false
121
+ }
122
+ }
123
+
124
+ function openCreateModal() {
125
+ resetForm()
126
+ isEditing.value = false
127
+ showModal.value = true
128
+ }
129
+
130
+ function openEditModal(shipper) {
131
+ selectedShipperId.value = shipper.ShipperID
132
+ form.value = { ...shipper }
133
+ isEditing.value = true
134
+ showModal.value = true
135
+ }
136
+
137
+ function resetForm() {
138
+ form.value = {
139
+ CompanyName: '',
140
+ Phone: ''
141
+ }
142
+ }
143
+
144
+ async function createShipper() {
145
+ try {
146
+ const { data } = await axios.post('/shippers', form.value)
147
+ state.shippers.push(data)
148
+ showModal.value = false
149
+ } catch (error) {
150
+ console.error(error)
151
+ }
152
+ }
153
+
154
+ async function updateShipper() {
155
+ try {
156
+ await axios.put(`/shippers/${selectedShipperId.value}`, form.value)
157
+ const index = state.shippers.findIndex(s => s.ShipperID === selectedShipperId.value)
158
+ if (index !== -1) state.shippers[index] = { ...form.value, ShipperID: selectedShipperId.value }
159
+ showModal.value = false
160
+ } catch (error) {
161
+ console.error(error)
162
+ }
163
+ }
164
+
165
+ async function deleteShipper(id) {
166
+ try {
167
+ await axios.delete(`/api/shippers/${id}`)
168
+ state.shippers = state.shippers.filter(s => s.ShipperID !== id)
169
+ } catch (error) {
170
+ console.error(error)
171
+ }
172
+ }
173
+
174
+ onMounted(fetchShippers)
175
+ </script>
src/components/grids/SupplierList.vue ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="p-8 bg-gray-100 min-h-screen">
3
+ <div class="flex justify-between items-center mb-6">
4
+ <h1 class="text-3xl font-bold text-gray-800">🏢 Suppliers</h1>
5
+ <button
6
+ @click="openCreateModal"
7
+ class="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-full shadow hover:bg-blue-700 transition"
8
+ >
9
+ ➕ New Supplier
10
+ </button>
11
+ </div>
12
+
13
+ <!-- Suppliers Table -->
14
+ <div v-if="!state.isLoading">
15
+ <div class="overflow-x-auto bg-white rounded-2xl shadow">
16
+ <table class="min-w-full divide-y divide-gray-200">
17
+ <thead class="bg-gray-50 sticky top-0">
18
+ <tr>
19
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">ID</th>
20
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Company</th>
21
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Contact</th>
22
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Phone</th>
23
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">City</th>
24
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Country</th>
25
+ <th class="px-6 py-3 text-left text-sm font-medium text-gray-600 uppercase tracking-wider">Actions</th>
26
+ </tr>
27
+ </thead>
28
+ <tbody class="bg-white divide-y divide-gray-200">
29
+ <tr
30
+ v-for="supplier in state.suppliers"
31
+ :key="supplier.SupplierID"
32
+ class="hover:bg-gray-50 transition"
33
+ >
34
+ <td class="px-6 py-4 text-sm font-medium text-gray-800">{{ supplier.SupplierID }}</td>
35
+ <td class="px-6 py-4 text-sm font-medium text-gray-800">{{ supplier.CompanyName }}</td>
36
+ <td class="px-6 py-4 text-sm text-gray-700">{{ supplier.ContactName }}</td>
37
+ <td class="px-6 py-4 text-sm text-gray-700">{{ supplier.Phone }}</td>
38
+ <td class="px-6 py-4 text-sm text-gray-700">{{ supplier.City }}</td>
39
+ <td class="px-6 py-4 text-sm text-gray-700">{{ supplier.Country }}</td>
40
+ <td class="px-6 py-4 text-sm font-medium text-gray-500">
41
+ <div class="flex items-center space-x-2">
42
+ <button
43
+ class="flex items-center text-blue-600 hover:text-blue-700"
44
+ @click="openEditModal(supplier)"
45
+ >
46
+ <span class="mr-1">📝</span>
47
+ <span>Edit</span>
48
+ </button>
49
+ <button
50
+ class="flex items-center text-red-600 hover:text-red-700"
51
+ @click="deleteSupplier(supplier.SupplierID)"
52
+ >
53
+ <span class="mr-1">🗑️</span>
54
+ <span>Delete</span>
55
+ </button>
56
+ </div>
57
+ </td>
58
+ </tr>
59
+ </tbody>
60
+ </table>
61
+ </div>
62
+ </div>
63
+
64
+ <div v-else class="flex justify-center py-20">
65
+ <RotateLoader color="#102841" size="15px" />
66
+ </div>
67
+
68
+ <!-- Create or Edit Supplier Modal -->
69
+ <div v-if="showModal" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
70
+ <div class="bg-white p-6 rounded-lg w-full max-w-2xl shadow-lg overflow-y-auto max-h-[90vh]">
71
+ <h2 class="text-xl font-bold mb-4">{{ isEditing ? 'Edit Supplier' : 'Create New Supplier' }}</h2>
72
+
73
+ <div class="grid grid-cols-2 gap-4">
74
+ <div>
75
+ <label class="block text-gray-700">Company Name</label>
76
+ <input v-model="form.CompanyName" class="border rounded p-2 w-full" />
77
+ </div>
78
+ <div>
79
+ <label class="block text-gray-700">Contact Name</label>
80
+ <input v-model="form.ContactName" class="border rounded p-2 w-full" />
81
+ </div>
82
+ <div>
83
+ <label class="block text-gray-700">Contact Title</label>
84
+ <input v-model="form.ContactTitle" class="border rounded p-2 w-full" />
85
+ </div>
86
+ <div>
87
+ <label class="block text-gray-700">Phone</label>
88
+ <input v-model="form.Phone" class="border rounded p-2 w-full" />
89
+ </div>
90
+ <div class="col-span-2">
91
+ <label class="block text-gray-700">Address</label>
92
+ <input v-model="form.Address" class="border rounded p-2 w-full" />
93
+ </div>
94
+ <div>
95
+ <label class="block text-gray-700">City</label>
96
+ <input v-model="form.City" class="border rounded p-2 w-full" />
97
+ </div>
98
+ <div>
99
+ <label class="block text-gray-700">Region</label>
100
+ <input v-model="form.Region" class="border rounded p-2 w-full" />
101
+ </div>
102
+ <div>
103
+ <label class="block text-gray-700">Postal Code</label>
104
+ <input v-model="form.PostalCode" class="border rounded p-2 w-full" />
105
+ </div>
106
+ <div>
107
+ <label class="block text-gray-700">Country</label>
108
+ <input v-model="form.Country" class="border rounded p-2 w-full" />
109
+ </div>
110
+ <div>
111
+ <label class="block text-gray-700">Fax</label>
112
+ <input v-model="form.Fax" class="border rounded p-2 w-full" />
113
+ </div>
114
+ <div>
115
+ <label class="block text-gray-700">Homepage</label>
116
+ <input v-model="form.HomePage" class="border rounded p-2 w-full" />
117
+ </div>
118
+ </div>
119
+
120
+ <div class="flex justify-end space-x-2 mt-6">
121
+ <button
122
+ @click="showModal = false"
123
+ class="px-4 py-2 border rounded hover:bg-gray-100"
124
+ >
125
+ Cancel
126
+ </button>
127
+ <button
128
+ @click="isEditing ? updateSupplier() : createSupplier()"
129
+ class="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
130
+ >
131
+ {{ isEditing ? 'Save Changes' : 'Create Supplier' }}
132
+ </button>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </template>
138
+
139
+ <script setup>
140
+ import { ref, reactive, onMounted } from 'vue'
141
+ import RotateLoader from 'vue-spinner/src/RotateLoader.vue'
142
+ import axios from 'axios'
143
+
144
+ const state = reactive({
145
+ suppliers: [],
146
+ isLoading: true
147
+ })
148
+ const showModal = ref(false)
149
+ const isEditing = ref(false)
150
+ const selectedSupplierId = ref(null)
151
+
152
+ const form = ref({
153
+ CompanyName: '',
154
+ ContactName: '',
155
+ ContactTitle: '',
156
+ Address: '',
157
+ City: '',
158
+ Region: '',
159
+ PostalCode: '',
160
+ Country: '',
161
+ Phone: '',
162
+ Fax: '',
163
+ HomePage: ''
164
+ })
165
+
166
+ async function fetchSuppliers() {
167
+ try {
168
+ const { data } = await axios.get(`${import.meta.env.VITE_API_URL}/suppliers`)
169
+ state.suppliers = data
170
+ } finally {
171
+ state.isLoading = false
172
+ }
173
+ }
174
+
175
+ function openCreateModal() {
176
+ resetForm()
177
+ isEditing.value = false
178
+ showModal.value = true
179
+ }
180
+
181
+ function openEditModal(supplier) {
182
+ selectedSupplierId.value = supplier.SupplierID
183
+ form.value = { ...supplier }
184
+ isEditing.value = true
185
+ showModal.value = true
186
+ }
187
+
188
+ function resetForm() {
189
+ form.value = {
190
+ CompanyName: '',
191
+ ContactName: '',
192
+ ContactTitle: '',
193
+ Address: '',
194
+ City: '',
195
+ Region: '',
196
+ PostalCode: '',
197
+ Country: '',
198
+ Phone: '',
199
+ Fax: '',
200
+ HomePage: ''
201
+ }
202
+ }
203
+
204
+ async function createSupplier() {
205
+ try {
206
+ const { data } = await axios.post('/api/suppliers', form.value)
207
+ state.suppliers.push(data)
208
+ showModal.value = false
209
+ } catch (error) {
210
+ console.error(error)
211
+ }
212
+ }
213
+
214
+ async function updateSupplier() {
215
+ try {
216
+ await axios.put(`/api/suppliers/${selectedSupplierId.value}`, form.value)
217
+ const index = state.suppliers.findIndex(s => s.SupplierID === selectedSupplierId.value)
218
+ if (index !== -1) state.suppliers[index] = { ...form.value, SupplierID: selectedSupplierId.value }
219
+ showModal.value = false
220
+ } catch (error) {
221
+ console.error(error)
222
+ }
223
+ }
224
+
225
+ async function deleteSupplier(id) {
226
+ try {
227
+ await axios.delete(`/api/suppliers/${id}`)
228
+ state.suppliers = state.suppliers.filter(s => s.SupplierID !== id)
229
+ } catch (error) {
230
+ console.error(error)
231
+ }
232
+ }
233
+
234
+ onMounted(fetchSuppliers)
235
+ </script>
src/components/grids/TopCustomerList.vue ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ interface Customer {
3
+ name: string;
4
+ freight: number;
5
+ }
6
+
7
+ const props = defineProps<{
8
+ topCustomers: Customer[];
9
+ title: string;
10
+ }>();
11
+ console.log("TopCustomerList props: ", props.topCustomers.length);
12
+ </script>
13
+
14
+ <template>
15
+ <div class="bg-white p-6 rounded-2xl shadow-md">
16
+ <h2 class="text-xl font-semibold mb-4">{{title}}</h2>
17
+ <div class="overflow-x-auto">
18
+ <table class="w-full text-left table-auto">
19
+ <thead class="text-gray-600 border-b">
20
+ <tr>
21
+ <th class="pb-2">Customer</th>
22
+ <th class="pb-2">Total Freight</th>
23
+ </tr>
24
+ </thead>
25
+ <tbody>
26
+ <tr v-for="customer in props.topCustomers" :key="customer.name" class="border-t hover:bg-gray-50">
27
+ <td class="py-2">{{ customer.name }}</td>
28
+ <td class="py-2 text-green-600 font-semibold">${{ customer.freight.toFixed(2) }}</td>
29
+ </tr>
30
+ </tbody>
31
+ </table>
32
+ </div>
33
+ </div>
34
+ </template>
src/components/nav/Navbar.vue ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { RouterLink } from 'vue-router';
3
+ import { ref, onMounted, onUnmounted } from 'vue';
4
+
5
+ const profileMenuOpen = ref(false);
6
+
7
+ function toggleProfileMenu() {
8
+ profileMenuOpen.value = !profileMenuOpen.value;
9
+ }
10
+
11
+ // Optional: close profile menu when clicking outside
12
+ function handleClickOutside(event: MouseEvent) {
13
+ const profile = document.querySelector('.profile-menu');
14
+ if (profile && !profile.contains(event.target as Node)) {
15
+ profileMenuOpen.value = false;
16
+ }
17
+ }
18
+
19
+ onMounted(() => {
20
+ document.addEventListener('click', handleClickOutside);
21
+ });
22
+
23
+ onUnmounted(() => {
24
+ document.removeEventListener('click', handleClickOutside);
25
+ });
26
+ </script>
27
+
28
+ <template>
29
+ <header class="flex items-center justify-between p-2 md:px-8 bg-[#102841]">
30
+ <!-- Left Side: Logo -->
31
+ <RouterLink to="/">
32
+ <img
33
+ alt="Vue logo"
34
+ src="@/assets/logo.png"
35
+ width="75"
36
+ height="75"
37
+ class="block"
38
+ />
39
+ </RouterLink>
40
+
41
+ <!-- Right Side: Navigation and Profile -->
42
+ <div class="flex items-center space-x-6">
43
+ <nav class="flex items-center space-x-6 text-sm text-white">
44
+ <RouterLink
45
+ to="/"
46
+ class="hover:text-blue-400"
47
+ exact-active-class="text-blue-300"
48
+ >
49
+ Dashboard
50
+ </RouterLink>
51
+ <RouterLink
52
+ to="/customers"
53
+ class="hover:text-blue-400"
54
+ exact-active-class="text-blue-300"
55
+ >
56
+ Customer
57
+ </RouterLink>
58
+ <RouterLink
59
+ to="/orders"
60
+ class="hover:text-blue-400"
61
+ exact-active-class="text-blue-300"
62
+ >
63
+ Order
64
+ </RouterLink>
65
+
66
+ <!-- Admin with Submenu (hover-based) -->
67
+ <div class="relative group">
68
+ <button class="hover:text-blue-400 flex items-center focus:outline-none">
69
+ Admin
70
+ <svg
71
+ class="w-4 h-4 ml-1"
72
+ fill="none"
73
+ stroke="currentColor"
74
+ viewBox="0 0 24 24"
75
+ >
76
+ <path
77
+ stroke-linecap="round"
78
+ stroke-linejoin="round"
79
+ stroke-width="2"
80
+ d="M19 9l-7 7-7-7"
81
+ />
82
+ </svg>
83
+ </button>
84
+
85
+ <div
86
+ class="absolute right-0 mt-2 w-40 bg-white text-gray-800 rounded-md shadow-lg py-2 z-20 opacity-0 group-hover:opacity-100 invisible group-hover:visible transition-all"
87
+ >
88
+ <RouterLink
89
+ to="/admin/categories"
90
+ class="block px-4 py-2 hover:bg-gray-100"
91
+ >
92
+ Category
93
+ </RouterLink>
94
+ <RouterLink
95
+ to="/admin/products"
96
+ class="block px-4 py-2 hover:bg-gray-100"
97
+ >
98
+ Product
99
+ </RouterLink>
100
+ <RouterLink
101
+ to="/admin/shippers"
102
+ class="block px-4 py-2 hover:bg-gray-100"
103
+ >
104
+ Shipper
105
+ </RouterLink>
106
+ <RouterLink
107
+ to="/admin/suppliers"
108
+ class="block px-4 py-2 hover:bg-gray-100"
109
+ >
110
+ Supplier
111
+ </RouterLink>
112
+ </div>
113
+ </div>
114
+ </nav>
115
+
116
+ <!-- Profile Picture and Dropdown -->
117
+ <div class="relative profile-menu" @click="toggleProfileMenu">
118
+ <img
119
+ src="@/assets/profile.png"
120
+ alt="Profile"
121
+ class="w-10 h-10 rounded-full cursor-pointer border-2 border-white"
122
+ />
123
+ <div
124
+ v-if="profileMenuOpen"
125
+ class="absolute right-0 mt-2 w-32 bg-white rounded-md shadow-lg py-2 z-20"
126
+ >
127
+ <RouterLink
128
+ to="/login"
129
+ class="block px-4 py-2 text-gray-700 hover:bg-gray-100"
130
+ >
131
+ Login
132
+ </RouterLink>
133
+ </div>
134
+ </div>
135
+ </div>
136
+ </header>
137
+ </template>
src/main.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createApp } from 'vue'
2
+ import App from './App.vue'
3
+ import router from './router'
4
+
5
+ import '@/assets/main.css'
6
+
7
+ // console.log(process.env.VITE_API_URL);
8
+
9
+ const app = createApp(App)
10
+
11
+ app.use(router)
12
+
13
+ app.mount('#app')
src/router/index.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+ // import HomeView from '../views/HomeView.vue'
3
+ import CustomerView from '@/views/customer/CustomerView.vue'
4
+ import DashboardView from '@/views/dashboard/DashboardView.vue'
5
+ import SupplierView from '@/views/supplier/SupplierView.vue'
6
+ import CategoryView from '@/views/category/CategoryView.vue'
7
+ import ProductView from '@/views/product/ProductView.vue'
8
+ import ShipperView from '@/views/shipper/ShipperView.vue'
9
+ import OrderView from '@/views/order/OrderView.vue'
10
+
11
+ const router = createRouter({
12
+ history: createWebHistory(import.meta.env.BASE_URL),
13
+ routes: [
14
+ {
15
+ path: '/',
16
+ name: 'dashboard',
17
+ component: DashboardView,
18
+ },
19
+ {
20
+ path: '/customers',
21
+ name: 'customers',
22
+ component: CustomerView,
23
+ },
24
+ {
25
+ path: '/admin/categories',
26
+ name: 'categories',
27
+ component: CategoryView,
28
+ },
29
+ {
30
+ path: '/admin/products',
31
+ name: 'products',
32
+ component: ProductView,
33
+ },
34
+ {
35
+ path: '/admin/suppliers',
36
+ name: 'suppliers',
37
+ component: SupplierView,
38
+ },
39
+ {
40
+ path: '/admin/shippers',
41
+ name: 'shippers',
42
+ component: ShipperView,
43
+ },
44
+ {
45
+ path: '/orders',
46
+ name: 'orders',
47
+ component: OrderView,
48
+ },
49
+ {
50
+ path: '/about',
51
+ name: 'about',
52
+ // route level code-splitting
53
+ // this generates a separate chunk (About.[hash].js) for this route
54
+ // which is lazy-loaded when the route is visited.
55
+ component: () => import('../views/AboutView.vue'),
56
+ },
57
+ ],
58
+ })
59
+
60
+ export default router
src/services/orderService.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/services/orderService.js
2
+ let orders = [
3
+ {
4
+ id: 1,
5
+ customer: 'Alfreds',
6
+ employee: 'John Doe',
7
+ shipper: 'Speedy Express',
8
+ shipCity: 'Berlin',
9
+ orderDate: '2025-04-24',
10
+ shippedDate: '2025-05-01',
11
+ freight: 32,
12
+ items: [
13
+ { id: 1, product: 'Chai', quantity: 10, price: 18 },
14
+ { id: 2, product: 'Chang', quantity: 5, price: 19 }
15
+ ]
16
+ }
17
+ ]
18
+
19
+ export default {
20
+ getOrders() {
21
+ return Promise.resolve([...orders])
22
+ },
23
+ saveOrder(order) {
24
+ if (order.id) {
25
+ orders = orders.map(o => (o.id === order.id ? order : o))
26
+ } else {
27
+ order.id = Date.now()
28
+ orders.push(order)
29
+ }
30
+ return Promise.resolve()
31
+ },
32
+ deleteOrder(orderId) {
33
+ orders = orders.filter(o => o.id !== orderId)
34
+ return Promise.resolve()
35
+ },
36
+ getDropdownData() {
37
+ return Promise.resolve({
38
+ customers: ['Alfreds', 'Bon App', 'Around the Horn'],
39
+ employees: ['John Doe', 'Jane Smith', 'Alice Johnson'],
40
+ shippers: ['Speedy Express', 'United Package', 'Federal Shipping'],
41
+ products: ['Chai', 'Chang', 'Aniseed Syrup', 'Chef Anton\'s Gumbo Mix']
42
+ })
43
+ }
44
+ }
45
+
src/views/AboutView.vue ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="about">
3
+ <h1>This is an about page</h1>
4
+ </div>
5
+ </template>
6
+
7
+ <style>
8
+ @media (min-width: 1024px) {
9
+ .about {
10
+ min-height: 100vh;
11
+ display: flex;
12
+ align-items: center;
13
+ }
14
+ }
15
+ </style>
src/views/HomeView.vue ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ </script>
3
+
4
+ <template>
5
+ <h1 class="text-3xl font-bold underline">
6
+ Hello world!
7
+ </h1>
8
+ </template>
src/views/category/CategoryView.vue ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script setup>
2
+ import CategoryList from '@/components/grids/CategoryList.vue'
3
+ </script>
4
+
5
+ <template>
6
+ <CategoryList />
7
+ </template>
src/views/customer/CustomerView.vue ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script setup lang="ts">
2
+ import { reactive, onMounted } from 'vue';
3
+ import CustomerList from '@/components/grids/CustomerList.vue';
4
+ import RotateLoader from 'vue-spinner/src/RotateLoader.vue'
5
+ import axios from 'axios';
6
+
7
+ //const customers = ref<Array<any>>([]);
8
+ //const loading = ref(true);
9
+ const state = reactive({
10
+ customers: [],
11
+ isLoading: true
12
+ })
13
+
14
+ onMounted(async () => {
15
+ try {
16
+ // const apiUrl = import.meta.env.VITE_API_URL;
17
+ // console.log(apiUrl)
18
+ const response = await axios.get(`${import.meta.env.VITE_API_URL}/customers`); // replace with your actual API URL
19
+ //const data = await response.json();
20
+ state.customers = response.data;
21
+ } catch (error) {
22
+ console.error('Error fetching customers:', error);
23
+ } finally {
24
+ state.isLoading = false;
25
+ }
26
+ });
27
+ </script>
28
+
29
+ <template>
30
+ <div class="p-6">
31
+ <h1 class="text-2xl font-bold mb-6">Customers</h1>
32
+
33
+ <div v-if="state.isLoading" class="text-center text-gray-500"><RotateLoader color="#102841" size="15px" /></div>
34
+
35
+ <div v-else>
36
+ <CustomerList :customers="state.customers" />
37
+ </div>
38
+ </div>
39
+ </template>
src/views/dashboard/DashboardView.vue ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div v-if="!state.isLoading" class="p-8 bg-gray-100 min-h-screen">
3
+ <h1 class="text-3xl font-bold mb-8 text-gray-800">Dashboard</h1>
4
+
5
+ <!-- KPIs -->
6
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
7
+ <KPICard :totalOrders="`${totalOrders}`" color="blue" name="Total Order" />
8
+ <KPICard :totalOrders="`${totalFreight.toFixed(2)}`" color="green" name="Total Freight" />
9
+ <KPICard :totalOrders="`${topCustomer}`" color="purple" name="Top Customer" />
10
+ </div>
11
+
12
+ <!-- Main Charts -->
13
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-10">
14
+ <LineChart :chart-data="freightTrendData" title="Freight Trend by Year" />
15
+ <BarChart :chart-data="ordersByCountryData" title="Orders by Country" />
16
+ </div>
17
+
18
+ <!-- Additional Charts -->
19
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-10">
20
+ <PieChart :chart-data="ordersByShipperData" title="Order Distribution by Shipper" />
21
+ <StackedBarChart :chart-data="monthlyOrdersFreightData" title="Monthly Orders vs Freight"/>
22
+ </div>
23
+
24
+ <!-- Quick Stats Cards -->
25
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
26
+ <KPICard :totalOrders="`${avgFreight.toFixed(2)}`" :color="indigo" name="Avg Freight per Order" />
27
+ <KPICard :totalOrders="`${totalCountries}`" :color="cyan" name="Countries Served" />
28
+ <KPICard :totalOrders="`${topShippingCountry}`" :color="pink" name="Top Shipping Country" />
29
+ </div>
30
+
31
+ <!-- Top Customers Table -->
32
+ <!-- <TopCustomerList :topCustomers="`${topCustomers}`" title="Top 5 Customers by Freight" /> -->
33
+ <div class="bg-white p-6 rounded-2xl shadow-md">
34
+ <h2 class="text-xl font-semibold mb-4">{{title}}</h2>
35
+ <div class="overflow-x-auto">
36
+ <table class="w-full text-left table-auto">
37
+ <thead class="text-gray-600 border-b">
38
+ <tr>
39
+ <th class="pb-2">Customer</th>
40
+ <th class="pb-2">Total Freight</th>
41
+ </tr>
42
+ </thead>
43
+ <tbody>
44
+ <tr v-for="customer in topCustomers" :key="customer.name" class="border-t hover:bg-gray-50">
45
+ <td class="py-2">{{ customer.name }}</td>
46
+ <td class="py-2 text-green-600 font-semibold">${{ customer.freight.toFixed(2) }}</td>
47
+ </tr>
48
+ </tbody>
49
+ </table>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- Loading Spinner -->
55
+ <div v-else class="flex justify-center items-center h-screen">
56
+ <RotateLoader color="#102841" size="15px" />
57
+ </div>
58
+ </template>
59
+
60
+
61
+ <script setup>
62
+ import { ref, computed, onMounted, reactive } from 'vue'
63
+ import LineChart from '@/components/charts/LineChart.vue'
64
+ import BarChart from '@/components/charts/BarChart.vue'
65
+ import PieChart from '@/components/charts/PieChart.vue'
66
+ import StackedBarChart from '@/components/charts/StackedBarChart.vue'
67
+ import axios from 'axios'
68
+ import RotateLoader from 'vue-spinner/src/RotateLoader.vue'
69
+ import KPICard from '@/components/cards/KPICard.vue'
70
+ import TopCustomerList from '@/components/grids/TopCustomerList.vue'
71
+
72
+
73
+ // Data State
74
+ const state = reactive({
75
+ orders: [],
76
+ isLoading: true
77
+ });
78
+
79
+
80
+
81
+ onMounted(async () => {
82
+ try {
83
+ console.log("API_URL: " + import.meta.env.VITE_API_URL);
84
+
85
+ const response = await axios.get(`${import.meta.env.VITE_API_URL}/orders`);
86
+ state.orders = response.data;
87
+ } catch (err) {
88
+ console.error(err);
89
+ } finally {
90
+ state.isLoading = false;
91
+ }
92
+ });
93
+
94
+ // KPIs
95
+ const totalOrders = computed(() => state.orders.length);
96
+
97
+ const totalFreight = computed(() =>
98
+ state.orders.reduce((sum, order) => sum + parseFloat(order.Freight), 0)
99
+ );
100
+
101
+ const customerFreightMap = computed(() => {
102
+ const map = {};
103
+ state.orders.forEach(order => {
104
+ const name = order.Customer.CompanyName;
105
+ if (!map[name]) map[name] = 0;
106
+ map[name] += parseFloat(order.Freight);
107
+ });
108
+ return map;
109
+ });
110
+
111
+ const topCustomer = computed(() => {
112
+ const sorted = Object.entries(customerFreightMap.value).sort((a, b) => b[1] - a[1]);
113
+ return sorted.length ? sorted[0][0] : 'N/A';
114
+ });
115
+
116
+ // Freight Trend by Year
117
+ const freightTrendData = computed(() => {
118
+ const map = {};
119
+ state.orders.forEach(order => {
120
+ const year = new Date(order.OrderDate).getFullYear();
121
+ if (!map[year]) map[year] = 0;
122
+ map[year] += parseFloat(order.Freight);
123
+ });
124
+ return {
125
+ labels: Object.keys(map),
126
+ datasets: [{
127
+ label: 'Freight',
128
+ data: Object.values(map),
129
+ fill: false,
130
+ borderColor: '#3b82f6',
131
+ tension: 0.4
132
+ }]
133
+ };
134
+ });
135
+
136
+ // Orders by Country
137
+ const ordersByCountryData = computed(() => {
138
+ const map = {};
139
+ state.orders.forEach(order => {
140
+ const country = order.ShipCountry;
141
+ if (!map[country]) map[country] = 0;
142
+ map[country] += 1;
143
+ });
144
+ return {
145
+ labels: Object.keys(map),
146
+ datasets: [{
147
+ label: 'Orders',
148
+ data: Object.values(map),
149
+ backgroundColor: '#002641'
150
+ }]
151
+ };
152
+ });
153
+
154
+ // Orders by Shipper (Pie Chart)
155
+ const ordersByShipperData = computed(() => {
156
+ const map = {};
157
+ state.orders.forEach(order => {
158
+ const shipper = order.ShipVia.CompanyName;
159
+ if (!map[shipper]) map[shipper] = 0;
160
+ map[shipper] += 1;
161
+ });
162
+ return {
163
+ labels: Object.keys(map),
164
+ datasets: [{
165
+ data: Object.values(map),
166
+ backgroundColor: ['#3b82f6', '#6366f1', '#f59e0b', '#10b981', '#ef4444'],
167
+ }]
168
+ };
169
+ });
170
+
171
+ // Monthly Orders vs Freight (Stacked Bar)
172
+ const monthlyOrdersFreightData = computed(() => {
173
+ const ordersMap = {};
174
+ const freightMap = {};
175
+
176
+ state.orders.forEach(order => {
177
+ const month = new Date(order.OrderDate).toLocaleString('default', { month: 'short', year: 'numeric' });
178
+ if (!ordersMap[month]) ordersMap[month] = 0;
179
+ if (!freightMap[month]) freightMap[month] = 0;
180
+ ordersMap[month]++;
181
+ freightMap[month] += parseFloat(order.Freight);
182
+ });
183
+
184
+ const months = Object.keys(ordersMap);
185
+ return {
186
+ labels: months,
187
+ datasets: [
188
+ {
189
+ label: 'Orders',
190
+ data: months.map(m => ordersMap[m]),
191
+ backgroundColor: '#3b82f6',
192
+ },
193
+ {
194
+ label: 'Freight',
195
+ data: months.map(m => freightMap[m]),
196
+ backgroundColor: '#10b981',
197
+ }
198
+ ]
199
+ };
200
+ });
201
+
202
+ // Quick Stats
203
+ const avgFreight = computed(() => {
204
+ return totalFreight.value / (totalOrders.value || 1);
205
+ });
206
+
207
+ const totalCountries = computed(() => {
208
+ const countries = new Set(state.orders.map(order => order.ShipCountry));
209
+ return countries.size;
210
+ });
211
+
212
+ const topShippingCountry = computed(() => {
213
+ const map = {};
214
+ state.orders.forEach(order => {
215
+ const country = order.ShipCountry;
216
+ if (!map[country]) map[country] = 0;
217
+ map[country]++;
218
+ });
219
+
220
+ const sorted = Object.entries(map).sort((a, b) => b[1] - a[1]);
221
+ return sorted.length ? sorted[0][0] : 'N/A';
222
+ });
223
+
224
+ // Top Customers for Table
225
+ // const topCustomers = ref([]);
226
+
227
+ const topCustomers = computed(() => {
228
+ const entries = Object.entries(customerFreightMap.value);
229
+ return entries
230
+ .sort((a, b) => b[1] - a[1])
231
+ .slice(0, 5)
232
+ .map(([name, freight]) => ({ name, freight }));
233
+ });
234
+ </script>
235
+
236
+ <style scoped>
237
+ /* Optional: Add extra styling for better looks */
238
+ </style>
src/views/order/OrderView.vue ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script setup>
2
+ import OrderList from '@/components/grids/OrderList.vue'
3
+ </script>
4
+
5
+ <template>
6
+ <OrderList />
7
+ </template>
src/views/product/ProductView.vue ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script setup>
2
+ import ProductList from '@/components/grids/ProductList.vue'
3
+ </script>
4
+
5
+ <template>
6
+ <ProductList />
7
+ </template>
src/views/shipper/ShipperView.vue ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script setup>
2
+ import ShipperList from '@/components/grids/ShipperList.vue'
3
+ </script>
4
+
5
+ <template>
6
+ <ShipperList />
7
+ </template>
src/views/supplier/SupplierView.vue ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <script setup>
2
+ import SupplierList from '@/components/grids/SupplierList.vue'
3
+ </script>
4
+
5
+ <template>
6
+ <SupplierList />
7
+ </template>
tsconfig.app.json.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
3
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4
+ "exclude": ["src/**/__tests__/*"],
5
+ "compilerOptions": {
6
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
7
+
8
+ "paths": {
9
+ "@/*": ["./src/*"]
10
+ }
11
+ }
12
+ }
tsconfig.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "moduleResolution": "node",
6
+ "strict": true,
7
+ "jsx": "preserve",
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "allowSyntheticDefaultImports": true,
13
+ "baseUrl": "./",
14
+ "outDir": "./dist",
15
+ "paths": {
16
+ "@/*": ["src/*"]
17
+ }
18
+ },
19
+ "include": [
20
+ "src/**/*.ts",
21
+ "src/**/*.d.ts",
22
+ "src/**/*.tsx",
23
+ "src/**/*.vue",
24
+ "./env.d.ts",
25
+ ],
26
+ "exclude": [
27
+ "node_modules",
28
+ "dist"
29
+ ]
30
+ }
31
+
tsconfig.json.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ {
5
+ "path": "./tsconfig.node.json"
6
+ },
7
+ {
8
+ "path": "./tsconfig.app.json"
9
+ }
10
+ ]
11
+ }
tsconfig.node.json.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "@tsconfig/node22/tsconfig.json",
3
+ "include": [
4
+ "vite.config.*",
5
+ "vitest.config.*",
6
+ "cypress.config.*",
7
+ "nightwatch.conf.*",
8
+ "playwright.config.*",
9
+ "eslint.config.*"
10
+ ],
11
+ "compilerOptions": {
12
+ "noEmit": true,
13
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
14
+
15
+ "module": "ESNext",
16
+ "moduleResolution": "Bundler",
17
+ "types": ["node"]
18
+ }
19
+ }
vite.config.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { fileURLToPath, URL } from 'node:url'
2
+
3
+ import { defineConfig } from 'vite'
4
+ import vue from '@vitejs/plugin-vue'
5
+ import vueDevTools from 'vite-plugin-vue-devtools'
6
+ import tailwindcss from '@tailwindcss/vite'
7
+
8
+ // https://vite.dev/config/
9
+ export default defineConfig(({ mode }) => {
10
+
11
+ console.log(`Vite is running in ${mode} mode`);
12
+
13
+ return {
14
+
15
+ base: '/', // or '/your-subfolder/',
16
+ plugins: [
17
+ vue(),
18
+ vueDevTools(),
19
+ tailwindcss(),
20
+ ],
21
+ server: {
22
+ port: 3000,
23
+ proxy: {
24
+ '/api': {
25
+ target: 'http://localhost:3004',
26
+ changeOrigin: true,
27
+ rewrite: (path) => path.replace(/^\/api/, '/api'),
28
+ ws: false
29
+ }
30
+ },
31
+ watch: {
32
+ ignored: [
33
+ '**/*.json', // <-- Add this
34
+ ]
35
+ }
36
+ },
37
+ resolve: {
38
+ alias: {
39
+ '@': fileURLToPath(new URL('./src', import.meta.url))
40
+ },
41
+ },
42
+ }
43
+ })