diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md index e9858756..a0ab7154 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/commit/SKILL.md @@ -13,6 +13,9 @@ description: 提交並推送程式碼變更。用於每個開發階段完成後 - 要把程式碼推送到 GitHub,讓組員可以取得最新版本 - 遇到 Git 要求設定 `user.name` 或 `user.email` 的提示 +## 檢查 .gitignore +在推送之前檢查 .gitignore,確保虛擬環境、環境設定與非必要檔案有正確被排除在版本控制之外。 + ## ⚠️ 設定 Git 使用者身份 如果 Git 顯示以下提示,需要先設定身份才能 commit: diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..8760da8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Environment +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# SQLite database and instance directory +instance/ +*.db +*.sqlite3 + +# OS generated files +.DS_Store +Thumbs.db diff --git a/README.md b/README.md index 62cd639b..cc3edf2d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - [系統簡介](#系統簡介) - [技術棧](#技術棧) -- [組員與分工](#組員與分工) +- [系統截圖](#系統截圖) - [個人心得](#個人心得) --- @@ -25,13 +25,8 @@ --- -## 組員與分工(先寫自己就好) - -**第 X 組** - -| 姓名 | 學號 | 負責部分 | -| ---- | ---- | -------- | -| | | | +## 系統截圖 +請在此貼上系統的截圖畫面, 選擇一個功能邊操作邊截圖 --- diff --git a/app.py b/app.py new file mode 100644 index 00000000..41549209 --- /dev/null +++ b/app.py @@ -0,0 +1,16 @@ +from flask import Flask +from app.routes.recipe import recipe_bp + +def create_app(): + # 指定 template 與 static 的存放路徑,確保與專案結構相符 + app = Flask(__name__, template_folder='app/templates', static_folder='app/static') + + # 註冊 Blueprints + app.register_blueprint(recipe_bp) + + return app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 00000000..f01acc75 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +# initialize app/models module diff --git a/app/models/category.py b/app/models/category.py new file mode 100644 index 00000000..3a27c6d1 --- /dev/null +++ b/app/models/category.py @@ -0,0 +1,56 @@ +from .db import get_db + +class Category: + @staticmethod + def get_all(): + """取得所有分類""" + conn = get_db() + cursor = conn.execute('SELECT * FROM categories ORDER BY type, id') + rows = cursor.fetchall() + conn.close() + return [dict(row) for row in rows] + + @staticmethod + def get_by_id(cat_id): + """根據 ID 取得特定分類""" + conn = get_db() + cursor = conn.execute('SELECT * FROM categories WHERE id = ?', (cat_id,)) + row = cursor.fetchone() + conn.close() + return dict(row) if row else None + + @staticmethod + def create(name, type_, is_preset=0): + """新增自訂分類""" + conn = get_db() + cursor = conn.execute( + 'INSERT INTO categories (name, type, is_preset) VALUES (?, ?, ?)', + (name, type_, is_preset) + ) + conn.commit() + new_id = cursor.lastrowid + conn.close() + return new_id + + @staticmethod + def update(cat_id, name): + """更新分類名稱 (限自訂分類)""" + conn = get_db() + cursor = conn.execute( + 'UPDATE categories SET name = ? WHERE id = ? AND is_preset = 0', + (name, cat_id) + ) + conn.commit() + affected = cursor.rowcount + conn.close() + return affected > 0 + + @staticmethod + def delete(cat_id): + """刪除分類 (檢查限自訂分類)""" + conn = get_db() + cursor = conn.execute('DELETE FROM categories WHERE id = ? AND is_preset = 0', (cat_id,)) + conn.commit() + affected = cursor.rowcount + conn.close() + return affected > 0 diff --git a/app/models/db.py b/app/models/db.py new file mode 100644 index 00000000..44614cec --- /dev/null +++ b/app/models/db.py @@ -0,0 +1,24 @@ +import sqlite3 +import os + +# 自動推導 instance/expense.db 路徑 +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) +DB_PATH = os.path.join(BASE_DIR, 'instance', 'expense.db') + +def get_db(): + conn = sqlite3.connect(DB_PATH) + # 使用 sqlite3.Row 讓查詢結果能像 dictionary 那樣透過欄位名稱取值 + conn.row_factory = sqlite3.Row + return conn + +def init_db(): + schema_path = os.path.join(BASE_DIR, 'database', 'schema.sql') + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + + with open(schema_path, 'r', encoding='utf-8') as f: + schema_sql = f.read() + + conn = get_db() + conn.executescript(schema_sql) + conn.commit() + conn.close() diff --git a/app/models/expense.py b/app/models/expense.py new file mode 100644 index 00000000..09d46d0a --- /dev/null +++ b/app/models/expense.py @@ -0,0 +1,72 @@ +from .db import get_db +from datetime import datetime + +class Expense: + @staticmethod + def get_all(): + """取得所有收支紀錄 (包含分類名稱)""" + conn = get_db() + query = ''' + SELECT e.*, c.name as category_name, c.type as category_type + FROM expenses e + JOIN categories c ON e.category_id = c.id + ORDER BY e.date DESC, e.created_at DESC + ''' + cursor = conn.execute(query) + rows = cursor.fetchall() + conn.close() + return [dict(row) for row in rows] + + @staticmethod + def get_by_id(expense_id): + """根據 ID 取得單筆收支明細""" + conn = get_db() + query = ''' + SELECT e.*, c.name as category_name, c.type as category_type + FROM expenses e + JOIN categories c ON e.category_id = c.id + WHERE e.id = ? + ''' + cursor = conn.execute(query, (expense_id,)) + row = cursor.fetchone() + conn.close() + return dict(row) if row else None + + @staticmethod + def create(amount, category_id, note, date): + """新增一筆收支紀錄""" + conn = get_db() + created_at = datetime.now().isoformat() + cursor = conn.execute( + 'INSERT INTO expenses (amount, category_id, note, date, created_at) VALUES (?, ?, ?, ?, ?)', + (amount, category_id, note, date, created_at) + ) + conn.commit() + new_id = cursor.lastrowid + conn.close() + return new_id + + @staticmethod + def update(expense_id, amount, category_id, note, date): + """更新一筆收支紀錄""" + conn = get_db() + cursor = conn.execute( + '''UPDATE expenses + SET amount = ?, category_id = ?, note = ?, date = ? + WHERE id = ?''', + (amount, category_id, note, date, expense_id) + ) + conn.commit() + affected = cursor.rowcount + conn.close() + return affected > 0 + + @staticmethod + def delete(expense_id): + """刪除一筆收支紀錄""" + conn = get_db() + cursor = conn.execute('DELETE FROM expenses WHERE id = ?', (expense_id,)) + conn.commit() + affected = cursor.rowcount + conn.close() + return affected > 0 diff --git a/app/models/recipe.py b/app/models/recipe.py new file mode 100644 index 00000000..4102d3fa --- /dev/null +++ b/app/models/recipe.py @@ -0,0 +1,73 @@ +import sqlite3 +import os +from contextlib import contextmanager + +# 取得資料庫檔案存放的目錄 instance/ (位於專案根目錄) +DB_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'instance') +DB_PATH = os.path.join(DB_DIR, 'database.db') + +@contextmanager +def get_db_connection(): + # 若目錄不存在則建立一個 + os.makedirs(DB_DIR, exist_ok=True) + conn = sqlite3.connect(DB_PATH) + # 將回傳結果設為 dict-like 的 Row 物件 + conn.row_factory = sqlite3.Row + try: + yield conn + finally: + conn.commit() + conn.close() + +class Recipe: + @staticmethod + def create(data): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO recipes (title, description, ingredients, steps) + VALUES (?, ?, ?, ?) + ''', (data['title'], data.get('description', ''), data['ingredients'], data['steps'])) + return cursor.lastrowid + + @staticmethod + def get_all(query=None): + with get_db_connection() as conn: + cursor = conn.cursor() + if query: + # 簡單支援對 title 或 ingredients 的 LIKE 搜尋 (搜尋推薦食譜) + search_term = f"%{query}%" + cursor.execute(''' + SELECT * FROM recipes + WHERE title LIKE ? OR ingredients LIKE ? + ORDER BY created_at DESC + ''', (search_term, search_term)) + else: + cursor.execute('SELECT * FROM recipes ORDER BY created_at DESC') + return [dict(row) for row in cursor.fetchall()] + + @staticmethod + def get_by_id(recipe_id): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM recipes WHERE id = ?', (recipe_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + @staticmethod + def update(recipe_id, data): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE recipes + SET title = ?, description = ?, ingredients = ?, steps = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (data['title'], data.get('description', ''), data['ingredients'], data['steps'], recipe_id)) + return cursor.rowcount > 0 + + @staticmethod + def delete(recipe_id): + with get_db_connection() as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM recipes WHERE id = ?', (recipe_id,)) + return cursor.rowcount > 0 diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 00000000..d1a57de5 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1,2 @@ +# init routes +from app.routes.recipe import recipe_bp diff --git a/app/routes/category.py b/app/routes/category.py new file mode 100644 index 00000000..4fa81991 --- /dev/null +++ b/app/routes/category.py @@ -0,0 +1,64 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash + +category_bp = Blueprint('category', __name__, url_prefix='/categories') + +@category_bp.route('/') +def list_categories(): + """ + 分類列表 + HTTP Method: GET + 處理邏輯: 呼叫 Category.get_all() 取得所有分類資料 + 輸出: render_template('categories/index.html', categories=categories) + """ + pass + +@category_bp.route('/new', methods=['GET', 'POST']) +def new_category(): + """ + 新增分類 + HTTP Method: GET, POST + + [GET] + 處理邏輯: 單純回傳新增表單的介面 + 輸出: render_template('categories/form.html') + + [POST] + 輸入: request.form (包含 name, type) + 處理邏輯: 檢查名稱不為空,呼叫 Category.create(name, type, is_preset=0) + 輸出: redirect(url_for('category.list_categories')) + 錯誤處理: 名稱為空或類型不合法時,使用 flash() 紀錄錯誤,並回傳原表單頁面 + """ + pass + +@category_bp.route('//edit', methods=['GET', 'POST']) +def edit_category(id): + """ + 編輯自訂分類 + HTTP Method: GET, POST + + [GET] + 處理邏輯: + 1. Category.get_by_id(id) 若找不到回傳 404。 + 2. 檢查 is_preset == 1,若是預設分類則 flash 錯誤並拒絕編輯。 + 輸出: render_template('categories/form.html', category=category) + + [POST] + 輸入: request.form (包含 name) + 處理邏輯: Category.update(id, name) [僅允許更改名稱] + 輸出: redirect(url_for('category.list_categories')) + """ + pass + +@category_bp.route('//delete', methods=['POST']) +def delete_category(id): + """ + 刪除自訂分類 + HTTP Method: POST + + 處理邏輯: + 1. Category.get_by_id(id),檢查是否為預設分類,若是則拒絕。 + 2. (Nice to Have) 檢查是否有 expenses 依賴該分類,若有則閃爍錯誤提示不允許刪除。 + 3. Category.delete(id) + 輸出: redirect(url_for('category.list_categories')) + """ + pass diff --git a/app/routes/expense.py b/app/routes/expense.py new file mode 100644 index 00000000..5b2cc329 --- /dev/null +++ b/app/routes/expense.py @@ -0,0 +1,64 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash + +expense_bp = Blueprint('expense', __name__, url_prefix='/expenses') + +@expense_bp.route('/') +def list_expenses(): + """ + 列出所有收支明細 + HTTP Method: GET + + 輸入: (可選) Query String 提供月份過濾,例如 ?month=2024-03 + 處理邏輯: 呼叫 Expense.get_all() 載入所有紀錄 + 輸出: render_template('expenses/index.html', expenses=expenses) + """ + pass + +@expense_bp.route('/new', methods=['GET', 'POST']) +def new_expense(): + """ + 新增收支紀錄 + HTTP Method: GET, POST + + [GET] + 處理邏輯: 呼叫 Category.get_all() 將分類選項餵給前端的下拉選單 + 輸出: render_template('expenses/form.html', categories=categories) + + [POST] + 輸入: request.form (包含 amount, category_id, date, note) + 處理邏輯: 確保金額為正數、必填欄位不為空,接著呼叫 Expense.create() + 輸出: 新增成功後 redirect(url_for('expense.list_expenses')) + 錯誤處理: 資料缺失或格式錯誤時,使用 flash() 紀錄錯誤並重導回表單頁面 + """ + pass + +@expense_bp.route('//edit', methods=['GET', 'POST']) +def edit_expense(id): + """ + 編輯收支紀錄 + HTTP Method: GET, POST + + [GET] + 處理邏輯: + 1. Expense.get_by_id(id) 取得該筆紀錄內容,如果不存在則丟出 404。 + 2. 取 Category.get_all() 餵給前端的下拉選單。 + 輸出: render_template('expenses/form.html', expense=expense, categories=categories) + + [POST] + 輸入: request.form (包含 amount, category_id, date, note) + 處理邏輯: 基礎邏輯檢查,執行 Expense.update() + 輸出: redirect(url_for('expense.list_expenses')) + """ + pass + +@expense_bp.route('//delete', methods=['POST']) +def delete_expense(id): + """ + 刪除收支紀錄 + HTTP Method: POST + + 處理邏輯: 呼叫 Expense.delete(id) + 輸出: redirect(url_for('expense.list_expenses')) + 錯誤處理: 找不到對應 ID 紀錄時可回傳 404 或 flash() + """ + pass diff --git a/app/routes/index.py b/app/routes/index.py new file mode 100644 index 00000000..d8d1a6fb --- /dev/null +++ b/app/routes/index.py @@ -0,0 +1,19 @@ +from flask import Blueprint, render_template + +index_bp = Blueprint('index', __name__) + +@index_bp.route('/') +def dashboard(): + """ + 首頁:財務總覽 Dashboard + HTTP Method: GET + + 處理邏輯: + 1. 呼叫 Expense.get_all() 並過濾出「當月」的所有紀錄。 + 2. 根據 category_type 統計當月「總收入、總支出」,並算出「當前結餘」。 + 3. 準備收支結構圖表所需的圓餅圖資料。 + + 輸出: render_template('index.html', summary=..., chart_data=...) + 錯誤處理: 若當月尚無紀錄,則總計皆回傳 0,前端顯示空白或引導提示。 + """ + pass diff --git a/app/routes/recipe.py b/app/routes/recipe.py new file mode 100644 index 00000000..d446bcbc --- /dev/null +++ b/app/routes/recipe.py @@ -0,0 +1,56 @@ +from flask import Blueprint, request, render_template, redirect, url_for, abort + +recipe_bp = Blueprint('recipe', __name__) + +@recipe_bp.route('/') +def index(): + """ + 列出所有食譜。 + 若有 ?q= 參數則篩選標題與食材。 + """ + pass + +@recipe_bp.route('/recipes/new') +def new_recipe(): + """ + 顯示新增食譜表單頁面。 + """ + pass + +@recipe_bp.route('/recipes', methods=['POST']) +def create_recipe(): + """ + 處理新增食譜請求。 + 接收表單資料,寫入 DB,完成後導回首頁。 + """ + pass + +@recipe_bp.route('/recipes/') +def show_recipe(id): + """ + 顯示單一食譜的詳細資料。 + """ + pass + +@recipe_bp.route('/recipes//edit') +def edit_recipe(id): + """ + 顯示修改食譜的表單頁面,將原資料填入表單中。 + """ + pass + +@recipe_bp.route('/recipes//update', methods=['POST']) +def update_recipe(id): + """ + 處裡食譜修改請求並寫入。 + 更新後導回食譜明細頁。 + """ + pass + +@recipe_bp.route('/recipes//delete', methods=['POST']) +def delete_recipe(id): + """ + 刪除指定食譜。 + 刪除後導向回首頁。 + """ + pass diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 00000000..d7a96afe --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,23 @@ + + + + + + {% block title %}個人記帳簿{% endblock %} + + + + + +
+ {% block content %} + {% endblock %} +
+ +
+ +
+ + diff --git a/app/templates/categories/form.html b/app/templates/categories/form.html new file mode 100644 index 00000000..77194722 --- /dev/null +++ b/app/templates/categories/form.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block title %}收支分類表單{% endblock %} + +{% block content %} + +
+ +
+{% endblock %} diff --git a/app/templates/categories/index.html b/app/templates/categories/index.html new file mode 100644 index 00000000..578c8e03 --- /dev/null +++ b/app/templates/categories/index.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}收支分類管理{% endblock %} + +{% block content %} +

