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
Empty file added .agents/skills/commit/SKILL.md
Empty file.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SECRET_KEY=your_development_secret_key
39 changes: 39 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg

# Database
instance/
*.sqlite3
*.db

# VS Code
.vscode/
15 changes: 15 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os
from flask import Flask

def create_app():
"""Flask 應用程式工廠函式"""
app = Flask(__name__)

# 基本設定
app.config['SECRET_KEY'] = 'dev_secret_key' # 測試用,實務應從環境變數讀取

# 註冊路由 Blueprint
from .routes import bp as records_bp
app.register_blueprint(records_bp)

return app
3 changes: 3 additions & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .record import RecordModel, init_db

__all__ = ['RecordModel', 'init_db']
163 changes: 163 additions & 0 deletions app/models/record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import sqlite3
import os
import logging

# 預設資料庫路徑 (對應到 instance/database.db)
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'instance', 'database.db')

# 設定基本的 logging 記錄錯誤
logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger(__name__)

def get_db_connection():
"""取得資料庫連線"""
try:
# 確保 instance 資料夾存在
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row # 讓查詢結果可以像字典一樣存取欄位
return conn
except sqlite3.Error as e:
logger.error(f"Database connection error: {e}")
raise

def init_db():
"""初始化資料庫與資料表"""
schema_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'database', 'schema.sql')
if os.path.exists(schema_path):
try:
with open(schema_path, 'r', encoding='utf-8') as f:
schema_sql = f.read()
conn = get_db_connection()
conn.executescript(schema_sql)
conn.commit()
except Exception as e:
logger.error(f"Error initializing database: {e}")
raise
finally:
if 'conn' in locals() and conn:
conn.close()

class RecordModel:
@staticmethod
def create(record_type, amount, date, category='', description=''):
"""新增一筆收支紀錄"""
conn = None
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO records (type, amount, date, category, description)
VALUES (?, ?, ?, ?, ?)
''', (record_type, amount, date, category, description))
conn.commit()
new_id = cursor.lastrowid
return new_id
except sqlite3.Error as e:
logger.error(f"Error creating record: {e}")
if conn:
conn.rollback()
return None
finally:
if conn:
conn.close()

@staticmethod
def get_all():
"""取得所有收支紀錄 (依日期遞減排序)"""
conn = None
try:
conn = get_db_connection()
records = conn.execute('''
SELECT * FROM records ORDER BY date DESC, id DESC
''').fetchall()
return [dict(row) for row in records]
except sqlite3.Error as e:
logger.error(f"Error getting all records: {e}")
return []
finally:
if conn:
conn.close()

@staticmethod
def get_by_id(record_id):
"""根據 ID 取得單筆紀錄"""
conn = None
try:
conn = get_db_connection()
record = conn.execute('''
SELECT * FROM records WHERE id = ?
''', (record_id,)).fetchone()
return dict(record) if record else None
except sqlite3.Error as e:
logger.error(f"Error getting record by id: {e}")
return None
finally:
if conn:
conn.close()

@staticmethod
def update(record_id, record_type, amount, date, category='', description=''):
"""更新單筆收支紀錄"""
conn = None
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
UPDATE records
SET type = ?, amount = ?, date = ?, category = ?, description = ?
WHERE id = ?
''', (record_type, amount, date, category, description, record_id))
conn.commit()
updated_rows = cursor.rowcount
return updated_rows > 0
except sqlite3.Error as e:
logger.error(f"Error updating record: {e}")
if conn:
conn.rollback()
return False
finally:
if conn:
conn.close()

