Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- .gitignore +30 -0
- .npmrc +2 -0
- Dockerfile +31 -0
- README-bkp.md +115 -0
- README.md +47 -6
- env.d.ts +21 -0
- index.html +13 -0
- nginx.conf +25 -0
- package-lock.json +0 -0
- package.json +34 -0
- public/favicon.ico +0 -0
- src/App.vue +10 -0
- src/assets/logo.png +3 -0
- src/assets/main.css +1 -0
- src/assets/profile.png +0 -0
- src/components/cards/CustomerCard.vue +73 -0
- src/components/cards/KPICard.vue +35 -0
- src/components/charts/BarChart.vue +61 -0
- src/components/charts/LineChart.vue +66 -0
- src/components/charts/PieChart.vue +39 -0
- src/components/charts/StackedBarChart.vue +43 -0
- src/components/forms/OrderForm.vue +119 -0
- src/components/grids/CategoryList.vue +222 -0
- src/components/grids/CustomerList.vue +29 -0
- src/components/grids/OrderList.vue +147 -0
- src/components/grids/ProductList.vue +305 -0
- src/components/grids/ShipperList.vue +175 -0
- src/components/grids/SupplierList.vue +235 -0
- src/components/grids/TopCustomerList.vue +34 -0
- src/components/nav/Navbar.vue +137 -0
- src/main.ts +13 -0
- src/router/index.ts +60 -0
- src/services/orderService.js +45 -0
- src/views/AboutView.vue +15 -0
- src/views/HomeView.vue +8 -0
- src/views/category/CategoryView.vue +7 -0
- src/views/customer/CustomerView.vue +39 -0
- src/views/dashboard/DashboardView.vue +238 -0
- src/views/order/OrderView.vue +7 -0
- src/views/product/ProductView.vue +7 -0
- src/views/shipper/ShipperView.vue +7 -0
- src/views/supplier/SupplierView.vue +7 -0
- tsconfig.app.json.txt +12 -0
- tsconfig.json +31 -0
- tsconfig.json.txt +11 -0
- tsconfig.node.json.txt +19 -0
- 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:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
pinned: false
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
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 |
+
})
|