feat: 前端分離+功能api化

1. 新增資產管理CRUD、googleOAuth登入
2. 更改原記帳controller
This commit is contained in:
2026-06-25 14:19:07 +08:00
parent 7bb931c97b
commit 85972f950c
70 changed files with 4754 additions and 1520 deletions

View File

@@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Account;
use Illuminate\Http\Request;
class AccountController extends Controller
{
public function index(Request $request)
{
// 🟢 1. 預先載入 holdings、transactions 還有全新開荒好的 security 字典表!
$accounts = $request->user()->accounts()
->with(['holdings.transactions', 'holdings.security']) // 👈 確保能直接拿到最新價格
->get()
->map(function ($account) {
$data = $account->toArray();
// 🟢 2. 處理股票與基金的成本與估計現值計算
if (in_array($account->type, ['stock', 'fund'])) {
$totalCost = 0;
$totalValue = 0;
foreach ($account->holdings as $holding) {
// 🟢 修正 1直接拿資料庫存好的庫存股數不用再去 transactions 加減計算
$remainingShares = floatval($holding->shares);
if ($remainingShares > 0) {
// 🟢 修正 2拿到最新價格 (如果沒抓到就先給一個暫時的預設值,例如 10方便你 debug 畫面)
$currentPrice = floatval($holding->security?->latest_price ?? 0);
if ($currentPrice == 0) {
// 💡 Debug 觀測:如果還是 0我們先用 avg_cost 頂替,或者去手動確認 securities 表有沒有真的更新到價格
$currentPrice = floatval($holding->avg_cost ?? 0);
}
// 總現值 = 剩餘單位 * 最新價格
$totalValue += ($remainingShares * $currentPrice);
// 總成本:直接拿你畫面上已經有的 avg_cost * shares 算最快最準!
$avgCost = floatval($holding->avg_cost ?? 0);
$totalCost += ($remainingShares * $avgCost);
}
}
// 🟢 捨入並塞入回傳陣列
$data['total_cost'] = round($totalCost); // 持倉成本
$data['total_value'] = round($totalValue); // 目前市值
// 💡 關鍵補強:計算總損益與總報酬率並塞回陣列,消滅前端的 NaN%
$totalProfit = $totalValue - $totalCost;
$totalRate = $totalCost > 0 ? ($totalProfit / $totalCost) * 100 : 0;
// 🚨 請確認這裡的 Key 名稱(例如 total_profit 或是 unrealized_profit
// 要跟你在 Vue 前端 account.xxx 綁定的欄位名稱一模一樣喔!
$data['total_profit'] = round($totalProfit);
$data['total_rate'] = round($totalRate, 2);
}
// 處理信用卡
if ($account->type === 'credit_card') {
$data['unpaid_amount'] = $account->unpaidAmount();
}
return $data;
});
return response()->json($accounts);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'type' => 'required|in:cash,bank,stock,fund,wallet,credit_card',
'currency' => 'nullable|string|max:10',
'balance' => 'nullable|numeric',
'note' => 'nullable|string',
'billing_day' => 'nullable|integer|min:1|max:31',
'payment_day' => 'nullable|integer|min:1|max:31',
]);
$account = $request->user()->accounts()->create($validated);
return response()->json($account, 201);
}
public function show(Request $request, string $id)
{
$account = $request->user()->accounts()->findOrFail($id);
return response()->json($account);
}
public function update(Request $request, string $id)
{
$account = $request->user()->accounts()->findOrFail($id);
$validated = $request->validate([
'name' => 'sometimes|string|max:255',
'type' => 'sometimes|in:cash,bank,stock,fund,wallet,credit_card',
'currency' => 'nullable|string|max:10',
'balance' => 'nullable|numeric',
'note' => 'nullable|string',
'billing_day' => 'nullable|integer|min:1|max:31',
'payment_day' => 'nullable|integer|min:1|max:31',
]);
$account->update($validated);
return response()->json($account);
}
public function destroy(Request $request, string $id)
{
$account = $request->user()->accounts()->findOrFail($id);
$account->delete();
return response()->json(null, 204);
}
}