@staticmethod
def delete(record_id):
"""刪除單筆收支紀錄"""
conn = None
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
DELETE FROM records WHERE id = ?
''', (record_id,))
conn.commit()
deleted_rows = cursor.rowcount
return deleted_rows > 0
except sqlite3.Error as e:
logger.error(f"Error deleting record: {e}")
if conn:
conn.rollback()
return False
finally:
if conn:
conn.close()

@staticmethod
def get_balance():
"""計算目前總餘額"""
conn = None
try:
conn = get_db_connection()
result = conn.execute('''
SELECT
SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) -
SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as balance
FROM records
''').fetchone()
return result['balance'] or 0
except sqlite3.Error as e:
logger.error(f"Error calculating balance: {e}")
return 0
finally:
if conn:
conn.close()
116 changes: 116 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, abort
from app.models.record import RecordModel

# 建立一個 Blueprint 來管理收支紀錄相關路由
bp = Blueprint('records', __name__)

@bp.route('/')
def index():
"""
[GET] 首頁
顯示目前總餘額以及近期的收支歷史紀錄
"""
balance = RecordModel.get_balance()
records = RecordModel.get_all()
return render_template('index.html', balance=balance, records=records)

@bp.route('/records/new', methods=['GET'])
def new_record():
"""
[GET] 新增紀錄頁面
顯示填寫收支資料的表單
"""
return render_template('records/new.html')

@bp.route('/records', methods=['POST'])
def create_record():
"""
[POST] 送出新增紀錄
接收表單資料並寫入資料庫,完成後導回首頁
"""
record_type = request.form.get('type')
amount = request.form.get('amount')
date = request.form.get('date')
category = request.form.get('category', '')
description = request.form.get('description', '')

# 基本輸入驗證
if not record_type or not amount or not date:
flash('收支類型、金額與日期為必填欄位!', 'danger')
return redirect(url_for('records.new_record'))

try:
amount = int(amount)
except ValueError:
flash('金額必須為有效的數字!', 'danger')
return redirect(url_for('records.new_record'))

# 寫入資料庫
new_id = RecordModel.create(record_type, amount, date, category, description)
if new_id:
flash('新增紀錄成功!', 'success')
else:
flash('新增失敗,請稍後再試。', 'danger')

return redirect(url_for('records.index'))

@bp.route('/records/<int:record_id>/edit', methods=['GET'])
def edit_record(record_id):
"""
[GET] 編輯紀錄頁面
顯示帶有原始資料的編輯表單頁面
"""
record = RecordModel.get_by_id(record_id)
if not record:
abort(404)

return render_template('records/edit.html', record=record)

@bp.route('/records/<int:record_id>/update', methods=['POST'])
def update_record(record_id):
"""
[POST] 更新紀錄
接收更新資料並寫入資料庫,完成後導回首頁
"""
# 檢查該筆資料是否存在
record = RecordModel.get_by_id(record_id)
if not record:
abort(404)

record_type = request.form.get('type')
amount = request.form.get('amount')
date = request.form.get('date')
category = request.form.get('category', '')
description = request.form.get('description', '')

if not record_type or not amount or not date:
flash('收支類型、金額與日期為必填欄位!', 'danger')
return redirect(url_for('records.edit_record', record_id=record_id))

try:
amount = int(amount)
except ValueError:
flash('金額必須為有效的數字!', 'danger')
return redirect(url_for('records.edit_record', record_id=record_id))

success = RecordModel.update(record_id, record_type, amount, date, category, description)
if success:
flash('更新紀錄成功!', 'success')
else:
flash('更新失敗,請稍後再試。', 'danger')

return redirect(url_for('records.index'))

@bp.route('/records/<int:record_id>/delete', methods=['POST'])
def delete_record(record_id):
"""
[POST] 刪除紀錄
將指定 ID 的收支紀錄從資料庫中刪除,並導回首頁
"""
success = RecordModel.delete(record_id)
if success:
flash('已成功刪除紀錄!', 'success')
else:
flash('刪除失敗,找不到該筆紀錄或發生錯誤。', 'danger')

return redirect(url_for('records.index'))
4 changes: 4 additions & 0 deletions app/static/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* 基礎樣式設定 */
body {
font-family: Arial, sans-serif;
}
2 changes: 2 additions & 0 deletions app/static/js/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// 前端互動邏輯
console.log("App initialized.");
Loading