Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .agents/skills/commit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ description: 提交並推送程式碼變更。用於每個開發階段完成後
- 要把程式碼推送到 GitHub,讓組員可以取得最新版本
- 遇到 Git 要求設定 `user.name` 或 `user.email` 的提示

## 檢查 .gitignore
在推送之前檢查 .gitignore,確保虛擬環境、環境設定與非必要檔案有正確被排除在版本控制之外。

## ⚠️ 設定 Git 使用者身份

如果 Git 顯示以下提示,需要先設定身份才能 commit:
Expand Down
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

- [系統簡介](#系統簡介)
- [技術棧](#技術棧)
- [組員與分工](#組員與分工)
- [系統截圖](#系統截圖)
- [個人心得](#個人心得)

---
Expand All @@ -25,13 +25,8 @@

---

## 組員與分工(先寫自己就好)

**第 X 組**

| 姓名 | 學號 | 負責部分 |
| ---- | ---- | -------- |
| | | |
## 系統截圖
請在此貼上系統的截圖畫面, 選擇一個功能邊操作邊截圖

---

Expand Down
16 changes: 16 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# initialize app/models module
56 changes: 56 additions & 0 deletions app/models/category.py
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions app/models/db.py
Original file line number Diff line number Diff line change
@@ -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()
72 changes: 72 additions & 0 deletions app/models/expense.py
Original file line number Diff line number Diff line change
@@ -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
73 changes: 73 additions & 0 deletions app/models/recipe.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions app/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# init routes
from app.routes.recipe import recipe_bp
64 changes: 64 additions & 0 deletions app/routes/category.py
Original file line number Diff line number Diff line change
@@ -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('/<int:id>/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('/<int:id>/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
Loading