Initial commit
This commit is contained in:
7
DB_connection.txt
Normal file
7
DB_connection.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
資料庫資訊:
|
||||||
|
DB_HOST = mysql.theaken.com
|
||||||
|
DB_PORT = 33306
|
||||||
|
DB_NAME = db_A019
|
||||||
|
DB_USER = A019
|
||||||
|
DB_PASSWORD = 9wvKEkxBzVca
|
||||||
|
pip install -r requirements.txt
|
85
README.md
Normal file
85
README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Flask API 伺服器
|
||||||
|
|
||||||
|
這是一個使用 Flask 和 MySQL 建立的 RESTful API 伺服器,提供使用者資料的 CRUD 操作。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 獲取使用者列表 (支援過濾和分頁)
|
||||||
|
- 獲取單一使用者資料
|
||||||
|
- 新增使用者
|
||||||
|
- 更新使用者資料
|
||||||
|
- 刪除使用者
|
||||||
|
|
||||||
|
## API 端點
|
||||||
|
|
||||||
|
### GET /v1/users
|
||||||
|
獲取使用者列表,支援以下查詢參數:
|
||||||
|
- `min_age`: 最小年齡過濾
|
||||||
|
- `max_age`: 最大年齡過濾
|
||||||
|
- `page`: 頁碼 (預設: 1)
|
||||||
|
- `limit`: 每頁筆數 (預設: 10)
|
||||||
|
|
||||||
|
### GET /v1/users/<id>
|
||||||
|
獲取指定 ID 的使用者資料
|
||||||
|
|
||||||
|
### POST /v1/users
|
||||||
|
新增使用者,需提供以下 JSON 資料:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "使用者名稱",
|
||||||
|
"email": "使用者信箱",
|
||||||
|
"age": 使用者年齡
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PATCH /v1/users/<id>
|
||||||
|
更新指定 ID 的使用者資料,可提供以下任一欄位:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "新名稱",
|
||||||
|
"email": "新信箱",
|
||||||
|
"age": 新年齡
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### DELETE /v1/users/<id>
|
||||||
|
刪除指定 ID 的使用者
|
||||||
|
|
||||||
|
## 安裝與執行
|
||||||
|
|
||||||
|
1. 安裝相依套件:
|
||||||
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 執行伺服器:
|
||||||
|
```
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
伺服器將在 http://127.0.0.1:5000 啟動。
|
||||||
|
|
||||||
|
## 回應格式
|
||||||
|
|
||||||
|
所有 API 回應皆使用統一格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success" 或 "error",
|
||||||
|
"code": HTTP 狀態碼,
|
||||||
|
"message": "回應訊息",
|
||||||
|
"data": 回應資料 (僅在成功時提供)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 錯誤處理
|
||||||
|
|
||||||
|
錯誤回應格式:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"code": HTTP 錯誤碼,
|
||||||
|
"message": "錯誤訊息"
|
||||||
|
}
|
||||||
|
```
|
701
app.py
Normal file
701
app.py
Normal file
@@ -0,0 +1,701 @@
|
|||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
import mysql.connector
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app) # 啟用 CORS,允許本機前端存取
|
||||||
|
|
||||||
|
# 資料庫連線資訊
|
||||||
|
DB_CONFIG = {
|
||||||
|
'host': 'mysql.theaken.com',
|
||||||
|
'port': 33306,
|
||||||
|
'user': 'A019',
|
||||||
|
'password': '9wvKEkxBzVca',
|
||||||
|
'database': 'db_A019'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 建立資料庫連線
|
||||||
|
def get_db_connection():
|
||||||
|
try:
|
||||||
|
conn = mysql.connector.connect(**DB_CONFIG)
|
||||||
|
return conn
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
print(f"資料庫連線錯誤: {err}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 統一回應格式
|
||||||
|
def create_response(status, code, message, data=None):
|
||||||
|
response = {
|
||||||
|
"status": status,
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
if data is not None:
|
||||||
|
response["data"] = data
|
||||||
|
return response
|
||||||
|
|
||||||
|
# 錯誤處理
|
||||||
|
def create_error_response(code, message):
|
||||||
|
return create_response("error", code, message)
|
||||||
|
|
||||||
|
# 獲取所有使用者,支援過濾和分頁
|
||||||
|
@app.route('/v1/users', methods=['GET'])
|
||||||
|
def get_users():
|
||||||
|
try:
|
||||||
|
# 取得查詢參數
|
||||||
|
min_age = request.args.get('min_age', type=int)
|
||||||
|
max_age = request.args.get('max_age', type=int)
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
limit = request.args.get('limit', 10, type=int)
|
||||||
|
|
||||||
|
# 計算偏移量
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# 建立基本查詢
|
||||||
|
query = "SELECT * FROM users"
|
||||||
|
count_query = "SELECT COUNT(*) as total FROM users"
|
||||||
|
params = []
|
||||||
|
where_clauses = []
|
||||||
|
|
||||||
|
# 加入過濾條件
|
||||||
|
if min_age is not None:
|
||||||
|
where_clauses.append("age >= %s")
|
||||||
|
params.append(min_age)
|
||||||
|
|
||||||
|
if max_age is not None:
|
||||||
|
where_clauses.append("age <= %s")
|
||||||
|
params.append(max_age)
|
||||||
|
|
||||||
|
# 組合 WHERE 子句
|
||||||
|
if where_clauses:
|
||||||
|
query += " WHERE " + " AND ".join(where_clauses)
|
||||||
|
count_query += " WHERE " + " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
# 加入分頁
|
||||||
|
query += " LIMIT %s OFFSET %s"
|
||||||
|
params.extend([limit, offset])
|
||||||
|
|
||||||
|
# 執行查詢
|
||||||
|
cursor.execute(query, params)
|
||||||
|
users = cursor.fetchall()
|
||||||
|
|
||||||
|
# 獲取總記錄數
|
||||||
|
cursor.execute(count_query, params[:-2] if params else [])
|
||||||
|
total = cursor.fetchone()['total']
|
||||||
|
|
||||||
|
# 計算總頁數
|
||||||
|
total_pages = (total + limit - 1) // limit
|
||||||
|
|
||||||
|
# 建立 meta 資訊
|
||||||
|
meta = {
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
"total_pages": total_pages
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功獲取使用者列表", {"users": users, "meta": meta})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 獲取所有披薩,支援過濾和分頁
|
||||||
|
@app.route('/v1/pizzas', methods=['GET'])
|
||||||
|
def get_pizzas():
|
||||||
|
try:
|
||||||
|
# 取得查詢參數
|
||||||
|
min_price = request.args.get('min_price', type=float)
|
||||||
|
max_price = request.args.get('max_price', type=float)
|
||||||
|
size = request.args.get('size')
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
limit = request.args.get('limit', 10, type=int)
|
||||||
|
|
||||||
|
# 計算偏移量
|
||||||
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# 建立基本查詢
|
||||||
|
query = "SELECT * FROM pizzas"
|
||||||
|
count_query = "SELECT COUNT(*) as total FROM pizzas"
|
||||||
|
params = []
|
||||||
|
where_clauses = []
|
||||||
|
|
||||||
|
# 加入過濾條件
|
||||||
|
if min_price is not None:
|
||||||
|
where_clauses.append("price >= %s")
|
||||||
|
params.append(min_price)
|
||||||
|
|
||||||
|
if max_price is not None:
|
||||||
|
where_clauses.append("price <= %s")
|
||||||
|
params.append(max_price)
|
||||||
|
|
||||||
|
if size is not None:
|
||||||
|
where_clauses.append("size = %s")
|
||||||
|
params.append(size)
|
||||||
|
|
||||||
|
# 組合 WHERE 子句
|
||||||
|
if where_clauses:
|
||||||
|
query += " WHERE " + " AND ".join(where_clauses)
|
||||||
|
count_query += " WHERE " + " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
# 加入分頁
|
||||||
|
query += " LIMIT %s OFFSET %s"
|
||||||
|
params.extend([limit, offset])
|
||||||
|
|
||||||
|
# 執行查詢
|
||||||
|
cursor.execute(query, params)
|
||||||
|
pizzas = cursor.fetchall()
|
||||||
|
|
||||||
|
# 獲取總記錄數
|
||||||
|
cursor.execute(count_query, params[:-2] if params else [])
|
||||||
|
total = cursor.fetchone()['total']
|
||||||
|
|
||||||
|
# 計算總頁數
|
||||||
|
total_pages = (total + limit - 1) // limit
|
||||||
|
|
||||||
|
# 建立 meta 資訊
|
||||||
|
meta = {
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
"total_pages": total_pages
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功獲取披薩列表", {"pizzas": pizzas, "meta": meta})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 獲取單一使用者
|
||||||
|
@app.route('/v1/users/<int:user_id>', methods=['GET'])
|
||||||
|
def get_user(user_id):
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return jsonify(create_error_response(404, f"找不到 ID 為 {user_id} 的使用者")), 404
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功獲取使用者資料", {"user": user})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 獲取單一披薩
|
||||||
|
@app.route('/v1/pizzas/<int:pizza_id>', methods=['GET'])
|
||||||
|
def get_pizza_by_id(pizza_id):
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
|
||||||
|
pizza = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not pizza:
|
||||||
|
return jsonify(create_error_response(404, f"找不到 ID 為 {pizza_id} 的披薩")), 404
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功獲取披薩資料", {"pizza": pizza})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return jsonify(create_error_response(404, f"找不到 ID 為 {user_id} 的使用者")), 404
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功獲取使用者資料", {"user": user})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 獲取單一披薩
|
||||||
|
# 已在上方定義 GET /v1/pizzas/<int:pizza_id> 路由
|
||||||
|
|
||||||
|
# 新增披薩
|
||||||
|
@app.route('/v1/pizzas', methods=['POST'])
|
||||||
|
def create_pizza():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 驗證必要欄位
|
||||||
|
required_fields = ['name', 'price', 'size']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data:
|
||||||
|
return jsonify(create_error_response(400, f"缺少必要欄位: {field}")), 400
|
||||||
|
|
||||||
|
# 驗證資料類型
|
||||||
|
if not isinstance(data['name'], str):
|
||||||
|
return jsonify(create_error_response(400, "name 必須是字串")), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
price = float(data['price'])
|
||||||
|
if price <= 0:
|
||||||
|
return jsonify(create_error_response(400, "price 必須大於 0")), 400
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return jsonify(create_error_response(400, "price 必須是有效的數字")), 400
|
||||||
|
|
||||||
|
if not isinstance(data['size'], str) or data['size'] not in ['S', 'M', 'L']:
|
||||||
|
return jsonify(create_error_response(400, "size 必須是 S、M 或 L")), 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# 設定時間
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# 插入資料
|
||||||
|
insert_query = "INSERT INTO pizzas (name, price, size, created_at, updated_at) VALUES (%s, %s, %s, %s, %s)"
|
||||||
|
cursor.execute(insert_query, (data['name'], price, data['size'], now, now))
|
||||||
|
pizza_id = cursor.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 獲取新增的披薩資料
|
||||||
|
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
|
||||||
|
new_pizza = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 201, "成功新增披薩", {"pizza": new_pizza})), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 更新披薩
|
||||||
|
@app.route('/v1/pizzas/<int:pizza_id>', methods=['PATCH'])
|
||||||
|
def update_pizza(pizza_id):
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 至少需要一個欄位
|
||||||
|
if not data or not any(field in data for field in ['name', 'price', 'size']):
|
||||||
|
return jsonify(create_error_response(400, "至少需要提供一個欄位: name, price, size")), 400
|
||||||
|
|
||||||
|
# 驗證資料類型
|
||||||
|
if 'name' in data and not isinstance(data['name'], str):
|
||||||
|
return jsonify(create_error_response(400, "name 必須是字串")), 400
|
||||||
|
|
||||||
|
if 'price' in data:
|
||||||
|
try:
|
||||||
|
price = float(data['price'])
|
||||||
|
if price <= 0:
|
||||||
|
return jsonify(create_error_response(400, "price 必須大於 0")), 400
|
||||||
|
data['price'] = price
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return jsonify(create_error_response(400, "price 必須是有效的數字")), 400
|
||||||
|
|
||||||
|
if 'size' in data and (not isinstance(data['size'], str) or data['size'] not in ['S', 'M', 'L']):
|
||||||
|
return jsonify(create_error_response(400, "size 必須是 S、M 或 L")), 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# 檢查披薩是否存在
|
||||||
|
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
|
||||||
|
pizza = cursor.fetchone()
|
||||||
|
|
||||||
|
if not pizza:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify(create_error_response(404, f"找不到 ID 為 {pizza_id} 的披薩")), 404
|
||||||
|
|
||||||
|
# 設定更新時間
|
||||||
|
now = datetime.now()
|
||||||
|
data['updated_at'] = now
|
||||||
|
|
||||||
|
# 建立更新查詢
|
||||||
|
update_fields = []
|
||||||
|
update_values = []
|
||||||
|
|
||||||
|
for field in ['name', 'price', 'size', 'updated_at']:
|
||||||
|
if field in data:
|
||||||
|
update_fields.append(f"{field} = %s")
|
||||||
|
update_values.append(data[field])
|
||||||
|
|
||||||
|
update_values.append(pizza_id) # WHERE id = %s 的參數
|
||||||
|
|
||||||
|
update_query = f"UPDATE pizzas SET {', '.join(update_fields)} WHERE id = %s"
|
||||||
|
cursor.execute(update_query, update_values)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 獲取更新後的披薩資料
|
||||||
|
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
|
||||||
|
updated_pizza = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功更新披薩", {"pizza": updated_pizza})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 刪除披薩
|
||||||
|
@app.route('/v1/pizzas/<int:pizza_id>', methods=['DELETE'])
|
||||||
|
def delete_pizza(pizza_id):
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 檢查披薩是否存在
|
||||||
|
cursor.execute("SELECT id FROM pizzas WHERE id = %s", (pizza_id,))
|
||||||
|
pizza = cursor.fetchone()
|
||||||
|
|
||||||
|
if not pizza:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify(create_error_response(404, f"找不到 ID 為 {pizza_id} 的披薩")), 404
|
||||||
|
|
||||||
|
# 刪除披薩
|
||||||
|
cursor.execute("DELETE FROM pizzas WHERE id = %s", (pizza_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 204, "成功刪除披薩")), 204
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 新增使用者
|
||||||
|
@app.route('/v1/users', methods=['POST'])
|
||||||
|
def create_user():
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 驗證必要欄位
|
||||||
|
required_fields = ['name', 'email', 'age']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in data:
|
||||||
|
return jsonify(create_error_response(400, f"缺少必要欄位: {field}")), 400
|
||||||
|
|
||||||
|
# 驗證資料類型
|
||||||
|
if not isinstance(data['name'], str):
|
||||||
|
return jsonify(create_error_response(400, "name 必須是字串")), 400
|
||||||
|
|
||||||
|
# 驗證資料類型
|
||||||
|
if not isinstance(data['name'], str):
|
||||||
|
return jsonify(create_error_response(400, "name 必須是字串")), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
price = float(data['price'])
|
||||||
|
if price <= 0:
|
||||||
|
return jsonify(create_error_response(400, "price 必須大於 0")), 400
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return jsonify(create_error_response(400, "price 必須是有效的數字")), 400
|
||||||
|
|
||||||
|
if not isinstance(data['size'], str) or data['size'] not in ['S', 'M', 'L']:
|
||||||
|
return jsonify(create_error_response(400, "size 必須是 S、M 或 L")), 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 設定時間
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# 插入資料
|
||||||
|
insert_query = "INSERT INTO pizzas (name, price, size, created_at, updated_at) VALUES (%s, %s, %s, %s, %s)"
|
||||||
|
cursor.execute(insert_query, (data['name'], price, data['size'], now, now))
|
||||||
|
pizza_id = cursor.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 獲取新增的披薩資料
|
||||||
|
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
|
||||||
|
new_pizza = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 201, "成功新增披薩", {"pizza": new_pizza})), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
if not isinstance(data['email'], str):
|
||||||
|
return jsonify(create_error_response(400, "email 必須是字串")), 400
|
||||||
|
|
||||||
|
if not isinstance(data['age'], int):
|
||||||
|
return jsonify(create_error_response(400, "age 必須是整數")), 400
|
||||||
|
|
||||||
|
# 驗證 email 格式 (簡單驗證)
|
||||||
|
if '@' not in data['email']:
|
||||||
|
return jsonify(create_error_response(400, "email 格式不正確")), 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# 使用參數化查詢避免 SQL 注入
|
||||||
|
query = "INSERT INTO users (name, email, age) VALUES (%s, %s, %s)"
|
||||||
|
cursor.execute(query, (data['name'], data['email'], data['age']))
|
||||||
|
|
||||||
|
# 獲取新增的使用者 ID
|
||||||
|
user_id = cursor.lastrowid
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 獲取新增的使用者資料
|
||||||
|
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||||
|
new_user = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 201, "成功新增使用者", {"user": new_user})), 201
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 更新使用者
|
||||||
|
@app.route('/v1/users/<int:user_id>', methods=['PATCH'])
|
||||||
|
def update_user(user_id):
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 檢查是否有要更新的欄位
|
||||||
|
if not data:
|
||||||
|
return jsonify(create_error_response(400, "沒有提供要更新的資料")), 400
|
||||||
|
|
||||||
|
# 驗證資料類型
|
||||||
|
if 'name' in data and not isinstance(data['name'], str):
|
||||||
|
return jsonify(create_error_response(400, "name 必須是字串")), 400
|
||||||
|
|
||||||
|
if 'email' in data and not isinstance(data['email'], str):
|
||||||
|
return jsonify(create_error_response(400, "email 必須是字串")), 400
|
||||||
|
|
||||||
|
if 'age' in data and not isinstance(data['age'], int):
|
||||||
|
return jsonify(create_error_response(400, "age 必須是整數")), 400
|
||||||
|
|
||||||
|
# 驗證 email 格式 (簡單驗證)
|
||||||
|
if 'email' in data and '@' not in data['email']:
|
||||||
|
return jsonify(create_error_response(400, "email 格式不正確")), 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# 檢查使用者是否存在
|
||||||
|
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify(create_error_response(404, f"找不到 ID 為 {user_id} 的使用者")), 404
|
||||||
|
|
||||||
|
# 建立更新查詢
|
||||||
|
update_fields = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if 'name' in data:
|
||||||
|
update_fields.append("name = %s")
|
||||||
|
params.append(data['name'])
|
||||||
|
|
||||||
|
if 'email' in data:
|
||||||
|
update_fields.append("email = %s")
|
||||||
|
params.append(data['email'])
|
||||||
|
|
||||||
|
if 'age' in data:
|
||||||
|
update_fields.append("age = %s")
|
||||||
|
params.append(data['age'])
|
||||||
|
|
||||||
|
# 如果沒有要更新的欄位
|
||||||
|
if not update_fields:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify(create_error_response(400, "沒有提供有效的更新欄位")), 400
|
||||||
|
|
||||||
|
# 建立更新查詢
|
||||||
|
query = "UPDATE users SET " + ", ".join(update_fields) + " WHERE id = %s"
|
||||||
|
params.append(user_id)
|
||||||
|
|
||||||
|
# 執行更新
|
||||||
|
cursor.execute(query, params)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# 獲取更新後的使用者資料
|
||||||
|
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
||||||
|
updated_user = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功更新使用者資料", {"user": updated_user})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 刪除使用者
|
||||||
|
@app.route('/v1/users/<int:user_id>', methods=['DELETE'])
|
||||||
|
def delete_user(user_id):
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 檢查使用者是否存在
|
||||||
|
cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
|
||||||
|
user = cursor.fetchone()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return jsonify(create_error_response(404, f"找不到 ID 為 {user_id} 的使用者")), 404
|
||||||
|
|
||||||
|
# 刪除使用者
|
||||||
|
cursor.execute("DELETE FROM users WHERE id = %s", (user_id,))
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 204, "成功刪除使用者")), 204
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 獲取所有 pizzas,支援分頁
|
||||||
|
# 已在上方定義 GET /v1/pizzas 路由
|
||||||
|
|
||||||
|
# 獲取總記錄數
|
||||||
|
cursor.execute(count_query, params[:-2] if params else [])
|
||||||
|
total = cursor.fetchone()['total']
|
||||||
|
|
||||||
|
# 計算總頁數
|
||||||
|
total_pages = (total + limit - 1) // limit
|
||||||
|
|
||||||
|
# 建立 meta 資訊
|
||||||
|
meta = {
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"limit": limit,
|
||||||
|
"total_pages": total_pages
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功獲取披薩列表", {"pizzas": pizzas, "meta": meta})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 獲取單一 pizza
|
||||||
|
# 注意:此路由已在其他地方定義
|
||||||
|
def get_pizza(pizza_id):
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
return jsonify(create_error_response(500, "資料庫連線失敗")), 500
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT * FROM pizzas WHERE id = %s", (pizza_id,))
|
||||||
|
pizza = cursor.fetchone()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not pizza:
|
||||||
|
return jsonify(create_error_response(404, f"找不到 ID 為 {pizza_id} 的披薩")), 404
|
||||||
|
|
||||||
|
return jsonify(create_response("success", 200, "成功獲取披薩資料", {"pizza": pizza})), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify(create_error_response(500, f"伺服器錯誤: {str(e)}")), 500
|
||||||
|
|
||||||
|
# 新增 pizza
|
||||||
|
# 已在上方定義 POST /v1/pizzas 路由
|
||||||
|
|
||||||
|
# 更新 pizza
|
||||||
|
# 已在上方定義 PATCH /v1/pizzas/<int:pizza_id> 路由
|
||||||
|
|
||||||
|
# 刪除 pizza
|
||||||
|
# 已在上方定義 DELETE /v1/pizzas/<int:pizza_id> 路由
|
||||||
|
|
||||||
|
# 主程式入口
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 確保資料表存在
|
||||||
|
conn = get_db_connection()
|
||||||
|
if conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL,
|
||||||
|
age INT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# 確保 pizzas 資料表存在
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS pizzas (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(50) NOT NULL,
|
||||||
|
price DECIMAL(10, 2) NOT NULL,
|
||||||
|
size VARCHAR(2) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
app.run(debug=True)
|
191
create_pizzas_table.py
Normal file
191
create_pizzas_table.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import mysql.connector
|
||||||
|
import random
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 資料庫連線資訊
|
||||||
|
DB_CONFIG = {
|
||||||
|
'host': 'mysql.theaken.com',
|
||||||
|
'port': 33306,
|
||||||
|
'user': 'A019',
|
||||||
|
'password': '9wvKEkxBzVca',
|
||||||
|
'database': 'db_A019'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 建立資料庫連線
|
||||||
|
def get_db_connection():
|
||||||
|
try:
|
||||||
|
conn = mysql.connector.connect(**DB_CONFIG)
|
||||||
|
return conn
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
print(f"資料庫連線錯誤: {err}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 建立 pizzas 資料表
|
||||||
|
def create_pizzas_table():
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
print("無法連接到資料庫")
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 建立 pizzas 資料表
|
||||||
|
create_table_query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS pizzas (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(50) NOT NULL,
|
||||||
|
price DECIMAL(10, 2) NOT NULL,
|
||||||
|
size VARCHAR(2) NOT NULL,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute(create_table_query)
|
||||||
|
conn.commit()
|
||||||
|
print("成功建立 pizzas 資料表")
|
||||||
|
return True
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
print(f"建立資料表錯誤: {err}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 產生隨機 pizza 資料
|
||||||
|
def generate_random_pizzas(count=6):
|
||||||
|
# Pizza 名稱列表
|
||||||
|
pizza_names = [
|
||||||
|
"夏威夷披薩", "瑪格麗特披薩", "蘑菇披薩", "臘腸披薩",
|
||||||
|
"海鮮披薩", "素食披薩", "四季披薩", "墨西哥辣味披薩",
|
||||||
|
"起司披薩", "燒烤雞肉披薩", "牛肉披薩", "蔬菜披薩"
|
||||||
|
]
|
||||||
|
|
||||||
|
# 確保名稱不重複,如果 count 大於名稱列表長度,則使用所有可用名稱
|
||||||
|
if count > len(pizza_names):
|
||||||
|
count = len(pizza_names)
|
||||||
|
|
||||||
|
# 隨機選擇不重複的名稱
|
||||||
|
selected_names = random.sample(pizza_names, count)
|
||||||
|
|
||||||
|
# Pizza 尺寸
|
||||||
|
pizza_sizes = ["S", "M", "L"]
|
||||||
|
|
||||||
|
# 隨機生成 pizza 資料
|
||||||
|
pizzas = []
|
||||||
|
for name in selected_names:
|
||||||
|
size = random.choice(pizza_sizes)
|
||||||
|
|
||||||
|
# 根據尺寸設定價格範圍
|
||||||
|
if size == "S":
|
||||||
|
price = round(random.uniform(200, 300), 2)
|
||||||
|
elif size == "M":
|
||||||
|
price = round(random.uniform(300, 400), 2)
|
||||||
|
else: # L
|
||||||
|
price = round(random.uniform(400, 500), 2)
|
||||||
|
|
||||||
|
# 設定時間
|
||||||
|
now = datetime.now()
|
||||||
|
created_at = now
|
||||||
|
updated_at = now
|
||||||
|
|
||||||
|
pizzas.append((name, price, size, created_at, updated_at))
|
||||||
|
|
||||||
|
return pizzas
|
||||||
|
|
||||||
|
# 插入隨機 pizza 資料
|
||||||
|
def insert_random_pizzas(count=6):
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
print("無法連接到資料庫")
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 生成隨機 pizza 資料
|
||||||
|
pizzas = generate_random_pizzas(count)
|
||||||
|
|
||||||
|
# 插入資料
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO pizzas (name, price, size, created_at, updated_at)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.executemany(insert_query, pizzas)
|
||||||
|
conn.commit()
|
||||||
|
print(f"成功插入 {cursor.rowcount} 筆 pizza 資料")
|
||||||
|
return True
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
print(f"插入資料錯誤: {err}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 顯示所有 pizza 資料
|
||||||
|
def show_all_pizzas():
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
print("無法連接到資料庫")
|
||||||
|
return
|
||||||
|
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("SELECT * FROM pizzas")
|
||||||
|
pizzas = cursor.fetchall()
|
||||||
|
|
||||||
|
if not pizzas:
|
||||||
|
print("沒有找到任何 pizza 資料")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n所有 Pizza 資料:")
|
||||||
|
print("-" * 80)
|
||||||
|
print(f"{'ID':<5} {'名稱':<20} {'價格':<10} {'尺寸':<5} {'建立時間':<20} {'更新時間':<20}")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
for pizza in pizzas:
|
||||||
|
print(f"{pizza['id']:<5} {pizza['name']:<20} {pizza['price']:<10} {pizza['size']:<5} {pizza['created_at']} {pizza['updated_at']}")
|
||||||
|
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
print(f"查詢資料錯誤: {err}")
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 清空 pizzas 資料表
|
||||||
|
def truncate_pizzas_table():
|
||||||
|
conn = get_db_connection()
|
||||||
|
if not conn:
|
||||||
|
print("無法連接到資料庫")
|
||||||
|
return False
|
||||||
|
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("TRUNCATE TABLE pizzas")
|
||||||
|
conn.commit()
|
||||||
|
print("成功清空 pizzas 資料表")
|
||||||
|
return True
|
||||||
|
except mysql.connector.Error as err:
|
||||||
|
print(f"清空資料表錯誤: {err}")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 主程式
|
||||||
|
def main():
|
||||||
|
# 建立資料表
|
||||||
|
if create_pizzas_table():
|
||||||
|
# 清空資料表
|
||||||
|
if truncate_pizzas_table():
|
||||||
|
# 插入隨機資料
|
||||||
|
if insert_random_pizzas(6):
|
||||||
|
# 顯示所有資料
|
||||||
|
show_all_pizzas()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
flask==2.3.3
|
||||||
|
flask-cors==4.0.0
|
||||||
|
mysql-connector-python==8.1.0
|
317
sales_dashboard.html
Normal file
317
sales_dashboard.html
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Sales Dashboard</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
|
||||||
|
.container { max-width: 900px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
h1, h2 { color: #0056b3; border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 20px; }
|
||||||
|
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
||||||
|
.stat-box { background: #e9f7ff; padding: 15px; border-radius: 5px; text-align: center; }
|
||||||
|
.stat-box h3 { margin: 0 0 10px 0; color: #007bff; }
|
||||||
|
.stat-box p { font-size: 1.8em; font-weight: bold; color: #333; }
|
||||||
|
.chart-container { margin-bottom: 30px; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||||
|
.chart-container canvas { max-width: 100%; height: auto; }
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: #333;
|
||||||
|
padding: 10px 0;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.nav-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
.nav-link {
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: #575757;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New styles for summary boxes */
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.summary-box {
|
||||||
|
background: #e0f7fa; /* Light blue */
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.summary-box h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #00796b; /* Darker blue-green */
|
||||||
|
}
|
||||||
|
.summary-box p {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #004d40; /* Even darker blue-green */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for the data table */
|
||||||
|
.data-table-container {
|
||||||
|
margin-top: 30px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.data-table th, .data-table td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.data-table th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.data-table tbody tr:nth-child(even) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
.data-table tbody tr:hover {
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="navbar">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item"><a href="#" class="nav-link">Overview</a></li>
|
||||||
|
<li class="nav-item"><a href="#" class="nav-link">Product Sales</a></li>
|
||||||
|
<li class="nav-item"><a href="#" class="nav-link">Regional Sales</a></li>
|
||||||
|
<li class="nav-item"><a href="sales_records.html" class="nav-link">Sales Records</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<h1>Sales Dashboard</h1>
|
||||||
|
|
||||||
|
<div class="summary-grid">
|
||||||
|
<div class="summary-box">
|
||||||
|
<h3>Total Sales</h3>
|
||||||
|
<p id="totalSales">Loading...</p>
|
||||||
|
</div>
|
||||||
|
<div class="summary-box">
|
||||||
|
<h3>Total Orders</h3>
|
||||||
|
<p id="totalOrders">Loading...</p>
|
||||||
|
</div>
|
||||||
|
<div class="summary-box">
|
||||||
|
<h3>Product Categories</h3>
|
||||||
|
<p id="productCategories">Loading...</p>
|
||||||
|
</div>
|
||||||
|
<div class="summary-box">
|
||||||
|
<h3>Sales Regions</h3>
|
||||||
|
<p id="salesRegions">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Sales by Product Category</h2>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="categoryBarChart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Sales Over Time</h2>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="timeLineChart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>All Sales Data</h2>
|
||||||
|
<div class="data-table-container">
|
||||||
|
<table class="data-table" id="allSalesDataTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<!-- Headers will be dynamically generated -->
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Data will be dynamically loaded -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Function to fetch and display summary data
|
||||||
|
async function fetchSummaryData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:5000/dashboard_summary');
|
||||||
|
const data = await response.json();
|
||||||
|
document.getElementById('totalSales').innerText = `${data.total_sales.toFixed(2)}`;
|
||||||
|
document.getElementById('totalOrders').innerText = data.total_orders;
|
||||||
|
document.getElementById('productCategories').innerText = data.product_categories;
|
||||||
|
document.getElementById('salesRegions').innerText = data.sales_regions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching summary data:', error);
|
||||||
|
document.getElementById('totalSales').innerText = 'Error';
|
||||||
|
document.getElementById('totalOrders').innerText = 'Error';
|
||||||
|
document.getElementById('productCategories').innerText = 'Error';
|
||||||
|
document.getElementById('salesRegions').innerText = 'Error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fetch and render charts
|
||||||
|
async function fetchChartData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:5000/dashboard_charts');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Bar Chart: Sales by Product Category
|
||||||
|
const categoryBarCtx = document.getElementById('categoryBarChart').getContext('2d');
|
||||||
|
new Chart(categoryBarCtx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: data.category_sales.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Sales Amount',
|
||||||
|
data: data.category_sales.data,
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.6)',
|
||||||
|
borderColor: 'rgba(75, 192, 192, 1)',
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Sales Amount'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Sales by Product Category'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Line Chart: Sales Over Time
|
||||||
|
const timeLineCtx = document.getElementById('timeLineChart').getContext('2d');
|
||||||
|
new Chart(timeLineCtx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: data.time_sales.labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Sales Amount',
|
||||||
|
data: data.time_sales.data,
|
||||||
|
backgroundColor: 'rgba(153, 102, 255, 0.6)',
|
||||||
|
borderColor: 'rgba(153, 102, 255, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
fill: false
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Sales Amount'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Sales Over Time'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching chart data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to fetch and display all sales data in a table
|
||||||
|
async function fetchAllSalesData() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:5000/sales_data');
|
||||||
|
const data = await response.json();
|
||||||
|
const table = document.getElementById('allSalesDataTable');
|
||||||
|
const thead = table.querySelector('thead tr');
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
|
||||||
|
// Clear existing content
|
||||||
|
thead.innerHTML = '';
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
// Create headers
|
||||||
|
const headers = Object.keys(data[0]);
|
||||||
|
headers.forEach(header => {
|
||||||
|
const th = document.createElement('th');
|
||||||
|
th.innerText = header;
|
||||||
|
thead.appendChild(th);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Populate table rows
|
||||||
|
data.forEach(record => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
headers.forEach(header => {
|
||||||
|
const td = document.createElement('td');
|
||||||
|
td.innerText = record[header];
|
||||||
|
tr.appendChild(td);
|
||||||
|
});
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const td = document.createElement('td');
|
||||||
|
td.colSpan = 10; // Arbitrary large number to span all columns
|
||||||
|
td.innerText = 'No sales data available.';
|
||||||
|
tr.appendChild(td);
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching all sales data:', error);
|
||||||
|
const table = document.getElementById('allSalesDataTable');
|
||||||
|
const tbody = table.querySelector('tbody');
|
||||||
|
tbody.innerHTML = '<tr><td colspan="10">Error loading data.</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all fetch functions on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
fetchSummaryData();
|
||||||
|
fetchChartData();
|
||||||
|
fetchAllSalesData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
Reference in New Issue
Block a user