feat: add finance module with yearly income/expense tracking
- FinanceEntry model (income/expense x fixed/extra) - CRUD API endpoints for finance entries - FinancePage with summary cards and drill-down - Updated navigation with Finance tab - Updated todos default filter to active
This commit is contained in:
@@ -5,18 +5,20 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"lifelog/internal/model"
|
||||
"lifelog/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
todoSvc *service.TodoService
|
||||
dailyLogSvc *service.DailyLogService
|
||||
todoSvc *service.TodoService
|
||||
dailyLogSvc *service.DailyLogService
|
||||
financeSvc *service.FinanceService
|
||||
}
|
||||
|
||||
func NewHandler(todoSvc *service.TodoService, dailyLogSvc *service.DailyLogService) *Handler {
|
||||
return &Handler{todoSvc: todoSvc, dailyLogSvc: dailyLogSvc}
|
||||
func NewHandler(todoSvc *service.TodoService, dailyLogSvc *service.DailyLogService, financeSvc *service.FinanceService) *Handler {
|
||||
return &Handler{todoSvc: todoSvc, dailyLogSvc: dailyLogSvc, financeSvc: financeSvc}
|
||||
}
|
||||
|
||||
// Todo handlers
|
||||
@@ -165,3 +167,95 @@ func (h *Handler) SaveDailyLog(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": log})
|
||||
}
|
||||
|
||||
// Finance handlers
|
||||
|
||||
func (h *Handler) GetFinanceByYear(c *gin.Context) {
|
||||
yearStr := c.Param("year")
|
||||
year, err := strconv.Atoi(yearStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid year"})
|
||||
return
|
||||
}
|
||||
entries, err := h.financeSvc.GetByYear(year)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": entries})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateFinanceEntry(c *gin.Context) {
|
||||
var req struct {
|
||||
Year int `json:"year" binding:"required"`
|
||||
Month int `json:"month"`
|
||||
Type string `json:"type" binding:"required"` // income / expense
|
||||
Category string `json:"category" binding:"required"` // fixed / extra
|
||||
Description string `json:"description"`
|
||||
Amount float64 `json:"amount" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
entry := &model.FinanceEntry{
|
||||
Year: req.Year,
|
||||
Month: req.Month,
|
||||
Type: req.Type,
|
||||
Category: req.Category,
|
||||
Description: req.Description,
|
||||
Amount: req.Amount,
|
||||
}
|
||||
if err := h.financeSvc.Create(entry); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"data": entry})
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateFinanceEntry(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Year int `json:"year"`
|
||||
Month int `json:"month"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description"`
|
||||
Amount float64 `json:"amount"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err = h.financeSvc.Update(&model.FinanceEntry{
|
||||
ID: id,
|
||||
Year: req.Year,
|
||||
Month: req.Month,
|
||||
Type: req.Type,
|
||||
Category: req.Category,
|
||||
Description: req.Description,
|
||||
Amount: req.Amount,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "updated"})
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteFinanceEntry(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.financeSvc.Delete(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
@@ -31,3 +31,22 @@ type DailyLog struct {
|
||||
func (DailyLog) TableName() string {
|
||||
return "daily_logs"
|
||||
}
|
||||
|
||||
// FinanceEntry 收支条目
|
||||
// Type: income(收入) / expense(支出)
|
||||
// Category: fixed(固定) / extra(额外)
|
||||
type FinanceEntry struct {
|
||||
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
Year int `gorm:"type:int;not null;index" json:"year"` // 年份
|
||||
Month int `gorm:"type:int;not null;default:0" json:"month"` // 月份(0表示全年)
|
||||
Type string `gorm:"type:varchar(10);not null" json:"type"` // income / expense
|
||||
Category string `gorm:"type:varchar(10);not null" json:"category"` // fixed / extra
|
||||
Description string `gorm:"type:varchar(255)" json:"description"` // 条目描述
|
||||
Amount float64 `gorm:"type:decimal(12,2);not null" json:"amount"` // 金额(正数)
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (FinanceEntry) TableName() string {
|
||||
return "finance_entries"
|
||||
}
|
||||
|
||||
@@ -76,3 +76,35 @@ func (r *DailyLogRepository) Upsert(log *model.DailyLog) error {
|
||||
existing.Evening = log.Evening
|
||||
return r.db.Save(&existing).Error
|
||||
}
|
||||
|
||||
type FinanceRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewFinanceRepository(db *gorm.DB) *FinanceRepository {
|
||||
return &FinanceRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *FinanceRepository) GetByYear(year int) ([]model.FinanceEntry, error) {
|
||||
var entries []model.FinanceEntry
|
||||
err := r.db.Where("year = ?", year).Order("month ASC, id ASC").Find(&entries).Error
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func (r *FinanceRepository) Create(entry *model.FinanceEntry) error {
|
||||
return r.db.Create(entry).Error
|
||||
}
|
||||
|
||||
func (r *FinanceRepository) Update(entry *model.FinanceEntry) error {
|
||||
return r.db.Save(entry).Error
|
||||
}
|
||||
|
||||
func (r *FinanceRepository) Delete(id uint64) error {
|
||||
return r.db.Delete(&model.FinanceEntry{}, id).Error
|
||||
}
|
||||
|
||||
func (r *FinanceRepository) GetByID(id uint64) (*model.FinanceEntry, error) {
|
||||
var entry model.FinanceEntry
|
||||
err := r.db.First(&entry, id).Error
|
||||
return &entry, err
|
||||
}
|
||||
|
||||
@@ -97,3 +97,27 @@ func (s *DailyLogService) Save(date, morning, afternoon, evening string) (*model
|
||||
err := s.repo.Upsert(log)
|
||||
return log, err
|
||||
}
|
||||
|
||||
type FinanceService struct {
|
||||
repo *repository.FinanceRepository
|
||||
}
|
||||
|
||||
func NewFinanceService(repo *repository.FinanceRepository) *FinanceService {
|
||||
return &FinanceService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *FinanceService) GetByYear(year int) ([]model.FinanceEntry, error) {
|
||||
return s.repo.GetByYear(year)
|
||||
}
|
||||
|
||||
func (s *FinanceService) Create(entry *model.FinanceEntry) error {
|
||||
return s.repo.Create(entry)
|
||||
}
|
||||
|
||||
func (s *FinanceService) Update(entry *model.FinanceEntry) error {
|
||||
return s.repo.Update(entry)
|
||||
}
|
||||
|
||||
func (s *FinanceService) Delete(id uint64) error {
|
||||
return s.repo.Delete(id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user