chore: 完善模組化架構遷移與修復前端顯示錯誤
- 修正所有模組 Controller 的 Model 引用路徑 (App\Modules\...) - 更新 ProductionOrder 與 ProductionOrderItem 模型結構以符合新版邏輯 - 修復 resources/js/utils/format.ts 在處理空值時導致 toLocaleString 崩潰的問題 - 清除全域路徑與 Controller 遷移殘留檔案
This commit is contained in:
173
app/Modules/Finance/Controllers/AccountingReportController.php
Normal file
173
app/Modules/Finance/Controllers/AccountingReportController.php
Normal file
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Finance\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Finance\Models\UtilityFee;
|
||||
use App\Modules\Procurement\Models\PurchaseOrder;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class AccountingReportController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$dateStart = $request->input('date_start', Carbon::now()->toDateString());
|
||||
$dateEnd = $request->input('date_end', Carbon::now()->toDateString());
|
||||
|
||||
// 1. Get Purchase Orders (Completed or Received that are ready for accounting)
|
||||
$purchaseOrders = PurchaseOrder::with(['vendor'])
|
||||
->whereIn('status', ['received', 'completed'])
|
||||
->whereBetween('created_at', [$dateStart . ' 00:00:00', $dateEnd . ' 23:59:59'])
|
||||
->get()
|
||||
->map(function ($po) {
|
||||
return [
|
||||
'id' => 'PO-' . $po->id,
|
||||
'date' => Carbon::parse($po->created_at)->timezone(config('app.timezone'))->toDateString(),
|
||||
'source' => '採購單',
|
||||
'category' => '進貨支出',
|
||||
'item' => $po->vendor->name ?? '未知廠商',
|
||||
'reference' => $po->code,
|
||||
'invoice_number' => $po->invoice_number,
|
||||
'amount' => $po->grand_total,
|
||||
];
|
||||
});
|
||||
|
||||
// 2. Get Utility Fees
|
||||
$utilityFees = UtilityFee::whereBetween('transaction_date', [$dateStart, $dateEnd])
|
||||
->get()
|
||||
->map(function ($fee) {
|
||||
return [
|
||||
'id' => 'UF-' . $fee->id,
|
||||
'date' => $fee->transaction_date->format('Y-m-d'),
|
||||
'source' => '公共事業費',
|
||||
'category' => $fee->category,
|
||||
'item' => $fee->description ?: $fee->category,
|
||||
'reference' => '-',
|
||||
'invoice_number' => $fee->invoice_number,
|
||||
'amount' => $fee->amount,
|
||||
];
|
||||
});
|
||||
|
||||
// Combine and Sort
|
||||
$allRecords = $purchaseOrders->concat($utilityFees)
|
||||
->sortByDesc('date')
|
||||
->values();
|
||||
|
||||
// 3. Manual Pagination
|
||||
$perPage = $request->input('per_page', 10);
|
||||
$page = $request->input('page', 1);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$paginatedRecords = new LengthAwarePaginator(
|
||||
$allRecords->slice($offset, $perPage)->values(),
|
||||
$allRecords->count(),
|
||||
$perPage,
|
||||
$page,
|
||||
['path' => $request->url(), 'query' => $request->query()]
|
||||
);
|
||||
|
||||
$summary = [
|
||||
'total_amount' => $allRecords->sum('amount'),
|
||||
'purchase_total' => $purchaseOrders->sum('amount'),
|
||||
'utility_total' => $utilityFees->sum('amount'),
|
||||
'record_count' => $allRecords->count(),
|
||||
];
|
||||
|
||||
return Inertia::render('Accounting/Report', [
|
||||
'records' => $paginatedRecords,
|
||||
'summary' => $summary,
|
||||
'filters' => [
|
||||
'date_start' => $dateStart,
|
||||
'date_end' => $dateEnd,
|
||||
'per_page' => (int)$perPage,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
$dateStart = $request->input('date_start', Carbon::now()->toDateString());
|
||||
$dateEnd = $request->input('date_end', Carbon::now()->toDateString());
|
||||
$selectedIdsParam = $request->input('selected_ids');
|
||||
|
||||
$purchaseOrdersQuery = PurchaseOrder::with(['vendor'])
|
||||
->whereIn('status', ['received', 'completed']);
|
||||
|
||||
$utilityFeesQuery = UtilityFee::query();
|
||||
|
||||
if ($selectedIdsParam) {
|
||||
$ids = explode(',', $selectedIdsParam);
|
||||
$poIds = [];
|
||||
$ufIds = [];
|
||||
foreach ($ids as $id) {
|
||||
if (str_starts_with($id, 'PO-')) {
|
||||
$poIds[] = substr($id, 3);
|
||||
} elseif (str_starts_with($id, 'UF-')) {
|
||||
$ufIds[] = substr($id, 3);
|
||||
}
|
||||
}
|
||||
$purchaseOrders = $purchaseOrdersQuery->whereIn('id', $poIds)->get();
|
||||
$utilityFees = $utilityFeesQuery->whereIn('id', $ufIds)->get();
|
||||
} else {
|
||||
$purchaseOrders = $purchaseOrdersQuery
|
||||
->whereBetween('created_at', [$dateStart . ' 00:00:00', $dateEnd . ' 23:59:59'])
|
||||
->get();
|
||||
$utilityFees = $utilityFeesQuery
|
||||
->whereBetween('transaction_date', [$dateStart, $dateEnd])
|
||||
->get();
|
||||
}
|
||||
|
||||
$allRecords = collect();
|
||||
|
||||
foreach ($purchaseOrders as $po) {
|
||||
$allRecords->push([
|
||||
Carbon::parse($po->created_at)->toDateString(),
|
||||
'採購單',
|
||||
'進貨支出',
|
||||
$po->vendor->name ?? '',
|
||||
$po->code,
|
||||
$po->invoice_number,
|
||||
(float)$po->grand_total,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($utilityFees as $fee) {
|
||||
$allRecords->push([
|
||||
Carbon::parse($fee->transaction_date)->toDateString(),
|
||||
'公共事業費',
|
||||
$fee->category,
|
||||
$fee->description,
|
||||
'-',
|
||||
$fee->invoice_number,
|
||||
(float)$fee->amount,
|
||||
]);
|
||||
}
|
||||
|
||||
$allRecords = $allRecords->sortByDesc(0);
|
||||
|
||||
$filename = "accounting_report_{$dateStart}_{$dateEnd}.csv";
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||
];
|
||||
|
||||
$callback = function () use ($allRecords) {
|
||||
$file = fopen('php://output', 'w');
|
||||
// BOM for Excel compatibility with UTF-8
|
||||
fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
|
||||
|
||||
fputcsv($file, ['日期', '來源', '類別', '項目', '參考單號', '發票號碼', '金額']);
|
||||
|
||||
foreach ($allRecords as $row) {
|
||||
fputcsv($file, $row);
|
||||
}
|
||||
fclose($file);
|
||||
};
|
||||
|
||||
return response()->stream($callback, 200, $headers);
|
||||
}
|
||||
}
|
||||
178
app/Modules/Finance/Controllers/UtilityFeeController.php
Normal file
178
app/Modules/Finance/Controllers/UtilityFeeController.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Finance\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Finance\Models\UtilityFee;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class UtilityFeeController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = UtilityFee::query();
|
||||
|
||||
// Search
|
||||
if ($request->has('search')) {
|
||||
$search = $request->input('search');
|
||||
$query->where(function($q) use ($search) {
|
||||
$q->where('category', 'like', "%{$search}%")
|
||||
->orWhere('invoice_number', 'like', "%{$search}%")
|
||||
->orWhere('description', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filtering
|
||||
if ($request->filled('category') && $request->input('category') !== 'all') {
|
||||
$query->where('category', $request->input('category'));
|
||||
}
|
||||
|
||||
if ($request->filled('date_start')) {
|
||||
$query->where('transaction_date', '>=', $request->input('date_start'));
|
||||
}
|
||||
|
||||
if ($request->filled('date_end')) {
|
||||
$query->where('transaction_date', '<=', $request->input('date_end'));
|
||||
}
|
||||
|
||||
// Sorting
|
||||
$sortField = $request->input('sort_field');
|
||||
$sortDirection = $request->input('sort_direction');
|
||||
|
||||
if ($sortField && $sortDirection) {
|
||||
$query->orderBy($sortField, $sortDirection);
|
||||
} else {
|
||||
$query->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
$fees = $query->paginate($request->input('per_page', 10))->withQueryString();
|
||||
|
||||
$availableCategories = UtilityFee::distinct()->pluck('category');
|
||||
|
||||
return Inertia::render('UtilityFee/Index', [
|
||||
'fees' => $fees,
|
||||
'availableCategories' => $availableCategories,
|
||||
'filters' => $request->only(['search', 'category', 'date_start', 'date_end', 'sort_field', 'sort_direction', 'per_page']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'transaction_date' => 'required|date',
|
||||
'category' => 'required|string|max:255',
|
||||
'amount' => 'required|numeric|min:0',
|
||||
'invoice_number' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$fee = UtilityFee::create($validated);
|
||||
|
||||
// Log activity
|
||||
activity()
|
||||
->performedOn($fee)
|
||||
->causedBy(auth()->user())
|
||||
->event('created')
|
||||
->withProperties([
|
||||
'attributes' => $fee->getAttributes(),
|
||||
'snapshot' => [
|
||||
'category' => $fee->category,
|
||||
'amount' => $fee->amount,
|
||||
'transaction_date' => $fee->transaction_date->format('Y-m-d'),
|
||||
]
|
||||
])
|
||||
->log('created');
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function update(Request $request, UtilityFee $utility_fee)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'transaction_date' => 'required|date',
|
||||
'category' => 'required|string|max:255',
|
||||
'amount' => 'required|numeric|min:0',
|
||||
'invoice_number' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
// Capture old attributes before update
|
||||
$oldAttributes = $utility_fee->getAttributes();
|
||||
|
||||
$utility_fee->update($validated);
|
||||
|
||||
// Capture new attributes
|
||||
$newAttributes = $utility_fee->getAttributes();
|
||||
|
||||
// Manual logOnlyDirty: Filter attributes to only include changes
|
||||
$changedAttributes = [];
|
||||
$changedOldAttributes = [];
|
||||
|
||||
foreach ($newAttributes as $key => $value) {
|
||||
// Skip timestamps if they are the only change (optional, but good practice)
|
||||
if (in_array($key, ['updated_at'])) continue;
|
||||
|
||||
$oldValue = $oldAttributes[$key] ?? null;
|
||||
|
||||
// Simple comparison (casting to string to handle date objects vs strings if necessary,
|
||||
// but Eloquent attributes are usually consistent if casted.
|
||||
// Using loose comparison != handles most cases correctly)
|
||||
if ($value != $oldValue) {
|
||||
$changedAttributes[$key] = $value;
|
||||
$changedOldAttributes[$key] = $oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Only log if there are changes (excluding just updated_at)
|
||||
if (empty($changedAttributes)) {
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
// Log activity with before/after comparison
|
||||
activity()
|
||||
->performedOn($utility_fee)
|
||||
->causedBy(auth()->user())
|
||||
->event('updated')
|
||||
->withProperties([
|
||||
'attributes' => $changedAttributes,
|
||||
'old' => $changedOldAttributes,
|
||||
'snapshot' => [
|
||||
'category' => $utility_fee->category,
|
||||
'amount' => $utility_fee->amount,
|
||||
'transaction_date' => $utility_fee->transaction_date->format('Y-m-d'),
|
||||
]
|
||||
])
|
||||
->log('updated');
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function destroy(UtilityFee $utility_fee)
|
||||
{
|
||||
// Capture data snapshot before deletion
|
||||
$snapshot = [
|
||||
'category' => $utility_fee->category,
|
||||
'amount' => $utility_fee->amount,
|
||||
'transaction_date' => $utility_fee->transaction_date->format('Y-m-d'),
|
||||
'invoice_number' => $utility_fee->invoice_number,
|
||||
'description' => $utility_fee->description,
|
||||
];
|
||||
|
||||
// Log activity before deletion
|
||||
activity()
|
||||
->performedOn($utility_fee)
|
||||
->causedBy(auth()->user())
|
||||
->event('deleted')
|
||||
->withProperties([
|
||||
'attributes' => $utility_fee->getAttributes(),
|
||||
'snapshot' => $snapshot
|
||||
])
|
||||
->log('deleted');
|
||||
|
||||
$utility_fee->delete();
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user