分類列表

+ +{% endblock %} diff --git a/app/templates/expenses/form.html b/app/templates/expenses/form.html new file mode 100644 index 00000000..268a0c85 --- /dev/null +++ b/app/templates/expenses/form.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block title %}收支紀錄表單{% endblock %} + +{% block content %} + +
+ +
+{% endblock %} diff --git a/app/templates/expenses/index.html b/app/templates/expenses/index.html new file mode 100644 index 00000000..d683c30f --- /dev/null +++ b/app/templates/expenses/index.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% block title %}收支明細列表{% endblock %} + +{% block content %} +

收支明細

+ +{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 00000000..40e18759 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block title %}首頁 - 財務總覽{% endblock %} + +{% block content %} +

財務總覽

+ + +{% endblock %} diff --git a/database/schema.sql b/database/schema.sql new file mode 100644 index 00000000..63677e64 --- /dev/null +++ b/database/schema.sql @@ -0,0 +1,35 @@ +CREATE TABLE IF NOT EXISTS categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + is_preset INTEGER DEFAULT 1 +); + +CREATE TABLE IF NOT EXISTS expenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + amount INTEGER NOT NULL, + category_id INTEGER NOT NULL, + note TEXT, + date TEXT NOT NULL, + created_at TEXT NOT NULL, + FOREIGN KEY (category_id) REFERENCES categories(id) +); + +CREATE TABLE IF NOT EXISTS budgets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category_id INTEGER NOT NULL, + monthly_limit INTEGER NOT NULL, + month TEXT NOT NULL, + FOREIGN KEY (category_id) REFERENCES categories(id) +); + +-- 初始化時塞入預設的收支分類 +INSERT INTO categories (name, type, is_preset) VALUES +('飲食', 'expense', 1), +('交通', 'expense', 1), +('娛樂', 'expense', 1), +('居家', 'expense', 1), +('醫療', 'expense', 1), +('薪資', 'income', 1), +('獎金', 'income', 1), +('投資', 'income', 1); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..e6c20f27 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,86 @@ +# 系統架構文件 (Architecture) + +## 專案名稱:個人記帳簿 (Personal Expense Tracker) + +### 1. 技術架構說明 +- **選用技術與原因**: + - **後端 (Python + Flask)**:Flask 是一個輕量級且靈活的 Web 框架,非常適合快速構建 MVP 與中小型應用。Python 生態系豐富且語法簡潔,能大幅提升開發效率。 + - **視圖渲染 (Jinja2)**:與 Flask 深度整合,讓伺服器端能夠在回傳 HTML 前動態填入資料,不需要額外維護一套複雜的前後端分離架構,降低初期開發門檻。 + - **資料庫 (SQLite)**:內建於 Python 環境的檔案型資料庫,無需額外建立或維護資料庫伺服器(如 MySQL 或 PostgreSQL)。對於輕量級記帳系統而言,效能已非常足夠,且易於備份與轉移。 + - **前端樣式 (Vanilla CSS + HTML5)**:直接使用純 CSS 搭配 HTML5 控制版面,確保專案輕量並達成響應式設計 (RWD)。圖表渲染部分可考慮引入輕量級 JS 函式庫 (如 Chart.js)。 + +- **Flask MVC 模式說明**: + - **Model (模型)**:負責定義資料表結構(Schema)並與 SQLite 進行互動。封裝了與資料庫溝通的相關羅輯(如當月總收支計算、單筆紀錄的新增刪除)。 + - **View (視圖)**:Jinja2 樣板檔案 (`.html`),負責決定使用者在瀏覽器上看到的畫面樣貌,包含變數的顯示與邏輯分頁。 + - **Controller (控制器/路由)**:Flask 中的路由功能(Route),負責接收瀏覽器傳來的要求(Request)、向 Model 請求必要資料,最後把資料拋給 View 進行渲染拼裝,再回傳給使用者(Response)。 + +--- + +### 2. 專案資料夾結構 + +本專案依照關注點分離 (Separation of Concerns) 的原則,將程式目錄架構規劃如下: + +```text +web_app_development/ +├── app/ +│ ├── __init__.py ← 初始化 Flask 應用程式、載入設定 +│ ├── models/ ← 資料庫模型目錄 (MVC 的 M) +│ │ ├── __init__.py +│ │ └── database.py ← 資料庫連線處理、Schema 定義與所有 CRUD 查詢函式 +│ ├── routes/ ← 路由目錄 (MVC 的 C) +│ │ ├── __init__.py +│ │ ├── index.py ← 首頁總覽 (Dashboard) 的路由邏輯 +│ │ └── expense.py ← 新增/修改/刪除記帳紀錄的路由邏輯 +│ ├── templates/ ← HTML 模板目錄 (MVC 的 V) +│ │ ├── base.html ← 共用的版面 (如 Navbar 頭部、尾部) +│ │ ├── index.html ← 首頁總覽頁面 (顯示財務總覽、統計圖表) +│ │ └── expense.html ← 記帳紀錄表單頁面 +│ └── static/ ← 靜態資源檔案 +│ ├── css/ +│ │ └── style.css← 共用的樣式定義 +│ └── js/ +│ └── main.js ← 前端互動邏輯 (如表單驗證、圖表繪製) +├── instance/ +│ └── expense.db ← SQLite 資料庫檔案 (運作時將動態產生) +├── docs/ ← 專案文件 (包含 PRD.md, ARCHITECTURE.md 等) +├── .gitignore ← 忽略加入版本控制的檔案清單 +└── app.py ← 專案啟動入口程式,負責執行 Server +``` + +--- + +### 3. 元件關係圖 + +以下展示了系統核心處理「取得當月財務總覽」的要求時,各元件如何進行互動: + +```mermaid +sequenceDiagram + participant B as 瀏覽器 (Browser) + participant C as Flask Route (Controller) + participant M as Model (database.py) + participant DB as SQLite (expense.db) + participant V as Jinja2 Template (index.html) + + B->>C: 1. 發送 GET / 請求 (造訪首頁) + C->>M: 2. 呼叫 get_dashboard_summary() + M->>DB: 3. 執行 SQL 查詢 + DB-->>M: 4. 回傳當月總收入與支出資料 + M-->>C: 5. 整理為 Dict 回傳給 Route + C->>V: 6. 將資料傳入 index.html 進行模板渲染 + V-->>C: 7. 產生含有完整資料的 HTML 字串 + C-->>B: 8. 回傳 HTTP Response (展示網頁畫面給使用者) +``` + +--- + +### 4. 關鍵設計決策 + +1. **選擇 Server-Side Rendering (SSR) 單體式架構** + - **決策**:網頁畫面將由 Jinja2 在伺服器端渲染完成後,以完整 HTML 回傳給瀏覽器,而非採用前端框架 (SPA) 來抓取 API。 + - **原因**:初期專案的主要功能為 CRUD 展示,重視開發效率與展示可行性(MVP)。此架構能免去前端 API 狀態管理的負擔,開發起來最迅速。 +2. **選用輕量級 SQLite 資料庫** + - **決策**:直接使用檔案型 SQLite,不建置額外的 MySQL 伺服器。 + - **原因**:個人記帳系統的使用者只有單人,也不具有複雜的多人寫入衝突,且可有效減少開發環境的配置複雜度。後續若需要升級也能輕鬆轉移。 +3. **模組化的路由結構設計 (Modular Routing)** + - **決策**:引入 Blueprint 或是把路由打散到 `routes/` 資料夾,不將所有邏輯全寫在 `app.py` 內。 + - **原因**:由於系統有許多潛在功能 (如收支紀錄、分類管理、圖表分析),拆分資料夾可預防單一檔案落落長;若未來要增加「預算追蹤設定」,可直接在 `routes` 理增添 `budget.py`,保持系統擴充性。 diff --git a/docs/DB_DESIGN.md b/docs/DB_DESIGN.md new file mode 100644 index 00000000..69723e57 --- /dev/null +++ b/docs/DB_DESIGN.md @@ -0,0 +1,64 @@ +# 資料庫設計文件 (DB Design) + +## 1. ER 圖 (實體關係圖) + +```mermaid +erDiagram + CATEGORIES { + INTEGER id PK + TEXT name + TEXT type + INTEGER is_preset + } + EXPENSES { + INTEGER id PK + INTEGER amount + INTEGER category_id FK + TEXT note + TEXT date + TEXT created_at + } + BUDGETS { + INTEGER id PK + INTEGER category_id FK + INTEGER monthly_limit + TEXT month + } + + CATEGORIES ||--o{ EXPENSES : "contains" + CATEGORIES ||--o{ BUDGETS : "has" +``` + +## 2. 資料表詳細說明 + +### CATEGORIES (分類資料表) +負責儲存收支的類別(例如:飲食、交通、薪資等),包含系統預設分類與使用者自訂分類。 +- `id` (INTEGER): Primary Key,自動遞增。 +- `name` (TEXT): 分類名稱,必填。 +- `type` (TEXT): 收支類型,限定為 `'income'` (收入) 或 `'expense'` (支出),必填。 +- `is_preset` (INTEGER): 判斷是否為預設分類,`1` 為預設,`0` 為使用者自訂。預設值為 `1`。 + +### EXPENSES (收支紀錄表) +儲存每一筆記帳明細,是最核心的交易紀錄表。 +- `id` (INTEGER): Primary Key,自動遞增。 +- `amount` (INTEGER): 交易金額,必填。 +- `category_id` (INTEGER): Foreign Key,對應 `categories.id`,必填。 +- `note` (TEXT): 消費備註與說明文字,選填。 +- `date` (TEXT): 交易發生日期,格式為 `YYYY-MM-DD`,必填。 +- `created_at` (TEXT): 該筆資料寫入系統的時間戳記 (ISO 格式),必填。 + +### BUDGETS (預算設定表) +因應 Nice to Have 需求所規劃的各分類月預算設定。 +- `id` (INTEGER): Primary Key,自動遞增。 +- `category_id` (INTEGER): Foreign Key,對應 `categories.id`,必填。 +- `monthly_limit` (INTEGER): 設定的該月預算金額上限,必填。 +- `month` (TEXT): 對應的月份,格式為 `YYYY-MM`,必填。 + +## 3. SQL 建表語法與位置 +完整的 SQLite CREATE 語法以及預設值的 INSERT 腳本,已產出並儲存於專案內部的 `database/schema.sql` 檔案中。 + +## 4. Python Model 實作 +後端以 Python `sqlite3` 提供單純且高效的連線存取: +- **`app/models/db.py`**: 提供統一的 `get_db()` 資料庫連線函式,並負責處理資料庫初始化 (`init_db`)。 +- **`app/models/category.py`**: 分類模型的 CRUD。 +- **`app/models/expense.py`**: 紀錄模型的 CRUD,包含合併分類查詢的 JOIN 指令。 diff --git a/docs/FLOWCHART.md b/docs/FLOWCHART.md new file mode 100644 index 00000000..3f4c7384 --- /dev/null +++ b/docs/FLOWCHART.md @@ -0,0 +1,81 @@ +# 流程圖文件 (Flowchart) + +## 專案名稱:個人記帳簿 (Personal Expense Tracker) + +### 1. 使用者流程圖 (User Flow) + +此流程圖展示使用者進入「個人記帳簿」系統後,所能進行的操作路徑,包含瀏覽總覽、新增收支紀錄、編輯與刪除等核心功能。 + +```mermaid +flowchart LR + A([使用者造訪首頁]) --> B[首頁 - 財務總覽 (Dashboard)] + + B --> C{要執行什麼操作?} + + C -->|查看明細| D[記帳紀錄清單頁 (瀏覽所有的收支明細)] + C -->|新增紀錄| E[新增收支頁面 (填寫表單)] + + D -->|點擊編輯| F[編輯收支頁面 (修改表單)] + D -->|點擊刪除| G[確認刪除視窗] + + E -->|送出表單| H((儲存成功)) + F -->|送出修改| H + G -->|確認| I((刪除成功)) + + H -->|自動重導向| D + I -->|自動重導向| D + D -->|返回| B +``` + +--- + +### 2. 系統序列圖 (Sequence Diagram) + +此序列圖描述「使用者點擊新增一筆記帳紀錄」到「資料被寫入 SQLite 並重新顯示列表」的完整系統流轉過程。 + +```mermaid +sequenceDiagram + actor User as 使用者 + participant Browser as 瀏覽器 (HTML/CSS/JS) + participant Route as Flask Route (expense.py) + participant Model as Flask Model (database.py) + participant DB as SQLite 資料庫 + + User->>Browser: 1. 在新增表單填寫金額、分類並點擊「送出」 + Browser->>Route: 2. 發送 POST /expense/add 請求 (附帶表單資料) + + Route->>Route: 3. 驗證表單資料是否齊全與合法 + + alt 資料無效 + Route-->>Browser: 4a. 顯示錯誤訊息 (400 Bad Request) + else 資料有效 + Route->>Model: 4b. 呼叫 add_expense(amount, category, note, date) + Model->>DB: 5. 執行 INSERT INTO expenses 語法 + + DB-->>Model: 6. 回傳資料庫執行成功結果 + Model-->>Route: 7. 回傳新增成功結果 + + Route-->>Browser: 8. 回傳 HTTP 302 Redirect 重導向 + Browser->>Route: 9. 發送 GET /expense (重新載入列表頁) + Route->>Model: 10. 取得最新資料並由 Jinja2 渲染 + Route-->>Browser: 11. 顯示最新的記帳清單頁面 + end +``` + +--- + +### 3. 功能清單與 API 對照表 (Routes & Endpoint Mapping) + +根據在架構中所定義的 Flask Routes,各功能的預期存取路徑與對應 HTTP 方法如下: + +| 功能描述 | 模組 (Route) | HTTP 方法 | URL 路徑 (Endpoint) | 返回操作 (Response) | +| --- | --- | --- | --- | --- | +| **首頁:財務總覽** | index.py | `GET` | `/` | 回傳 `index.html` (含統計圖表) | +| **收支清單:檢視所有紀錄** | expense.py | `GET` | `/expense` | 回傳 `expense_list.html` | +| **新增紀錄:顯示表單** | expense.py | `GET` | `/expense/add` | 回傳 `expense_form.html` (供填寫) | +| **新增紀錄:送出資料** | expense.py | `POST` | `/expense/add` | 資料庫 INSERT,重導向至 `/expense` | +| **編輯紀錄:顯示表單** | expense.py | `GET` | `/expense/edit/` | 回傳 `expense_form.html` (帶入原資料)| +| **編輯紀錄:送出修改** | expense.py | `POST` | `/expense/edit/` | 資料庫 UPDATE,重導向至 `/expense` | +| **刪除紀錄:送出刪除** | expense.py | `POST` | `/expense/delete/`| 資料庫 DELETE,重導向至 `/expense` | + +> **備註**:由於一般的 HTML `
` 只能使用 GET 與 POST,因此刪除與修改送出暫時不使用 RESTful 嚴格定義的 `PUT`/`DELETE`,而統一使用 `POST`。 diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 00000000..1512b84e --- /dev/null +++ b/docs/PRD.md @@ -0,0 +1,64 @@ +# 產品需求文件 (PRD) + +## 專案名稱:個人記帳簿 (Personal Expense Tracker) + +**專案描述**:一款簡潔易用的個人記帳應用,幫助使用者輕鬆記錄每日收支並透過視覺化圖表追蹤財務狀況。 + +--- + +## 1. 專案概述 +- **背景與動機**:現代人常因缺乏記帳習慣或覺得市售記帳軟體過於繁瑣而放棄記帳,導致無法掌握金錢流向。本專案旨在提供一個輕量、直覺的記帳系統,幫助使用者無痛建立記帳習慣。 +- **目標用戶**:想要掌握個人財務狀況、減少不必要開銷的學生、上班族與一般大眾。 +- **核心價值主張**:簡單、快速、一目了然的財務管理,讓每個人都能輕鬆追蹤收支。 + +## 2. 功能需求 +以下為本系統的主要核心功能及對應的使用者故事: + +1. **財務總覽 (Dashboard)** + - **作為** 使用者,**我希望** 在首頁能一眼看到當月總收入、總支出與目前結餘,**以便** 快速掌握當下的財務健康狀態。 + +2. **收支記錄管理** + - **作為** 使用者,**我希望** 能夠快速新增、編輯和刪除記帳紀錄(包含金額、日期、分類與備註),**以便** 詳細記錄每一筆金錢的流向。 + +3. **分類管理** + - **作為** 使用者,**我希望** 系統提供預設分類,並且能讓我自訂常用的收支分類(如:伙食、交通、娛樂),**以便** 將帳務資料做系統化的整理。 + +4. **圖表統計分析** + - **作為** 使用者,**我希望** 能透過長條圖或圓餅圖檢視每週/每月的各類別收支比例,**以便** 清楚分析我的花費結構與習慣。 + +5. **預算追蹤設定** + - **作為** 使用者,**我希望** 能針對特定支出分類設定每月預算,並能看到預算使用進度條,**以便** 提醒自己控制消費,避免超支。 + +## 3. 非功能需求 +- **技術限制**: + - 後端:**Flask (Python)** 框架。 + - 前端:HTML + Vanilla CSS,並使用 **Jinja2** 作為模板引擎。 + - 資料庫:**SQLite**,適合單一使用者的輕量級資料儲存。 +- **效能與安全考量**: + - **響應式設計 (RWD)**:介面需適配桌機與手機螢幕尺寸,提供友善操作體驗。 + - **資料安全**:對於表單輸入資料需進行基本的邏輯驗證與過濾(防範 XSS 和 SQL Injection)。 + - **效能考量**:系統載入與操作回應時間應保持在 1-2 秒以內,以維持流暢的使用體驗。 + +## 4. MVP 範圍 +為了在初期快速推出可運作的版本,功能範圍劃分如下: + +- **Must Have (必須要有 - MVP)** + - 首頁財務總覽(當月收入、支出、結餘計算)。 + - 收支紀錄的 CRUD (新增、讀取、修改、刪除)。 + - 基礎預設分類列表。 +- **Should Have (應該要有)** + - 簡單的圓餅圖表/長條圖統計分析。 + - 使用者自訂新增收支分類。 +- **Nice to Have (有會更好)** + - 每月花費預算設定與超支提醒進度條。 + - 資料匯出(如 CSV 格式)功能。 + +## 5. 專案成員與分工 + +| 角色 | 姓名 | 負責項目 | +| --- | --- | ------- | +| **Project Manager** / Planner | | | +| **UI/UX Designer** | | | +| **Frontend Developer** | | | +| **Backend Developer** | | | +| **QA / Tester** | | | diff --git a/docs/ROUTES.md b/docs/ROUTES.md new file mode 100644 index 00000000..290be81a --- /dev/null +++ b/docs/ROUTES.md @@ -0,0 +1,43 @@ +# 路由設計文件 (ROUTES) + +## 專案名稱:個人記帳簿 (Personal Expense Tracker) + +### 1. 路由總覽表格 + +| 功能 | HTTP 方法 | URL 路徑 | 對應模板 | 說明 | +| --- | --- | --- | --- | --- | +| **首頁:財務總覽** | GET | `/` | `templates/index.html` | 首頁,顯示單月總收支與圖表 | +| **收支列表** | GET | `/expenses` | `templates/expenses/index.html` | 列出所有收支明細 | +| **新增收支頁面** | GET | `/expenses/new` | `templates/expenses/form.html` | 提供新增明細的表單 | +| **送出新增紀錄** | POST | `/expenses/new` | — | 接收新增表單並存入 DB,重導向到 `/expenses` | +| **編輯收支頁面** | GET | `/expenses//edit` | `templates/expenses/form.html` | 提供編輯明細的表單 | +| **送出更新紀錄** | POST | `/expenses//edit` | — | 接收編輯表單並更新 DB,重導向到 `/expenses` | +| **刪除紀錄** | POST | `/expenses//delete` | — | 刪除單筆明細,重導向到 `/expenses` | +| **分類列表** | GET | `/categories` | `templates/categories/index.html` | 列出所有分類 (包含預設與自訂) | +| **新增分類頁面** | GET | `/categories/new` | `templates/categories/form.html` | 提供新增自訂分類的表單 | +| **送出新增分類** | POST | `/categories/new` | — | 接收分類表單並存入 DB,重導向到 `/categories` | +| **編輯分類頁面** | GET | `/categories//edit` | `templates/categories/form.html` | 提供編輯自訂分類的表單 | +| **送出更新分類** | POST | `/categories//edit` | — | 接收分類更新並寫入 DB,重導向到 `/categories` | +| **刪除分類** | POST | `/categories//delete` | — | 刪除單筆自訂分類,重導向到 `/categories` | + +### 2. 每個路由的詳細說明 + +各 Endpoint 的邏輯規劃、呼叫的 Models Methods、輸入以及錯誤處理 (Error Handling),都已寫在 `app/routes/` 裡的各個 `.py` 檔案的 **Docstrings** 之中。詳細邏輯包括「不能刪除預設分類」、「表單漏填驗證的 Flash 提示」等。 + +### 3. Jinja2 模板清單 + +所有的視圖介面規劃如下,部分模板會共用: + +1. **`templates/base.html`**: 共用全站骨架,定義 `{% block content %}`。包含頂部的 Navbar 和引入共用 CSS 的設置。 +2. **`templates/index.html`**: 首頁總覽,繼承自 `base.html`,展示上方的 Dashboard 卡片與下方的 Chart 圖表。 +3. **`templates/expenses/index.html`**: 收支明細列表表格,繼承自 `base.html`。 +4. **`templates/expenses/form.html`**: 收支的「新增」與「編輯」共用表單版面,透過 Action URL 或變數切換狀態,繼承自 `base.html`。 +5. **`templates/categories/index.html`**: 分類清單,展示收入/支出類別列表,繼承自 `base.html`。 +6. **`templates/categories/form.html`**: 分類的「新增」與「編輯」共用表單版面,繼承自 `base.html`。 + +### 4. 路由骨架程式碼 + +基於模組化原則,我們為首頁、收支、與分類分別設立 Flask Blueprint (藍圖): +- `app/routes/index.py`: 處理 `/` +- `app/routes/expense.py`: 處理 `/expenses/*` +- `app/routes/category.py`: 處理 `/categories/*` diff --git "a/\345\257\246\344\275\234\350\252\252\346\230\216.md" "b/\345\257\246\344\275\234\350\252\252\346\230\216.md" index 83b7a6b9..cc8c9399 100644 --- "a/\345\257\246\344\275\234\350\252\252\346\230\216.md" +++ "b/\345\257\246\344\275\234\350\252\252\346\230\216.md" @@ -195,8 +195,7 @@ commit 訊息:docs: add user flowchart **Step 4.4** — 請 AI 提交並推送: ``` -請幫我 commit 並推送目前的變更。 -commit 訊息:feat: add database schema and models +/commit ``` --- @@ -222,8 +221,7 @@ commit 訊息:feat: add database schema and models **Step 5.4** — 請 AI 提交並推送: ``` -請幫我 commit 並推送目前的變更。 -commit 訊息:feat: add route skeleton and template plan +/commit ``` ---