Compare commits

...

15 Commits

Author SHA1 Message Date
ed7169b64e Add internal task notes to README
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 10:06:45 +08:00
642b879dd4 Add membership fee system with disability discount and fix document permissions
Features:
- Implement two fee types: entrance fee and annual fee (both NT$1,000)
- Add 50% discount for disability certificate holders
- Add disability certificate upload in member profile
- Integrate disability verification into cashier approval workflow
- Add membership fee settings in system admin

Document permissions:
- Fix hard-coded role logic in Document model
- Use permission-based authorization instead of role checks

Additional features:
- Add announcements, general ledger, and trial balance modules
- Add income management and accounting entries
- Add comprehensive test suite with factories
- Update UI translations to Traditional Chinese

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 09:56:01 +08:00
83ce1f7fc8 chore: ignore admin data directory 2025-11-30 14:49:45 +08:00
bcff65cf67 Fix audit logs and issue reports pages, rename Issues to Tasks
修復稽核日誌與任務報表頁面,並將「問題」改名為「任務」

## Changes 變更內容

### Bug Fixes 錯誤修復
1. Fixed audit logs page 500 error
   - Added missing $auditableTypes variable to controller
   - Changed $events to $actions in view
   - Added description and ip_address columns to audit_logs table
   - Updated AuditLog model fillable array

2. Fixed issue reports page SQLite compatibility errors
   - Replaced MySQL NOW() function with Laravel now() helper
   - Replaced TIMESTAMPDIFF() with PHP-based date calculation
   - Fixed request->date() default value handling

### Feature Changes 功能變更
3. Renamed "Issues" terminology to "Tasks" throughout the system
   - Updated navigation menus (Admin: Issues → Admin: Tasks)
   - Updated all issue-related views to use task terminology
   - Changed Chinese labels from "問題" to "任務"
   - Updated dashboard, issue tracker, and reports pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 10:47:04 +08:00
bf6179c457 Remove default sizing from ApplicationLogo component
- Removed 'h-10 w-auto' from resources/views/components/application-logo.blade.php.
- This resolves CSS conflicts where default classes were persisting alongside custom width/height classes (e.g., 'h-10 w-auto h-auto w-32').
- All logo instances now fully respect the classes passed to them.
2025-11-28 00:51:51 +08:00
21c82a5f18 Strictly limit logo width on homepage and guest pages
- Updated welcome.blade.php (homepage) to use 'w-32 h-auto' (128px width).
- Updated guest.blade.php (login/register) to use 'w-48 h-auto' (192px width).
- This addresses the user's feedback that the logo width was not constrained, causing it to appear too large.
2025-11-28 00:49:29 +08:00
6860a98f61 Optimize homepage logo size
- Reduced the height of the application logo on guest pages (login/register) from h-20 (80px) to h-12 (48px).
- This addresses the user's feedback that the logo was too large on pre-login pages.
2025-11-28 00:46:48 +08:00
b6be6578c4 Restrict access to forbidden links and widgets based on roles
- Wrapped Admin/Management navigation links in @role and @can permission checks.
- Restricted dashboard 'Management/Ops' and 'Finance Application' widgets to authorized roles.
- Applied granular visibility control to 'To-do' buckets on the dashboard for Applicant, Cashier, Accountant, and Chair.
2025-11-28 00:38:10 +08:00
ebf7f4b42d Translate UI to Traditional Chinese
- Created 'lang/zh_TW.json' with comprehensive translations for UI elements, forms, navigation, and messages.
- Updated 'config/app.php' to set locale to 'zh_TW' and timezone to 'Asia/Taipei'.
- This ensures the entire application interface is presented in Traditional Chinese.
2025-11-28 00:31:05 +08:00
6890cf085d Fix 'My Membership' 404 by adding missing profile flow
- Added a 'Create Member Profile' page for existing users who don't have a member record.
- Updated MemberDashboardController to redirect to profile creation instead of aborting 404.
- Added 'member.profile.create' and 'member.profile.store' routes.
2025-11-28 00:25:04 +08:00
c7a1f9130e Refactor 'My Membership' page scripts
- Replaced inline JS function generation in loops with Alpine.js event handlers.
- Improved safety of rejection reason output using json_encode.
2025-11-28 00:21:57 +08:00
cf367fe6e0 Fix 404 error in finance document emails
- Update invalid 'finance.show' route to the correct 'admin.finance.show' in email templates.
- This prevents broken links in approval, rejection, and status update emails.
2025-11-28 00:20:53 +08:00
70dec7615e Optimize Navigation Bar for overflow handling
- Grouped all Admin links into a 'Management' dropdown menu on desktop to prevent navbar overflow.
- Added a 'Management' section header to the mobile menu for better organization.
- Ensured the 'Management' dropdown trigger visually matches other nav links and indicates active state.
2025-11-28 00:17:02 +08:00
56692bc540 Fix undefined $active variable in nav components
- Set default value for 'active' prop to false in nav-link.blade.php and responsive-nav-link.blade.php.
- This resolves the ErrorException when the prop is not explicitly passed.
2025-11-28 00:15:15 +08:00
86f22f2a76 Enhance UI and Accessibility (WCAG)
- Add 'Skip to main content' links to App and Guest layouts.
- Add aria-current to navigation links for active page indication.
- Add aria-label to mobile menu button.
- Include pending UI updates for Dashboard and Welcome pages with improved structure.
2025-11-28 00:13:04 +08:00
228 changed files with 20893 additions and 3354 deletions

2
.gitignore vendored
View File

@@ -9,6 +9,8 @@
.env.backup .env.backup
.env.production .env.production
.phpunit.result.cache .phpunit.result.cache
# Sensitive association admin data
/協會行政資料
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
auth.json auth.json

View File

@@ -1,5 +1,9 @@
# UsherManage # UsherManage
## 內部雜務
1. 會員資料整合匯入,開放大家測試是否可以正常登入查看會籍
2. 確認財務審核流程
**完整的台灣NPO組織管理平台** **完整的台灣NPO組織管理平台**
全功能非營利組織管理系統,包含會員管理、財務工作流程、問題追蹤、文件管理、預算編列等模組。基於 Laravel 11 + Breeze (Blade/Tailwind/Alpine) 開發,支援 SQLite/MySQL實現完整的 RBAC 權限控制與審計追蹤。 全功能非營利組織管理系統,包含會員管理、財務工作流程、問題追蹤、文件管理、預算編列等模組。基於 Laravel 11 + Breeze (Blade/Tailwind/Alpine) 開發,支援 SQLite/MySQL實現完整的 RBAC 權限控制與審計追蹤。

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Maatwebsite\Excel\Facades\Excel;
use PhpOffice\PhpSpreadsheet\IOFactory;
class AnalyzeAccountingData extends Command
{
protected $signature = 'analyze:accounting';
protected $description = 'Analyze accounting data files';
public function handle()
{
$this->info('=== 協會帳務資料分析 ===');
$this->newLine();
$files = [
'2024尤塞氏症及視聽雙弱協會帳務.xlsx' => '協會行政資料/協會帳務/2024尤塞氏症及視聽雙弱協會帳務.xlsx',
'2025 收入支出總表 (含會計科目編號).xlsx' => '協會行政資料/協會帳務/2025 收入支出總表 (含會計科目編號).xlsx',
'2025 協會預算試編.xlsx' => '協會行政資料/協會帳務/2025 協會預算試編.xlsx',
];
foreach ($files as $name => $path) {
if (!file_exists($path)) {
$this->error("檔案不存在: {$name}");
continue;
}
$this->info("📊 分析檔案: {$name}");
$this->info("📅 最後更新: " . date('Y-m-d H:i:s', filemtime($path)));
$this->info("📦 檔案大小: " . number_format(filesize($path)) . " bytes");
$this->newLine();
try {
$spreadsheet = IOFactory::load($path);
$this->info("工作表列表:");
foreach ($spreadsheet->getAllSheets() as $index => $sheet) {
$sheetName = $sheet->getTitle();
$highestRow = $sheet->getHighestRow();
$highestColumn = $sheet->getHighestColumn();
$this->line(" - {$sheetName} (範圍: A1:{$highestColumn}{$highestRow}, 共 {$highestRow} 列)");
// 讀取前5行作為預覽
if ($highestRow > 0) {
$this->info(" 前5行預覽:");
for ($row = 1; $row <= min(5, $highestRow); $row++) {
$rowData = [];
for ($col = 'A'; $col <= min('J', $highestColumn); $col++) {
$cellValue = $sheet->getCell($col . $row)->getValue();
if (!empty($cellValue)) {
$rowData[] = substr($cellValue, 0, 30);
}
}
if (!empty($rowData)) {
$this->line(" Row {$row}: " . implode(' | ', $rowData));
}
}
$this->newLine();
}
}
} catch (\Exception $e) {
$this->error("讀取失敗: " . $e->getMessage());
}
$this->info(str_repeat('─', 80));
$this->newLine();
}
return 0;
}
}

View File

@@ -0,0 +1,436 @@
<?php
namespace App\Console\Commands;
use App\Models\AccountingEntry;
use App\Models\ChartOfAccount;
use App\Models\FinanceDocument;
use Illuminate\Console\Command;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
class ImportAccountingData extends Command
{
protected $signature = 'import:accounting-data
{file? : Path to Excel file}
{--dry-run : Preview without importing}';
protected $description = 'Import accounting data from Excel files';
protected $mapping;
protected $expenseKeywords;
protected $accountCache = [];
protected $stats = [
'income_count' => 0,
'expense_count' => 0,
'skipped_count' => 0,
'error_count' => 0,
];
public function handle()
{
$this->info('=== 會計資料匯入工具 ===');
$this->newLine();
// Load configuration
$this->mapping = config('accounting_mapping.excel_to_system', []);
$this->expenseKeywords = config('accounting_mapping.expense_keywords', []);
// Get file path
$filePath = $this->argument('file') ?? $this->askForFile();
if (!file_exists($filePath)) {
$this->error("檔案不存在: {$filePath}");
return 1;
}
$this->info("📂 檔案: {$filePath}");
$this->info("📅 檔案日期: " . date('Y-m-d H:i:s', filemtime($filePath)));
$this->newLine();
if ($this->option('dry-run')) {
$this->warn('⚠️ DRY RUN MODE - 不會實際寫入資料庫');
$this->newLine();
}
try {
$spreadsheet = IOFactory::load($filePath);
// Import income
$this->info('📊 匯入收入資料...');
$this->importIncome($spreadsheet);
$this->newLine();
// Import expenses
$this->info('📊 匯入支出資料...');
$this->importExpenses($spreadsheet);
$this->newLine();
// Show summary
$this->showSummary();
// Verify balance
if (!$this->option('dry-run')) {
$this->verifyBalance();
}
return 0;
} catch (\Exception $e) {
$this->error('匯入失敗: ' . $e->getMessage());
$this->error($e->getTraceAsString());
return 1;
}
}
protected function askForFile(): string
{
$defaultPath = '協會行政資料/協會帳務/2025 收入支出總表 (含會計科目編號).xlsx';
$this->info('可用檔案:');
$this->line('1. ' . $defaultPath);
return $this->ask('請輸入檔案路徑', $defaultPath);
}
protected function importIncome($spreadsheet)
{
// Find the "收入" sheet
$sheet = null;
foreach ($spreadsheet->getAllSheets() as $s) {
if (in_array($s->getTitle(), ['收入', 'Income', '收入明細'])) {
$sheet = $s;
break;
}
}
if (!$sheet) {
$this->warn('找不到「收入」工作表,跳過');
return;
}
$this->info("工作表: {$sheet->getTitle()}");
// Read header row to find columns
$headerRow = 1;
$headers = [];
$maxCol = $sheet->getHighestColumn();
for ($col = 'A'; $col <= $maxCol; $col++) {
$value = $sheet->getCell($col . $headerRow)->getValue();
if ($value) {
$headers[$col] = $value;
}
}
$this->line('欄位: ' . implode(', ', $headers));
// Detect columns
$dateCol = $this->findColumn($headers, ['日期', 'Date']);
$accountCodeCol = $this->findColumn($headers, ['科目編號', '科目代碼', 'Code']);
$accountNameCol = $this->findColumn($headers, ['科目名稱', 'Account']);
$amountCol = $this->findColumn($headers, ['收入金額', '金額', 'Amount']);
$descCol = $this->findColumn($headers, ['收入來源備註', '備註', '說明', 'Description']);
if (!$dateCol || !$amountCol) {
$this->error('缺少必要欄位(日期、金額)');
return;
}
// Import rows
$highestRow = $sheet->getHighestRow();
$bar = $this->output->createProgressBar($highestRow - 1);
for ($row = $headerRow + 1; $row <= $highestRow; $row++) {
$amount = $sheet->getCell($amountCol . $row)->getValue();
if (empty($amount) || $amount == 0) {
$this->stats['skipped_count']++;
$bar->advance();
continue;
}
try {
$date = $this->parseDate($sheet->getCell($dateCol . $row)->getValue());
$excelAccountCode = $accountCodeCol ? $sheet->getCell($accountCodeCol . $row)->getValue() : null;
$description = $descCol ? $sheet->getCell($descCol . $row)->getValue() : '';
// Map to system account
$systemAccountCode = $this->mapAccountCode($excelAccountCode);
$account = $this->getAccount($systemAccountCode);
if (!$account) {
$this->stats['error_count']++;
$this->warn("\n找不到科目: {$systemAccountCode} (Excel: {$excelAccountCode})");
$bar->advance();
continue;
}
if (!$this->option('dry-run')) {
$this->createIncomeEntry($date, $account, $amount, $description);
}
$this->stats['income_count']++;
} catch (\Exception $e) {
$this->stats['error_count']++;
$this->warn("\nRow {$row} 錯誤: " . $e->getMessage());
}
$bar->advance();
}
$bar->finish();
$this->newLine();
}
protected function importExpenses($spreadsheet)
{
// Find the "支出" sheet
$sheet = null;
foreach ($spreadsheet->getAllSheets() as $s) {
if (in_array($s->getTitle(), ['支出', 'Expense', '支出明細'])) {
$sheet = $s;
break;
}
}
if (!$sheet) {
$this->warn('找不到「支出」工作表,跳過');
return;
}
$this->info("工作表: {$sheet->getTitle()}");
// Read header row
$headerRow = 1;
$headers = [];
$maxCol = $sheet->getHighestColumn();
for ($col = 'A'; $col <= $maxCol; $col++) {
$value = $sheet->getCell($col . $headerRow)->getValue();
if ($value) {
$headers[$col] = $value;
}
}
$this->line('欄位: ' . implode(', ', $headers));
// Detect columns
$dateCol = $this->findColumn($headers, ['日期', 'Date']);
$accountCodeCol = $this->findColumn($headers, ['科目編號', '科目代碼', 'Code']);
$amountCol = $this->findColumn($headers, ['支出金額', '金額', 'Amount']);
$descCol = $this->findColumn($headers, ['支出用途備註', '用途', '備註', 'Description']);
if (!$dateCol || !$amountCol) {
$this->error('缺少必要欄位(日期、金額)');
return;
}
// Import rows
$highestRow = $sheet->getHighestRow();
$bar = $this->output->createProgressBar($highestRow - 1);
for ($row = $headerRow + 1; $row <= $highestRow; $row++) {
$amount = $sheet->getCell($amountCol . $row)->getValue();
if (empty($amount) || $amount == 0) {
$this->stats['skipped_count']++;
$bar->advance();
continue;
}
try {
$date = $this->parseDate($sheet->getCell($dateCol . $row)->getValue());
$excelAccountCode = $accountCodeCol ? $sheet->getCell($accountCodeCol . $row)->getValue() : '5100';
$description = $descCol ? $sheet->getCell($descCol . $row)->getValue() : '';
// For 5100, classify by keywords
if ($excelAccountCode == '5100') {
$systemAccountCode = $this->classifyExpense($description);
} else {
$systemAccountCode = $this->mapAccountCode($excelAccountCode);
}
$account = $this->getAccount($systemAccountCode);
if (!$account) {
$this->stats['error_count']++;
$this->warn("\n找不到科目: {$systemAccountCode}");
$bar->advance();
continue;
}
if (!$this->option('dry-run')) {
$this->createExpenseEntry($date, $account, $amount, $description);
}
$this->stats['expense_count']++;
} catch (\Exception $e) {
$this->stats['error_count']++;
$this->warn("\nRow {$row} 錯誤: " . $e->getMessage());
}
$bar->advance();
}
$bar->finish();
$this->newLine();
}
protected function createIncomeEntry($date, $account, $amount, $description)
{
// Create finance document
$document = FinanceDocument::create([
'title' => '收入 - ' . $account->account_name_zh,
'amount' => $amount,
'description' => $description,
'chart_of_account_id' => $account->id,
'submitted_at' => $date,
'status' => FinanceDocument::STATUS_APPROVED_CHAIR,
]);
// Create accounting entries (double-entry)
// Debit: Cash
AccountingEntry::create([
'finance_document_id' => $document->id,
'chart_of_account_id' => $this->getAccount('1101')->id,
'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT,
'amount' => $amount,
'entry_date' => $date,
'description' => '收入 - ' . $description,
]);
// Credit: Income account
AccountingEntry::create([
'finance_document_id' => $document->id,
'chart_of_account_id' => $account->id,
'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT,
'amount' => $amount,
'entry_date' => $date,
'description' => $description,
]);
}
protected function createExpenseEntry($date, $account, $amount, $description)
{
// Create finance document
$document = FinanceDocument::create([
'title' => '支出 - ' . $account->account_name_zh,
'amount' => $amount,
'description' => $description,
'chart_of_account_id' => $account->id,
'submitted_at' => $date,
'status' => FinanceDocument::STATUS_APPROVED_CHAIR,
]);
// Create accounting entries (double-entry)
// Debit: Expense account
AccountingEntry::create([
'finance_document_id' => $document->id,
'chart_of_account_id' => $account->id,
'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT,
'amount' => $amount,
'entry_date' => $date,
'description' => $description,
]);
// Credit: Cash
AccountingEntry::create([
'finance_document_id' => $document->id,
'chart_of_account_id' => $this->getAccount('1101')->id,
'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT,
'amount' => $amount,
'entry_date' => $date,
'description' => '支出 - ' . $description,
]);
}
protected function mapAccountCode($excelCode)
{
return $this->mapping[$excelCode] ?? $excelCode;
}
protected function classifyExpense($description): string
{
foreach ($this->expenseKeywords as $rule) {
if (empty($rule['keywords'])) {
if ($rule['is_default'] ?? false) {
return $rule['account_code'];
}
continue;
}
foreach ($rule['keywords'] as $keyword) {
if (mb_strpos($description, $keyword) !== false) {
return $rule['account_code'];
}
}
}
return '5901'; // Default: 雜項支出
}
protected function getAccount($accountCode)
{
if (!isset($this->accountCache[$accountCode])) {
$this->accountCache[$accountCode] = ChartOfAccount::where('account_code', $accountCode)->first();
}
return $this->accountCache[$accountCode];
}
protected function parseDate($value)
{
if (is_numeric($value)) {
return ExcelDate::excelToDateTimeObject($value);
}
if ($value instanceof \DateTime) {
return $value;
}
return new \DateTime($value);
}
protected function findColumn($headers, $patterns)
{
foreach ($headers as $col => $header) {
foreach ($patterns as $pattern) {
if (mb_strpos($header, $pattern) !== false) {
return $col;
}
}
}
return null;
}
protected function showSummary()
{
$this->info('=== 匯入統計 ===');
$this->table(
['項目', '數量'],
[
['收入筆數', $this->stats['income_count']],
['支出筆數', $this->stats['expense_count']],
['跳過筆數', $this->stats['skipped_count']],
['錯誤筆數', $this->stats['error_count']],
]
);
}
protected function verifyBalance()
{
$this->info('=== 驗證借貸平衡 ===');
$debitTotal = AccountingEntry::where('entry_type', AccountingEntry::ENTRY_TYPE_DEBIT)->sum('amount');
$creditTotal = AccountingEntry::where('entry_type', AccountingEntry::ENTRY_TYPE_CREDIT)->sum('amount');
$this->line("借方總計: " . number_format($debitTotal, 2));
$this->line("貸方總計: " . number_format($creditTotal, 2));
if (bccomp((string)$debitTotal, (string)$creditTotal, 2) === 0) {
$this->info('✅ 借貸平衡');
} else {
$diff = $debitTotal - $creditTotal;
$this->error("❌ 借貸不平衡,差額: " . number_format($diff, 2));
}
}
}

View File

@@ -0,0 +1,346 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Announcement;
use App\Models\AuditLog;
use Illuminate\Http\Request;
class AnnouncementController extends Controller
{
public function __construct()
{
$this->middleware('can:view_announcements')->only(['index', 'show']);
$this->middleware('can:create_announcements')->only(['create', 'store']);
$this->middleware('can:edit_announcements')->only(['edit', 'update']);
$this->middleware('can:delete_announcements')->only(['destroy']);
$this->middleware('can:publish_announcements')->only(['publish', 'archive']);
}
/**
* Display a listing of announcements
*/
public function index(Request $request)
{
$query = Announcement::with(['creator', 'lastUpdatedBy'])
->orderByDesc('is_pinned')
->orderByDesc('created_at');
// Filter by status
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Filter by access level
if ($request->filled('access_level')) {
$query->where('access_level', $request->access_level);
}
// Filter by pinned
if ($request->filled('pinned')) {
$query->where('is_pinned', $request->pinned === 'yes');
}
// Search
if ($request->filled('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('content', 'like', "%{$search}%");
});
}
$announcements = $query->paginate(20);
// Statistics
$stats = [
'total' => Announcement::count(),
'draft' => Announcement::draft()->count(),
'published' => Announcement::published()->count(),
'archived' => Announcement::archived()->count(),
'pinned' => Announcement::pinned()->count(),
];
return view('admin.announcements.index', compact('announcements', 'stats'));
}
/**
* Show the form for creating a new announcement
*/
public function create()
{
return view('admin.announcements.create');
}
/**
* Store a newly created announcement
*/
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'access_level' => 'required|in:public,members,board,admin',
'published_at' => 'nullable|date',
'expires_at' => 'nullable|date|after:published_at',
'is_pinned' => 'boolean',
'display_order' => 'nullable|integer',
'save_action' => 'required|in:draft,publish',
]);
$announcement = Announcement::create([
'title' => $validated['title'],
'content' => $validated['content'],
'access_level' => $validated['access_level'],
'status' => $validated['save_action'] === 'publish' ? Announcement::STATUS_PUBLISHED : Announcement::STATUS_DRAFT,
'published_at' => $validated['save_action'] === 'publish' ? ($validated['published_at'] ?? now()) : null,
'expires_at' => $validated['expires_at'] ?? null,
'is_pinned' => $validated['is_pinned'] ?? false,
'display_order' => $validated['display_order'] ?? 0,
'created_by_user_id' => auth()->id(),
'last_updated_by_user_id' => auth()->id(),
]);
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'announcement.created',
'description' => "建立公告:{$announcement->title} (狀態:{$announcement->getStatusLabel()})",
'ip_address' => request()->ip(),
]);
$message = $validated['save_action'] === 'publish' ? '公告已成功發布' : '公告已儲存為草稿';
return redirect()
->route('admin.announcements.show', $announcement)
->with('status', $message);
}
/**
* Display the specified announcement
*/
public function show(Announcement $announcement)
{
// Check if user can view this announcement
if (!$announcement->canBeViewedBy(auth()->user())) {
abort(403, '您沒有權限查看此公告');
}
$announcement->load(['creator', 'lastUpdatedBy']);
// Increment view count if viewing published announcement
if ($announcement->isPublished()) {
$announcement->incrementViewCount();
}
return view('admin.announcements.show', compact('announcement'));
}
/**
* Show the form for editing the specified announcement
*/
public function edit(Announcement $announcement)
{
// Check if user can edit this announcement
if (!$announcement->canBeEditedBy(auth()->user())) {
abort(403, '您沒有權限編輯此公告');
}
return view('admin.announcements.edit', compact('announcement'));
}
/**
* Update the specified announcement
*/
public function update(Request $request, Announcement $announcement)
{
// Check if user can edit this announcement
if (!$announcement->canBeEditedBy(auth()->user())) {
abort(403, '您沒有權限編輯此公告');
}
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'access_level' => 'required|in:public,members,board,admin',
'published_at' => 'nullable|date',
'expires_at' => 'nullable|date|after:published_at',
'is_pinned' => 'boolean',
'display_order' => 'nullable|integer',
]);
$announcement->update([
'title' => $validated['title'],
'content' => $validated['content'],
'access_level' => $validated['access_level'],
'published_at' => $validated['published_at'],
'expires_at' => $validated['expires_at'] ?? null,
'is_pinned' => $validated['is_pinned'] ?? false,
'display_order' => $validated['display_order'] ?? 0,
'last_updated_by_user_id' => auth()->id(),
]);
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'announcement.updated',
'description' => "更新公告:{$announcement->title}",
'ip_address' => request()->ip(),
]);
return redirect()
->route('admin.announcements.show', $announcement)
->with('status', '公告已成功更新');
}
/**
* Remove the specified announcement (soft delete)
*/
public function destroy(Announcement $announcement)
{
// Check if user can delete this announcement
if (!$announcement->canBeEditedBy(auth()->user())) {
abort(403, '您沒有權限刪除此公告');
}
$title = $announcement->title;
$announcement->delete();
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'announcement.deleted',
'description' => "刪除公告:{$title}",
'ip_address' => request()->ip(),
]);
return redirect()
->route('admin.announcements.index')
->with('status', '公告已成功刪除');
}
/**
* Publish a draft announcement
*/
public function publish(Announcement $announcement)
{
// Check permission
if (!auth()->user()->can('publish_announcements')) {
abort(403, '您沒有權限發布公告');
}
// Check if user can edit this announcement
if (!$announcement->canBeEditedBy(auth()->user())) {
abort(403, '您沒有權限發布此公告');
}
if ($announcement->isPublished()) {
return back()->with('error', '此公告已經發布');
}
$announcement->publish(auth()->user());
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'announcement.published',
'description' => "發布公告:{$announcement->title}",
'ip_address' => request()->ip(),
]);
return back()->with('status', '公告已成功發布');
}
/**
* Archive an announcement
*/
public function archive(Announcement $announcement)
{
// Check permission
if (!auth()->user()->can('publish_announcements')) {
abort(403, '您沒有權限歸檔公告');
}
// Check if user can edit this announcement
if (!$announcement->canBeEditedBy(auth()->user())) {
abort(403, '您沒有權限歸檔此公告');
}
if ($announcement->isArchived()) {
return back()->with('error', '此公告已經歸檔');
}
$announcement->archive(auth()->user());
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'announcement.archived',
'description' => "歸檔公告:{$announcement->title}",
'ip_address' => request()->ip(),
]);
return back()->with('status', '公告已成功歸檔');
}
/**
* Pin an announcement
*/
public function pin(Request $request, Announcement $announcement)
{
// Check permission
if (!auth()->user()->can('edit_announcements')) {
abort(403, '您沒有權限置頂公告');
}
// Check if user can edit this announcement
if (!$announcement->canBeEditedBy(auth()->user())) {
abort(403, '您沒有權限置頂此公告');
}
$validated = $request->validate([
'display_order' => 'nullable|integer',
]);
$announcement->pin($validated['display_order'] ?? 0, auth()->user());
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'announcement.pinned',
'description' => "置頂公告:{$announcement->title}",
'ip_address' => request()->ip(),
]);
return back()->with('status', '公告已成功置頂');
}
/**
* Unpin an announcement
*/
public function unpin(Announcement $announcement)
{
// Check permission
if (!auth()->user()->can('edit_announcements')) {
abort(403, '您沒有權限取消置頂公告');
}
// Check if user can edit this announcement
if (!$announcement->canBeEditedBy(auth()->user())) {
abort(403, '您沒有權限取消置頂此公告');
}
$announcement->unpin(auth()->user());
// Audit log
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'announcement.unpinned',
'description' => "取消置頂公告:{$announcement->title}",
'ip_address' => request()->ip(),
]);
return back()->with('status', '公告已取消置頂');
}
}

View File

@@ -83,8 +83,8 @@ class DocumentController extends Controller
$document = Document::create([ $document = Document::create([
'document_category_id' => $validated['document_category_id'], 'document_category_id' => $validated['document_category_id'],
'title' => $validated['title'], 'title' => $validated['title'],
'document_number' => $validated['document_number'], 'document_number' => $validated['document_number'] ?? null,
'description' => $validated['description'], 'description' => $validated['description'] ?? null,
'access_level' => $validated['access_level'], 'access_level' => $validated['access_level'],
'status' => 'active', 'status' => 'active',
'created_by_user_id' => auth()->id(), 'created_by_user_id' => auth()->id(),
@@ -360,7 +360,7 @@ class DocumentController extends Controller
->get(); ->get();
// Monthly upload trends (last 6 months) // Monthly upload trends (last 6 months)
$uploadTrends = Document::selectRaw('DATE_FORMAT(created_at, "%Y-%m") as month, COUNT(*) as count') $uploadTrends = Document::selectRaw("strftime('%Y-%m', created_at) as month, COUNT(*) as count")
->where('created_at', '>=', now()->subMonths(6)) ->where('created_at', '>=', now()->subMonths(6))
->groupBy('month') ->groupBy('month')
->orderBy('month', 'desc') ->orderBy('month', 'desc')

View File

@@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AccountingEntry;
use App\Models\ChartOfAccount;
use Illuminate\Http\Request;
class GeneralLedgerController extends Controller
{
/**
* Display the general ledger
*/
public function index(Request $request)
{
$accounts = ChartOfAccount::where('is_active', true)
->orderBy('account_code')
->get();
$selectedAccountId = $request->input('account_id');
$startDate = $request->input('start_date', now()->startOfYear()->format('Y-m-d'));
$endDate = $request->input('end_date', now()->format('Y-m-d'));
$entries = null;
$selectedAccount = null;
$openingBalance = 0;
$debitTotal = 0;
$creditTotal = 0;
$closingBalance = 0;
if ($selectedAccountId) {
$selectedAccount = ChartOfAccount::findOrFail($selectedAccountId);
// Get opening balance (all entries before start date)
$openingDebit = AccountingEntry::where('chart_of_account_id', $selectedAccountId)
->where('entry_date', '<', $startDate)
->where('entry_type', AccountingEntry::ENTRY_TYPE_DEBIT)
->sum('amount');
$openingCredit = AccountingEntry::where('chart_of_account_id', $selectedAccountId)
->where('entry_date', '<', $startDate)
->where('entry_type', AccountingEntry::ENTRY_TYPE_CREDIT)
->sum('amount');
// Calculate opening balance based on account type
if (in_array($selectedAccount->account_type, ['asset', 'expense'])) {
// Assets and Expenses: Debit increases, Credit decreases
$openingBalance = $openingDebit - $openingCredit;
} else {
// Liabilities, Equity, Income: Credit increases, Debit decreases
$openingBalance = $openingCredit - $openingDebit;
}
// Get entries for the period
$entries = AccountingEntry::with(['financeDocument', 'chartOfAccount'])
->where('chart_of_account_id', $selectedAccountId)
->whereBetween('entry_date', [$startDate, $endDate])
->orderBy('entry_date')
->orderBy('id')
->get();
// Calculate totals for the period
$debitTotal = $entries->where('entry_type', AccountingEntry::ENTRY_TYPE_DEBIT)->sum('amount');
$creditTotal = $entries->where('entry_type', AccountingEntry::ENTRY_TYPE_CREDIT)->sum('amount');
// Calculate closing balance
if (in_array($selectedAccount->account_type, ['asset', 'expense'])) {
$closingBalance = $openingBalance + $debitTotal - $creditTotal;
} else {
$closingBalance = $openingBalance + $creditTotal - $debitTotal;
}
}
return view('admin.general-ledger.index', compact(
'accounts',
'selectedAccount',
'entries',
'startDate',
'endDate',
'openingBalance',
'debitTotal',
'creditTotal',
'closingBalance'
));
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\AuditLog; use App\Models\AuditLog;
use App\Models\SystemSetting; use App\Models\SystemSetting;
use App\Services\MembershipFeeCalculator;
use App\Services\SettingsService; use App\Services\SettingsService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -270,4 +271,45 @@ class SystemSettingsController extends Controller
return redirect()->route('admin.settings.advanced')->with('status', '進階設定已更新'); return redirect()->route('admin.settings.advanced')->with('status', '進階設定已更新');
} }
/**
* Show membership fee settings page
*/
public function membership()
{
$feeCalculator = app(MembershipFeeCalculator::class);
$settings = [
'entrance_fee' => $feeCalculator->getEntranceFee(),
'annual_fee' => $feeCalculator->getAnnualFee(),
'disability_discount_rate' => $feeCalculator->getDisabilityDiscountRate() * 100, // Convert to percentage
];
return view('admin.settings.membership', compact('settings'));
}
/**
* Update membership fee settings
*/
public function updateMembership(Request $request)
{
$validated = $request->validate([
'entrance_fee' => 'required|numeric|min:0|max:100000',
'annual_fee' => 'required|numeric|min:0|max:100000',
'disability_discount_rate' => 'required|numeric|min:0|max:100',
]);
SystemSetting::set('membership_fee.entrance_fee', $validated['entrance_fee'], 'float', 'membership');
SystemSetting::set('membership_fee.annual_fee', $validated['annual_fee'], 'float', 'membership');
SystemSetting::set('membership_fee.disability_discount_rate', $validated['disability_discount_rate'] / 100, 'float', 'membership');
AuditLog::create([
'user_id' => auth()->id(),
'action' => 'settings.membership.updated',
'description' => '更新會費設定',
'ip_address' => $request->ip(),
]);
return redirect()->route('admin.settings.membership')->with('status', '會費設定已更新');
}
} }

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AccountingEntry;
use App\Models\ChartOfAccount;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TrialBalanceController extends Controller
{
/**
* Display the trial balance
*/
public function index(Request $request)
{
$startDate = $request->input('start_date', now()->startOfYear()->format('Y-m-d'));
$endDate = $request->input('end_date', now()->format('Y-m-d'));
// Get all active accounts with their balances
$accounts = ChartOfAccount::where('is_active', true)
->orderBy('account_code')
->get()
->map(function ($account) use ($startDate, $endDate) {
// Get debit and credit totals for this account
$debitTotal = AccountingEntry::where('chart_of_account_id', $account->id)
->whereBetween('entry_date', [$startDate, $endDate])
->where('entry_type', AccountingEntry::ENTRY_TYPE_DEBIT)
->sum('amount');
$creditTotal = AccountingEntry::where('chart_of_account_id', $account->id)
->whereBetween('entry_date', [$startDate, $endDate])
->where('entry_type', AccountingEntry::ENTRY_TYPE_CREDIT)
->sum('amount');
// Only include accounts with activity
if ($debitTotal == 0 && $creditTotal == 0) {
return null;
}
return [
'account' => $account,
'debit_total' => $debitTotal,
'credit_total' => $creditTotal,
];
})
->filter() // Remove null entries
->values();
// Calculate grand totals
$grandDebitTotal = $accounts->sum('debit_total');
$grandCreditTotal = $accounts->sum('credit_total');
// Check if balanced
$isBalanced = bccomp((string)$grandDebitTotal, (string)$grandCreditTotal, 2) === 0;
$difference = $grandDebitTotal - $grandCreditTotal;
// Group accounts by type
$accountsByType = $accounts->groupBy(function ($item) {
return $item['account']->account_type;
});
return view('admin.trial-balance.index', compact(
'accounts',
'accountsByType',
'startDate',
'endDate',
'grandDebitTotal',
'grandCreditTotal',
'isBalanced',
'difference'
));
}
}

View File

@@ -46,6 +46,7 @@ class AdminAuditLogController extends Controller
$users = AuditLog::with('user')->whereNotNull('user_id')->select('user_id')->distinct()->get()->map(function ($log) { $users = AuditLog::with('user')->whereNotNull('user_id')->select('user_id')->distinct()->get()->map(function ($log) {
return $log->user; return $log->user;
})->filter(); })->filter();
$auditableTypes = AuditLog::select('auditable_type')->distinct()->whereNotNull('auditable_type')->orderBy('auditable_type')->pluck('auditable_type');
return view('admin.audit.index', [ return view('admin.audit.index', [
'logs' => $logs, 'logs' => $logs,
@@ -56,6 +57,7 @@ class AdminAuditLogController extends Controller
'endDate' => $end, 'endDate' => $end,
'actions' => $actions, 'actions' => $actions,
'users' => $users, 'users' => $users,
'auditableTypes' => $auditableTypes,
]); ]);
} }

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Models\Member; use App\Models\Member;
use App\Models\MembershipPayment; use App\Models\MembershipPayment;
use App\Models\FinanceDocument; use App\Models\FinanceDocument;
use App\Models\Announcement;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class AdminDashboardController extends Controller class AdminDashboardController extends Controller
@@ -47,16 +48,26 @@ class AdminDashboardController extends Controller
// Documents pending user's approval // Documents pending user's approval
$user = auth()->user(); $user = auth()->user();
$myPendingApprovals = 0; $myPendingApprovals = 0;
if ($user->hasRole('cashier')) { if ($user->hasRole('finance_cashier')) {
$myPendingApprovals += FinanceDocument::where('status', FinanceDocument::STATUS_PENDING)->count(); $myPendingApprovals += FinanceDocument::where('status', FinanceDocument::STATUS_PENDING)->count();
} }
if ($user->hasRole('accountant')) { if ($user->hasRole('finance_accountant')) {
$myPendingApprovals += FinanceDocument::where('status', FinanceDocument::STATUS_APPROVED_CASHIER)->count(); $myPendingApprovals += FinanceDocument::where('status', FinanceDocument::STATUS_APPROVED_CASHIER)->count();
} }
if ($user->hasRole('chair')) { if ($user->hasRole('finance_chair')) {
$myPendingApprovals += FinanceDocument::where('status', FinanceDocument::STATUS_APPROVED_ACCOUNTANT)->count(); $myPendingApprovals += FinanceDocument::where('status', FinanceDocument::STATUS_APPROVED_ACCOUNTANT)->count();
} }
// Recent announcements
$recentAnnouncements = Announcement::query()
->published()
->active()
->forAccessLevel($user)
->orderByDesc('is_pinned')
->orderByDesc('published_at')
->limit(5)
->get();
return view('admin.dashboard.index', compact( return view('admin.dashboard.index', compact(
'totalMembers', 'totalMembers',
'activeMembers', 'activeMembers',
@@ -70,7 +81,8 @@ class AdminDashboardController extends Controller
'pendingApprovals', 'pendingApprovals',
'fullyApprovedDocs', 'fullyApprovedDocs',
'rejectedDocs', 'rejectedDocs',
'myPendingApprovals' 'myPendingApprovals',
'recentAnnouncements'
)); ));
} }
} }

View File

@@ -217,7 +217,7 @@ class AdminMemberController extends Controller
public function showActivate(Member $member) public function showActivate(Member $member)
{ {
// Check if user has permission // Check if user has permission
if (!auth()->user()->can('activate_memberships') && !auth()->user()->is_admin) { if (!auth()->user()->can('activate_memberships') && !auth()->user()->hasRole('admin')) {
abort(403, 'You do not have permission to activate memberships.'); abort(403, 'You do not have permission to activate memberships.');
} }
@@ -227,7 +227,7 @@ class AdminMemberController extends Controller
->latest() ->latest()
->first(); ->first();
if (!$approvedPayment && !auth()->user()->is_admin) { if (!$approvedPayment && !auth()->user()->hasRole('admin')) {
return redirect()->route('admin.members.show', $member) return redirect()->route('admin.members.show', $member)
->with('error', __('Member must have an approved payment before activation.')); ->with('error', __('Member must have an approved payment before activation.'));
} }
@@ -241,7 +241,7 @@ class AdminMemberController extends Controller
public function activate(Request $request, Member $member) public function activate(Request $request, Member $member)
{ {
// Check if user has permission // Check if user has permission
if (!auth()->user()->can('activate_memberships') && !auth()->user()->is_admin) { if (!auth()->user()->can('activate_memberships') && !auth()->user()->hasRole('admin')) {
abort(403, 'You do not have permission to activate memberships.'); abort(403, 'You do not have permission to activate memberships.');
} }
@@ -344,4 +344,53 @@ class AdminMemberController extends Controller
return $response; return $response;
} }
public function batchDestroy(Request $request)
{
$validated = $request->validate([
'ids' => ['required', 'array'],
'ids.*' => ['exists:members,id'],
]);
$count = Member::whereIn('id', $validated['ids'])->delete();
AuditLogger::log('members.batch_deleted', null, ['ids' => $validated['ids'], 'count' => $count]);
return back()->with('status', __(':count members deleted successfully.', ['count' => $count]));
}
public function batchUpdateStatus(Request $request)
{
$validated = $request->validate([
'ids' => ['required', 'array'],
'ids.*' => ['exists:members,id'],
'status' => ['required', 'in:pending,active,expired,suspended'],
]);
$count = Member::whereIn('id', $validated['ids'])->update(['membership_status' => $validated['status']]);
AuditLogger::log('members.batch_status_updated', null, [
'ids' => $validated['ids'],
'status' => $validated['status'],
'count' => $count
]);
return back()->with('status', __(':count members updated successfully.', ['count' => $count]));
}
/**
* View member's disability certificate
*/
public function viewDisabilityCertificate(Member $member)
{
if (!$member->disability_certificate_path) {
abort(404, '找不到身心障礙手冊');
}
if (!\Illuminate\Support\Facades\Storage::disk('private')->exists($member->disability_certificate_path)) {
abort(404, '檔案不存在');
}
return \Illuminate\Support\Facades\Storage::disk('private')->response($member->disability_certificate_path);
}
} }

View File

@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Member;
use App\Models\User; use App\Models\User;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Events\Registered;
@@ -42,6 +43,15 @@ class RegisteredUserController extends Controller
'password' => Hash::make($request->password), 'password' => Hash::make($request->password),
]); ]);
// Auto-create member record
Member::create([
'user_id' => $user->id,
'full_name' => $user->name,
'email' => $user->email,
'membership_status' => Member::STATUS_PENDING,
'membership_type' => Member::TYPE_REGULAR,
]);
event(new Registered($user)); event(new Registered($user));
Auth::login($user); Auth::login($user);

View File

@@ -201,7 +201,7 @@ class BudgetController extends Controller
// Check if user has permission (admin or chair) // Check if user has permission (admin or chair)
$user = $request->user(); $user = $request->user();
if (!$user->hasRole('chair') && !$user->is_admin && !$user->hasRole('admin')) { if (!$user->hasRole('finance_chair') && !$user->hasRole('admin')) {
abort(403, 'Only chair can approve budgets.'); abort(403, 'Only chair can approve budgets.');
} }

View File

@@ -26,11 +26,6 @@ class FinanceDocumentController extends Controller
$query->where('status', $request->status); $query->where('status', $request->status);
} }
// Filter by request type
if ($request->filled('request_type')) {
$query->where('request_type', $request->request_type);
}
// Filter by amount tier // Filter by amount tier
if ($request->filled('amount_tier')) { if ($request->filled('amount_tier')) {
$query->where('amount_tier', $request->amount_tier); $query->where('amount_tier', $request->amount_tier);
@@ -79,7 +74,6 @@ class FinanceDocumentController extends Controller
'member_id' => ['nullable', 'exists:members,id'], 'member_id' => ['nullable', 'exists:members,id'],
'title' => ['required', 'string', 'max:255'], 'title' => ['required', 'string', 'max:255'],
'amount' => ['required', 'numeric', 'min:0'], 'amount' => ['required', 'numeric', 'min:0'],
'request_type' => ['required', 'in:expense_reimbursement,advance_payment,purchase_request,petty_cash'],
'description' => ['nullable', 'string'], 'description' => ['nullable', 'string'],
'attachment' => ['nullable', 'file', 'max:10240'], // 10MB max 'attachment' => ['nullable', 'file', 'max:10240'], // 10MB max
]); ]);
@@ -95,7 +89,6 @@ class FinanceDocumentController extends Controller
'submitted_by_user_id' => $request->user()->id, 'submitted_by_user_id' => $request->user()->id,
'title' => $validated['title'], 'title' => $validated['title'],
'amount' => $validated['amount'], 'amount' => $validated['amount'],
'request_type' => $validated['request_type'],
'description' => $validated['description'] ?? null, 'description' => $validated['description'] ?? null,
'attachment_path' => $attachmentPath, 'attachment_path' => $attachmentPath,
'status' => FinanceDocument::STATUS_PENDING, 'status' => FinanceDocument::STATUS_PENDING,
@@ -115,17 +108,13 @@ class FinanceDocumentController extends Controller
// Send email notification to finance cashiers // Send email notification to finance cashiers
$cashiers = User::role('finance_cashier')->get(); $cashiers = User::role('finance_cashier')->get();
if ($cashiers->isEmpty()) {
// Fallback to old cashier role for backward compatibility
$cashiers = User::role('cashier')->get();
}
foreach ($cashiers as $cashier) { foreach ($cashiers as $cashier) {
Mail::to($cashier->email)->queue(new FinanceDocumentSubmitted($document)); Mail::to($cashier->email)->queue(new FinanceDocumentSubmitted($document));
} }
return redirect() return redirect()
->route('admin.finance.index') ->route('admin.finance.index')
->with('status', '財務申請單已提交。申請類型:' . $document->getRequestTypeText() . '金額級別:' . $document->getAmountTierText()); ->with('status', '報銷申請單已提交。金額級別:' . $document->getAmountTierText());
} }
public function show(FinanceDocument $financeDocument) public function show(FinanceDocument $financeDocument)
@@ -133,13 +122,19 @@ class FinanceDocumentController extends Controller
$financeDocument->load([ $financeDocument->load([
'member', 'member',
'submittedBy', 'submittedBy',
// 新工作流程 relationships
'approvedBySecretary',
'approvedByChair',
'approvedByBoardMeeting',
'requesterConfirmedBy',
'cashierConfirmedBy',
'accountantRecordedBy',
// Legacy relationships
'approvedByCashier', 'approvedByCashier',
'approvedByAccountant', 'approvedByAccountant',
'approvedByChair',
'rejectedBy', 'rejectedBy',
'chartOfAccount', 'chartOfAccount',
'budgetItem', 'budgetItem',
'approvedByBoardMeeting',
'paymentOrderCreatedByAccountant', 'paymentOrderCreatedByAccountant',
'paymentVerifiedByCashier', 'paymentVerifiedByCashier',
'paymentExecutedByCashier', 'paymentExecutedByCashier',
@@ -159,72 +154,48 @@ class FinanceDocumentController extends Controller
{ {
$user = $request->user(); $user = $request->user();
// Check if user has any finance approval permissions // 新工作流程:秘書長 → 理事長 → 董理事會
$isCashier = $user->hasRole('finance_cashier') || $user->hasRole('cashier'); $isSecretary = $user->hasRole('secretary_general');
$isAccountant = $user->hasRole('finance_accountant') || $user->hasRole('accountant'); $isChair = $user->hasRole('finance_chair');
$isChair = $user->hasRole('finance_chair') || $user->hasRole('chair'); $isBoardMember = $user->hasRole('finance_board_member');
$isAdmin = $user->hasRole('admin');
// Determine which level of approval based on current status and user role // 秘書長審核(第一階段)
if ($financeDocument->canBeApprovedByCashier() && $isCashier) { if ($financeDocument->canBeApprovedBySecretary($user) && ($isSecretary || $isAdmin)) {
$financeDocument->update([ $financeDocument->update([
'approved_by_cashier_id' => $user->id, 'approved_by_secretary_id' => $user->id,
'cashier_approved_at' => now(), 'secretary_approved_at' => now(),
'status' => FinanceDocument::STATUS_APPROVED_CASHIER, 'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
]); ]);
AuditLogger::log('finance_document.approved_by_cashier', $financeDocument, [ AuditLogger::log('finance_document.approved_by_secretary', $financeDocument, [
'approved_by' => $user->name, 'approved_by' => $user->name,
'amount_tier' => $financeDocument->amount_tier, 'amount_tier' => $financeDocument->amount_tier,
]); ]);
// Send email notification to accountants // 小額:審核完成
$accountants = User::role('finance_accountant')->get();
if ($accountants->isEmpty()) {
$accountants = User::role('accountant')->get();
}
foreach ($accountants as $accountant) {
Mail::to($accountant->email)->queue(new FinanceDocumentApprovedByCashier($financeDocument));
}
return redirect()
->route('admin.finance.show', $financeDocument)
->with('status', '出納已審核通過。已送交會計審核。');
}
if ($financeDocument->canBeApprovedByAccountant() && $isAccountant) {
$financeDocument->update([
'approved_by_accountant_id' => $user->id,
'accountant_approved_at' => now(),
'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
]);
AuditLogger::log('finance_document.approved_by_accountant', $financeDocument, [
'approved_by' => $user->name,
'amount_tier' => $financeDocument->amount_tier,
]);
// For small amounts, approval is complete (no chair needed)
if ($financeDocument->amount_tier === FinanceDocument::AMOUNT_TIER_SMALL) { if ($financeDocument->amount_tier === FinanceDocument::AMOUNT_TIER_SMALL) {
// 通知申請人審核已完成,可以領款
Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument));
return redirect() return redirect()
->route('admin.finance.show', $financeDocument) ->route('admin.finance.show', $financeDocument)
->with('status', '會計已審核通過。小額申請審核完成,可以製作付款單。'); ->with('status', '秘書長已核准。小額申請審核完成,申請人可向出納領款。');
} }
// For medium and large amounts, send to chair // 中額/大額:送交理事長
$chairs = User::role('finance_chair')->get(); $chairs = User::role('finance_chair')->get();
if ($chairs->isEmpty()) {
$chairs = User::role('chair')->get();
}
foreach ($chairs as $chair) { foreach ($chairs as $chair) {
Mail::to($chair->email)->queue(new FinanceDocumentApprovedByAccountant($financeDocument)); Mail::to($chair->email)->queue(new FinanceDocumentApprovedByAccountant($financeDocument));
} }
return redirect() return redirect()
->route('admin.finance.show', $financeDocument) ->route('admin.finance.show', $financeDocument)
->with('status', '會計已審核通過。已送交理事長審核。'); ->with('status', '秘書長已核准。已送交理事長審核。');
} }
if ($financeDocument->canBeApprovedByChair() && $isChair) { // 理事長審核(第二階段:中額或大額)
if ($financeDocument->canBeApprovedByChair($user) && ($isChair || $isAdmin)) {
$financeDocument->update([ $financeDocument->update([
'approved_by_chair_id' => $user->id, 'approved_by_chair_id' => $user->id,
'chair_approved_at' => now(), 'chair_approved_at' => now(),
@@ -234,25 +205,147 @@ class FinanceDocumentController extends Controller
AuditLogger::log('finance_document.approved_by_chair', $financeDocument, [ AuditLogger::log('finance_document.approved_by_chair', $financeDocument, [
'approved_by' => $user->name, 'approved_by' => $user->name,
'amount_tier' => $financeDocument->amount_tier, 'amount_tier' => $financeDocument->amount_tier,
'requires_board_meeting' => $financeDocument->requires_board_meeting,
]); ]);
// For large amounts, notify that board meeting approval is still needed // 中額:審核完成
if ($financeDocument->requires_board_meeting && !$financeDocument->board_meeting_approved_at) { if ($financeDocument->amount_tier === FinanceDocument::AMOUNT_TIER_MEDIUM) {
Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument));
return redirect() return redirect()
->route('admin.finance.show', $financeDocument) ->route('admin.finance.show', $financeDocument)
->with('status', '理事長已審核通過。大額申請仍需理事會核准。'); ->with('status', '理事長已核准。中額申請審核完成,申請人可向出納領款。');
} }
// For medium amounts or large amounts with board approval, complete // 大額:送交董理事會
$boardMembers = User::role('finance_board_member')->get();
foreach ($boardMembers as $member) {
Mail::to($member->email)->queue(new FinanceDocumentApprovedByAccountant($financeDocument));
}
return redirect()
->route('admin.finance.show', $financeDocument)
->with('status', '理事長已核准。大額申請需送交董理事會審核。');
}
// 董理事會審核(第三階段:大額)
if ($financeDocument->canBeApprovedByBoard($user) && ($isBoardMember || $isAdmin)) {
$financeDocument->update([
'board_meeting_approved_by_id' => $user->id,
'board_meeting_approved_at' => now(),
'status' => FinanceDocument::STATUS_APPROVED_BOARD,
]);
AuditLogger::log('finance_document.approved_by_board', $financeDocument, [
'approved_by' => $user->name,
'amount_tier' => $financeDocument->amount_tier,
]);
Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument)); Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument));
return redirect() return redirect()
->route('admin.finance.show', $financeDocument) ->route('admin.finance.show', $financeDocument)
->with('status', '審核流程完成。會計可以製作付款單。'); ->with('status', '董理事會已核准。審核流程完成,申請人可向出納領款。');
} }
abort(403, 'You are not authorized to approve this document at this stage.'); abort(403, '您無權在此階段審核此文件。');
}
/**
* 出帳確認(雙重確認:申請人 + 出納)
*/
public function confirmDisbursement(Request $request, FinanceDocument $financeDocument)
{
$user = $request->user();
$isRequester = $financeDocument->submitted_by_user_id === $user->id;
$isCashier = $user->hasRole('finance_cashier');
$isAdmin = $user->hasRole('admin');
// 申請人確認
if ($isRequester && $financeDocument->canRequesterConfirmDisbursement($user)) {
$financeDocument->update([
'requester_confirmed_at' => now(),
'requester_confirmed_by_id' => $user->id,
]);
AuditLogger::log('finance_document.requester_confirmed_disbursement', $financeDocument, [
'confirmed_by' => $user->name,
]);
// 檢查是否雙重確認完成
if ($financeDocument->isDisbursementComplete()) {
$financeDocument->update(['disbursement_status' => FinanceDocument::DISBURSEMENT_COMPLETED]);
return redirect()
->route('admin.finance.show', $financeDocument)
->with('status', '出帳確認完成。等待會計入帳。');
}
return redirect()
->route('admin.finance.show', $financeDocument)
->with('status', '申請人已確認領款。等待出納確認。');
}
// 出納確認
if (($isCashier || $isAdmin) && $financeDocument->canCashierConfirmDisbursement()) {
$financeDocument->update([
'cashier_confirmed_at' => now(),
'cashier_confirmed_by_id' => $user->id,
]);
AuditLogger::log('finance_document.cashier_confirmed_disbursement', $financeDocument, [
'confirmed_by' => $user->name,
]);
// 檢查是否雙重確認完成
if ($financeDocument->isDisbursementComplete()) {
$financeDocument->update(['disbursement_status' => FinanceDocument::DISBURSEMENT_COMPLETED]);
return redirect()
->route('admin.finance.show', $financeDocument)
->with('status', '出帳確認完成。等待會計入帳。');
}
return redirect()
->route('admin.finance.show', $financeDocument)
->with('status', '出納已確認出帳。等待申請人確認。');
}
abort(403, '您無權確認此出帳。');
}
/**
* 入帳確認(會計)
*/
public function confirmRecording(Request $request, FinanceDocument $financeDocument)
{
$user = $request->user();
$isAccountant = $user->hasRole('finance_accountant');
$isAdmin = $user->hasRole('admin');
if (!$financeDocument->canAccountantConfirmRecording()) {
abort(403, '此文件尚未完成出帳確認,無法入帳。');
}
if (!$isAccountant && !$isAdmin) {
abort(403, '只有會計可以確認入帳。');
}
$financeDocument->update([
'accountant_recorded_at' => now(),
'accountant_recorded_by_id' => $user->id,
'recording_status' => FinanceDocument::RECORDING_COMPLETED,
]);
// 自動產生會計分錄
$financeDocument->autoGenerateAccountingEntries();
AuditLogger::log('finance_document.accountant_confirmed_recording', $financeDocument, [
'confirmed_by' => $user->name,
]);
return redirect()
->route('admin.finance.show', $financeDocument)
->with('status', '會計已確認入帳。財務流程完成。');
} }
public function reject(Request $request, FinanceDocument $financeDocument) public function reject(Request $request, FinanceDocument $financeDocument)
@@ -269,9 +362,12 @@ class FinanceDocumentController extends Controller
} }
// Check if user has permission to reject // Check if user has permission to reject
$canReject = $user->hasRole('finance_cashier') || $user->hasRole('cashier') || $canReject = $user->hasRole('admin') ||
$user->hasRole('finance_accountant') || $user->hasRole('accountant') || $user->hasRole('secretary_general') ||
$user->hasRole('finance_chair') || $user->hasRole('chair'); $user->hasRole('finance_cashier') ||
$user->hasRole('finance_accountant') ||
$user->hasRole('finance_chair') ||
$user->hasRole('finance_board_member');
if (!$canReject) { if (!$canReject) {
abort(403, '您無權駁回此文件。'); abort(403, '您無權駁回此文件。');
@@ -295,7 +391,7 @@ class FinanceDocumentController extends Controller
return redirect() return redirect()
->route('admin.finance.show', $financeDocument) ->route('admin.finance.show', $financeDocument)
->with('status', '財務申請單已駁回。'); ->with('status', '報銷申請單已駁回。');
} }
public function download(FinanceDocument $financeDocument) public function download(FinanceDocument $financeDocument)

View File

@@ -0,0 +1,409 @@
<?php
namespace App\Http\Controllers;
use App\Models\ChartOfAccount;
use App\Models\Income;
use App\Models\Member;
use App\Support\AuditLogger;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class IncomeController extends Controller
{
/**
* 收入列表
*/
public function index(Request $request)
{
$query = Income::query()
->with(['chartOfAccount', 'member', 'recordedByCashier', 'confirmedByAccountant'])
->orderByDesc('income_date')
->orderByDesc('id');
// 篩選狀態
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// 篩選收入類型
if ($request->filled('income_type')) {
$query->where('income_type', $request->income_type);
}
// 篩選付款方式
if ($request->filled('payment_method')) {
$query->where('payment_method', $request->payment_method);
}
// 篩選會員
if ($request->filled('member_id')) {
$query->where('member_id', $request->member_id);
}
// 篩選日期範圍
if ($request->filled('date_from')) {
$query->where('income_date', '>=', $request->date_from);
}
if ($request->filled('date_to')) {
$query->where('income_date', '<=', $request->date_to);
}
$incomes = $query->paginate(20);
// 統計資料
$statistics = [
'pending_count' => Income::pending()->count(),
'pending_amount' => Income::pending()->sum('amount'),
'confirmed_count' => Income::confirmed()->count(),
'confirmed_amount' => Income::confirmed()->sum('amount'),
];
return view('admin.incomes.index', [
'incomes' => $incomes,
'statistics' => $statistics,
]);
}
/**
* 新增收入表單
*/
public function create(Request $request)
{
// 取得收入類會計科目
$chartOfAccounts = ChartOfAccount::where('account_type', 'income')
->where('is_active', true)
->orderBy('account_code')
->get();
// 取得會員列表(可選關聯)
$members = Member::orderBy('full_name')->get();
// 預選會員
$selectedMember = null;
if ($request->filled('member_id')) {
$selectedMember = Member::find($request->member_id);
}
return view('admin.incomes.create', [
'chartOfAccounts' => $chartOfAccounts,
'members' => $members,
'selectedMember' => $selectedMember,
]);
}
/**
* 儲存收入(出納記錄)
*/
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'income_date' => ['required', 'date'],
'amount' => ['required', 'numeric', 'min:0.01'],
'income_type' => ['required', 'in:membership_fee,entrance_fee,donation,activity,grant,interest,other'],
'chart_of_account_id' => ['required', 'exists:chart_of_accounts,id'],
'payment_method' => ['required', 'in:cash,bank_transfer,check'],
'bank_account' => ['nullable', 'string', 'max:255'],
'payer_name' => ['nullable', 'string', 'max:255'],
'member_id' => ['nullable', 'exists:members,id'],
'receipt_number' => ['nullable', 'string', 'max:255'],
'transaction_reference' => ['nullable', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'notes' => ['nullable', 'string'],
'attachment' => ['nullable', 'file', 'max:10240'],
]);
// 處理附件上傳
$attachmentPath = null;
if ($request->hasFile('attachment')) {
$attachmentPath = $request->file('attachment')->store('incomes', 'local');
}
$income = Income::create([
'title' => $validated['title'],
'income_date' => $validated['income_date'],
'amount' => $validated['amount'],
'income_type' => $validated['income_type'],
'chart_of_account_id' => $validated['chart_of_account_id'],
'payment_method' => $validated['payment_method'],
'bank_account' => $validated['bank_account'] ?? null,
'payer_name' => $validated['payer_name'] ?? null,
'member_id' => $validated['member_id'] ?? null,
'receipt_number' => $validated['receipt_number'] ?? null,
'transaction_reference' => $validated['transaction_reference'] ?? null,
'description' => $validated['description'] ?? null,
'notes' => $validated['notes'] ?? null,
'attachment_path' => $attachmentPath,
'status' => Income::STATUS_PENDING,
'recorded_by_cashier_id' => $request->user()->id,
'recorded_at' => now(),
]);
AuditLogger::log('income.created', $income, $validated);
return redirect()
->route('admin.incomes.show', $income)
->with('status', '收入記錄已建立,等待會計確認。收入編號:' . $income->income_number);
}
/**
* 收入詳情
*/
public function show(Income $income)
{
$income->load([
'chartOfAccount',
'member',
'recordedByCashier',
'confirmedByAccountant',
'cashierLedgerEntry',
'accountingEntries.chartOfAccount',
]);
return view('admin.incomes.show', [
'income' => $income,
]);
}
/**
* 會計確認收入
*/
public function confirm(Request $request, Income $income)
{
$user = $request->user();
// 檢查權限
$canConfirm = $user->hasRole('admin') ||
$user->hasRole('finance_accountant');
if (!$canConfirm) {
abort(403, '您無權確認此收入。');
}
if (!$income->canBeConfirmed()) {
return redirect()
->route('admin.incomes.show', $income)
->with('error', '此收入無法確認。');
}
try {
$income->confirmByAccountant($user);
AuditLogger::log('income.confirmed', $income, [
'confirmed_by' => $user->name,
]);
return redirect()
->route('admin.incomes.show', $income)
->with('status', '收入已確認。已自動產生出納日記帳和會計分錄。');
} catch (\Exception $e) {
return redirect()
->route('admin.incomes.show', $income)
->with('error', '確認失敗:' . $e->getMessage());
}
}
/**
* 取消收入
*/
public function cancel(Request $request, Income $income)
{
$user = $request->user();
// 檢查權限
$canCancel = $user->hasRole('admin') ||
$user->hasRole('finance_accountant');
if (!$canCancel) {
abort(403, '您無權取消此收入。');
}
if (!$income->canBeCancelled()) {
return redirect()
->route('admin.incomes.show', $income)
->with('error', '此收入無法取消。');
}
$validated = $request->validate([
'cancel_reason' => ['nullable', 'string', 'max:1000'],
]);
$income->cancel();
AuditLogger::log('income.cancelled', $income, [
'cancelled_by' => $user->name,
'reason' => $validated['cancel_reason'] ?? null,
]);
return redirect()
->route('admin.incomes.show', $income)
->with('status', '收入已取消。');
}
/**
* 收入統計
*/
public function statistics(Request $request)
{
$year = $request->input('year', date('Y'));
$month = $request->input('month');
// 依收入類型統計
$byTypeQuery = Income::confirmed()
->whereYear('income_date', $year);
if ($month) {
$byTypeQuery->whereMonth('income_date', $month);
}
$byType = $byTypeQuery
->selectRaw('income_type, SUM(amount) as total_amount, COUNT(*) as count')
->groupBy('income_type')
->get();
// 依月份統計
$byMonth = Income::confirmed()
->whereYear('income_date', $year)
->selectRaw("CAST(strftime('%m', income_date) AS INTEGER) as month, SUM(amount) as total_amount, COUNT(*) as count")
->groupBy('month')
->orderBy('month')
->get();
// 依會計科目統計
$byAccountQuery = Income::confirmed()
->whereYear('income_date', $year);
if ($month) {
$byAccountQuery->whereMonth('income_date', $month);
}
$byAccountResults = $byAccountQuery
->selectRaw('chart_of_account_id, SUM(amount) as total_amount, COUNT(*) as count')
->groupBy('chart_of_account_id')
->get();
// 手動載入會計科目關聯
$accountIds = $byAccountResults->pluck('chart_of_account_id')->filter()->unique();
$accounts = \App\Models\ChartOfAccount::whereIn('id', $accountIds)->get()->keyBy('id');
$byAccount = $byAccountResults->map(function ($item) use ($accounts) {
$item->chartOfAccount = $accounts->get($item->chart_of_account_id);
return $item;
});
// 總計
$totalQuery = Income::confirmed()
->whereYear('income_date', $year);
if ($month) {
$totalQuery->whereMonth('income_date', $month);
}
$total = [
'amount' => $totalQuery->sum('amount'),
'count' => $totalQuery->count(),
];
return view('admin.incomes.statistics', [
'year' => $year,
'month' => $month,
'byType' => $byType,
'byMonth' => $byMonth,
'byAccount' => $byAccount,
'total' => $total,
]);
}
/**
* 匯出收入
*/
public function export(Request $request)
{
$query = Income::confirmed()
->with(['chartOfAccount', 'member', 'recordedByCashier'])
->orderByDesc('income_date');
// 篩選日期範圍
if ($request->filled('date_from')) {
$query->where('income_date', '>=', $request->date_from);
}
if ($request->filled('date_to')) {
$query->where('income_date', '<=', $request->date_to);
}
$incomes = $query->get();
// 產生 CSV
$filename = 'incomes_' . date('Y-m-d_His') . '.csv';
$headers = [
'Content-Type' => 'text/csv; charset=UTF-8',
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
];
$callback = function () use ($incomes) {
$file = fopen('php://output', 'w');
// BOM for Excel UTF-8
fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF));
// Header
fputcsv($file, [
'收入編號',
'日期',
'標題',
'金額',
'收入類型',
'會計科目',
'付款方式',
'付款人',
'會員',
'收據編號',
'狀態',
'記錄人',
'確認人',
]);
foreach ($incomes as $income) {
fputcsv($file, [
$income->income_number,
$income->income_date->format('Y-m-d'),
$income->title,
$income->amount,
$income->getIncomeTypeText(),
$income->chartOfAccount->account_name_zh ?? '',
$income->getPaymentMethodText(),
$income->payer_name,
$income->member->full_name ?? '',
$income->receipt_number,
$income->getStatusText(),
$income->recordedByCashier->name ?? '',
$income->confirmedByAccountant->name ?? '',
]);
}
fclose($file);
};
return response()->stream($callback, 200, $headers);
}
/**
* 下載附件
*/
public function download(Income $income)
{
if (!$income->attachment_path) {
abort(404, '找不到附件。');
}
$path = storage_path('app/' . $income->attachment_path);
if (!file_exists($path)) {
abort(404, '附件檔案不存在。');
}
return response()->download($path);
}
}

View File

@@ -196,7 +196,7 @@ class IssueController extends Controller
public function edit(Issue $issue) public function edit(Issue $issue)
{ {
if ($issue->isClosed() && !Auth::user()->is_admin) { if ($issue->isClosed() && !Auth::user()->hasRole('admin')) {
return redirect()->route('admin.issues.show', $issue) return redirect()->route('admin.issues.show', $issue)
->with('error', __('Cannot edit closed issues.')); ->with('error', __('Cannot edit closed issues.'));
} }
@@ -211,7 +211,7 @@ class IssueController extends Controller
public function update(Request $request, Issue $issue) public function update(Request $request, Issue $issue)
{ {
if ($issue->isClosed() && !Auth::user()->is_admin) { if ($issue->isClosed() && !Auth::user()->hasRole('admin')) {
return redirect()->route('admin.issues.show', $issue) return redirect()->route('admin.issues.show', $issue)
->with('error', __('Cannot edit closed issues.')); ->with('error', __('Cannot edit closed issues.'));
} }
@@ -262,7 +262,7 @@ class IssueController extends Controller
public function destroy(Issue $issue) public function destroy(Issue $issue)
{ {
if (!Auth::user()->is_admin) { if (!Auth::user()->hasRole('admin')) {
abort(403, 'Only administrators can delete issues.'); abort(403, 'Only administrators can delete issues.');
} }

View File

@@ -63,7 +63,7 @@ class IssueLabelController extends Controller
public function destroy(IssueLabel $issueLabel) public function destroy(IssueLabel $issueLabel)
{ {
if (!Auth::user()->is_admin) { if (!Auth::user()->hasRole('admin')) {
abort(403, 'Only administrators can delete labels.'); abort(403, 'Only administrators can delete labels.');
} }

View File

@@ -12,8 +12,8 @@ class IssueReportsController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
// Date range filter (default: last 30 days) // Date range filter (default: last 30 days)
$startDate = $request->date('start_date', now()->subDays(30)); $startDate = $request->date('start_date') ?? now()->subDays(30);
$endDate = $request->date('end_date', now()); $endDate = $request->date('end_date') ?? now();
// Overview Statistics // Overview Statistics
$stats = [ $stats = [
@@ -63,11 +63,12 @@ class IssueReportsController extends Controller
->get(); ->get();
// Assignee Performance // Assignee Performance
$now = now();
$assigneePerformance = User::select('users.id', 'users.name') $assigneePerformance = User::select('users.id', 'users.name')
->leftJoin('issues', 'users.id', '=', 'issues.assigned_to_user_id') ->leftJoin('issues', 'users.id', '=', 'issues.assigned_to_user_id')
->selectRaw('count(issues.id) as total_assigned') ->selectRaw('count(issues.id) as total_assigned')
->selectRaw('sum(case when issues.status = ? then 1 else 0 end) as completed', [Issue::STATUS_CLOSED]) ->selectRaw('sum(case when issues.status = ? then 1 else 0 end) as completed', [Issue::STATUS_CLOSED])
->selectRaw('sum(case when issues.due_date < NOW() and issues.status != ? then 1 else 0 end) as overdue', [Issue::STATUS_CLOSED]) ->selectRaw('sum(case when issues.due_date < ? and issues.status != ? then 1 else 0 end) as overdue', [$now, Issue::STATUS_CLOSED])
->groupBy('users.id', 'users.name') ->groupBy('users.id', 'users.name')
->having('total_assigned', '>', 0) ->having('total_assigned', '>', 0)
->orderByDesc('total_assigned') ->orderByDesc('total_assigned')
@@ -101,9 +102,15 @@ class IssueReportsController extends Controller
->get(); ->get();
// Average Resolution Time (days) // Average Resolution Time (days)
$avgResolutionTime = Issue::whereNotNull('closed_at') $closedIssues = Issue::whereNotNull('closed_at')
->selectRaw('avg(TIMESTAMPDIFF(DAY, created_at, closed_at)) as avg_days') ->select('created_at', 'closed_at')
->value('avg_days'); ->get();
$avgResolutionTime = $closedIssues->isNotEmpty()
? $closedIssues->avg(function ($issue) {
return $issue->created_at->diffInDays($issue->closed_at);
})
: null;
// Recent Activity (last 10 issues) // Recent Activity (last 10 issues)
$recentIssues = Issue::with(['creator', 'assignee']) $recentIssues = Issue::with(['creator', 'assignee'])

View File

@@ -2,7 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Member;
use App\Support\AuditLogger;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class MemberDashboardController extends Controller class MemberDashboardController extends Controller
{ {
@@ -12,7 +15,7 @@ class MemberDashboardController extends Controller
$member = $user->member; $member = $user->member;
if (! $member) { if (! $member) {
abort(404); return redirect()->route('member.profile.create');
} }
$member->load([ $member->load([
@@ -31,5 +34,58 @@ class MemberDashboardController extends Controller
'pendingPayment' => $pendingPayment, 'pendingPayment' => $pendingPayment,
]); ]);
} }
}
public function createProfile()
{
$user = Auth::user();
if ($user->member) {
return redirect()->route('member.dashboard');
}
return view('member.create-profile');
}
public function storeProfile(Request $request)
{
$user = Auth::user();
if ($user->member) {
return redirect()->route('member.dashboard');
}
$validated = $request->validate([
'full_name' => ['required', 'string', 'max:255'],
'phone' => ['nullable', 'string', 'max:20'],
'national_id' => ['nullable', 'string', 'max:20'],
'address_line_1' => ['nullable', 'string', 'max:255'],
'address_line_2' => ['nullable', 'string', 'max:255'],
'city' => ['nullable', 'string', 'max:100'],
'postal_code' => ['nullable', 'string', 'max:10'],
'emergency_contact_name' => ['nullable', 'string', 'max:255'],
'emergency_contact_phone' => ['nullable', 'string', 'max:20'],
'terms_accepted' => ['required', 'accepted'],
]);
$member = Member::create([
'user_id' => $user->id,
'full_name' => $validated['full_name'],
'email' => $user->email,
'phone' => $validated['phone'] ?? null,
'national_id' => $validated['national_id'] ?? null,
'address_line_1' => $validated['address_line_1'] ?? null,
'address_line_2' => $validated['address_line_2'] ?? null,
'city' => $validated['city'] ?? null,
'postal_code' => $validated['postal_code'] ?? null,
'emergency_contact_name' => $validated['emergency_contact_name'] ?? null,
'emergency_contact_phone' => $validated['emergency_contact_phone'] ?? null,
'membership_status' => Member::STATUS_PENDING,
'membership_type' => Member::TYPE_REGULAR,
]);
AuditLogger::log('member.created_profile', $member, [
'user_id' => $user->id,
'name' => $member->full_name,
]);
return redirect()->route('member.dashboard')
->with('status', __('Profile completed! Please submit your membership payment.'));
}
}

View File

@@ -6,6 +6,7 @@ use App\Mail\PaymentSubmittedMail;
use App\Models\Member; use App\Models\Member;
use App\Models\MembershipPayment; use App\Models\MembershipPayment;
use App\Models\User; use App\Models\User;
use App\Services\MembershipFeeCalculator;
use App\Support\AuditLogger; use App\Support\AuditLogger;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@@ -14,6 +15,13 @@ use Illuminate\Validation\Rule;
class MemberPaymentController extends Controller class MemberPaymentController extends Controller
{ {
protected MembershipFeeCalculator $feeCalculator;
public function __construct(MembershipFeeCalculator $feeCalculator)
{
$this->feeCalculator = $feeCalculator;
}
/** /**
* Show payment submission form * Show payment submission form
*/ */
@@ -32,7 +40,10 @@ class MemberPaymentController extends Controller
->with('error', __('You cannot submit payment at this time. You may already have a pending payment or your membership is already active.')); ->with('error', __('You cannot submit payment at this time. You may already have a pending payment or your membership is already active.'));
} }
return view('member.submit-payment', compact('member')); // Calculate fee details
$feeDetails = $this->feeCalculator->calculateNextFee($member);
return view('member.submit-payment', compact('member', 'feeDetails'));
} }
/** /**
@@ -47,8 +58,11 @@ class MemberPaymentController extends Controller
->with('error', __('You cannot submit payment at this time.')); ->with('error', __('You cannot submit payment at this time.'));
} }
// Calculate fee details
$feeDetails = $this->feeCalculator->calculateNextFee($member);
$validated = $request->validate([ $validated = $request->validate([
'amount' => ['required', 'numeric', 'min:0'], 'amount' => ['required', 'numeric', 'min:' . $feeDetails['final_amount']],
'paid_at' => ['required', 'date', 'before_or_equal:today'], 'paid_at' => ['required', 'date', 'before_or_equal:today'],
'payment_method' => ['required', Rule::in([ 'payment_method' => ['required', Rule::in([
MembershipPayment::METHOD_BANK_TRANSFER, MembershipPayment::METHOD_BANK_TRANSFER,
@@ -65,10 +79,15 @@ class MemberPaymentController extends Controller
$receiptFile = $request->file('receipt'); $receiptFile = $request->file('receipt');
$receiptPath = $receiptFile->store('payment-receipts', 'private'); $receiptPath = $receiptFile->store('payment-receipts', 'private');
// Create payment record // Create payment record with fee details
$payment = MembershipPayment::create([ $payment = MembershipPayment::create([
'member_id' => $member->id, 'member_id' => $member->id,
'fee_type' => $feeDetails['fee_type'],
'amount' => $validated['amount'], 'amount' => $validated['amount'],
'base_amount' => $feeDetails['base_amount'],
'discount_amount' => $feeDetails['discount_amount'],
'final_amount' => $feeDetails['final_amount'],
'disability_discount' => $feeDetails['disability_discount'],
'paid_at' => $validated['paid_at'], 'paid_at' => $validated['paid_at'],
'payment_method' => $validated['payment_method'], 'payment_method' => $validated['payment_method'],
'reference' => $validated['reference'] ?? null, 'reference' => $validated['reference'] ?? null,

View File

@@ -59,14 +59,14 @@ class PaymentOrderController extends Controller
if (!$financeDocument->canCreatePaymentOrder()) { if (!$financeDocument->canCreatePaymentOrder()) {
return redirect() return redirect()
->route('admin.finance.show', $financeDocument) ->route('admin.finance.show', $financeDocument)
->with('error', '此財務申請單尚未完成審核流程,無法製作付款單。'); ->with('error', '此報銷申請單尚未完成審核流程,無法製作付款單。');
} }
// Check if payment order already exists // Check if payment order already exists
if ($financeDocument->paymentOrder !== null) { if ($financeDocument->paymentOrder !== null) {
return redirect() return redirect()
->route('admin.payment-orders.show', $financeDocument->paymentOrder) ->route('admin.payment-orders.show', $financeDocument->paymentOrder)
->with('error', '此財務申請單已有付款單。'); ->with('error', '此報銷申請單已有付款單。');
} }
$financeDocument->load(['member', 'submittedBy']); $financeDocument->load(['member', 'submittedBy']);
@@ -98,7 +98,7 @@ class PaymentOrderController extends Controller
} }
return redirect() return redirect()
->route('admin.finance.show', $financeDocument) ->route('admin.finance.show', $financeDocument)
->with('error', '此財務申請單尚未完成審核流程,無法製作付款單。'); ->with('error', '此報銷申請單尚未完成審核流程,無法製作付款單。');
} }
$validated = $request->validate([ $validated = $request->validate([

View File

@@ -88,8 +88,20 @@ class PaymentVerificationController extends Controller
$validated = $request->validate([ $validated = $request->validate([
'notes' => ['nullable', 'string', 'max:1000'], 'notes' => ['nullable', 'string', 'max:1000'],
'disability_action' => ['nullable', 'in:approve,reject'],
'disability_rejection_reason' => ['required_if:disability_action,reject', 'nullable', 'string', 'max:500'],
]); ]);
// Handle disability certificate verification if applicable
$member = $payment->member;
if ($member && $member->hasDisabilityCertificate() && $member->isDisabilityPending()) {
if ($validated['disability_action'] === 'approve') {
$member->approveDisabilityCertificate(Auth::user());
} elseif ($validated['disability_action'] === 'reject') {
$member->rejectDisabilityCertificate(Auth::user(), $validated['disability_rejection_reason']);
}
}
$payment->update([ $payment->update([
'status' => MembershipPayment::STATUS_APPROVED_CASHIER, 'status' => MembershipPayment::STATUS_APPROVED_CASHIER,
'verified_by_cashier_id' => Auth::id(), 'verified_by_cashier_id' => Auth::id(),

View File

@@ -97,4 +97,57 @@ class ProfileController extends Controller
return Redirect::to('/'); return Redirect::to('/');
} }
/**
* Upload disability certificate.
*/
public function uploadDisabilityCertificate(Request $request): RedirectResponse
{
$request->validate([
'disability_certificate' => 'required|file|mimes:jpg,jpeg,png,pdf|max:10240',
]);
$member = $request->user()->member;
if (!$member) {
return Redirect::route('profile.edit')->with('error', '請先建立會員資料');
}
// Delete old certificate if exists
if ($member->disability_certificate_path) {
Storage::disk('private')->delete($member->disability_certificate_path);
}
// Upload new certificate
$path = $request->file('disability_certificate')->store('disability-certificates', 'private');
// Update member record
$member->update([
'disability_certificate_path' => $path,
'disability_certificate_status' => Member::DISABILITY_STATUS_PENDING,
'disability_verified_by' => null,
'disability_verified_at' => null,
'disability_rejection_reason' => null,
]);
return Redirect::route('profile.edit')->with('status', 'disability-certificate-uploaded');
}
/**
* View disability certificate.
*/
public function viewDisabilityCertificate(Request $request)
{
$member = $request->user()->member;
if (!$member || !$member->disability_certificate_path) {
abort(404, '找不到身心障礙手冊');
}
if (!Storage::disk('private')->exists($member->disability_certificate_path)) {
abort(404, '檔案不存在');
}
return Storage::disk('private')->response($member->disability_certificate_path);
}
} }

View File

@@ -22,7 +22,7 @@ class PublicDocumentController extends Controller
if (!$user) { if (!$user) {
// Only public documents for guests // Only public documents for guests
$query->where('access_level', 'public'); $query->where('access_level', 'public');
} elseif (!$user->is_admin && !$user->hasRole('admin')) { } elseif (!$user->hasRole('admin')) {
// Members can see public + members-only // Members can see public + members-only
$query->whereIn('access_level', ['public', 'members']); $query->whereIn('access_level', ['public', 'members']);
} }
@@ -49,7 +49,7 @@ class PublicDocumentController extends Controller
'activeDocuments' => function($query) use ($user) { 'activeDocuments' => function($query) use ($user) {
if (!$user) { if (!$user) {
$query->where('access_level', 'public'); $query->where('access_level', 'public');
} elseif (!$user->is_admin && !$user->hasRole('admin')) { } elseif (!$user->hasRole('admin')) {
$query->whereIn('access_level', ['public', 'members']); $query->whereIn('access_level', ['public', 'members']);
} }
} }

View File

@@ -17,7 +17,7 @@ class EnsureUserIsAdmin
} }
// Allow access for admins or any user with explicit permissions (e.g. finance/cashier roles) // Allow access for admins or any user with explicit permissions (e.g. finance/cashier roles)
if (! $user->is_admin && ! $user->hasRole('admin') && $user->getAllPermissions()->isEmpty()) { if (! $user->hasRole('admin') && $user->getAllPermissions()->isEmpty()) {
abort(403); abort(403);
} }

View File

@@ -12,7 +12,8 @@ class TrustProxies extends Middleware
* *
* @var array<int, string>|string|null * @var array<int, string>|string|null
*/ */
protected $proxies; // Trust all proxies (Traefik handles TLS and forwards client info)
protected $proxies = '*';
/** /**
* The headers that should be used to detect proxies. * The headers that should be used to detect proxies.

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AccountingEntry extends Model
{
use HasFactory;
const ENTRY_TYPE_DEBIT = 'debit';
const ENTRY_TYPE_CREDIT = 'credit';
protected $fillable = [
'finance_document_id',
'income_id',
'chart_of_account_id',
'entry_type',
'amount',
'entry_date',
'description',
];
protected $casts = [
'entry_date' => 'date',
'amount' => 'decimal:2',
];
/**
* Get the finance document that owns this entry
*/
public function financeDocument()
{
return $this->belongsTo(FinanceDocument::class);
}
/**
* Get the income that owns this entry
*/
public function income()
{
return $this->belongsTo(Income::class);
}
/**
* Get the chart of account for this entry
*/
public function chartOfAccount()
{
return $this->belongsTo(ChartOfAccount::class);
}
/**
* Check if this is a debit entry
*/
public function isDebit(): bool
{
return $this->entry_type === self::ENTRY_TYPE_DEBIT;
}
/**
* Check if this is a credit entry
*/
public function isCredit(): bool
{
return $this->entry_type === self::ENTRY_TYPE_CREDIT;
}
/**
* Scope to filter debit entries
*/
public function scopeDebits($query)
{
return $query->where('entry_type', self::ENTRY_TYPE_DEBIT);
}
/**
* Scope to filter credit entries
*/
public function scopeCredits($query)
{
return $query->where('entry_type', self::ENTRY_TYPE_CREDIT);
}
/**
* Scope to filter by account
*/
public function scopeForAccount($query, $accountId)
{
return $query->where('chart_of_account_id', $accountId);
}
/**
* Scope to filter by date range
*/
public function scopeDateRange($query, $startDate, $endDate)
{
return $query->whereBetween('entry_date', [$startDate, $endDate]);
}
}

427
app/Models/Announcement.php Normal file
View File

@@ -0,0 +1,427 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
class Announcement extends Model
{
use HasFactory, SoftDeletes;
// ==================== Constants ====================
const STATUS_DRAFT = 'draft';
const STATUS_PUBLISHED = 'published';
const STATUS_ARCHIVED = 'archived';
const ACCESS_LEVEL_PUBLIC = 'public';
const ACCESS_LEVEL_MEMBERS = 'members';
const ACCESS_LEVEL_BOARD = 'board';
const ACCESS_LEVEL_ADMIN = 'admin';
// ==================== Configuration ====================
protected $fillable = [
'title',
'content',
'status',
'is_pinned',
'display_order',
'access_level',
'published_at',
'expires_at',
'archived_at',
'view_count',
'created_by_user_id',
'last_updated_by_user_id',
];
protected $casts = [
'is_pinned' => 'boolean',
'display_order' => 'integer',
'view_count' => 'integer',
'published_at' => 'datetime',
'expires_at' => 'datetime',
'archived_at' => 'datetime',
];
// ==================== Relationships ====================
/**
* Get the user who created this announcement
*/
public function creator()
{
return $this->belongsTo(User::class, 'created_by_user_id');
}
/**
* Get the user who last updated this announcement
*/
public function lastUpdatedBy()
{
return $this->belongsTo(User::class, 'last_updated_by_user_id');
}
// ==================== Status Check Methods ====================
/**
* Check if announcement is draft
*/
public function isDraft(): bool
{
return $this->status === self::STATUS_DRAFT;
}
/**
* Check if announcement is published
*/
public function isPublished(): bool
{
return $this->status === self::STATUS_PUBLISHED;
}
/**
* Check if announcement is archived
*/
public function isArchived(): bool
{
return $this->status === self::STATUS_ARCHIVED;
}
/**
* Check if announcement is pinned
*/
public function isPinned(): bool
{
return $this->is_pinned;
}
/**
* Check if announcement is expired
*/
public function isExpired(): bool
{
if (!$this->expires_at) {
return false;
}
return $this->expires_at->isPast();
}
/**
* Check if announcement is scheduled (published_at is in the future)
*/
public function isScheduled(): bool
{
if (!$this->published_at) {
return false;
}
return $this->published_at->isFuture();
}
/**
* Check if announcement is currently active
*/
public function isActive(): bool
{
return $this->isPublished()
&& !$this->isExpired()
&& (!$this->published_at || $this->published_at->isPast());
}
// ==================== Access Control Methods ====================
/**
* Check if a user can view this announcement
*/
public function canBeViewedBy(?User $user): bool
{
// Draft announcements - only creator and admins can view
if ($this->isDraft()) {
if (!$user) {
return false;
}
return $user->id === $this->created_by_user_id
|| $user->hasRole('admin')
|| $user->can('manage_all_announcements');
}
// Archived announcements - only admins can view
if ($this->isArchived()) {
if (!$user) {
return false;
}
return $user->hasRole('admin') || $user->can('manage_all_announcements');
}
// Expired announcements - hidden from regular users
if ($this->isExpired()) {
if (!$user) {
return false;
}
return $user->hasRole('admin') || $user->can('manage_all_announcements');
}
// Scheduled announcements - not yet visible
if ($this->isScheduled()) {
if (!$user) {
return false;
}
return $user->id === $this->created_by_user_id
|| $user->hasRole('admin')
|| $user->can('manage_all_announcements');
}
// Check access level for published announcements
if ($this->access_level === self::ACCESS_LEVEL_PUBLIC) {
return true;
}
if (!$user) {
return false;
}
if ($user->hasRole('admin')) {
return true;
}
if ($this->access_level === self::ACCESS_LEVEL_MEMBERS) {
return $user->member && $user->member->hasPaidMembership();
}
if ($this->access_level === self::ACCESS_LEVEL_BOARD) {
return $user->hasRole(['admin', 'finance_chair', 'finance_board_member']);
}
if ($this->access_level === self::ACCESS_LEVEL_ADMIN) {
return $user->hasRole('admin');
}
return false;
}
/**
* Check if a user can edit this announcement
*/
public function canBeEditedBy(User $user): bool
{
// Admin and users with manage_all_announcements can edit all
if ($user->hasRole('admin') || $user->can('manage_all_announcements')) {
return true;
}
// User must have edit_announcements permission
if (!$user->can('edit_announcements')) {
return false;
}
// Can only edit own announcements
return $user->id === $this->created_by_user_id;
}
// ==================== Query Scopes ====================
/**
* Scope to only published announcements
*/
public function scopePublished(Builder $query): Builder
{
return $query->where('status', self::STATUS_PUBLISHED);
}
/**
* Scope to only draft announcements
*/
public function scopeDraft(Builder $query): Builder
{
return $query->where('status', self::STATUS_DRAFT);
}
/**
* Scope to only archived announcements
*/
public function scopeArchived(Builder $query): Builder
{
return $query->where('status', self::STATUS_ARCHIVED);
}
/**
* Scope to only active announcements (published, not expired, not scheduled)
*/
public function scopeActive(Builder $query): Builder
{
return $query->where('status', self::STATUS_PUBLISHED)
->where(function ($q) {
$q->whereNull('published_at')
->orWhere('published_at', '<=', now());
})
->where(function ($q) {
$q->whereNull('expires_at')
->orWhere('expires_at', '>', now());
});
}
/**
* Scope to only pinned announcements
*/
public function scopePinned(Builder $query): Builder
{
return $query->where('is_pinned', true);
}
/**
* Scope to filter by access level
*/
public function scopeForAccessLevel(Builder $query, User $user): Builder
{
if ($user->hasRole('admin')) {
return $query;
}
$accessLevels = [self::ACCESS_LEVEL_PUBLIC];
if ($user->member && $user->member->hasPaidMembership()) {
$accessLevels[] = self::ACCESS_LEVEL_MEMBERS;
}
if ($user->hasRole(['finance_chair', 'finance_board_member'])) {
$accessLevels[] = self::ACCESS_LEVEL_BOARD;
}
return $query->whereIn('access_level', $accessLevels);
}
// ==================== Helper Methods ====================
/**
* Publish this announcement
*/
public function publish(?User $user = null): void
{
$updates = [
'status' => self::STATUS_PUBLISHED,
];
if (!$this->published_at) {
$updates['published_at'] = now();
}
if ($user) {
$updates['last_updated_by_user_id'] = $user->id;
}
$this->update($updates);
}
/**
* Archive this announcement
*/
public function archive(?User $user = null): void
{
$updates = [
'status' => self::STATUS_ARCHIVED,
'archived_at' => now(),
];
if ($user) {
$updates['last_updated_by_user_id'] = $user->id;
}
$this->update($updates);
}
/**
* Pin this announcement
*/
public function pin(?int $order = null, ?User $user = null): void
{
$updates = [
'is_pinned' => true,
'display_order' => $order ?? 0,
];
if ($user) {
$updates['last_updated_by_user_id'] = $user->id;
}
$this->update($updates);
}
/**
* Unpin this announcement
*/
public function unpin(?User $user = null): void
{
$updates = [
'is_pinned' => false,
'display_order' => 0,
];
if ($user) {
$updates['last_updated_by_user_id'] = $user->id;
}
$this->update($updates);
}
/**
* Increment view count
*/
public function incrementViewCount(): void
{
$this->increment('view_count');
}
/**
* Get the access level label in Chinese
*/
public function getAccessLevelLabel(): string
{
return match($this->access_level) {
self::ACCESS_LEVEL_PUBLIC => '公開',
self::ACCESS_LEVEL_MEMBERS => '會員',
self::ACCESS_LEVEL_BOARD => '理事會',
self::ACCESS_LEVEL_ADMIN => '管理員',
default => '未知',
};
}
/**
* Get status label in Chinese
*/
public function getStatusLabel(): string
{
return match($this->status) {
self::STATUS_DRAFT => '草稿',
self::STATUS_PUBLISHED => '已發布',
self::STATUS_ARCHIVED => '已歸檔',
default => '未知',
};
}
/**
* Get status badge color
*/
public function getStatusBadgeColor(): string
{
return match($this->status) {
self::STATUS_DRAFT => 'gray',
self::STATUS_PUBLISHED => 'green',
self::STATUS_ARCHIVED => 'yellow',
default => 'gray',
};
}
/**
* Get content excerpt (first 150 characters)
*/
public function getExcerpt(int $length = 150): string
{
return \Illuminate\Support\Str::limit($this->content, $length);
}
}

View File

@@ -12,9 +12,11 @@ class AuditLog extends Model
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',
'action', 'action',
'description',
'auditable_type', 'auditable_type',
'auditable_id', 'auditable_id',
'metadata', 'metadata',
'ip_address',
]; ];
protected $casts = [ protected $casts = [

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class BoardMeeting extends Model
{
use HasFactory;
protected $fillable = [
'meeting_date',
'title',
'notes',
'status',
];
protected $casts = [
'meeting_date' => 'date',
];
/**
* Get the finance documents approved by this board meeting.
*/
public function approvedFinanceDocuments(): HasMany
{
return $this->hasMany(FinanceDocument::class, 'approved_by_board_meeting_id');
}
}

View File

@@ -45,7 +45,7 @@ class CashierLedgerEntry extends Model
const PAYMENT_METHOD_CASH = 'cash'; const PAYMENT_METHOD_CASH = 'cash';
/** /**
* 關聯到財務申請單 * 關聯到報銷申請單
*/ */
public function financeDocument(): BelongsTo public function financeDocument(): BelongsTo
{ {

View File

@@ -178,7 +178,7 @@ class Document extends Model
'original_filename' => $originalFilename, 'original_filename' => $originalFilename,
'mime_type' => $mimeType, 'mime_type' => $mimeType,
'file_size' => $fileSize, 'file_size' => $fileSize,
'file_hash' => hash_file('sha256', storage_path('app/' . $filePath)), 'file_hash' => hash_file('sha256', \Illuminate\Support\Facades\Storage::disk('private')->path($filePath)),
'uploaded_by_user_id' => $uploadedBy->id, 'uploaded_by_user_id' => $uploadedBy->id,
'uploaded_at' => now(), 'uploaded_at' => now(),
]); ]);
@@ -265,24 +265,44 @@ class Document extends Model
*/ */
public function canBeViewedBy(?User $user): bool public function canBeViewedBy(?User $user): bool
{ {
// 公開文件:任何人可看
if ($this->isPublic()) { if ($this->isPublic()) {
return true; return true;
} }
// 非公開文件需要登入
if (!$user) { if (!$user) {
return false; return false;
} }
if ($user->is_admin || $user->hasRole('admin')) { // 有文件管理權限者可看所有文件
if ($user->can('manage_documents')) {
return true; return true;
} }
// 會員等級:已繳費會員可看
if ($this->access_level === 'members') { if ($this->access_level === 'members') {
return $user->member && $user->member->hasPaidMembership(); return $user->member && $user->member->hasPaidMembership();
} }
// 管理員等級:有任何管理權限者可看
if ($this->access_level === 'admin') {
return $user->hasAnyPermission([
'manage_documents',
'manage_members',
'manage_finance',
'manage_system_settings',
]);
}
// 理事會等級:有理事會相關權限者可看
if ($this->access_level === 'board') { if ($this->access_level === 'board') {
return $user->hasRole(['admin', 'chair', 'board']); return $user->hasAnyPermission([
'manage_documents',
'approve_finance_documents',
'verify_payments_chair',
'activate_memberships',
]);
} }
return false; return false;

View File

@@ -11,18 +11,26 @@ class FinanceDocument extends Model
{ {
use HasFactory; use HasFactory;
// Status constants // Status constants (審核階段)
public const STATUS_PENDING = 'pending'; public const STATUS_PENDING = 'pending'; // 待審核
public const STATUS_APPROVED_SECRETARY = 'approved_secretary'; // 秘書長已核准
public const STATUS_APPROVED_CHAIR = 'approved_chair'; // 理事長已核准
public const STATUS_APPROVED_BOARD = 'approved_board'; // 董理事會已核准
public const STATUS_REJECTED = 'rejected'; // 已駁回
// Legacy status constants (保留向後相容)
public const STATUS_APPROVED_CASHIER = 'approved_cashier'; public const STATUS_APPROVED_CASHIER = 'approved_cashier';
public const STATUS_APPROVED_ACCOUNTANT = 'approved_accountant'; public const STATUS_APPROVED_ACCOUNTANT = 'approved_accountant';
public const STATUS_APPROVED_CHAIR = 'approved_chair';
public const STATUS_REJECTED = 'rejected';
// Request type constants // Disbursement status constants (出帳階段)
public const REQUEST_TYPE_EXPENSE_REIMBURSEMENT = 'expense_reimbursement'; public const DISBURSEMENT_PENDING = 'pending'; // 待出帳
public const REQUEST_TYPE_ADVANCE_PAYMENT = 'advance_payment'; public const DISBURSEMENT_REQUESTER_CONFIRMED = 'requester_confirmed'; // 申請人已確認
public const REQUEST_TYPE_PURCHASE_REQUEST = 'purchase_request'; public const DISBURSEMENT_CASHIER_CONFIRMED = 'cashier_confirmed'; // 出納已確認
public const REQUEST_TYPE_PETTY_CASH = 'petty_cash'; public const DISBURSEMENT_COMPLETED = 'completed'; // 已出帳
// Recording status constants (入帳階段)
public const RECORDING_PENDING = 'pending'; // 待入帳
public const RECORDING_COMPLETED = 'completed'; // 已入帳
// Amount tier constants // Amount tier constants
public const AMOUNT_TIER_SMALL = 'small'; // < 5,000 public const AMOUNT_TIER_SMALL = 'small'; // < 5,000
@@ -63,7 +71,6 @@ class FinanceDocument extends Model
'rejected_at', 'rejected_at',
'rejection_reason', 'rejection_reason',
// New payment stage fields // New payment stage fields
'request_type',
'amount_tier', 'amount_tier',
'chart_of_account_id', 'chart_of_account_id',
'budget_item_id', 'budget_item_id',
@@ -89,6 +96,17 @@ class FinanceDocument extends Model
'bank_reconciliation_id', 'bank_reconciliation_id',
'reconciliation_status', 'reconciliation_status',
'reconciled_at', 'reconciled_at',
// 新工作流程欄位
'approved_by_secretary_id',
'secretary_approved_at',
'disbursement_status',
'requester_confirmed_at',
'requester_confirmed_by_id',
'cashier_confirmed_at',
'cashier_confirmed_by_id',
'recording_status',
'accountant_recorded_at',
'accountant_recorded_by_id',
]; ];
protected $casts = [ protected $casts = [
@@ -106,6 +124,11 @@ class FinanceDocument extends Model
'payment_executed_at' => 'datetime', 'payment_executed_at' => 'datetime',
'actual_payment_amount' => 'decimal:2', 'actual_payment_amount' => 'decimal:2',
'reconciled_at' => 'datetime', 'reconciled_at' => 'datetime',
// 新工作流程欄位
'secretary_approved_at' => 'datetime',
'requester_confirmed_at' => 'datetime',
'cashier_confirmed_at' => 'datetime',
'accountant_recorded_at' => 'datetime',
]; ];
public function member() public function member()
@@ -138,6 +161,29 @@ class FinanceDocument extends Model
return $this->belongsTo(User::class, 'rejected_by_user_id'); return $this->belongsTo(User::class, 'rejected_by_user_id');
} }
/**
* 新工作流程 Relationships
*/
public function approvedBySecretary(): BelongsTo
{
return $this->belongsTo(User::class, 'approved_by_secretary_id');
}
public function requesterConfirmedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'requester_confirmed_by_id');
}
public function cashierConfirmedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'cashier_confirmed_by_id');
}
public function accountantRecordedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'accountant_recorded_by_id');
}
/** /**
* New payment stage relationships * New payment stage relationships
*/ */
@@ -187,9 +233,140 @@ class FinanceDocument extends Model
} }
/** /**
* Check if document can be approved by cashier * Get all accounting entries for this document
*/ */
public function canBeApprovedByCashier(?User $user = null): bool public function accountingEntries()
{
return $this->hasMany(AccountingEntry::class);
}
/**
* Get debit entries for this document
*/
public function debitEntries()
{
return $this->accountingEntries()->where('entry_type', AccountingEntry::ENTRY_TYPE_DEBIT);
}
/**
* Get credit entries for this document
*/
public function creditEntries()
{
return $this->accountingEntries()->where('entry_type', AccountingEntry::ENTRY_TYPE_CREDIT);
}
/**
* Validate that debit and credit entries balance
*/
public function validateBalance(): bool
{
$debitTotal = $this->debitEntries()->sum('amount');
$creditTotal = $this->creditEntries()->sum('amount');
return bccomp((string)$debitTotal, (string)$creditTotal, 2) === 0;
}
/**
* Generate accounting entries for this document
* This creates the double-entry bookkeeping records
*/
public function generateAccountingEntries(array $entries): void
{
// Delete existing entries
$this->accountingEntries()->delete();
// Create new entries
foreach ($entries as $entry) {
$this->accountingEntries()->create([
'chart_of_account_id' => $entry['chart_of_account_id'],
'entry_type' => $entry['entry_type'],
'amount' => $entry['amount'],
'entry_date' => $entry['entry_date'] ?? $this->submitted_at ?? now(),
'description' => $entry['description'] ?? $this->description,
]);
}
}
/**
* Auto-generate simple accounting entries based on document type
* For basic income/expense transactions
*/
public function autoGenerateAccountingEntries(): void
{
// Only auto-generate if chart_of_account_id is set
if (!$this->chart_of_account_id) {
return;
}
$entries = [];
$entryDate = $this->submitted_at ?? now();
// Determine if this is income or expense based on request type or account type
$account = $this->chartOfAccount;
if (!$account) {
return;
}
if ($account->account_type === 'income') {
// Income: Debit Cash, Credit Income Account
$entries[] = [
'chart_of_account_id' => $this->getCashAccountId(),
'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT,
'amount' => $this->amount,
'entry_date' => $entryDate,
'description' => '收入 - ' . ($this->description ?? $this->title),
];
$entries[] = [
'chart_of_account_id' => $this->chart_of_account_id,
'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT,
'amount' => $this->amount,
'entry_date' => $entryDate,
'description' => $this->description ?? $this->title,
];
} elseif ($account->account_type === 'expense') {
// Expense: Debit Expense Account, Credit Cash
$entries[] = [
'chart_of_account_id' => $this->chart_of_account_id,
'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT,
'amount' => $this->amount,
'entry_date' => $entryDate,
'description' => $this->description ?? $this->title,
];
$entries[] = [
'chart_of_account_id' => $this->getCashAccountId(),
'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT,
'amount' => $this->amount,
'entry_date' => $entryDate,
'description' => '支出 - ' . ($this->description ?? $this->title),
];
}
if (!empty($entries)) {
$this->generateAccountingEntries($entries);
}
}
/**
* Get the cash account ID (1101 - 現金)
*/
protected function getCashAccountId(): int
{
static $cashAccountId = null;
if ($cashAccountId === null) {
$cashAccount = ChartOfAccount::where('account_code', '1101')->first();
$cashAccountId = $cashAccount ? $cashAccount->id : 1;
}
return $cashAccountId;
}
/**
* 新工作流程:秘書長可審核
* 條件:待審核狀態 + 不能審核自己的申請
*/
public function canBeApprovedBySecretary(?User $user = null): bool
{ {
if ($this->status !== self::STATUS_PENDING) { if ($this->status !== self::STATUS_PENDING) {
return false; return false;
@@ -203,27 +380,164 @@ class FinanceDocument extends Model
} }
/** /**
* Check if document can be approved by accountant * 新工作流程:理事長可審核
* 條件:秘書長已核准 + 中額或大額
*/ */
public function canBeApprovedByAccountant(): bool public function canBeApprovedByChair(?User $user = null): bool
{ {
return $this->status === self::STATUS_APPROVED_CASHIER; $tier = $this->amount_tier ?? $this->determineAmountTier();
if ($this->status !== self::STATUS_APPROVED_SECRETARY) {
return false;
}
if (!in_array($tier, [self::AMOUNT_TIER_MEDIUM, self::AMOUNT_TIER_LARGE])) {
return false;
}
if ($user && $this->submitted_by_user_id && $this->submitted_by_user_id === $user->id) {
return false;
}
return true;
} }
/** /**
* Check if document can be approved by chair * 新工作流程:董理事會可審核
* 條件:理事長已核准 + 大額
*/ */
public function canBeApprovedByChair(): bool public function canBeApprovedByBoard(?User $user = null): bool
{ {
return $this->status === self::STATUS_APPROVED_ACCOUNTANT; $tier = $this->amount_tier ?? $this->determineAmountTier();
if ($this->status !== self::STATUS_APPROVED_CHAIR) {
return false;
}
if ($tier !== self::AMOUNT_TIER_LARGE) {
return false;
}
return true;
} }
/** /**
* Check if document is fully approved * 新工作流程:審核是否完成
* 依金額級別判斷
*/
public function isApprovalComplete(): bool
{
$tier = $this->amount_tier ?? $this->determineAmountTier();
// 小額:秘書長核准即可
if ($tier === self::AMOUNT_TIER_SMALL) {
return $this->status === self::STATUS_APPROVED_SECRETARY;
}
// 中額:理事長核准
if ($tier === self::AMOUNT_TIER_MEDIUM) {
return $this->status === self::STATUS_APPROVED_CHAIR;
}
// 大額:董理事會核准
return $this->status === self::STATUS_APPROVED_BOARD;
}
/**
* Check if document is fully approved (alias for isApprovalComplete)
*/ */
public function isFullyApproved(): bool public function isFullyApproved(): bool
{ {
return $this->status === self::STATUS_APPROVED_CHAIR; return $this->isApprovalComplete();
}
// ========== 出帳階段方法 ==========
/**
* 申請人可確認出帳
* 條件:審核完成 + 尚未確認 + 是原申請人
*/
public function canRequesterConfirmDisbursement(?User $user = null): bool
{
if (!$this->isApprovalComplete()) {
return false;
}
if ($this->requester_confirmed_at !== null) {
return false;
}
// 只有原申請人可以確認
if ($user && $this->submitted_by_user_id !== $user->id) {
return false;
}
return true;
}
/**
* 出納可確認出帳
* 條件:審核完成 + 尚未確認
*/
public function canCashierConfirmDisbursement(): bool
{
if (!$this->isApprovalComplete()) {
return false;
}
if ($this->cashier_confirmed_at !== null) {
return false;
}
return true;
}
/**
* 出帳是否完成(雙重確認)
*/
public function isDisbursementComplete(): bool
{
return $this->requester_confirmed_at !== null
&& $this->cashier_confirmed_at !== null;
}
// ========== 入帳階段方法 ==========
/**
* 會計可入帳
* 條件:出帳完成 + 尚未入帳
*/
public function canAccountantConfirmRecording(): bool
{
return $this->isDisbursementComplete()
&& $this->accountant_recorded_at === null;
}
/**
* 入帳是否完成
*/
public function isRecordingComplete(): bool
{
return $this->accountant_recorded_at !== null;
}
// ========== Legacy methods for backward compatibility ==========
/**
* @deprecated Use canBeApprovedBySecretary instead
*/
public function canBeApprovedByCashier(?User $user = null): bool
{
return $this->canBeApprovedBySecretary($user);
}
/**
* @deprecated Use isApprovalComplete with amount tier logic
*/
public function canBeApprovedByAccountant(): bool
{
// Legacy: accountant approval after cashier
return $this->status === self::STATUS_APPROVED_CASHIER;
} }
/** /**
@@ -235,20 +549,87 @@ class FinanceDocument extends Model
} }
/** /**
* Get human-readable status * Get human-readable status (中文)
*/ */
public function getStatusLabelAttribute(): string public function getStatusLabelAttribute(): string
{ {
return match($this->status) { return match($this->status) {
self::STATUS_PENDING => 'Pending Cashier Approval', self::STATUS_PENDING => '待審核',
self::STATUS_APPROVED_CASHIER => 'Pending Accountant Approval', self::STATUS_APPROVED_SECRETARY => '秘書長已核准',
self::STATUS_APPROVED_ACCOUNTANT => 'Pending Chair Approval', self::STATUS_APPROVED_CHAIR => '理事長已核准',
self::STATUS_APPROVED_CHAIR => 'Fully Approved', self::STATUS_APPROVED_BOARD => '董理事會已核准',
self::STATUS_REJECTED => 'Rejected', self::STATUS_REJECTED => '已駁回',
// Legacy statuses
self::STATUS_APPROVED_CASHIER => '出納已審核',
self::STATUS_APPROVED_ACCOUNTANT => '會計已審核',
default => ucfirst($this->status), default => ucfirst($this->status),
}; };
} }
/**
* Get disbursement status label (中文)
*/
public function getDisbursementStatusLabelAttribute(): string
{
if (!$this->isApprovalComplete()) {
return '審核中';
}
if ($this->isDisbursementComplete()) {
return '已出帳';
}
if ($this->requester_confirmed_at !== null && $this->cashier_confirmed_at === null) {
return '申請人已確認,待出納確認';
}
if ($this->requester_confirmed_at === null && $this->cashier_confirmed_at !== null) {
return '出納已確認,待申請人確認';
}
return '待出帳';
}
/**
* Get recording status label (中文)
*/
public function getRecordingStatusLabelAttribute(): string
{
if (!$this->isDisbursementComplete()) {
return '尚未出帳';
}
if ($this->accountant_recorded_at !== null) {
return '已入帳';
}
return '待入帳';
}
/**
* Get overall workflow stage label (中文)
*/
public function getWorkflowStageLabelAttribute(): string
{
if ($this->isRejected()) {
return '已駁回';
}
if (!$this->isApprovalComplete()) {
return '審核階段';
}
if (!$this->isDisbursementComplete()) {
return '出帳階段';
}
if (!$this->isRecordingComplete()) {
return '入帳階段';
}
return '已完成';
}
/** /**
* New payment stage business logic methods * New payment stage business logic methods
*/ */
@@ -277,29 +658,12 @@ class FinanceDocument extends Model
} }
/** /**
* Check if approval stage is complete (ready for payment order creation) * Check if approval stage is complete (ready for disbursement)
* 新工作流程:使用 isApprovalComplete()
*/ */
public function isApprovalStageComplete(): bool public function isApprovalStageComplete(): bool
{ {
$tier = $this->amount_tier ?? $this->determineAmountTier(); return $this->isApprovalComplete();
// For small amounts: cashier + accountant
if ($tier === self::AMOUNT_TIER_SMALL) {
return $this->status === self::STATUS_APPROVED_ACCOUNTANT;
}
// For medium amounts: cashier + accountant + chair
if ($tier === self::AMOUNT_TIER_MEDIUM) {
return $this->status === self::STATUS_APPROVED_CHAIR;
}
// For large amounts: cashier + accountant + chair + board meeting
if ($tier === self::AMOUNT_TIER_LARGE) {
return $this->status === self::STATUS_APPROVED_CHAIR &&
$this->board_meeting_approved_at !== null;
}
return false;
} }
/** /**
@@ -341,21 +705,13 @@ class FinanceDocument extends Model
return $this->payment_executed_at !== null; return $this->payment_executed_at !== null;
} }
/**
* Check if recording stage is complete
*/
public function isRecordingComplete(): bool
{
return $this->cashier_recorded_at !== null;
}
/** /**
* Check if document is fully processed (all stages complete) * Check if document is fully processed (all stages complete)
*/ */
public function isFullyProcessed(): bool public function isFullyProcessed(): bool
{ {
return $this->isApprovalStageComplete() && return $this->isApprovalComplete() &&
$this->isPaymentCompleted() && $this->isDisbursementComplete() &&
$this->isRecordingComplete(); $this->isRecordingComplete();
} }
@@ -425,20 +781,6 @@ class FinanceDocument extends Model
$this->attributes['approved_by_board_meeting_id'] = $value; $this->attributes['approved_by_board_meeting_id'] = $value;
} }
/**
* Get request type text
*/
public function getRequestTypeText(): string
{
return match ($this->request_type) {
self::REQUEST_TYPE_EXPENSE_REIMBURSEMENT => '費用報銷',
self::REQUEST_TYPE_ADVANCE_PAYMENT => '預支款項',
self::REQUEST_TYPE_PURCHASE_REQUEST => '採購申請',
self::REQUEST_TYPE_PETTY_CASH => '零用金',
default => '未知',
};
}
/** /**
* Get amount tier text * Get amount tier text
*/ */

446
app/Models/Income.php Normal file
View File

@@ -0,0 +1,446 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB;
class Income extends Model
{
use HasFactory;
// 收入類型常數
const TYPE_MEMBERSHIP_FEE = 'membership_fee'; // 會費收入
const TYPE_ENTRANCE_FEE = 'entrance_fee'; // 入會費收入
const TYPE_DONATION = 'donation'; // 捐款收入
const TYPE_ACTIVITY = 'activity'; // 活動收入
const TYPE_GRANT = 'grant'; // 補助收入
const TYPE_INTEREST = 'interest'; // 利息收入
const TYPE_OTHER = 'other'; // 其他收入
// 狀態常數
const STATUS_PENDING = 'pending'; // 待確認
const STATUS_CONFIRMED = 'confirmed'; // 已確認
const STATUS_CANCELLED = 'cancelled'; // 已取消
// 付款方式常數
const PAYMENT_METHOD_CASH = 'cash';
const PAYMENT_METHOD_BANK_TRANSFER = 'bank_transfer';
const PAYMENT_METHOD_CHECK = 'check';
protected $fillable = [
'income_number',
'title',
'description',
'income_date',
'amount',
'income_type',
'chart_of_account_id',
'payment_method',
'bank_account',
'payer_name',
'receipt_number',
'transaction_reference',
'attachment_path',
'member_id',
'status',
'recorded_by_cashier_id',
'recorded_at',
'confirmed_by_accountant_id',
'confirmed_at',
'cashier_ledger_entry_id',
'notes',
];
protected $casts = [
'income_date' => 'date',
'amount' => 'decimal:2',
'recorded_at' => 'datetime',
'confirmed_at' => 'datetime',
];
/**
* Boot 方法 - 自動產生收入編號
*/
protected static function boot()
{
parent::boot();
static::creating(function ($income) {
if (empty($income->income_number)) {
$income->income_number = self::generateIncomeNumber();
}
if (empty($income->recorded_at)) {
$income->recorded_at = now();
}
});
}
/**
* 產生收入編號 INC-2025-0001
*/
public static function generateIncomeNumber(): string
{
$year = date('Y');
$prefix = "INC-{$year}-";
$lastIncome = self::where('income_number', 'like', "{$prefix}%")
->orderBy('income_number', 'desc')
->first();
if ($lastIncome) {
$lastNumber = (int) substr($lastIncome->income_number, -4);
$newNumber = $lastNumber + 1;
} else {
$newNumber = 1;
}
return $prefix . str_pad($newNumber, 4, '0', STR_PAD_LEFT);
}
// ========== 關聯 ==========
/**
* 會計科目
*/
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class);
}
/**
* 關聯會員
*/
public function member(): BelongsTo
{
return $this->belongsTo(Member::class);
}
/**
* 記錄的出納人員
*/
public function recordedByCashier(): BelongsTo
{
return $this->belongsTo(User::class, 'recorded_by_cashier_id');
}
/**
* 確認的會計人員
*/
public function confirmedByAccountant(): BelongsTo
{
return $this->belongsTo(User::class, 'confirmed_by_accountant_id');
}
/**
* 關聯的出納日記帳
*/
public function cashierLedgerEntry(): BelongsTo
{
return $this->belongsTo(CashierLedgerEntry::class);
}
/**
* 會計分錄
*/
public function accountingEntries(): HasMany
{
return $this->hasMany(AccountingEntry::class);
}
// ========== 狀態查詢 ==========
/**
* 是否待確認
*/
public function isPending(): bool
{
return $this->status === self::STATUS_PENDING;
}
/**
* 是否已確認
*/
public function isConfirmed(): bool
{
return $this->status === self::STATUS_CONFIRMED;
}
/**
* 是否已取消
*/
public function isCancelled(): bool
{
return $this->status === self::STATUS_CANCELLED;
}
/**
* 是否可以被會計確認
*/
public function canBeConfirmed(): bool
{
return $this->status === self::STATUS_PENDING;
}
/**
* 是否可以被取消
*/
public function canBeCancelled(): bool
{
return $this->status === self::STATUS_PENDING;
}
// ========== 業務方法 ==========
/**
* 會計確認收入
*/
public function confirmByAccountant(User $accountant): void
{
if (!$this->canBeConfirmed()) {
throw new \Exception('此收入無法確認');
}
DB::transaction(function () use ($accountant) {
// 1. 更新收入狀態
$this->update([
'status' => self::STATUS_CONFIRMED,
'confirmed_by_accountant_id' => $accountant->id,
'confirmed_at' => now(),
]);
// 2. 產生出納日記帳記錄
$ledgerEntry = $this->createCashierLedgerEntry();
// 3. 產生會計分錄
$this->generateAccountingEntries();
});
}
/**
* 取消收入
*/
public function cancel(): void
{
if (!$this->canBeCancelled()) {
throw new \Exception('此收入無法取消');
}
$this->update([
'status' => self::STATUS_CANCELLED,
]);
}
/**
* 建立出納日記帳記錄
*/
protected function createCashierLedgerEntry(): CashierLedgerEntry
{
$bankAccount = $this->bank_account ?? 'Main Account';
$balanceBefore = CashierLedgerEntry::getLatestBalance($bankAccount);
$ledgerEntry = CashierLedgerEntry::create([
'entry_date' => $this->income_date,
'entry_type' => CashierLedgerEntry::ENTRY_TYPE_RECEIPT,
'payment_method' => $this->payment_method,
'bank_account' => $bankAccount,
'amount' => $this->amount,
'balance_before' => $balanceBefore,
'balance_after' => $balanceBefore + $this->amount,
'receipt_number' => $this->receipt_number,
'transaction_reference' => $this->transaction_reference,
'recorded_by_cashier_id' => $this->recorded_by_cashier_id,
'recorded_at' => now(),
'notes' => "收入確認:{$this->title} ({$this->income_number})",
]);
$this->update(['cashier_ledger_entry_id' => $ledgerEntry->id]);
return $ledgerEntry;
}
/**
* 產生會計分錄
*/
protected function generateAccountingEntries(): void
{
// 借方:資產帳戶(現金或銀行存款)
$assetAccountId = $this->getAssetAccountId();
AccountingEntry::create([
'income_id' => $this->id,
'chart_of_account_id' => $assetAccountId,
'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT,
'amount' => $this->amount,
'entry_date' => $this->income_date,
'description' => "收入:{$this->title} ({$this->income_number})",
]);
// 貸方:收入科目
AccountingEntry::create([
'income_id' => $this->id,
'chart_of_account_id' => $this->chart_of_account_id,
'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT,
'amount' => $this->amount,
'entry_date' => $this->income_date,
'description' => "收入:{$this->title} ({$this->income_number})",
]);
}
/**
* 根據付款方式取得資產帳戶 ID
*/
protected function getAssetAccountId(): int
{
$accountCode = match ($this->payment_method) {
self::PAYMENT_METHOD_BANK_TRANSFER => '1201', // 銀行存款
self::PAYMENT_METHOD_CHECK => '1201', // 銀行存款
default => '1101', // 現金
};
return ChartOfAccount::where('account_code', $accountCode)->value('id') ?? 1;
}
// ========== 文字取得 ==========
/**
* 取得收入類型文字
*/
public function getIncomeTypeText(): string
{
return match ($this->income_type) {
self::TYPE_MEMBERSHIP_FEE => '會費收入',
self::TYPE_ENTRANCE_FEE => '入會費收入',
self::TYPE_DONATION => '捐款收入',
self::TYPE_ACTIVITY => '活動收入',
self::TYPE_GRANT => '補助收入',
self::TYPE_INTEREST => '利息收入',
self::TYPE_OTHER => '其他收入',
default => '未知',
};
}
/**
* 取得狀態文字
*/
public function getStatusText(): string
{
return match ($this->status) {
self::STATUS_PENDING => '待確認',
self::STATUS_CONFIRMED => '已確認',
self::STATUS_CANCELLED => '已取消',
default => '未知',
};
}
/**
* 取得付款方式文字
*/
public function getPaymentMethodText(): string
{
return match ($this->payment_method) {
self::PAYMENT_METHOD_CASH => '現金',
self::PAYMENT_METHOD_BANK_TRANSFER => '銀行轉帳',
self::PAYMENT_METHOD_CHECK => '支票',
default => '未知',
};
}
/**
* 取得狀態標籤屬性
*/
public function getStatusLabelAttribute(): string
{
return $this->getStatusText();
}
// ========== 收入類型與科目對應 ==========
/**
* 取得收入類型對應的預設會計科目代碼
*/
public static function getDefaultAccountCode(string $incomeType): string
{
return match ($incomeType) {
self::TYPE_MEMBERSHIP_FEE => '4101',
self::TYPE_ENTRANCE_FEE => '4102',
self::TYPE_DONATION => '4201',
self::TYPE_ACTIVITY => '4402',
self::TYPE_GRANT => '4301',
self::TYPE_INTEREST => '4401',
self::TYPE_OTHER => '4901',
default => '4901',
};
}
/**
* 取得收入類型對應的預設會計科目 ID
*/
public static function getDefaultAccountId(string $incomeType): ?int
{
$accountCode = self::getDefaultAccountCode($incomeType);
return ChartOfAccount::where('account_code', $accountCode)->value('id');
}
/**
* 靜態方法:取得收入類型文字標籤
*/
public static function getIncomeTypeLabel(string $incomeType): string
{
return match ($incomeType) {
self::TYPE_MEMBERSHIP_FEE => '會費收入',
self::TYPE_ENTRANCE_FEE => '入會費收入',
self::TYPE_DONATION => '捐款收入',
self::TYPE_ACTIVITY => '活動收入',
self::TYPE_GRANT => '補助收入',
self::TYPE_INTEREST => '利息收入',
self::TYPE_OTHER => '其他收入',
default => '未知',
};
}
// ========== 查詢範圍 ==========
/**
* 篩選待確認的收入
*/
public function scopePending($query)
{
return $query->where('status', self::STATUS_PENDING);
}
/**
* 篩選已確認的收入
*/
public function scopeConfirmed($query)
{
return $query->where('status', self::STATUS_CONFIRMED);
}
/**
* 篩選特定收入類型
*/
public function scopeOfType($query, string $type)
{
return $query->where('income_type', $type);
}
/**
* 篩選特定會員
*/
public function scopeForMember($query, int $memberId)
{
return $query->where('member_id', $memberId);
}
/**
* 篩選日期範圍
*/
public function scopeDateRange($query, $startDate, $endDate)
{
return $query->whereBetween('income_date', [$startDate, $endDate]);
}
}

View File

@@ -22,6 +22,11 @@ class Member extends Model
const TYPE_LIFETIME = 'lifetime'; const TYPE_LIFETIME = 'lifetime';
const TYPE_STUDENT = 'student'; const TYPE_STUDENT = 'student';
// Disability certificate status constants
const DISABILITY_STATUS_PENDING = 'pending';
const DISABILITY_STATUS_APPROVED = 'approved';
const DISABILITY_STATUS_REJECTED = 'rejected';
protected $fillable = [ protected $fillable = [
'user_id', 'user_id',
'full_name', 'full_name',
@@ -39,11 +44,17 @@ class Member extends Model
'membership_expires_at', 'membership_expires_at',
'membership_status', 'membership_status',
'membership_type', 'membership_type',
'disability_certificate_path',
'disability_certificate_status',
'disability_verified_by',
'disability_verified_at',
'disability_rejection_reason',
]; ];
protected $casts = [ protected $casts = [
'membership_started_at' => 'date', 'membership_started_at' => 'date',
'membership_expires_at' => 'date', 'membership_expires_at' => 'date',
'disability_verified_at' => 'datetime',
]; ];
protected $appends = ['national_id']; protected $appends = ['national_id'];
@@ -58,6 +69,37 @@ class Member extends Model
return $this->hasMany(MembershipPayment::class); return $this->hasMany(MembershipPayment::class);
} }
/**
* 關聯的收入記錄
*/
public function incomes()
{
return $this->hasMany(Income::class);
}
/**
* 取得會員的會費收入記錄
*/
public function getMembershipFeeIncomes()
{
return $this->incomes()
->whereIn('income_type', [
Income::TYPE_MEMBERSHIP_FEE,
Income::TYPE_ENTRANCE_FEE
])
->get();
}
/**
* 取得會員的總收入金額
*/
public function getTotalIncomeAttribute(): float
{
return $this->incomes()
->where('status', Income::STATUS_CONFIRMED)
->sum('amount');
}
/** /**
* Get the decrypted national ID * Get the decrypted national ID
*/ */
@@ -203,4 +245,120 @@ class Member extends Model
// Can submit if pending status and no pending payment // Can submit if pending status and no pending payment
return $this->isPending() && !$this->getPendingPayment(); return $this->isPending() && !$this->getPendingPayment();
} }
// ========== 身心障礙相關 ==========
/**
* 身心障礙手冊審核人
*/
public function disabilityVerifiedBy()
{
return $this->belongsTo(User::class, 'disability_verified_by');
}
/**
* 是否有上傳身心障礙手冊
*/
public function hasDisabilityCertificate(): bool
{
return !empty($this->disability_certificate_path);
}
/**
* 身心障礙手冊是否待審核
*/
public function isDisabilityPending(): bool
{
return $this->disability_certificate_status === self::DISABILITY_STATUS_PENDING;
}
/**
* 身心障礙手冊是否已通過審核
*/
public function hasApprovedDisability(): bool
{
return $this->disability_certificate_status === self::DISABILITY_STATUS_APPROVED;
}
/**
* 身心障礙手冊是否被駁回
*/
public function isDisabilityRejected(): bool
{
return $this->disability_certificate_status === self::DISABILITY_STATUS_REJECTED;
}
/**
* 取得身心障礙狀態標籤
*/
public function getDisabilityStatusLabelAttribute(): string
{
if (!$this->hasDisabilityCertificate()) {
return '未上傳';
}
return match ($this->disability_certificate_status) {
self::DISABILITY_STATUS_PENDING => '審核中',
self::DISABILITY_STATUS_APPROVED => '已通過',
self::DISABILITY_STATUS_REJECTED => '已駁回',
default => '未知',
};
}
/**
* 取得身心障礙狀態的 Badge 樣式
*/
public function getDisabilityStatusBadgeAttribute(): string
{
if (!$this->hasDisabilityCertificate()) {
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
return match ($this->disability_certificate_status) {
self::DISABILITY_STATUS_PENDING => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
self::DISABILITY_STATUS_APPROVED => 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
self::DISABILITY_STATUS_REJECTED => 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
default => 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
};
}
/**
* 審核通過身心障礙手冊
*/
public function approveDisabilityCertificate(User $verifier): void
{
$this->update([
'disability_certificate_status' => self::DISABILITY_STATUS_APPROVED,
'disability_verified_by' => $verifier->id,
'disability_verified_at' => now(),
'disability_rejection_reason' => null,
]);
}
/**
* 駁回身心障礙手冊
*/
public function rejectDisabilityCertificate(User $verifier, string $reason): void
{
$this->update([
'disability_certificate_status' => self::DISABILITY_STATUS_REJECTED,
'disability_verified_by' => $verifier->id,
'disability_verified_at' => now(),
'disability_rejection_reason' => $reason,
]);
}
/**
* 判斷下一次應繳哪種會費
*/
public function getNextFeeType(): string
{
// 新會員(從未啟用過)= 入會會費
if ($this->membership_started_at === null) {
return MembershipPayment::FEE_TYPE_ENTRANCE;
}
// 已有會籍 = 常年會費
return MembershipPayment::FEE_TYPE_ANNUAL;
}
} }

View File

@@ -23,10 +23,19 @@ class MembershipPayment extends Model
const METHOD_CASH = 'cash'; const METHOD_CASH = 'cash';
const METHOD_CREDIT_CARD = 'credit_card'; const METHOD_CREDIT_CARD = 'credit_card';
// Fee type constants
const FEE_TYPE_ENTRANCE = 'entrance_fee'; // 入會會費
const FEE_TYPE_ANNUAL = 'annual_fee'; // 常年會費
protected $fillable = [ protected $fillable = [
'member_id', 'member_id',
'fee_type',
'paid_at', 'paid_at',
'amount', 'amount',
'base_amount',
'discount_amount',
'final_amount',
'disability_discount',
'method', 'method',
'reference', 'reference',
'status', 'status',
@@ -51,6 +60,10 @@ class MembershipPayment extends Model
'accountant_verified_at' => 'datetime', 'accountant_verified_at' => 'datetime',
'chair_verified_at' => 'datetime', 'chair_verified_at' => 'datetime',
'rejected_at' => 'datetime', 'rejected_at' => 'datetime',
'base_amount' => 'decimal:2',
'discount_amount' => 'decimal:2',
'final_amount' => 'decimal:2',
'disability_discount' => 'boolean',
]; ];
// Relationships // Relationships
@@ -151,6 +164,36 @@ class MembershipPayment extends Model
}; };
} }
// Accessor for fee type label
public function getFeeTypeLabelAttribute(): string
{
return match($this->fee_type) {
self::FEE_TYPE_ENTRANCE => '入會會費',
self::FEE_TYPE_ANNUAL => '常年會費',
default => $this->fee_type ?? '未指定',
};
}
/**
* 是否有使用身心障礙優惠
*/
public function hasDisabilityDiscount(): bool
{
return (bool) $this->disability_discount;
}
/**
* 取得折扣說明
*/
public function getDiscountDescriptionAttribute(): ?string
{
if (!$this->hasDisabilityDiscount()) {
return null;
}
return '身心障礙優惠 50%';
}
// Clean up receipt file when payment is deleted // Clean up receipt file when payment is deleted
protected static function boot() protected static function boot()
{ {

View File

@@ -67,7 +67,7 @@ class PaymentOrder extends Model
const PAYMENT_METHOD_CASH = 'cash'; const PAYMENT_METHOD_CASH = 'cash';
/** /**
* 關聯到財務申請單 * 關聯到報銷申請單
*/ */
public function financeDocument(): BelongsTo public function financeDocument(): BelongsTo
{ {

View File

@@ -23,7 +23,6 @@ class User extends Authenticatable
protected $fillable = [ protected $fillable = [
'name', 'name',
'email', 'email',
'is_admin',
'profile_photo_path', 'profile_photo_path',
'password', 'password',
]; ];
@@ -46,7 +45,6 @@ class User extends Authenticatable
protected $casts = [ protected $casts = [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'is_admin' => 'boolean',
]; ];
public function member(): HasOne public function member(): HasOne
@@ -54,6 +52,15 @@ class User extends Authenticatable
return $this->hasOne(Member::class); return $this->hasOne(Member::class);
} }
/**
* 檢查使用者是否為管理員
* 使用 Spatie Permission admin 角色取代舊版 is_admin 欄位
*/
public function isAdmin(): bool
{
return $this->hasRole('admin');
}
public function profilePhotoUrl(): ?string public function profilePhotoUrl(): ?string
{ {
if (! $this->profile_photo_path) { if (! $this->profile_photo_path) {

View File

@@ -0,0 +1,154 @@
<?php
namespace App\Services;
use App\Models\Member;
use App\Models\MembershipPayment;
class MembershipFeeCalculator
{
protected SettingsService $settings;
public function __construct(SettingsService $settings)
{
$this->settings = $settings;
}
/**
* 計算會費金額
*
* @param Member $member 會員
* @param string $feeType 會費類型 (entrance_fee | annual_fee)
* @return array{base_amount: float, discount_amount: float, final_amount: float, disability_discount: bool, fee_type: string}
*/
public function calculate(Member $member, string $feeType): array
{
$baseAmount = $this->getBaseAmount($feeType);
$discountRate = $member->hasApprovedDisability()
? $this->getDisabilityDiscountRate()
: 0;
$discountAmount = round($baseAmount * $discountRate, 2);
$finalAmount = round($baseAmount - $discountAmount, 2);
return [
'fee_type' => $feeType,
'base_amount' => $baseAmount,
'discount_amount' => $discountAmount,
'final_amount' => $finalAmount,
'disability_discount' => $discountRate > 0,
];
}
/**
* 為會員計算下一次應繳的會費
*
* @param Member $member
* @return array{base_amount: float, discount_amount: float, final_amount: float, disability_discount: bool, fee_type: string}
*/
public function calculateNextFee(Member $member): array
{
$feeType = $member->getNextFeeType();
return $this->calculate($member, $feeType);
}
/**
* 取得基本會費金額
*
* @param string $feeType
* @return float
*/
public function getBaseAmount(string $feeType): float
{
return match($feeType) {
MembershipPayment::FEE_TYPE_ENTRANCE => $this->getEntranceFee(),
MembershipPayment::FEE_TYPE_ANNUAL => $this->getAnnualFee(),
default => 0,
};
}
/**
* 取得入會會費金額
*
* @return float
*/
public function getEntranceFee(): float
{
return (float) $this->settings->get('membership_fee.entrance_fee', 1000);
}
/**
* 取得常年會費金額
*
* @return float
*/
public function getAnnualFee(): float
{
return (float) $this->settings->get('membership_fee.annual_fee', 1000);
}
/**
* 取得身心障礙折扣比例
*
* @return float 折扣比例 (0-1)
*/
public function getDisabilityDiscountRate(): float
{
return (float) $this->settings->get('membership_fee.disability_discount_rate', 0.5);
}
/**
* 取得身心障礙折扣百分比 (用於顯示)
*
* @return int 百分比 (0-100)
*/
public function getDisabilityDiscountPercentage(): int
{
return (int) ($this->getDisabilityDiscountRate() * 100);
}
/**
* 驗證繳費金額是否正確
*
* @param MembershipPayment $payment
* @return bool
*/
public function validatePaymentAmount(MembershipPayment $payment): bool
{
$member = $payment->member;
$expected = $this->calculate($member, $payment->fee_type);
// 繳費金額應等於或大於應繳金額
return $payment->amount >= $expected['final_amount'];
}
/**
* 取得會費類型的標籤
*
* @param string $feeType
* @return string
*/
public function getFeeTypeLabel(string $feeType): string
{
return match($feeType) {
MembershipPayment::FEE_TYPE_ENTRANCE => '入會會費',
MembershipPayment::FEE_TYPE_ANNUAL => '常年會費',
default => $feeType,
};
}
/**
* 取得所有會費設定(用於管理界面)
*
* @return array
*/
public function getFeeSettings(): array
{
return [
'entrance_fee' => $this->getEntranceFee(),
'annual_fee' => $this->getAnnualFee(),
'disability_discount_rate' => $this->getDisabilityDiscountRate(),
'disability_discount_percentage' => $this->getDisabilityDiscountPercentage(),
];
}
}

View File

@@ -11,12 +11,14 @@
"laravel/framework": "^10.10", "laravel/framework": "^10.10",
"laravel/sanctum": "^3.3", "laravel/sanctum": "^3.3",
"laravel/tinker": "^2.8", "laravel/tinker": "^2.8",
"maatwebsite/excel": "^3.1",
"simplesoftwareio/simple-qrcode": "^4.2", "simplesoftwareio/simple-qrcode": "^4.2",
"spatie/laravel-permission": "^6.23" "spatie/laravel-permission": "^6.23"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.9.1", "fakerphp/faker": "^1.9.1",
"laravel/breeze": "^1.29", "laravel/breeze": "^1.29",
"laravel/dusk": "^8.3",
"laravel/pint": "^1.0", "laravel/pint": "^1.0",
"laravel/sail": "^1.18", "laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4", "mockery/mockery": "^1.4.4",

737
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "823199d76778549dda38d7d7c8a1967a", "content-hash": "10268750f724780736201053fb4871bf",
"packages": [ "packages": [
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@@ -266,6 +266,162 @@
], ],
"time": "2023-12-11T17:09:12+00:00" "time": "2023-12-11T17:09:12+00:00"
}, },
{
"name": "composer/pcre",
"version": "3.3.2",
"source": {
"type": "git",
"url": "https://github.com/composer/pcre.git",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0"
},
"conflict": {
"phpstan/phpstan": "<1.11.10"
},
"require-dev": {
"phpstan/phpstan": "^1.12 || ^2",
"phpstan/phpstan-strict-rules": "^1 || ^2",
"phpunit/phpunit": "^8 || ^9"
},
"type": "library",
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Pcre\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "PCRE wrapping library that offers type-safe preg_* replacements.",
"keywords": [
"PCRE",
"preg",
"regex",
"regular expression"
],
"support": {
"issues": "https://github.com/composer/pcre/issues",
"source": "https://github.com/composer/pcre/tree/3.3.2"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2024-11-12T16:29:46+00:00"
},
{
"name": "composer/semver",
"version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.11",
"symfony/phpunit-bridge": "^3 || ^7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Composer\\Semver\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nils Adermann",
"email": "naderman@naderman.de",
"homepage": "http://www.naderman.de"
},
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
},
{
"name": "Rob Bast",
"email": "rob.bast@gmail.com",
"homepage": "http://robbast.nl"
}
],
"description": "Semver library that offers utilities, version constraint parsing and validation.",
"keywords": [
"semantic",
"semver",
"validation",
"versioning"
],
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
"source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
}
],
"time": "2025-08-20T19:15:30+00:00"
},
{ {
"name": "dasprid/enum", "name": "dasprid/enum",
"version": "1.0.7", "version": "1.0.7",
@@ -844,6 +1000,67 @@
], ],
"time": "2025-03-06T22:45:56+00:00" "time": "2025-03-06T22:45:56+00:00"
}, },
{
"name": "ezyang/htmlpurifier",
"version": "v4.19.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
"reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
},
"time": "2025-10-17T16:34:55+00:00"
},
{ {
"name": "fruitcake/php-cors", "name": "fruitcake/php-cors",
"version": "v1.3.0", "version": "v1.3.0",
@@ -2224,6 +2441,272 @@
], ],
"time": "2024-09-21T08:32:55+00:00" "time": "2024-09-21T08:32:55+00:00"
}, },
{
"name": "maatwebsite/excel",
"version": "3.1.67",
"source": {
"type": "git",
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d",
"reference": "e508e34a502a3acc3329b464dad257378a7edb4d",
"shasum": ""
},
"require": {
"composer/semver": "^3.3",
"ext-json": "*",
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0",
"php": "^7.0||^8.0",
"phpoffice/phpspreadsheet": "^1.30.0",
"psr/simple-cache": "^1.0||^2.0||^3.0"
},
"require-dev": {
"laravel/scout": "^7.0||^8.0||^9.0||^10.0",
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0",
"predis/predis": "^1.1"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
},
"providers": [
"Maatwebsite\\Excel\\ExcelServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Maatwebsite\\Excel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Patrick Brouwers",
"email": "patrick@spartner.nl"
}
],
"description": "Supercharged Excel exports and imports in Laravel",
"keywords": [
"PHPExcel",
"batch",
"csv",
"excel",
"export",
"import",
"laravel",
"php",
"phpspreadsheet"
],
"support": {
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67"
},
"funding": [
{
"url": "https://laravel-excel.com/commercial-support",
"type": "custom"
},
{
"url": "https://github.com/patrickbrouwers",
"type": "github"
}
],
"time": "2025-08-26T09:13:16+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
"reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.3"
},
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^12.0",
"vimeo/psalm": "^6.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2025-07-17T11:15:13+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{ {
"name": "masterminds/html5", "name": "masterminds/html5",
"version": "2.10.0", "version": "2.10.0",
@@ -2798,6 +3281,112 @@
], ],
"time": "2024-11-21T10:36:35+00:00" "time": "2024-11-21T10:36:35+00:00"
}, },
{
"name": "phpoffice/phpspreadsheet",
"version": "1.30.1",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "fa8257a579ec623473eabfe49731de5967306c4c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
"reference": "fa8257a579ec623473eabfe49731de5967306c4c",
"shasum": ""
},
"require": {
"composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": ">=7.4.0 <8.5.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
},
"time": "2025-10-26T16:01:04+00:00"
},
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
"version": "1.9.4", "version": "1.9.4",
@@ -6499,6 +7088,80 @@
}, },
"time": "2024-03-04T14:35:21+00:00" "time": "2024-03-04T14:35:21+00:00"
}, },
{
"name": "laravel/dusk",
"version": "v8.3.4",
"source": {
"type": "git",
"url": "https://github.com/laravel/dusk.git",
"reference": "33a4211c7b63ffe430bf30ec3c014012dcb6dfa6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/dusk/zipball/33a4211c7b63ffe430bf30ec3c014012dcb6dfa6",
"reference": "33a4211c7b63ffe430bf30ec3c014012dcb6dfa6",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-zip": "*",
"guzzlehttp/guzzle": "^7.5",
"illuminate/console": "^10.0|^11.0|^12.0",
"illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.1",
"php-webdriver/webdriver": "^1.15.2",
"symfony/console": "^6.2|^7.0",
"symfony/finder": "^6.2|^7.0",
"symfony/process": "^6.2|^7.0",
"vlucas/phpdotenv": "^5.2"
},
"require-dev": {
"laravel/framework": "^10.0|^11.0|^12.0",
"mockery/mockery": "^1.6",
"orchestra/testbench-core": "^8.19|^9.17|^10.8",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.1|^11.0|^12.0.1",
"psy/psysh": "^0.11.12|^0.12",
"symfony/yaml": "^6.2|^7.0"
},
"suggest": {
"ext-pcntl": "Used to gracefully terminate Dusk when tests are running."
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Laravel\\Dusk\\DuskServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Dusk\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Laravel Dusk provides simple end-to-end testing and browser automation.",
"keywords": [
"laravel",
"testing",
"webdriver"
],
"support": {
"issues": "https://github.com/laravel/dusk/issues",
"source": "https://github.com/laravel/dusk/tree/v8.3.4"
},
"time": "2025-11-20T16:26:16+00:00"
},
{ {
"name": "laravel/pint", "name": "laravel/pint",
"version": "v1.25.1", "version": "v1.25.1",
@@ -6985,6 +7648,72 @@
}, },
"time": "2022-02-21T01:04:05+00:00" "time": "2022-02-21T01:04:05+00:00"
}, },
{
"name": "php-webdriver/webdriver",
"version": "1.15.2",
"source": {
"type": "git",
"url": "https://github.com/php-webdriver/php-webdriver.git",
"reference": "998e499b786805568deaf8cbf06f4044f05d91bf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/998e499b786805568deaf8cbf06f4044f05d91bf",
"reference": "998e499b786805568deaf8cbf06f4044f05d91bf",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-zip": "*",
"php": "^7.3 || ^8.0",
"symfony/polyfill-mbstring": "^1.12",
"symfony/process": "^5.0 || ^6.0 || ^7.0"
},
"replace": {
"facebook/webdriver": "*"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.20.0",
"ondram/ci-detector": "^4.0",
"php-coveralls/php-coveralls": "^2.4",
"php-mock/php-mock-phpunit": "^2.0",
"php-parallel-lint/php-parallel-lint": "^1.2",
"phpunit/phpunit": "^9.3",
"squizlabs/php_codesniffer": "^3.5",
"symfony/var-dumper": "^5.0 || ^6.0 || ^7.0"
},
"suggest": {
"ext-SimpleXML": "For Firefox profile creation"
},
"type": "library",
"autoload": {
"files": [
"lib/Exception/TimeoutException.php"
],
"psr-4": {
"Facebook\\WebDriver\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.",
"homepage": "https://github.com/php-webdriver/php-webdriver",
"keywords": [
"Chromedriver",
"geckodriver",
"php",
"selenium",
"webdriver"
],
"support": {
"issues": "https://github.com/php-webdriver/php-webdriver/issues",
"source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.2"
},
"time": "2024-11-21T15:12:59+00:00"
},
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "10.1.16", "version": "10.1.16",
@@ -8878,12 +9607,12 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": {}, "stability-flags": [],
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^8.1" "php": "^8.1"
}, },
"platform-dev": {}, "platform-dev": [],
"plugin-api-version": "2.9.0" "plugin-api-version": "2.6.0"
} }

View File

@@ -0,0 +1,161 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Excel to System Account Code Mapping
|--------------------------------------------------------------------------
|
| This configuration maps Excel accounting codes to system account codes
| for importing accounting data from Excel files.
|
*/
'excel_to_system' => [
// Assets (1000 series)
'1000' => '1101', // 現金及約當現金 → 現金
'1100' => '1102', // 庫存現金 → 零用金
'1101' => '1201', // 銀行存款 → 銀行存款
'1107' => '1201', // 其他現金等價物 → 銀行存款
// Income (4000 series)
'4100' => '4201', // 一般捐款收入 → 捐贈收入
'4310' => '4102', // 入會費 → 入會費收入
'4101' => '4101', // 會費收入 → 會費收入 (direct match)
// Expenses (5000 series)
// 5100 業務費用需要根據描述細分,見下方 expense_keywords
],
/*
|--------------------------------------------------------------------------
| Expense Classification Keywords
|--------------------------------------------------------------------------
|
| Excel 中的 5100 業務費用需要根據描述關鍵字分類到具體科目
|
*/
'expense_keywords' => [
// 5206 - 旅運費
[
'keywords' => ['交通', '車費', '計程車', '高鐵', '台鐵', '客運', '機票', '油資', '停車'],
'account_code' => '5206',
'account_name' => '旅運費',
],
// 5209 - 會議費
[
'keywords' => ['會場', '場地', '清潔', '點心', '餐', '便當', '茶水', '會議'],
'account_code' => '5209',
'account_name' => '會議費',
],
// 5203 - 郵電費
[
'keywords' => ['郵寄', '郵資', '郵票', '快遞', '宅配', '電話', '網路', '通訊', '電信'],
'account_code' => '5203',
'account_name' => '郵電費',
],
// 5205 - 印刷費
[
'keywords' => ['影印', '列印', '印刷', '裝訂', '海報', '傳單', '文宣'],
'account_code' => '5205',
'account_name' => '印刷費',
],
// 5204 - 文具用品
[
'keywords' => ['文具', '用品', '紙張', '筆', '資料夾', '辦公用品'],
'account_code' => '5204',
'account_name' => '文具用品',
],
// 5202 - 水電費
[
'keywords' => ['水費', '電費', '水電'],
'account_code' => '5202',
'account_name' => '水電費',
],
// 5201 - 租金支出
[
'keywords' => ['租金', '房租', '場租'],
'account_code' => '5201',
'account_name' => '租金支出',
],
// 5208 - 修繕費
[
'keywords' => ['修繕', '維修', '修理', '保養'],
'account_code' => '5208',
'account_name' => '修繕費',
],
// 5212 - 廣告宣傳費
[
'keywords' => ['廣告', '宣傳', '行銷', '推廣'],
'account_code' => '5212',
'account_name' => '廣告宣傳費',
],
// 5213 - 專案活動費
[
'keywords' => ['活動', '專案', '講座', '課程', '研習', '訓練'],
'account_code' => '5213',
'account_name' => '專案活動費',
],
// 5211 - 交際費
[
'keywords' => ['交際', '禮品', '贈品', '紀念品'],
'account_code' => '5211',
'account_name' => '交際費',
],
// 5304 - 銀行手續費
[
'keywords' => ['手續費', '匯費', '轉帳費'],
'account_code' => '5304',
'account_name' => '銀行手續費',
],
// 5308 - 資訊系統費
[
'keywords' => ['軟體', '系統', '網站', 'domain', '主機', '雲端'],
'account_code' => '5308',
'account_name' => '資訊系統費',
],
// Default: 5901 雜項支出 (if no keywords match)
[
'keywords' => [],
'account_code' => '5901',
'account_name' => '雜項支出',
'is_default' => true,
],
],
/*
|--------------------------------------------------------------------------
| Cash Account
|--------------------------------------------------------------------------
|
| Default cash account for generating journal entries
|
*/
'cash_account_code' => '1101', // 現金
/*
|--------------------------------------------------------------------------
| Bank Account
|--------------------------------------------------------------------------
|
| Default bank account
|
*/
'bank_account_code' => '1201', // 銀行存款
];

View File

@@ -70,7 +70,7 @@ return [
| |
*/ */
'timezone' => 'UTC', 'timezone' => 'Asia/Taipei',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@@ -83,7 +83,7 @@ return [
| |
*/ */
'locale' => 'en', 'locale' => 'zh_TW',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@@ -0,0 +1,40 @@
<?php
namespace Database\Factories;
use App\Models\AuditLog;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class AuditLogFactory extends Factory
{
protected $model = AuditLog::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'action' => $this->faker->randomElement([
'member_created',
'member_status_changed',
'payment_approved',
'payment_rejected',
'finance_document_created',
'finance_document_approved',
'user_login',
'role_assigned',
]),
'auditable_type' => $this->faker->randomElement([
'App\Models\Member',
'App\Models\MembershipPayment',
'App\Models\FinanceDocument',
'App\Models\User',
]),
'auditable_id' => $this->faker->numberBetween(1, 100),
'metadata' => [
'ip_address' => $this->faker->ipv4(),
'user_agent' => $this->faker->userAgent(),
],
];
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Database\Factories;
use App\Models\BankReconciliation;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class BankReconciliationFactory extends Factory
{
protected $model = BankReconciliation::class;
public function definition(): array
{
$bankBalance = $this->faker->numberBetween(50000, 500000);
$bookBalance = $bankBalance + $this->faker->numberBetween(-5000, 5000);
return [
'reconciliation_month' => now()->startOfMonth(),
'bank_statement_date' => now(),
'bank_statement_balance' => $bankBalance,
'system_book_balance' => $bookBalance,
'outstanding_checks' => [],
'deposits_in_transit' => [],
'bank_charges' => [],
'prepared_by_cashier_id' => User::factory(),
'prepared_at' => now(),
'reconciliation_status' => 'pending',
'discrepancy_amount' => abs($bankBalance - $bookBalance),
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class BudgetCategoryFactory extends Factory
{
public function definition(): array
{
return [
'name' => $this->faker->unique()->words(2, true),
'description' => $this->faker->sentence(),
];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Database\Factories;
use App\Models\ChartOfAccount;
use Illuminate\Database\Eloquent\Factories\Factory;
class ChartOfAccountFactory extends Factory
{
protected $model = ChartOfAccount::class;
public function definition(): array
{
return [
'code' => $this->faker->unique()->numerify('####'),
'name' => $this->faker->words(3, true),
'type' => $this->faker->randomElement(['asset', 'liability', 'equity', 'revenue', 'expense']),
'description' => $this->faker->sentence(),
'is_active' => true,
];
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use App\Models\DocumentCategory;
use Illuminate\Database\Eloquent\Factories\Factory;
class DocumentCategoryFactory extends Factory
{
protected $model = DocumentCategory::class;
public function definition(): array
{
return [
'name' => $this->faker->unique()->words(2, true),
'slug' => $this->faker->unique()->slug(2),
'description' => $this->faker->sentence(),
'icon' => $this->faker->randomElement(['📄', '📁', '📋', '📊', '📈']),
'sort_order' => $this->faker->numberBetween(1, 100),
'default_access_level' => $this->faker->randomElement(['public', 'members', 'admin', 'board']),
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use App\Models\Document;
use App\Models\DocumentCategory;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class DocumentFactory extends Factory
{
protected $model = Document::class;
public function definition(): array
{
return [
'document_category_id' => DocumentCategory::factory(),
'title' => $this->faker->sentence(3),
'document_number' => 'DOC-'.now()->format('Y').'-'.str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT),
'description' => $this->faker->paragraph(),
'public_uuid' => (string) Str::uuid(),
'access_level' => $this->faker->randomElement(['public', 'members', 'admin']),
'status' => 'active',
'created_by_user_id' => User::factory(),
'view_count' => $this->faker->numberBetween(0, 100),
'download_count' => $this->faker->numberBetween(0, 50),
'version_count' => 1,
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* 移除 is_admin 欄位,統一使用 Spatie Permission admin 角色進行權限管理
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_admin');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('is_admin')->default(false)->after('email');
});
}
};

View File

@@ -0,0 +1,48 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (Schema::hasTable('cashier_ledger_entries')) {
return;
}
Schema::create('cashier_ledger_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('finance_document_id')->nullable()->constrained()->nullOnDelete();
$table->date('entry_date');
$table->string('entry_type'); // receipt, payment
$table->string('payment_method'); // bank_transfer, check, cash
$table->string('bank_account')->nullable();
$table->decimal('amount', 12, 2);
$table->decimal('balance_before', 12, 2)->default(0);
$table->decimal('balance_after', 12, 2)->default(0);
$table->string('receipt_number')->nullable();
$table->string('transaction_reference')->nullable();
$table->foreignId('recorded_by_cashier_id')->constrained('users');
$table->timestamp('recorded_at');
$table->text('notes')->nullable();
$table->timestamps();
$table->index('entry_date');
$table->index('entry_type');
$table->index('bank_account');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('cashier_ledger_entries');
}
};

View File

@@ -0,0 +1,62 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('announcements', function (Blueprint $table) {
$table->id();
// 基本資訊
$table->string('title');
$table->text('content');
// 狀態管理
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
// 顯示控制
$table->boolean('is_pinned')->default(false);
$table->integer('display_order')->default(0);
// 訪問控制
$table->enum('access_level', ['public', 'members', 'board', 'admin'])->default('members');
// 時間控制
$table->timestamp('published_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamp('archived_at')->nullable();
// 統計
$table->integer('view_count')->default(0);
// 用戶關聯
$table->foreignId('created_by_user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('last_updated_by_user_id')->nullable()->constrained('users')->onDelete('set null');
$table->timestamps();
$table->softDeletes();
// 索引
$table->index('status');
$table->index('access_level');
$table->index('published_at');
$table->index('expires_at');
$table->index(['is_pinned', 'display_order']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('announcements');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('audit_logs', function (Blueprint $table) {
$table->text('description')->nullable()->after('action');
$table->string('ip_address', 45)->nullable()->after('metadata');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('audit_logs', function (Blueprint $table) {
$table->dropColumn(['description', 'ip_address']);
});
}
};

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('accounting_entries', function (Blueprint $table) {
$table->id();
$table->foreignId('finance_document_id')->constrained()->onDelete('cascade');
$table->foreignId('chart_of_account_id')->constrained();
$table->enum('entry_type', ['debit', 'credit']);
$table->decimal('amount', 15, 2);
$table->date('entry_date');
$table->text('description')->nullable();
$table->timestamps();
// Indexes for performance
$table->index(['finance_document_id', 'entry_type']);
$table->index(['chart_of_account_id', 'entry_date']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('accounting_entries');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('board_meetings', function (Blueprint $table) {
$table->id();
$table->date('meeting_date');
$table->string('title');
$table->text('notes')->nullable();
$table->enum('status', ['scheduled', 'completed', 'cancelled'])->default('scheduled');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('board_meetings');
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('finance_documents', function (Blueprint $table) {
// 只新增尚未存在的欄位
if (!Schema::hasColumn('finance_documents', 'accountant_recorded_by_id')) {
$table->unsignedBigInteger('accountant_recorded_by_id')->nullable();
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('finance_documents', function (Blueprint $table) {
if (Schema::hasColumn('finance_documents', 'accountant_recorded_by_id')) {
$table->dropColumn('accountant_recorded_by_id');
}
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('finance_documents', function (Blueprint $table) {
if (Schema::hasColumn('finance_documents', 'request_type')) {
$table->dropColumn('request_type');
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('finance_documents', function (Blueprint $table) {
if (!Schema::hasColumn('finance_documents', 'request_type')) {
$table->string('request_type')->nullable()->after('amount');
}
});
}
};

View File

@@ -0,0 +1,92 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('incomes', function (Blueprint $table) {
$table->id();
// 基本資訊
$table->string('income_number')->unique(); // 收入編號INC-2025-0001
$table->string('title'); // 收入標題
$table->text('description')->nullable(); // 說明
$table->date('income_date'); // 收入日期
$table->decimal('amount', 12, 2); // 金額
// 收入分類
$table->string('income_type'); // 收入類型
$table->foreignId('chart_of_account_id') // 會計科目
->constrained('chart_of_accounts');
// 付款資訊
$table->string('payment_method'); // 付款方式
$table->string('bank_account')->nullable(); // 銀行帳戶
$table->string('payer_name')->nullable(); // 付款人姓名
$table->string('receipt_number')->nullable(); // 收據編號
$table->string('transaction_reference')->nullable(); // 銀行交易參考號
$table->string('attachment_path')->nullable(); // 附件路徑
// 會員關聯
$table->foreignId('member_id')->nullable()
->constrained()->nullOnDelete();
// 審核流程
$table->string('status')->default('pending'); // pending, confirmed, cancelled
// 出納記錄
$table->foreignId('recorded_by_cashier_id')
->constrained('users');
$table->timestamp('recorded_at');
// 會計確認
$table->foreignId('confirmed_by_accountant_id')->nullable()
->constrained('users');
$table->timestamp('confirmed_at')->nullable();
// 關聯出納日記帳
$table->foreignId('cashier_ledger_entry_id')->nullable()
->constrained('cashier_ledger_entries')->nullOnDelete();
$table->text('notes')->nullable();
$table->timestamps();
// 索引
$table->index('income_date');
$table->index('income_type');
$table->index('status');
$table->index(['member_id', 'income_type']);
});
// 在 accounting_entries 表新增 income_id 欄位
Schema::table('accounting_entries', function (Blueprint $table) {
if (!Schema::hasColumn('accounting_entries', 'income_id')) {
$table->foreignId('income_id')->nullable()
->after('finance_document_id')
->constrained('incomes')->nullOnDelete();
}
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('accounting_entries', function (Blueprint $table) {
if (Schema::hasColumn('accounting_entries', 'income_id')) {
$table->dropForeign(['income_id']);
$table->dropColumn('income_id');
}
});
Schema::dropIfExists('incomes');
}
};

View File

@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('members', function (Blueprint $table) {
// 身心障礙手冊相關欄位
$table->string('disability_certificate_path')->nullable()->after('membership_type');
$table->string('disability_certificate_status')->nullable()->after('disability_certificate_path');
$table->foreignId('disability_verified_by')->nullable()->after('disability_certificate_status')
->constrained('users')->nullOnDelete();
$table->timestamp('disability_verified_at')->nullable()->after('disability_verified_by');
$table->text('disability_rejection_reason')->nullable()->after('disability_verified_at');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('members', function (Blueprint $table) {
$table->dropForeign(['disability_verified_by']);
$table->dropColumn([
'disability_certificate_path',
'disability_certificate_status',
'disability_verified_by',
'disability_verified_at',
'disability_rejection_reason',
]);
});
}
};

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('membership_payments', function (Blueprint $table) {
// 會費類型欄位
$table->string('fee_type')->default('entrance_fee')->after('member_id');
$table->decimal('base_amount', 10, 2)->nullable()->after('amount');
$table->decimal('discount_amount', 10, 2)->default(0)->after('base_amount');
$table->decimal('final_amount', 10, 2)->nullable()->after('discount_amount');
$table->boolean('disability_discount')->default(false)->after('final_amount');
});
// 為現有記錄設定預設值
\DB::statement("UPDATE membership_payments SET base_amount = amount, final_amount = amount WHERE base_amount IS NULL");
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('membership_payments', function (Blueprint $table) {
$table->dropColumn([
'fee_type',
'base_amount',
'discount_amount',
'final_amount',
'disability_discount',
]);
});
}
};

View File

@@ -484,7 +484,10 @@ class ChartOfAccountSeeder extends Seeder
]; ];
foreach ($accounts as $account) { foreach ($accounts as $account) {
ChartOfAccount::create($account); ChartOfAccount::firstOrCreate(
['account_code' => $account['account_code']],
$account
);
} }
} }
} }

View File

@@ -11,48 +11,85 @@ class FinancialWorkflowPermissionsSeeder extends Seeder
{ {
/** /**
* Run the database seeds. * Run the database seeds.
*
* Seeder 建立統一的角色與權限系統,整合:
* - 財務工作流程權限
* - 會員繳費審核權限(原 PaymentVerificationRolesSeeder
* - 基礎角色(原 RoleSeeder
*/ */
public function run(): void public function run(): void
{ {
// Create permissions for financial workflow // Create permissions for financial workflow
$permissions = [ $permissions = [
// Approval Stage Permissions // ===== 會員繳費審核權限(原 PaymentVerificationRolesSeeder =====
'approve_finance_cashier' => '出納審核財務申請單(第一階段)', 'verify_payments_cashier' => '出納審核會員繳費(第一階段)',
'approve_finance_accountant' => '會計審核財務申請單(第二階段)', 'verify_payments_accountant' => '會計審核會員繳費(第二階段)',
'approve_finance_chair' => '理事長審核財務申請單(第三階段)', 'verify_payments_chair' => '理事長審核會員繳費(第三階段)',
'approve_finance_board' => '理事會審核大額財務申請大於50,000', 'activate_memberships' => '啟用會員帳號',
'view_payment_verifications' => '查看繳費審核儀表板',
// Payment Stage Permissions // ===== 財務申請單審核權限(新工作流程) =====
'approve_finance_secretary' => '秘書長審核財務申請單(第一階段)',
'approve_finance_chair' => '理事長審核財務申請單(第二階段:中額以上)',
'approve_finance_board' => '董理事會審核財務申請單(第三階段:大額)',
// Legacy permissions
'approve_finance_cashier' => '出納審核財務申請單(舊流程)',
'approve_finance_accountant' => '會計審核財務申請單(舊流程)',
// ===== 出帳確認權限 =====
'confirm_disbursement_requester' => '申請人確認領款',
'confirm_disbursement_cashier' => '出納確認出帳',
// ===== 入帳確認權限 =====
'confirm_recording_accountant' => '會計確認入帳',
// ===== 收入管理權限 =====
'view_incomes' => '查看收入記錄',
'record_income' => '記錄收入(出納)',
'confirm_income' => '確認收入(會計)',
'cancel_income' => '取消收入',
'export_incomes' => '匯出收入報表',
'view_income_statistics' => '查看收入統計',
// ===== 付款階段權限 =====
'create_payment_order' => '會計製作付款單', 'create_payment_order' => '會計製作付款單',
'verify_payment_order' => '出納覆核付款單', 'verify_payment_order' => '出納覆核付款單',
'execute_payment' => '出納執行付款', 'execute_payment' => '出納執行付款',
'upload_payment_receipt' => '上傳付款憑證', 'upload_payment_receipt' => '上傳付款憑證',
// Recording Stage Permissions // ===== 記錄階段權限 =====
'record_cashier_ledger' => '出納記錄現金簿', 'record_cashier_ledger' => '出納記錄現金簿',
'record_accounting_transaction' => '會計記錄會計分錄', 'record_accounting_transaction' => '會計記錄會計分錄',
'view_cashier_ledger' => '查看出納現金簿', 'view_cashier_ledger' => '查看出納現金簿',
'view_accounting_transactions' => '查看會計分錄', 'view_accounting_transactions' => '查看會計分錄',
// Reconciliation Permissions // ===== 銀行調節權限 =====
'prepare_bank_reconciliation' => '出納製作銀行調節表', 'prepare_bank_reconciliation' => '出納製作銀行調節表',
'review_bank_reconciliation' => '會計覆核銀行調節表', 'review_bank_reconciliation' => '會計覆核銀行調節表',
'approve_bank_reconciliation' => '主管核准銀行調節表', 'approve_bank_reconciliation' => '主管核准銀行調節表',
// General Finance Document Permissions // ===== 財務文件權限 =====
'view_finance_documents' => '查看財務申請單', 'view_finance_documents' => '查看財務申請單',
'create_finance_documents' => '建立財務申請單', 'create_finance_documents' => '建立財務申請單',
'edit_finance_documents' => '編輯財務申請單', 'edit_finance_documents' => '編輯財務申請單',
'delete_finance_documents' => '刪除財務申請單', 'delete_finance_documents' => '刪除財務申請單',
// Chart of Accounts & Budget Permissions // ===== 會計科目與預算權限 =====
'assign_chart_of_account' => '指定會計科目', 'assign_chart_of_account' => '指定會計科目',
'assign_budget_item' => '指定預算項目', 'assign_budget_item' => '指定預算項目',
// Dashboard & Reports Permissions // ===== 儀表板與報表權限 =====
'view_finance_dashboard' => '查看財務儀表板', 'view_finance_dashboard' => '查看財務儀表板',
'view_finance_reports' => '查看財務報表', 'view_finance_reports' => '查看財務報表',
'export_finance_reports' => '匯出財務報表', 'export_finance_reports' => '匯出財務報表',
// ===== 公告系統權限 =====
'view_announcements' => '查看公告',
'create_announcements' => '建立公告',
'edit_announcements' => '編輯公告',
'delete_announcements' => '刪除公告',
'publish_announcements' => '發布公告',
'manage_all_announcements' => '管理所有公告',
]; ];
foreach ($permissions as $name => $description) { foreach ($permissions as $name => $description) {
@@ -63,81 +100,175 @@ class FinancialWorkflowPermissionsSeeder extends Seeder
$this->command->info("Permission created: {$name}"); $this->command->info("Permission created: {$name}");
} }
// Create roles for financial workflow // ===== 建立基礎角色(原 RoleSeeder =====
$baseRoles = [
'admin' => '系統管理員 - 擁有系統所有權限,負責使用者管理、系統設定與維護',
'staff' => '工作人員 - 一般協會工作人員,可檢視文件與協助行政事務',
];
foreach ($baseRoles as $roleName => $description) {
Role::updateOrCreate(
['name' => $roleName, 'guard_name' => 'web'],
['description' => $description]
);
$this->command->info("Base role created: {$roleName}");
}
// ===== 建立財務與會員管理角色 =====
$roles = [ $roles = [
'secretary_general' => [
'permissions' => [
// 財務申請單審核(新工作流程第一階段)
'approve_finance_secretary',
// 一般
'view_finance_documents',
'view_finance_dashboard',
'view_finance_reports',
// 公告系統
'view_announcements',
'create_announcements',
'edit_announcements',
'delete_announcements',
'publish_announcements',
'manage_all_announcements',
],
'description' => '秘書長 - 協會行政負責人,負責初審所有財務申請',
],
'finance_cashier' => [ 'finance_cashier' => [
'permissions' => [ 'permissions' => [
// Approval stage // 會員繳費審核(原 payment_cashier
'verify_payments_cashier',
'view_payment_verifications',
// 財務申請單審核(舊流程,保留)
'approve_finance_cashier', 'approve_finance_cashier',
// Payment stage // 出帳確認(新工作流程)
'confirm_disbursement_cashier',
// 收入管理
'view_incomes',
'record_income',
// 付款階段
'verify_payment_order', 'verify_payment_order',
'execute_payment', 'execute_payment',
'upload_payment_receipt', 'upload_payment_receipt',
// Recording stage // 記錄階段
'record_cashier_ledger', 'record_cashier_ledger',
'view_cashier_ledger', 'view_cashier_ledger',
// Reconciliation // 銀行調節
'prepare_bank_reconciliation', 'prepare_bank_reconciliation',
// General // 一般
'view_finance_documents', 'view_finance_documents',
'view_finance_dashboard', 'view_finance_dashboard',
// 公告系統
'view_announcements',
'create_announcements',
'edit_announcements',
'delete_announcements',
'publish_announcements',
], ],
'description' => '出納 - 管錢(覆核付款單、執行付款、記錄現金簿、製作銀行調節表)', 'description' => '出納 - 負責現金收付、銀行調節表製作、出帳確認、記錄收入',
], ],
'finance_accountant' => [ 'finance_accountant' => [
'permissions' => [ 'permissions' => [
// Approval stage // 會員繳費審核(原 payment_accountant
'verify_payments_accountant',
'view_payment_verifications',
// 財務申請單審核(舊流程,保留)
'approve_finance_accountant', 'approve_finance_accountant',
// Payment stage // 入帳確認(新工作流程)
'confirm_recording_accountant',
// 收入管理
'view_incomes',
'confirm_income',
'cancel_income',
'export_incomes',
'view_income_statistics',
// 付款階段
'create_payment_order', 'create_payment_order',
// Recording stage // 記錄階段
'record_accounting_transaction', 'record_accounting_transaction',
'view_accounting_transactions', 'view_accounting_transactions',
// Reconciliation // 銀行調節
'review_bank_reconciliation', 'review_bank_reconciliation',
// Chart of accounts & budget // 會計科目與預算
'assign_chart_of_account', 'assign_chart_of_account',
'assign_budget_item', 'assign_budget_item',
// General // 一般
'view_finance_documents', 'view_finance_documents',
'view_finance_dashboard', 'view_finance_dashboard',
'view_finance_reports', 'view_finance_reports',
'export_finance_reports', 'export_finance_reports',
// 公告系統
'view_announcements',
'create_announcements',
'edit_announcements',
'delete_announcements',
'publish_announcements',
], ],
'description' => '會計 - 管帳(製作付款單、記錄會計分錄、覆核銀行調節表、指定會計科目)', 'description' => '會計 - 負責會計傳票製作、財務報表編製、入帳確認、確認收入',
], ],
'finance_chair' => [ 'finance_chair' => [
'permissions' => [ 'permissions' => [
// Approval stage // 會員繳費審核(原 payment_chair
'verify_payments_chair',
'view_payment_verifications',
// 財務申請單審核
'approve_finance_chair', 'approve_finance_chair',
// Reconciliation // 銀行調節
'approve_bank_reconciliation', 'approve_bank_reconciliation',
// General // 一般
'view_finance_documents', 'view_finance_documents',
'view_finance_dashboard', 'view_finance_dashboard',
'view_finance_reports', 'view_finance_reports',
'export_finance_reports', 'export_finance_reports',
// 公告系統
'view_announcements',
'create_announcements',
'edit_announcements',
'delete_announcements',
'publish_announcements',
'manage_all_announcements',
], ],
'description' => '理事長 - 審核中大額財務申請、核准銀行調節表', 'description' => '理事長 - 協會負責人,負責核決重大財務支出與會員繳費最終審核',
], ],
'finance_board_member' => [ 'finance_board_member' => [
'permissions' => [ 'permissions' => [
// Approval stage (for large amounts) // 大額審核
'approve_finance_board', 'approve_finance_board',
// General // 一般
'view_finance_documents', 'view_finance_documents',
'view_finance_dashboard', 'view_finance_dashboard',
'view_finance_reports', 'view_finance_reports',
// 公告系統
'view_announcements',
'create_announcements',
'edit_announcements',
'delete_announcements',
'publish_announcements',
], ],
'description' => '理事 - 審核大額財務申請大於50,000', 'description' => '理事 - 理事會成員,協助監督協會運作與審核特定議案',
], ],
'finance_requester' => [ 'finance_requester' => [
'permissions' => [ 'permissions' => [
'view_finance_documents', 'view_finance_documents',
'create_finance_documents', 'create_finance_documents',
'edit_finance_documents', 'edit_finance_documents',
// 出帳確認(新工作流程)
'confirm_disbursement_requester',
], ],
'description' => '財務申請人 - 可建立和編輯自己的財務申請單', 'description' => '財務申請人 - 一般有權申請款項之人員(如活動負責人),可確認領款',
],
'membership_manager' => [
'permissions' => [
'activate_memberships',
'view_payment_verifications',
// 公告系統
'view_announcements',
'create_announcements',
'edit_announcements',
'delete_announcements',
'publish_announcements',
],
'description' => '會員管理員 - 專責處理會員入會審核、資料維護與會籍管理',
], ],
]; ];
@@ -153,24 +284,32 @@ class FinancialWorkflowPermissionsSeeder extends Seeder
$this->command->info("Role created: {$roleName} with permissions: " . implode(', ', $roleData['permissions'])); $this->command->info("Role created: {$roleName} with permissions: " . implode(', ', $roleData['permissions']));
} }
// Assign all financial workflow permissions to admin role (if exists) // Assign all permissions to admin role
$adminRole = Role::where('name', 'admin')->first(); $adminRole = Role::where('name', 'admin')->first();
if ($adminRole) { if ($adminRole) {
$adminRole->givePermissionTo(array_keys($permissions)); $adminRole->givePermissionTo(array_keys($permissions));
$this->command->info("Admin role updated with all financial workflow permissions"); $this->command->info("Admin role updated with all permissions");
} }
$this->command->info("\n=== Financial Workflow Roles & Permissions Created ==="); $this->command->info("\n=== 統一角色系統建立完成 ===");
$this->command->info("Roles created:"); $this->command->info("基礎角色:");
$this->command->info("1. finance_cashier - 出納(管錢)"); $this->command->info(" - admin - 系統管理員");
$this->command->info("2. finance_accountant - 會計(管帳)"); $this->command->info(" - staff - 工作人員");
$this->command->info("3. finance_chair - 理事長"); $this->command->info("\n財務角色:");
$this->command->info("4. finance_board_member - 理事"); $this->command->info(" - secretary_general - 秘書長(新增:財務申請初審)");
$this->command->info("5. finance_requester - 財務申請人"); $this->command->info(" - finance_cashier - 出納(出帳確認)");
$this->command->info("\nWorkflow stages:"); $this->command->info(" - finance_accountant - 會計(入帳確認)");
$this->command->info("1. Approval Stage: Cashier → Accountant → Chair (→ Board for large amounts)"); $this->command->info(" - finance_chair - 理事長(中額以上審核)");
$this->command->info("2. Payment Stage: Accountant creates order → Cashier verifies → Cashier executes"); $this->command->info(" - finance_board_member - 理事(大額審核)");
$this->command->info("3. Recording Stage: Cashier records ledger + Accountant records transactions"); $this->command->info(" - finance_requester - 財務申請人(可確認領款)");
$this->command->info("4. Reconciliation: Cashier prepares → Accountant reviews → Chair approves"); $this->command->info("\n會員管理角色:");
$this->command->info(" - membership_manager - 會員管理員");
$this->command->info("\n新財務申請審核工作流程:");
$this->command->info(" 審核階段:");
$this->command->info(" 小額 (< 5,000): secretary_general");
$this->command->info(" 中額 (5,000-50,000): secretary_general → finance_chair");
$this->command->info(" 大額 (> 50,000): secretary_general → finance_chair → finance_board_member");
$this->command->info(" 出帳階段: finance_requester申請人確認 + finance_cashier出納確認");
$this->command->info(" 入帳階段: finance_accountant會計入帳");
} }
} }

View File

@@ -1,79 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class PaymentVerificationRolesSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create permissions for payment verification workflow
$permissions = [
'verify_payments_cashier' => 'Verify membership payments as cashier (Tier 1)',
'verify_payments_accountant' => 'Verify membership payments as accountant (Tier 2)',
'verify_payments_chair' => 'Verify membership payments as chair (Tier 3)',
'activate_memberships' => 'Activate member accounts after payment approval',
'view_payment_verifications' => 'View payment verification dashboard',
];
foreach ($permissions as $name => $description) {
Permission::firstOrCreate(
['name' => $name],
['guard_name' => 'web']
);
$this->command->info("Permission created: {$name}");
}
// Create roles for payment verification
$roles = [
'payment_cashier' => [
'permissions' => ['verify_payments_cashier', 'view_payment_verifications'],
'description' => 'Cashier - First tier payment verification',
],
'payment_accountant' => [
'permissions' => ['verify_payments_accountant', 'view_payment_verifications'],
'description' => 'Accountant - Second tier payment verification',
],
'payment_chair' => [
'permissions' => ['verify_payments_chair', 'view_payment_verifications'],
'description' => 'Chair - Final tier payment verification',
],
'membership_manager' => [
'permissions' => ['activate_memberships', 'view_payment_verifications'],
'description' => 'Membership Manager - Can activate memberships after approval',
],
];
foreach ($roles as $roleName => $roleData) {
$role = Role::firstOrCreate(
['name' => $roleName],
['guard_name' => 'web']
);
// Assign permissions to role
$role->syncPermissions($roleData['permissions']);
$this->command->info("Role created: {$roleName} with permissions: " . implode(', ', $roleData['permissions']));
}
// Assign all payment verification permissions to admin role (if exists)
$adminRole = Role::where('name', 'admin')->first();
if ($adminRole) {
$adminRole->givePermissionTo([
'verify_payments_cashier',
'verify_payments_accountant',
'verify_payments_chair',
'activate_memberships',
'view_payment_verifications',
]);
$this->command->info("Admin role updated with all payment verification permissions");
}
}
}

View File

@@ -1,27 +0,0 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
class RoleSeeder extends Seeder
{
public function run(): void
{
$roles = [
'admin' => 'Full system administrator',
'staff' => 'General staff with access to internal tools',
'cashier' => 'Handles payment recording and finance intake',
'accountant' => 'Reviews finance docs and approvals',
'chair' => 'Board chairperson for final approvals',
];
collect($roles)->each(function ($description, $role) {
Role::updateOrCreate(
['name' => $role, 'guard_name' => 'web'],
['description' => $description]
);
});
}
}

View File

@@ -38,8 +38,7 @@ class TestDataSeeder extends Seeder
// Ensure required seeders have run // Ensure required seeders have run
$this->call([ $this->call([
RoleSeeder::class, FinancialWorkflowPermissionsSeeder::class,
PaymentVerificationRolesSeeder::class,
ChartOfAccountSeeder::class, ChartOfAccountSeeder::class,
IssueLabelSeeder::class, IssueLabelSeeder::class,
]); ]);
@@ -86,62 +85,68 @@ class TestDataSeeder extends Seeder
$users = []; $users = [];
// 1. Super Admin // 1. Super Admin
$admin = User::create([ $admin = User::firstOrCreate(
'name' => 'Admin User', ['email' => 'admin@test.com'],
'email' => 'admin@test.com', [
'password' => Hash::make('password'), 'name' => 'Admin User',
'is_admin' => true, 'password' => Hash::make('password'),
]); ]
);
$admin->assignRole('admin'); $admin->assignRole('admin');
$users['admin'] = $admin; $users['admin'] = $admin;
// 2. Payment Cashier // 2. Finance Cashier (整合原 payment_cashier)
$cashier = User::create([ $cashier = User::firstOrCreate(
'name' => 'Cashier User', ['email' => 'cashier@test.com'],
'email' => 'cashier@test.com', [
'password' => Hash::make('password'), 'name' => 'Cashier User',
'is_admin' => true, 'password' => Hash::make('password'),
]); ]
$cashier->assignRole('payment_cashier'); );
$cashier->assignRole('finance_cashier');
$users['cashier'] = $cashier; $users['cashier'] = $cashier;
// 3. Payment Accountant // 3. Finance Accountant (整合原 payment_accountant)
$accountant = User::create([ $accountant = User::firstOrCreate(
'name' => 'Accountant User', ['email' => 'accountant@test.com'],
'email' => 'accountant@test.com', [
'password' => Hash::make('password'), 'name' => 'Accountant User',
'is_admin' => true, 'password' => Hash::make('password'),
]); ]
$accountant->assignRole('payment_accountant'); );
$accountant->assignRole('finance_accountant');
$users['accountant'] = $accountant; $users['accountant'] = $accountant;
// 4. Payment Chair // 4. Finance Chair (整合原 payment_chair)
$chair = User::create([ $chair = User::firstOrCreate(
'name' => 'Chair User', ['email' => 'chair@test.com'],
'email' => 'chair@test.com', [
'password' => Hash::make('password'), 'name' => 'Chair User',
'is_admin' => true, 'password' => Hash::make('password'),
]); ]
$chair->assignRole('payment_chair'); );
$chair->assignRole('finance_chair');
$users['chair'] = $chair; $users['chair'] = $chair;
// 5. Membership Manager // 5. Membership Manager
$manager = User::create([ $manager = User::firstOrCreate(
'name' => 'Membership Manager', ['email' => 'manager@test.com'],
'email' => 'manager@test.com', [
'password' => Hash::make('password'), 'name' => 'Membership Manager',
'is_admin' => true, 'password' => Hash::make('password'),
]); ]
);
$manager->assignRole('membership_manager'); $manager->assignRole('membership_manager');
$users['manager'] = $manager; $users['manager'] = $manager;
// 6. Regular Member User // 6. Regular Member User
$member = User::create([ $member = User::firstOrCreate(
'name' => 'Regular Member', ['email' => 'member@test.com'],
'email' => 'member@test.com', [
'password' => Hash::make('password'), 'name' => 'Regular Member',
'is_admin' => false, 'password' => Hash::make('password'),
]); ]
);
$users['member'] = $member; $users['member'] = $member;
return $users; return $users;
@@ -158,97 +163,107 @@ class TestDataSeeder extends Seeder
// 5 Pending Members // 5 Pending Members
for ($i = 0; $i < 5; $i++) { for ($i = 0; $i < 5; $i++) {
$members[] = Member::create([ $members[] = Member::firstOrCreate(
'user_id' => $i === 0 ? $users['member']->id : null, ['email' => "pending{$counter}@test.com"],
'full_name' => "待審核會員 {$counter}", [
'email' => "pending{$counter}@test.com", 'user_id' => $i === 0 ? $users['member']->id : null,
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT), 'full_name' => "待審核會員 {$counter}",
'address_line_1' => "測試地址 {$counter}", 'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'city' => $taiwanCities[array_rand($taiwanCities)], 'address_line_1' => "測試地址 {$counter}",
'postal_code' => '100', 'city' => $taiwanCities[array_rand($taiwanCities)],
'membership_status' => Member::STATUS_PENDING, 'postal_code' => '100',
'membership_type' => Member::TYPE_REGULAR, 'membership_status' => Member::STATUS_PENDING,
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)), 'membership_type' => Member::TYPE_REGULAR,
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)), 'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]); 'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]
);
$counter++; $counter++;
} }
// 8 Active Members // 8 Active Members
for ($i = 0; $i < 8; $i++) { for ($i = 0; $i < 8; $i++) {
$startDate = now()->subMonths(rand(1, 6)); $startDate = now()->subMonths(rand(1, 6));
$members[] = Member::create([ $members[] = Member::firstOrCreate(
'user_id' => null, ['email' => "active{$counter}@test.com"],
'full_name' => "活躍會員 {$counter}", [
'email' => "active{$counter}@test.com", 'user_id' => null,
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT), 'full_name' => "活躍會員 {$counter}",
'address_line_1' => "測試地址 {$counter}", 'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'city' => $taiwanCities[array_rand($taiwanCities)], 'address_line_1' => "測試地址 {$counter}",
'postal_code' => '100', 'city' => $taiwanCities[array_rand($taiwanCities)],
'membership_status' => Member::STATUS_ACTIVE, 'postal_code' => '100',
'membership_type' => $i < 6 ? Member::TYPE_REGULAR : ($i === 6 ? Member::TYPE_HONORARY : Member::TYPE_STUDENT), 'membership_status' => Member::STATUS_ACTIVE,
'membership_started_at' => $startDate, 'membership_type' => $i < 6 ? Member::TYPE_REGULAR : ($i === 6 ? Member::TYPE_HONORARY : Member::TYPE_STUDENT),
'membership_expires_at' => $startDate->copy()->addYear(), 'membership_started_at' => $startDate,
'emergency_contact_name' => "緊急聯絡人 {$counter}", 'membership_expires_at' => $startDate->copy()->addYear(),
'emergency_contact_phone' => '02-12345678', 'emergency_contact_name' => "緊急聯絡人 {$counter}",
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)), 'emergency_contact_phone' => '02-12345678',
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)), 'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]); 'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]
);
$counter++; $counter++;
} }
// 3 Expired Members // 3 Expired Members
for ($i = 0; $i < 3; $i++) { for ($i = 0; $i < 3; $i++) {
$startDate = now()->subYears(2); $startDate = now()->subYears(2);
$members[] = Member::create([ $members[] = Member::firstOrCreate(
'user_id' => null, ['email' => "expired{$counter}@test.com"],
'full_name' => "過期會員 {$counter}", [
'email' => "expired{$counter}@test.com", 'user_id' => null,
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT), 'full_name' => "過期會員 {$counter}",
'address_line_1' => "測試地址 {$counter}", 'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'city' => $taiwanCities[array_rand($taiwanCities)], 'address_line_1' => "測試地址 {$counter}",
'postal_code' => '100', 'city' => $taiwanCities[array_rand($taiwanCities)],
'membership_status' => Member::STATUS_EXPIRED, 'postal_code' => '100',
'membership_type' => Member::TYPE_REGULAR, 'membership_status' => Member::STATUS_EXPIRED,
'membership_started_at' => $startDate, 'membership_type' => Member::TYPE_REGULAR,
'membership_expires_at' => $startDate->copy()->addYear(), 'membership_started_at' => $startDate,
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)), 'membership_expires_at' => $startDate->copy()->addYear(),
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)), 'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]); 'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]
);
$counter++; $counter++;
} }
// 2 Suspended Members // 2 Suspended Members
for ($i = 0; $i < 2; $i++) { for ($i = 0; $i < 2; $i++) {
$members[] = Member::create([ $members[] = Member::firstOrCreate(
'user_id' => null, ['email' => "suspended{$counter}@test.com"],
'full_name' => "停權會員 {$counter}", [
'email' => "suspended{$counter}@test.com", 'user_id' => null,
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT), 'full_name' => "停權會員 {$counter}",
'address_line_1' => "測試地址 {$counter}", 'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'city' => $taiwanCities[array_rand($taiwanCities)], 'address_line_1' => "測試地址 {$counter}",
'postal_code' => '100', 'city' => $taiwanCities[array_rand($taiwanCities)],
'membership_status' => Member::STATUS_SUSPENDED, 'postal_code' => '100',
'membership_type' => Member::TYPE_REGULAR, 'membership_status' => Member::STATUS_SUSPENDED,
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)), 'membership_type' => Member::TYPE_REGULAR,
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)), 'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]); 'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]
);
$counter++; $counter++;
} }
// 2 Additional Pending Members (total 20) // 2 Additional Pending Members (total 20)
for ($i = 0; $i < 2; $i++) { for ($i = 0; $i < 2; $i++) {
$members[] = Member::create([ $members[] = Member::firstOrCreate(
'user_id' => null, ['email' => "newmember{$counter}@test.com"],
'full_name' => "新申請會員 {$counter}", [
'email' => "newmember{$counter}@test.com", 'user_id' => null,
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT), 'full_name' => "新申請會員 {$counter}",
'address_line_1' => "測試地址 {$counter}", 'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'city' => $taiwanCities[array_rand($taiwanCities)], 'address_line_1' => "測試地址 {$counter}",
'postal_code' => '100', 'city' => $taiwanCities[array_rand($taiwanCities)],
'membership_status' => Member::STATUS_PENDING, 'postal_code' => '100',
'membership_type' => Member::TYPE_REGULAR, 'membership_status' => Member::STATUS_PENDING,
]); 'membership_type' => Member::TYPE_REGULAR,
]
);
$counter++; $counter++;
} }
@@ -264,38 +279,41 @@ class TestDataSeeder extends Seeder
$paymentMethods = [ $paymentMethods = [
MembershipPayment::METHOD_BANK_TRANSFER, MembershipPayment::METHOD_BANK_TRANSFER,
MembershipPayment::METHOD_CASH, MembershipPayment::METHOD_CASH,
MembershipPayment::METHOD_CHECK,
]; ];
// 10 Pending Payments // 10 Pending Payments
for ($i = 0; $i < 10; $i++) { for ($i = 0; $i < 10; $i++) {
$payments[] = MembershipPayment::create([ $payments[] = MembershipPayment::firstOrCreate(
'member_id' => $members[$i]->id, ['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
'amount' => 1000, [
'paid_at' => now()->subDays(rand(1, 10)), 'member_id' => $members[$i]->id,
'payment_method' => $paymentMethods[array_rand($paymentMethods)], 'amount' => 1000,
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT), 'paid_at' => now()->subDays(rand(1, 10)),
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf', 'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'status' => MembershipPayment::STATUS_PENDING, 'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'notes' => '待審核的繳費記錄', 'status' => MembershipPayment::STATUS_PENDING,
]); 'notes' => '待審核的繳費記錄',
]
);
} }
// 8 Approved by Cashier // 8 Approved by Cashier
for ($i = 10; $i < 18; $i++) { for ($i = 10; $i < 18; $i++) {
$payments[] = MembershipPayment::create([ $payments[] = MembershipPayment::firstOrCreate(
'member_id' => $members[$i % count($members)]->id, ['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
'amount' => 1000, [
'paid_at' => now()->subDays(rand(5, 15)), 'member_id' => $members[$i % count($members)]->id,
'payment_method' => $paymentMethods[array_rand($paymentMethods)], 'amount' => 1000,
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT), 'paid_at' => now()->subDays(rand(5, 15)),
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf', 'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'status' => MembershipPayment::STATUS_APPROVED_CASHIER, 'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'verified_by_cashier_id' => $users['cashier']->id, 'status' => MembershipPayment::STATUS_APPROVED_CASHIER,
'cashier_verified_at' => now()->subDays(rand(3, 12)), 'verified_by_cashier_id' => $users['cashier']->id,
'cashier_notes' => '收據已核對,金額無誤', 'cashier_verified_at' => now()->subDays(rand(3, 12)),
'notes' => '已通過出納審核', 'cashier_notes' => '收據已核對,金額無誤',
]); 'notes' => '已通過出納審核',
]
);
} }
// 6 Approved by Accountant // 6 Approved by Accountant
@@ -303,22 +321,24 @@ class TestDataSeeder extends Seeder
$cashierVerifiedAt = now()->subDays(rand(10, 20)); $cashierVerifiedAt = now()->subDays(rand(10, 20));
$accountantVerifiedAt = $cashierVerifiedAt->copy()->addDays(rand(1, 3)); $accountantVerifiedAt = $cashierVerifiedAt->copy()->addDays(rand(1, 3));
$payments[] = MembershipPayment::create([ $payments[] = MembershipPayment::firstOrCreate(
'member_id' => $members[$i % count($members)]->id, ['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
'amount' => 1000, [
'paid_at' => now()->subDays(rand(15, 25)), 'member_id' => $members[$i % count($members)]->id,
'payment_method' => $paymentMethods[array_rand($paymentMethods)], 'amount' => 1000,
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT), 'paid_at' => now()->subDays(rand(15, 25)),
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf', 'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT, 'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'verified_by_cashier_id' => $users['cashier']->id, 'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT,
'cashier_verified_at' => $cashierVerifiedAt, 'verified_by_cashier_id' => $users['cashier']->id,
'cashier_notes' => '收據已核對,金額無誤', 'cashier_verified_at' => $cashierVerifiedAt,
'verified_by_accountant_id' => $users['accountant']->id, 'cashier_notes' => '收據已核對,金額無誤',
'accountant_verified_at' => $accountantVerifiedAt, 'verified_by_accountant_id' => $users['accountant']->id,
'accountant_notes' => '帳務核對完成', 'accountant_verified_at' => $accountantVerifiedAt,
'notes' => '已通過會計審核', 'accountant_notes' => '帳務核對完成',
]); 'notes' => '已通過會計審核',
]
);
} }
// 4 Fully Approved (Chair approved - member activated) // 4 Fully Approved (Chair approved - member activated)
@@ -327,42 +347,46 @@ class TestDataSeeder extends Seeder
$accountantVerifiedAt = $cashierVerifiedAt->copy()->addDays(rand(1, 3)); $accountantVerifiedAt = $cashierVerifiedAt->copy()->addDays(rand(1, 3));
$chairVerifiedAt = $accountantVerifiedAt->copy()->addDays(rand(1, 2)); $chairVerifiedAt = $accountantVerifiedAt->copy()->addDays(rand(1, 2));
$payments[] = MembershipPayment::create([ $payments[] = MembershipPayment::firstOrCreate(
'member_id' => $members[$i % count($members)]->id, ['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
'amount' => 1000, [
'paid_at' => now()->subDays(rand(25, 35)), 'member_id' => $members[$i % count($members)]->id,
'payment_method' => $paymentMethods[array_rand($paymentMethods)], 'amount' => 1000,
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT), 'paid_at' => now()->subDays(rand(25, 35)),
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf', 'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'status' => MembershipPayment::STATUS_APPROVED_CHAIR, 'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'verified_by_cashier_id' => $users['cashier']->id, 'status' => MembershipPayment::STATUS_APPROVED_CHAIR,
'cashier_verified_at' => $cashierVerifiedAt, 'verified_by_cashier_id' => $users['cashier']->id,
'cashier_notes' => '收據已核對,金額無誤', 'cashier_verified_at' => $cashierVerifiedAt,
'verified_by_accountant_id' => $users['accountant']->id, 'cashier_notes' => '收據已核對,金額無誤',
'accountant_verified_at' => $accountantVerifiedAt, 'verified_by_accountant_id' => $users['accountant']->id,
'accountant_notes' => '帳務核對完成', 'accountant_verified_at' => $accountantVerifiedAt,
'verified_by_chair_id' => $users['chair']->id, 'accountant_notes' => '帳務核對完成',
'chair_verified_at' => $chairVerifiedAt, 'verified_by_chair_id' => $users['chair']->id,
'chair_notes' => '最終批准', 'chair_verified_at' => $chairVerifiedAt,
'notes' => '已完成三階段審核', 'chair_notes' => '最終批准',
]); 'notes' => '已完成三階段審核',
]
);
} }
// 2 Rejected Payments // 2 Rejected Payments
for ($i = 28; $i < 30; $i++) { for ($i = 28; $i < 30; $i++) {
$payments[] = MembershipPayment::create([ $payments[] = MembershipPayment::firstOrCreate(
'member_id' => $members[$i % count($members)]->id, ['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
'amount' => 1000, [
'paid_at' => now()->subDays(rand(5, 10)), 'member_id' => $members[$i % count($members)]->id,
'payment_method' => $paymentMethods[array_rand($paymentMethods)], 'amount' => 1000,
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT), 'paid_at' => now()->subDays(rand(5, 10)),
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf', 'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'status' => MembershipPayment::STATUS_REJECTED, 'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'rejected_by_user_id' => $users['cashier']->id, 'status' => MembershipPayment::STATUS_REJECTED,
'rejected_at' => now()->subDays(rand(3, 8)), 'rejected_by_user_id' => $users['cashier']->id,
'rejection_reason' => $i === 28 ? '收據影像不清晰,無法辨識' : '金額與收據不符', 'rejected_at' => now()->subDays(rand(3, 8)),
'notes' => '已退回', 'rejection_reason' => $i === 28 ? '收據影像不清晰,無法辨識' : '金額與收據不符',
]); 'notes' => '已退回',
]
);
} }
return $payments; return $payments;
@@ -747,12 +771,12 @@ class TestDataSeeder extends Seeder
$this->command->table( $this->command->table(
['Role', 'Email', 'Password', 'Permissions'], ['Role', 'Email', 'Password', 'Permissions'],
[ [
['Admin', 'admin@test.com', 'password', 'All permissions'], ['Admin (admin)', 'admin@test.com', 'password', 'All permissions'],
['Cashier', 'cashier@test.com', 'password', 'Tier 1 payment verification'], ['Finance Cashier (finance_cashier)', 'cashier@test.com', 'password', 'Payment + Finance cashier'],
['Accountant', 'accountant@test.com', 'password', 'Tier 2 payment verification'], ['Finance Accountant (finance_accountant)', 'accountant@test.com', 'password', 'Payment + Finance accountant'],
['Chair', 'chair@test.com', 'password', 'Tier 3 payment verification'], ['Finance Chair (finance_chair)', 'chair@test.com', 'password', 'Payment + Finance chair'],
['Manager', 'manager@test.com', 'password', 'Membership activation'], ['Membership Manager (membership_manager)', 'manager@test.com', 'password', 'Membership activation'],
['Member', 'member@test.com', 'password', 'Member dashboard access'], ['Member (no role)', 'member@test.com', 'password', 'Member dashboard access'],
] ]
); );
$this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); $this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

518
lang/zh_TW.json Normal file
View File

@@ -0,0 +1,518 @@
{
"Dashboard": "儀表板",
"My Membership": "我的會籍",
"Documents": "文件",
"Admin: Members": "管理:會員",
"Admin: Roles": "管理:角色",
"Admin: Finance": "管理:財務",
"Admin: Budgets": "管理:預算",
"Admin: Transactions": "管理:交易",
"Admin: Issues": "管理:問題",
"Admin: Audit Logs": "管理:審計日誌",
"Admin: Document Categories": "管理:文件分類",
"Admin: Documents": "管理:文件",
"Admin: System Settings": "管理:系統設定",
"Log Out": "登出",
"Profile": "個人檔案",
"Management": "管理後台",
"Skip to main content": "跳至主要內容",
"Complete Your Membership Profile": "完成您的會員資料",
"To access the member area, please provide your membership details. This information is required for our records.": "若要存取會員專區,請提供您的會員詳細資料。我們需要這些資訊以建立檔案。",
"Basic Information": "基本資訊",
"Full Name": "全名",
"Phone": "電話",
"National ID (Optional)": "身分證字號(選填)",
"Your national ID will be encrypted for security.": "您的身分證字號將會加密儲存以確保安全。",
"Address": "地址",
"Address Line 1": "地址第一行",
"Address Line 2": "地址第二行",
"City": "城市",
"Postal Code": "郵遞區號",
"Emergency Contact": "緊急聯絡人",
"Emergency Contact Name": "緊急聯絡人姓名",
"Emergency Contact Phone": "緊急聯絡人電話",
"I accept the terms and conditions and agree to submit payment for membership activation.": "我接受條款與條件,並同意提交繳費以啟用會籍。",
"Complete Profile": "完成資料",
"Submit Membership Payment": "提交會費",
"Payment Instructions": "繳費說明",
"Annual membership fee: TWD 1,000": "年度會費:新台幣 1,000 元",
"Please upload your payment receipt (bank transfer, convenience store payment, etc.)": "請上傳您的繳費收據(銀行轉帳、超商繳費等)。",
"Your payment will be reviewed by our staff. You will receive an email notification once approved.": "我們的工作人員將會審核您的款項。審核通過後,您將收到電子郵件通知。",
"Payment Amount (TWD)": "繳費金額 (TWD)",
"Payment Date": "繳費日期",
"Payment Method": "繳費方式",
"Select payment method": "選擇繳費方式",
"Bank Transfer / ATM": "銀行轉帳 / ATM",
"Convenience Store (7-11, FamilyMart, etc.)": "便利商店 (7-11, 全家等)",
"Cash (In-person)": "現金 (親自繳納)",
"Credit Card": "信用卡",
"Transaction / Reference Number": "交易 / 參考編號",
"e.g., Bank transaction number, receipt number": "例如:銀行交易序號、收據號碼",
"Payment Receipt": "繳費收據",
"Upload your payment receipt (JPG, PNG, or PDF, max 10MB)": "上傳您的繳費收據 (JPG, PNG 或 PDF最大 10MB)",
"Notes (Optional)": "備註(選填)",
"Any additional information...": "任何額外資訊...",
"Cancel": "取消",
"Submit Payment": "提交繳費",
"Register as a member to access exclusive resources and participate in our organization activities.": "註冊成為會員以存取專屬資源並參與我們的組織活動。",
"Contact Information": "聯絡資訊",
"Already have an account?": "已經有帳號了嗎?",
"Register as Member": "註冊為會員",
"Email": "電子郵件",
"Password": "密碼",
"Confirm Password": "確認密碼",
"Remember me": "記住我",
"Forgot your password?": "忘記密碼?",
"Log in": "登入",
"Payment Under Review": "款項審核中",
"Your payment of TWD :amount submitted on :date is currently being verified.": "您於 :date 提交的新台幣 :amount 元款項正在審核中。",
"Current status": "目前狀態",
"Activate Your Membership": "啟用您的會籍",
"To activate your membership and access member-only resources, please submit your payment proof.": "若要啟用您的會籍並存取會員專屬資源,請提交您的繳費證明。",
"Membership Information": "會籍資訊",
"Member Name": "會員姓名",
"Membership Type": "會員類型",
"Membership Status": "會籍狀態",
"Membership Start Date": "會籍開始日期",
"Not set": "未設定",
"Membership Expiry Date": "會籍到期日期",
"Payment History": "繳費紀錄",
"Paid At": "繳費時間",
"Amount": "金額",
"Method": "方式",
"Status": "狀態",
"Details": "詳細資訊",
"N/A": "無",
"Rejection Reason": "退件原因",
"View Reason": "查看原因",
"Cashier": "出納",
"Accountant": "會計",
"Chair": "理事長",
"Approved on :date": "於 :date 核准",
"No payment records found.": "找不到繳費紀錄。",
"Submit your first payment to activate your membership.": "提交您的第一筆繳費以啟用會籍。",
"Admin Dashboard": "管理員儀表板",
"You have :count finance document(s) waiting for your approval.": "您有 :count 份財務文件等待審核。",
"View pending approvals": "查看待審核項目",
"Total Members": "會員總數",
"View all": "查看全部",
"Active Members": "有效會員",
"View active": "查看有效",
"Expired Members": "過期會員",
"View expired": "查看過期",
"Expiring in 30 Days": "30 天內到期",
"Renewal reminders needed": "需要續費提醒",
"Total Revenue": "總收入",
"total payments": "筆總繳費",
"This Month": "本月",
"payments this month": "筆本月繳費",
"Finance Documents": "財務文件",
"View pending": "查看待處理",
"Recent Payments": "近期繳費",
"Finance Document Status": "財務文件狀態",
"Pending Approval": "等待審核",
"Fully Approved": "已完全核准",
"Rejected": "已退件",
"New Finance Document": "新財務文件",
"Member (optional)": "會員(選填)",
"Not linked to a member": "未連結至會員",
"Title": "標題",
"Description": "描述",
"Attachment (optional)": "附件(選填)",
"Max file size: 10MB": "檔案大小上限10MB",
"Submit Document": "提交文件",
"Edit Issue": "編輯問題",
"Brief summary of the issue": "問題簡述",
"Detailed description of the issue...": "問題詳細描述...",
"Issue Type": "問題類型",
"Select type...": "選擇類型...",
"Work Item": "工作項目",
"Project Task": "專案任務",
"Maintenance": "維護",
"Member Request": "會員請求",
"Priority": "優先級",
"Select priority...": "選擇優先級...",
"Low": "低",
"Medium": "中",
"High": "高",
"Urgent": "緊急",
"Assign To": "指派給",
"Unassigned": "未指派",
"Reviewer": "審閱者",
"None": "無",
"Due Date": "截止日期",
"Estimated Hours": "預估工時",
"Related Member": "相關會員",
"Parent Issue": "父問題",
"None (top-level issue)": "無(頂層問題)",
"Labels": "標籤",
"Update Issue": "更新問題",
"Created by": "建立者",
"Edit": "編輯",
"Delete": "刪除",
"Assigned To": "指派給",
"Overdue by :days days": "逾期 :days 天",
":days days left": "剩餘 :days 天",
"No due date": "無截止日期",
"Time Tracking": "工時追蹤",
"estimated": "預估",
"Sub-tasks": "子任務",
"Actions": "動作",
"New": "新",
"Assigned": "已指派",
"In Progress": "進行中",
"Review": "審閱中",
"Closed": "已結案",
"Update Status": "更新狀態",
"Assign": "指派",
"Comments": "留言",
"Internal": "內部",
"No comments yet": "尚無留言",
"Add a comment...": "新增留言...",
"Internal comment": "內部留言",
"Add Comment": "新增留言",
"Attachments": "附件",
"Download": "下載",
"Delete this attachment?": "刪除此附件?",
"No attachments": "無附件",
"Upload": "上傳",
"Max size: 10MB": "最大 10MB",
"No time logged yet": "尚無工時紀錄",
"Hours": "小時",
"What did you do?": "您做了什麼?",
"Log Time": "記錄工時",
"Progress": "進度",
"Completion": "完成度",
"Watchers": "關注者",
"Remove": "移除",
"Add watcher...": "新增關注者...",
"Add": "新增",
"Issues": "問題",
"Issue Tracker": "問題追蹤器",
"Manage work items, tasks, and member requests": "管理工作項目、任務與會員請求",
"Create Issue": "建立問題",
"Total Open": "未結案總數",
"Assigned to Me": "指派給我",
"Overdue": "逾期",
"High Priority": "高優先級",
"Type": "類型",
"All Types": "所有類型",
"All Statuses": "所有狀態",
"All Priorities": "所有優先級",
"Assignee": "受派者",
"All Assignees": "所有受派者",
"Search": "搜尋",
"Issue number, title, or description...": "問題編號、標題或描述...",
"Filter": "篩選",
"Show closed": "顯示已結案",
"Issue": "問題",
"View": "檢視",
"Create First Issue": "建立第一個問題",
"No issues found": "找不到問題",
"Budget Details": "預算詳情",
"Budgeted Income": "預算收入",
"Budgeted Expense": "預算支出",
"Actual Income": "實際收入",
"Actual Expense": "實際支出",
"Income": "收入",
"Account": "科目",
"Budgeted": "預算",
"Actual": "實際",
"Variance": "差異",
"Utilization": "使用率",
"Edit Budget": "編輯預算",
"Budget Name": "預算名稱",
"Period Start": "期間開始",
"Period End": "期間結束",
"Notes": "備註",
"Add Income Item": "新增收入項目",
"Select account...": "選擇科目...",
"No income items. Click \"Add Income Item\" to get started.": "無收入項目。點擊「新增收入項目」以開始。",
"Add Expense Item": "新增支出項目",
"No expense items. Click \"Add Expense Item\" to get started.": "無支出項目。點擊「新增支出項目」以開始。",
"Save Budget": "儲存預算",
"Budget List": "預算列表",
"Manage annual budgets and track financial performance": "管理年度預算與追蹤財務績效",
"Create new budget": "建立新預算",
"All Years": "所有年份",
"Draft": "草稿",
"Submitted": "已提交",
"Approved": "已核准",
"Active": "啟用",
"All Accounts": "所有科目",
"Start Date": "開始日期",
"End Date": "結束日期",
"Apply Filter": "套用篩選",
"Total Issues": "問題總數",
"Open Issues": "未結案問題",
"Closed Issues": "已結案問題",
"Overdue Issues": "逾期問題",
"Issues by Status": "依狀態分佈",
"Issues by Priority": "依優先級分佈",
"Issues by Type": "依類型分佈",
"Time Tracking Metrics": "工時追蹤指標",
"Total Estimated Hours": "總預估工時",
"Total Actual Hours": "總實際工時",
"Avg Estimated Hours": "平均預估工時",
"Avg Actual Hours": "平均實際工時",
"Average Resolution Time": "平均解決時間",
"days": "天",
"Assignee Performance (Top 10)": "受派者績效 (前 10 名)",
"Total Assigned": "總指派",
"Completed": "已完成",
"Completion Rate": "完成率",
"Top Labels Used": "最常用標籤",
"uses": "次使用",
"Recent Issues": "近期問題",
"Created": "建立時間",
"Member details": "會員詳情",
"Ready for Activation": "準備啟用",
"This member has a fully approved payment and is ready for membership activation.": "此會員已有完全核准的繳費,準備啟用會籍。",
"Activate Membership": "啟用會籍",
"Profile photo": "個人照片",
"Membership start": "會籍開始",
"Membership expires": "會籍到期",
"Roles": "角色",
"No roles assigned.": "未指派角色。",
"Select roles for this member's user account.": "為此會員的使用者帳號選擇角色。",
"Update Roles": "更新角色",
"Payment history": "繳費歷史",
"Record payment": "記錄繳費",
"Paid at": "繳費時間",
"Submitted By": "提交者",
"Verify": "審核",
"Receipt": "收據",
"Download Receipt": "下載收據",
"Edit member": "編輯會員",
"Full name": "全名",
"National ID": "身分證字號",
"Will be stored encrypted for security.": "將加密儲存以確保安全。",
"Membership start date": "會籍開始日期",
"Membership expiry date": "會籍到期日期",
"Create Issue": "建立問題",
"Optional: Assign to a team member": "選填:指派給團隊成員",
"Estimated time to complete this issue": "預估完成此問題所需時間",
"Link to a member for member requests": "連結至會員以追蹤會員請求",
"Make this a sub-task of another issue": "設為另一個問題的子任務",
"Select one or more labels to categorize this issue": "選擇一個或多個標籤以分類此問題",
"Creating Issues": "建立問題說明",
"Use work items for general tasks and todos": "使用工作項目於一般任務和待辦事項",
"Project tasks are for specific project milestones": "專案任務用於特定專案里程碑",
"member requests track inquiries or requests from members": "會員請求用於追蹤會員的詢問或需求",
"Assign issues to team members to track responsibility": "指派問題給團隊成員以追蹤責任",
"Use labels to categorize and filter issues easily": "使用標籤以便於分類與篩選問題",
"Create new member": "建立新會員",
"An activation email will be sent to this address.": "啟用信將會寄送至此地址。",
"Create member": "建立會員",
"Members": "會員",
"Search by name, email, phone, or national ID": "搜尋姓名、Email、電話或身分證字號",
"Enter search term...": "輸入搜尋關鍵字...",
"Searches in name, email, phone number, and national ID": "搜尋範圍包含姓名、Email、電話與身分證",
"Membership status": "會籍狀態",
"All": "全部",
"Expiring Soon (30 days)": "即將到期 (30 天)",
"Payment status": "繳費狀態",
"Has Payments": "有繳費",
"No Payments": "無繳費",
"Date range": "日期範圍",
"Toggle Date Filters": "切換日期篩選",
"Joined from": "加入日期(起)",
"Joined to": "加入日期(迄)",
"Apply filters": "套用篩選",
"Export CSV": "匯出 CSV",
"Create Member": "建立會員",
"Import CSV": "匯入 CSV",
"Name": "姓名",
"Membership Expires": "會籍到期",
"View": "檢視",
"No members found.": "找不到會員。",
"Create Role": "建立角色",
"Users": "使用者",
"Role Details": "角色詳情",
"No description": "無描述",
"Assign Users": "指派使用者",
"Select users to assign": "選擇要指派的使用者",
"Assign Selected": "指派已選",
"Users with this role": "擁有此角色的使用者",
"Search users": "搜尋使用者",
"Remove": "移除",
"Remove this role from the user?": "確定要移除此使用者的角色嗎?",
"No users assigned to this role.": "此角色尚未指派給任何使用者。",
"Import members from CSV": "從 CSV 匯入會員",
"Upload a CSV file with the following header columns (existing members matched by email are updated):": "上傳包含以下標題欄位的 CSV 檔案(已存在的會員將依 Email 更新):",
"CSV file": "CSV 檔案",
"Start import": "開始匯入",
"Transaction Type": "交易類型",
"Income Accounts": "收入科目",
"Expense Accounts": "支出科目",
"Transaction Date": "交易日期",
"Reference Number": "參考編號",
"Receipt or invoice number (optional)": "收據或發票號碼(選填)",
"Optional reference or receipt number": "選填的參考或收據編號",
"Link to Budget": "連結至預算",
"No budget link (standalone transaction)": "無預算連結(獨立交易)",
"Link this transaction to a budget item to track actual vs budgeted amounts": "將此交易連結至預算項目以追蹤實際與預算金額",
"Record Transaction": "記錄交易",
"Recording Transactions": "記錄交易說明",
"Choose income or expense type based on money flow": "依資金流向選擇收入或支出類型",
"Select the appropriate chart of account for categorization": "選擇適當的會計科目進行分類",
"Link to a budget item to automatically update budget vs actual tracking": "連結至預算項目以自動更新預算與實際追蹤",
"Add reference numbers for audit trails and reconciliation": "新增參考編號以利審計軌跡與對帳",
"Search actions or metadata": "搜尋動作或詮釋資料",
"User": "使用者",
"Start date": "開始日期",
"End date": "結束日期",
"Time": "時間",
"Metadata": "詮釋資料",
"System": "系統",
"Edit payment for :name": "編輯 :name 的繳費",
"Reference": "參考",
"Save changes": "儲存變更",
"Record payment for :name": "記錄 :name 的繳費",
"Save payment": "儲存繳費",
"Delete Account": "刪除帳號",
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "帳號一旦刪除,所有資源與資料將被永久刪除。在刪除帳號前,請下載您希望保留的任何資料或資訊。",
"Are you sure you want to delete your account?": "您確定要刪除帳號嗎?",
"Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "帳號一旦刪除,所有資源與資料將被永久刪除。請輸入密碼以確認您要永久刪除此帳號。",
"Profile Information": "個人資料",
"This is your current profile photo.": "這是您目前的個人照片。",
"Profile Photo": "個人照片",
"Your email address is unverified.": "您的電子郵件地址尚未驗證。",
"Click here to re-send the verification email.": "點擊此處以重新發送驗證信。",
"A new verification link has been sent to your email address.": "新的驗證連結已發送至您的電子郵件地址。",
"Update Password": "更新密碼",
"Ensure your account is using a long, random password to stay secure.": "請確保您的帳號使用長且隨機的密碼以保持安全。",
"Current Password": "目前密碼",
"New Password": "新密碼",
"Saved.": "已儲存。",
"Hello": "你好",
"This is a reminder that your membership with :app is scheduled to expire on :date.": "提醒您,您在 :app 的會籍預計於 :date 到期。",
"This is a reminder to check your membership status with :app.": "提醒您檢查您在 :app 的會籍狀態。",
"If you have already renewed, you can ignore this email.": "如果您已經續費,請忽略此郵件。",
"Thank you,": "謝謝,",
"You have been registered as a member on :app.": "您已在 :app 註冊成為會員。",
"To activate your online account and set your password, please open the link below:": "若要啟用您的線上帳號並設定密碼,請開啟以下連結:",
"If you did not expect this email, you can ignore it.": "如果您未預期收到此郵件,請忽略它。",
"Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.": "感謝您的註冊!在開始之前,請點擊我們剛寄給您的連結以驗證您的電子郵件地址。如果您沒有收到郵件,我們很樂意再寄一次。",
"A new verification link has been sent to the email address you provided during registration.": "新的驗證連結已發送至您註冊時提供的電子郵件地址。",
"Resend Verification Email": "重發驗證信",
"Already registered?": "已經註冊了?",
"Register": "註冊",
"Reset Password": "重設密碼",
"Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.": "忘記密碼?沒問題。只要告訴我們您的電子郵件地址,我們就會寄送密碼重設連結給您,讓您設定新密碼。",
"Email Password Reset Link": "寄送密碼重設連結",
"This is a secure area of the application. Please confirm your password before continuing.": "這是應用程式的安全區域。請在繼續之前確認您的密碼。",
"Confirm": "確認",
"Label": "標籤",
"Issues": "問題",
"Create Label": "建立標籤",
"Edit Label": "編輯標籤",
"Color": "顏色",
"Choose a color for this label": "選擇標籤顏色",
"Payment Verification Dashboard": "繳費審核儀表板",
"All Payments": "所有繳費",
"Cashier Queue": "出納佇列",
"Accountant Queue": "會計佇列",
"Chair Queue": "理事長佇列",
"View & Verify": "檢視與審核",
"No payments found": "找不到繳費紀錄",
"Payment Verification": "繳費審核",
"Payment Details": "繳費詳情",
"Verification History": "審核歷程",
"Verified by Cashier": "出納已審核",
"Verified by Accountant": "會計已審核",
"Final Approval by Chair": "理事長最終核准",
"Rejected by": "退件者",
"Verification Actions": "審核動作",
"Approve as Cashier": "以出納身分核准",
"Approve as Accountant": "以會計身分核准",
"Final Approval as Chair": "以理事長身分最終核准",
"Reject Payment": "退件",
"Back to List": "返回列表",
"Finance Document Details": "財務文件詳情",
"Back to list": "返回列表",
"Document Information": "文件資訊",
"Submitted by": "提交者",
"Submitted at": "提交時間",
"Attachment": "附件",
"Download Attachment": "下載附件",
"Approval Timeline": "審核時間軸",
"Cashier Approval": "出納審核",
"Pending": "待處理",
"Accountant Approval": "會計審核",
"Chair Approval": "理事長審核",
"Reason:": "原因:",
"Approve": "核准",
"Reject": "退件",
"Reject Document": "文件退件",
"Cancel": "取消",
"Issue Reports & Analytics": "問題報告與分析",
"Average Resolution Time": "平均解決時間",
"Label created successfully.": "標籤建立成功。",
"Label updated successfully.": "標籤更新成功。",
"Label deleted successfully.": "標籤刪除成功。",
"Payment recorded successfully.": "繳費記錄成功。",
"Payment updated successfully.": "繳費更新成功。",
"Payment deleted.": "繳費已刪除。",
"Role created.": "角色已建立。",
"Role updated.": "角色已更新。",
"Users assigned to role.": "使用者已指派至角色。",
"Role removed from user.": "角色已從使用者移除。",
"Profile completed! Please submit your membership payment.": "資料已完成!請提交您的會費。",
"Issue created successfully.": "問題建立成功。",
"Cannot edit closed issues.": "無法編輯已結案的問題。",
"Issue updated successfully.": "問題更新成功。",
"Issue deleted successfully.": "問題刪除成功。",
"Issue assigned successfully.": "問題指派成功。",
"Issue must be assigned before moving to in progress.": "問題必須先指派才能開始進行。",
"Issue must be in progress before moving to review.": "問題必須先進行中才能進入審閱。",
"Cannot close issue in current state.": "目前狀態無法結案。",
"Issue status updated successfully.": "問題狀態更新成功。",
"Comment added successfully.": "留言新增成功。",
"File uploaded successfully.": "檔案上傳成功。",
"Attachment deleted successfully.": "附件刪除成功。",
"Time logged successfully.": "工時記錄成功。",
"User is already watching this issue.": "使用者已經在關注此問題。",
"Watcher added successfully.": "關注者新增成功。",
"Watcher removed successfully.": "關注者移除成功。",
"Registration successful! Please submit your membership payment to complete your registration.": "註冊成功!請提交您的會費以完成註冊。",
"Budget created successfully. Add budget items below.": "預算建立成功。請在下方新增預算項目。",
"This budget cannot be edited.": "此預算無法編輯。",
"Budget updated successfully.": "預算更新成功。",
"Cannot submit budget without budget items.": "預算項目為空時無法提交。",
"Budget submitted for approval.": "預算已提交審核。",
"Budget approved successfully.": "預算核准成功。",
"Budget activated successfully.": "預算啟用成功。",
"Budget closed successfully.": "預算已結案。",
"Budget deleted successfully.": "預算刪除成功。",
"This payment cannot be approved at this stage.": "此繳費目前無法核准。",
"Payment approved by cashier. Forwarded to accountant for review.": "出納已核准。轉交會計審閱。",
"Payment approved by accountant. Forwarded to chair for final approval.": "會計已核准。轉交理事長最終核准。",
"Payment fully approved! Member can now be activated by membership manager.": "繳費已完全核准!管理員現在可以啟用會員。",
"Cannot reject a fully approved payment.": "無法退件已完全核准的繳費。",
"Payment rejected. Member has been notified.": "繳費已退件。已通知會員。",
"You must have a member account to submit payment.": "您必須擁有會員帳號才能提交繳費。",
"You cannot submit payment at this time. You may already have a pending payment or your membership is already active.": "您目前無法提交繳費。您可能有待處理的繳費或會籍已啟用。",
"You cannot submit payment at this time.": "您目前無法提交繳費。",
"Payment submitted successfully! We will review your payment and notify you once verified.": "繳費提交成功!我們將審核您的款項,驗證後會通知您。",
"Transaction recorded successfully.": "交易記錄成功。",
"Cannot edit auto-generated transactions.": "無法編輯自動產生的交易。",
"Transaction updated successfully.": "交易更新成功。",
"Transaction deleted successfully.": "交易刪除成功。",
"Member created successfully. Activation email has been sent.": "會員建立成功。啟用信已發送。",
"Member updated successfully.": "會員更新成功。",
"Import completed.": "匯入完成。",
"Roles updated.": "角色已更新。",
"Member must have an approved payment before activation.": "會員必須有已核准的繳費才能啟用。",
"Membership activated successfully! Member has been notified.": "會籍啟用成功!已通知會員。",
"You must be a registered member to access this resource.": "您必須是註冊會員才能存取此資源。",
"This resource is only available to active paid members. Please complete your membership payment and activation.": "此資源僅供有效付費會員使用。請完成您的會費繳納與啟用。",
"Blocks": "阻塞",
"Blocked by": "被阻塞於",
"Related to": "相關於",
"Duplicate of": "重複於",
"Your membership is expiring soon": "您的會籍即將到期",
"Activate your membership account": "啟用您的會員帳號",
"Default: One year from start date": "預設:從開始日期起一年",
"After activation, the member will receive a confirmation email and gain access to member-only resources.": "啟用後,會員將收到確認信並可存取會員專屬資源。"
}

5
package-lock.json generated
View File

@@ -1064,7 +1064,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.25", "baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754", "caniuse-lite": "^1.0.30001754",
@@ -1767,7 +1766,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -2091,7 +2089,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -2576,7 +2573,6 @@
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -2696,7 +2692,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,129 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
建立公告
</h2>
<a href="{{ route('admin.announcements.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
返回列表
</a>
</div>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<form method="POST" action="{{ route('admin.announcements.store') }}" class="space-y-6 p-6">
@csrf
<!-- Title -->
<div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">標題 <span class="text-red-500">*</span></label>
<input type="text" name="title" id="title" value="{{ old('title') }}" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="輸入公告標題">
@error('title')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Content -->
<div>
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">內容 <span class="text-red-500">*</span></label>
<textarea name="content" id="content" rows="10" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="輸入公告內容">{{ old('content') }}</textarea>
@error('content')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Access Level -->
<div>
<label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">存取權限 <span class="text-red-500">*</span></label>
<select name="access_level" id="access_level" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="members" {{ old('access_level') === 'members' ? 'selected' : '' }}>會員(需付費會籍)</option>
<option value="public" {{ old('access_level') === 'public' ? 'selected' : '' }}>公開(所有人可見)</option>
<option value="board" {{ old('access_level') === 'board' ? 'selected' : '' }}>理事會(僅理事可見)</option>
<option value="admin" {{ old('access_level') === 'admin' ? 'selected' : '' }}>管理員(僅管理員可見)</option>
</select>
@error('access_level')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Published At -->
<div>
<label for="published_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">發布時間(選填,留空則立即發布)</label>
<input type="datetime-local" name="published_at" id="published_at" value="{{ old('published_at') }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">設定未來時間可排程發布</p>
@error('published_at')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Expires At -->
<div>
<label for="expires_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">過期時間(選填)</label>
<input type="datetime-local" name="expires_at" id="expires_at" value="{{ old('expires_at') }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">過期後將自動隱藏</p>
@error('expires_at')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Is Pinned -->
<div class="flex items-center">
<input type="checkbox" name="is_pinned" id="is_pinned" value="1" {{ old('is_pinned') ? 'checked' : '' }}
class="h-4 w-4 rounded border-gray-300 dark:border-gray-700 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-900">
<label for="is_pinned" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">置頂此公告</label>
</div>
<!-- Display Order (only shown when pinned) -->
<div id="display_order_container" style="display: none;">
<label for="display_order" class="block text-sm font-medium text-gray-700 dark:text-gray-300">顯示順序</label>
<input type="number" name="display_order" id="display_order" value="{{ old('display_order', 0) }}" min="0"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">數字越小越優先顯示</p>
</div>
<!-- Action Buttons -->
<div class="flex items-center justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6">
<a href="{{ route('admin.announcements.index') }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
取消
</a>
<button type="submit" name="save_action" value="draft" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
儲存為草稿
</button>
<button type="submit" name="save_action" value="publish" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
發布公告
</button>
</div>
</form>
</div>
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const isPinnedCheckbox = document.getElementById('is_pinned');
const displayOrderContainer = document.getElementById('display_order_container');
function toggleDisplayOrder() {
if (isPinnedCheckbox.checked) {
displayOrderContainer.style.display = 'block';
} else {
displayOrderContainer.style.display = 'none';
}
}
isPinnedCheckbox.addEventListener('change', toggleDisplayOrder);
toggleDisplayOrder();
});
</script>
@endpush
</x-app-layout>

View File

@@ -0,0 +1,126 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
編輯公告
</h2>
<a href="{{ route('admin.announcements.show', $announcement) }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
返回查看
</a>
</div>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<form method="POST" action="{{ route('admin.announcements.update', $announcement) }}" class="space-y-6 p-6">
@csrf
@method('PATCH')
<!-- Title -->
<div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">標題 <span class="text-red-500">*</span></label>
<input type="text" name="title" id="title" value="{{ old('title', $announcement->title) }}" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
@error('title')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Content -->
<div>
<label for="content" class="block text-sm font-medium text-gray-700 dark:text-gray-300">內容 <span class="text-red-500">*</span></label>
<textarea name="content" id="content" rows="10" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">{{ old('content', $announcement->content) }}</textarea>
@error('content')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Access Level -->
<div>
<label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">存取權限 <span class="text-red-500">*</span></label>
<select name="access_level" id="access_level" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="members" {{ old('access_level', $announcement->access_level) === 'members' ? 'selected' : '' }}>會員</option>
<option value="public" {{ old('access_level', $announcement->access_level) === 'public' ? 'selected' : '' }}>公開</option>
<option value="board" {{ old('access_level', $announcement->access_level) === 'board' ? 'selected' : '' }}>理事會</option>
<option value="admin" {{ old('access_level', $announcement->access_level) === 'admin' ? 'selected' : '' }}>管理員</option>
</select>
@error('access_level')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Published At -->
<div>
<label for="published_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">發布時間</label>
<input type="datetime-local" name="published_at" id="published_at"
value="{{ old('published_at', $announcement->published_at?->format('Y-m-d\TH:i')) }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
@error('published_at')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Expires At -->
<div>
<label for="expires_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">過期時間</label>
<input type="datetime-local" name="expires_at" id="expires_at"
value="{{ old('expires_at', $announcement->expires_at?->format('Y-m-d\TH:i')) }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
@error('expires_at')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
<!-- Is Pinned -->
<div class="flex items-center">
<input type="checkbox" name="is_pinned" id="is_pinned" value="1"
{{ old('is_pinned', $announcement->is_pinned) ? 'checked' : '' }}
class="h-4 w-4 rounded border-gray-300 dark:border-gray-700 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-900">
<label for="is_pinned" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">置頂此公告</label>
</div>
<!-- Display Order -->
<div id="display_order_container">
<label for="display_order" class="block text-sm font-medium text-gray-700 dark:text-gray-300">顯示順序</label>
<input type="number" name="display_order" id="display_order"
value="{{ old('display_order', $announcement->display_order) }}" min="0"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
</div>
<!-- Action Buttons -->
<div class="flex items-center justify-end space-x-3 border-t border-gray-200 dark:border-gray-700 pt-6">
<a href="{{ route('admin.announcements.show', $announcement) }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
取消
</a>
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
儲存變更
</button>
</div>
</form>
</div>
</div>
</div>
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const isPinnedCheckbox = document.getElementById('is_pinned');
const displayOrderContainer = document.getElementById('display_order_container');
function toggleDisplayOrder() {
if (isPinnedCheckbox.checked) {
displayOrderContainer.style.display = 'block';
} else {
displayOrderContainer.style.display = 'none';
}
}
isPinnedCheckbox.addEventListener('change', toggleDisplayOrder);
toggleDisplayOrder();
});
</script>
@endpush
</x-app-layout>

View File

@@ -0,0 +1,186 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
公告管理
</h2>
<a href="{{ route('admin.announcements.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
+ 建立公告
</a>
</div>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6">
@if (session('status'))
<div class="rounded-md bg-green-50 dark:bg-green-900/50 p-4">
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div>
@endif
@if (session('error'))
<div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
<p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div>
@endif
<!-- Statistics -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-5">
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">總計</div>
<div class="mt-2 text-3xl font-bold text-gray-900 dark:text-gray-100">{{ $stats['total'] }}</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">草稿</div>
<div class="mt-2 text-3xl font-bold text-gray-600 dark:text-gray-400">{{ $stats['draft'] }}</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">已發布</div>
<div class="mt-2 text-3xl font-bold text-green-600 dark:text-green-400">{{ $stats['published'] }}</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">已歸檔</div>
<div class="mt-2 text-3xl font-bold text-yellow-600 dark:text-yellow-400">{{ $stats['archived'] }}</div>
</div>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">置頂中</div>
<div class="mt-2 text-3xl font-bold text-blue-600 dark:text-blue-400">{{ $stats['pinned'] }}</div>
</div>
</div>
<!-- Search and Filter -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<form method="GET" action="{{ route('admin.announcements.index') }}" class="space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div>
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">搜尋</label>
<input type="text" name="search" id="search" value="{{ request('search') }}" placeholder="標題、內容..."
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
</div>
<div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">狀態</label>
<select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部</option>
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }}>草稿</option>
<option value="published" {{ request('status') === 'published' ? 'selected' : '' }}>已發布</option>
<option value="archived" {{ request('status') === 'archived' ? 'selected' : '' }}>已歸檔</option>
</select>
</div>
<div>
<label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">存取權限</label>
<select name="access_level" id="access_level" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部</option>
<option value="public" {{ request('access_level') === 'public' ? 'selected' : '' }}>公開</option>
<option value="members" {{ request('access_level') === 'members' ? 'selected' : '' }}>會員</option>
<option value="board" {{ request('access_level') === 'board' ? 'selected' : '' }}>理事會</option>
<option value="admin" {{ request('access_level') === 'admin' ? 'selected' : '' }}>管理員</option>
</select>
</div>
<div>
<label for="pinned" class="block text-sm font-medium text-gray-700 dark:text-gray-300">置頂</label>
<select name="pinned" id="pinned" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部</option>
<option value="yes" {{ request('pinned') === 'yes' ? 'selected' : '' }}>已置頂</option>
<option value="no" {{ request('pinned') === 'no' ? 'selected' : '' }}>未置頂</option>
</select>
</div>
</div>
<div class="flex justify-end space-x-2">
<a href="{{ route('admin.announcements.index') }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
清除
</a>
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
搜尋
</button>
</div>
</form>
</div>
<div class="flex justify-between items-center">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400"> {{ $announcements->total() }} 則公告</p>
</div>
</div>
<!-- Announcements Table -->
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">公告</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">狀態</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">存取權限</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">建立者</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">瀏覽次數</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">建立時間</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">操作</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@forelse($announcements as $announcement)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
@if($announcement->is_pinned)
<span class="mr-2 text-blue-500" title="置頂公告">📌</span>
@endif
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
<a href="{{ route('admin.announcements.show', $announcement) }}" class="hover:text-indigo-600 dark:hover:text-indigo-400">
{{ $announcement->title }}
</a>
</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ \Illuminate\Support\Str::limit($announcement->content, 60) }}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5
@if($announcement->status === 'draft') bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300
@elseif($announcement->status === 'published') bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300
@else bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300
@endif">
{{ $announcement->getStatusLabel() }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $announcement->getAccessLevelLabel() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $announcement->creator->name ?? 'N/A' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $announcement->view_count }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $announcement->created_at->format('Y-m-d H:i') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
<a href="{{ route('admin.announcements.show', $announcement) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">查看</a>
@if($announcement->canBeEditedBy(auth()->user()))
<a href="{{ route('admin.announcements.edit', $announcement) }}" class="text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-300">編輯</a>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-6 py-12 text-center text-sm text-gray-500 dark:text-gray-400">
沒有找到公告。<a href="{{ route('admin.announcements.create') }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">建立第一則公告</a>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
@if($announcements->hasPages())
<div class="bg-white dark:bg-gray-800 px-4 py-3 border-t border-gray-200 dark:border-gray-700 sm:px-6">
{{ $announcements->links() }}
</div>
@endif
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,170 @@
<x-app-layout>
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
公告詳情
</h2>
<div class="flex items-center space-x-2">
<a href="{{ route('admin.announcements.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
返回列表
</a>
@if($announcement->canBeEditedBy(auth()->user()))
<a href="{{ route('admin.announcements.edit', $announcement) }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
編輯公告
</a>
@endif
</div>
</div>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-6">
@if (session('status'))
<div class="rounded-md bg-green-50 dark:bg-green-900/50 p-4">
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div>
@endif
@if (session('error'))
<div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
<p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div>
@endif
<!-- Announcement Content -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<div class="mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
@if($announcement->is_pinned)
<span class="text-blue-500" title="置頂公告">📌</span>
@endif
{{ $announcement->title }}
</h3>
<span class="inline-flex rounded-full px-3 py-1 text-sm font-semibold
@if($announcement->status === 'draft') bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300
@elseif($announcement->status === 'published') bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300
@else bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300
@endif">
{{ $announcement->getStatusLabel() }}
</span>
</div>
<div class="prose dark:prose-invert max-w-none">
<div class="whitespace-pre-wrap text-gray-700 dark:text-gray-300">{{ $announcement->content }}</div>
</div>
</div>
</div>
<!-- Metadata -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<h4 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">公告資訊</h4>
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">存取權限</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->getAccessLevelLabel() }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">瀏覽次數</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->view_count }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">建立者</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->creator->name ?? 'N/A' }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">建立時間</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->created_at->format('Y-m-d H:i:s') }}</dd>
</div>
@if($announcement->published_at)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">發布時間</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $announcement->published_at->format('Y-m-d H:i:s') }}
@if($announcement->isScheduled())
<span class="ml-2 inline-flex rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-1 text-xs font-semibold text-blue-800 dark:text-blue-300">排程中</span>
@endif
</dd>
</div>
@endif
@if($announcement->expires_at)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">過期時間</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $announcement->expires_at->format('Y-m-d H:i:s') }}
@if($announcement->isExpired())
<span class="ml-2 inline-flex rounded-full bg-red-100 dark:bg-red-900/50 px-2 py-1 text-xs font-semibold text-red-800 dark:text-red-300">已過期</span>
@endif
</dd>
</div>
@endif
@if($announcement->lastUpdatedBy)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">最後更新者</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->lastUpdatedBy->name }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">最後更新時間</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $announcement->updated_at->format('Y-m-d H:i:s') }}</dd>
</div>
@endif
</dl>
</div>
<!-- Actions -->
@if($announcement->canBeEditedBy(auth()->user()))
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<h4 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">操作</h4>
<div class="flex flex-wrap gap-3">
@if($announcement->isDraft() && auth()->user()->can('publish_announcements'))
<form method="POST" action="{{ route('admin.announcements.publish', $announcement) }}">
@csrf
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-green-600 dark:bg-green-500 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 dark:hover:bg-green-600">
發布公告
</button>
</form>
@endif
@if($announcement->isPublished() && auth()->user()->can('publish_announcements'))
<form method="POST" action="{{ route('admin.announcements.archive', $announcement) }}">
@csrf
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
歸檔公告
</button>
</form>
@endif
@if(!$announcement->is_pinned && auth()->user()->can('edit_announcements'))
<form method="POST" action="{{ route('admin.announcements.pin', $announcement) }}">
@csrf
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
📌 置頂公告
</button>
</form>
@endif
@if($announcement->is_pinned && auth()->user()->can('edit_announcements'))
<form method="POST" action="{{ route('admin.announcements.unpin', $announcement) }}">
@csrf
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
取消置頂
</button>
</form>
@endif
@if(auth()->user()->can('delete_announcements'))
<form method="POST" action="{{ route('admin.announcements.destroy', $announcement) }}"
onsubmit="return confirm('確定要刪除此公告嗎?');">
@csrf
@method('DELETE')
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-red-600 dark:bg-red-500 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 dark:hover:bg-red-600">
刪除公告
</button>
</form>
@endif
</div>
</div>
@endif
</div>
</div>
</x-app-layout>

View File

@@ -1,137 +1,143 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Audit Logs') }} 稽核日誌
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-6"> <div class="px-4 py-5 sm:p-6">
<form method="GET" action="{{ route('admin.audit.index') }}" class="space-y-4" role="search" aria-label="{{ __('Filter audit logs') }}"> <form method="GET" action="{{ route('admin.audit.index') }}" class="space-y-4" role="search" aria-label="篩選稽核日誌">
<div class="grid gap-4 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div> <div>
<label for="search" class="block text-sm font-medium text-gray-700"> <label for="user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Search actions or metadata') }} 使用者
</label>
<input
type="text"
id="search"
name="search"
value="{{ $search }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
</div>
<div>
<label for="action" class="block text-sm font-medium text-gray-700">
{{ __('Action') }}
</label> </label>
<select <select
id="action"
name="action"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
<option value="">{{ __('All') }}</option>
@foreach ($actions as $actionOption)
<option value="{{ $actionOption }}" @selected($actionFilter === $actionOption)>{{ $actionOption }}</option>
@endforeach
</select>
</div>
<div>
<label for="user_id" class="block text-sm font-medium text-gray-700">
{{ __('User') }}
</label>
<select
id="user_id"
name="user_id" name="user_id"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" id="user_id"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100"
> >
<option value="">{{ __('All') }}</option> <option value="">所有使用者</option>
@foreach ($users as $user) @foreach ($users as $user)
<option value="{{ $user->id }}" @selected($userFilter == $user->id)>{{ $user->name }} ({{ $user->email }})</option> <option value="{{ $user->id }}" @selected(request('user_id') == $user->id)>
{{ $user->name }}
</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="grid sm:grid-cols-2 gap-4"> <div>
<div> <label for="event" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<label for="start_date" class="block text-sm font-medium text-gray-700"> 事件
{{ __('Start date') }} </label>
</label> <select
<input name="event"
type="date" id="event"
id="start_date" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100"
name="start_date" >
value="{{ $startDate }}" <option value="">所有事件</option>
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" @foreach ($actions as $action)
> <option value="{{ $action }}" @selected(request('action') == $action)>
</div> {{ ucfirst($action) }}
<div> </option>
<label for="end_date" class="block text-sm font-medium text-gray-700"> @endforeach
{{ __('End date') }} </select>
</label>
<input
type="date"
id="end_date"
name="end_date"
value="{{ $endDate }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
>
</div>
</div> </div>
</div>
<div class="flex flex-wrap gap-2"> <div>
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <label for="auditable_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Apply filters') }} 模型類型
</button> </label>
<a href="{{ route('admin.audit.export', request()->only('search','action','user_id','start_date','end_date')) }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <select
{{ __('Export CSV') }} name="auditable_type"
</a> id="auditable_type"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100"
>
<option value="">所有類型</option>
@foreach ($auditableTypes as $type)
<option value="{{ $type }}" @selected(request('auditable_type') == $type)>
{{ class_basename($type) }}
</option>
@endforeach
</select>
</div>
<div class="flex items-end">
<button type="submit" class="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
篩選
</button>
</div>
</div> </div>
</form> </form>
<div class="overflow-x-auto"> <div class="mt-8 overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg dark:ring-gray-700">
<table class="min-w-full divide-y divide-gray-200" role="table"> <table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-900">
<tr> <tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">
{{ __('Time') }} 使用者
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ __('User') }} 事件
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ __('Action') }} 模型
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ __('Metadata') }} 詳情
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
IP位址
</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
日期
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800">
@forelse ($logs as $log) @forelse ($logs as $log)
<tr> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900"> <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-6">
{{ $log->created_at->toDateTimeString() }} {{ $log->user->name ?? __('System') }}
</td> </td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $log->user?->name ?? __('System') }} <span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5
@if(str_contains($log->action, 'created')) bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200
@elseif(str_contains($log->action, 'updated')) bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200
@elseif(str_contains($log->action, 'deleted')) bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200
@else bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200
@endif">
{{ $log->action }}
</span>
</td> </td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $log->action }} {{ class_basename($log->auditable_type) }} #{{ $log->auditable_id }}
</td> </td>
<td class="px-4 py-3 text-sm text-gray-900"> <td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<pre class="whitespace-pre-wrap break-all text-xs bg-gray-50 p-2 rounded">{{ json_encode($log->metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre> <div>{{ $log->description ?: '—' }}</div>
@if(!empty($log->metadata))
<details class="cursor-pointer group mt-1">
<summary class="text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">元數據</summary>
<div class="mt-2 p-2 bg-gray-50 dark:bg-gray-900 rounded text-xs font-mono overflow-x-auto">
<pre class="whitespace-pre-wrap text-gray-800 dark:text-gray-200">{{ json_encode($log->metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
</div>
</details>
@endif
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $log->ip_address }}
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $log->created_at->format('Y-m-d H:i:s') }}
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="4" class="px-4 py-4 text-sm text-gray-500"> <td colspan="6" class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center">
{{ __('No audit logs found.') }} 找不到稽核日誌。
</td> </td>
</tr> </tr>
@endforelse @endforelse
@@ -139,11 +145,11 @@
</table> </table>
</div> </div>
<div> <div class="mt-4">
{{ $logs->links() }} {{ $logs->withQueryString()->links() }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</x-app-layout> </x-app-layout>

View File

@@ -1,6 +1,6 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
製作銀行調節表 製作銀行調節表
</h2> </h2>
</x-slot> </x-slot>
@@ -8,22 +8,22 @@
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-5xl sm:px-6 lg:px-8 space-y-4"> <div class="mx-auto max-w-5xl sm:px-6 lg:px-8 space-y-4">
@if (session('error')) @if (session('error'))
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4">
<p class="text-sm font-medium text-red-800">{{ session('error') }}</p> <p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div> </div>
@endif @endif
<!-- Help Info --> <!-- Help Info -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4"> <div class="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-5 w-5 text-blue-400 dark:text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">銀行調節表說明</h3> <h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">銀行調節表說明</h3>
<div class="mt-2 text-sm text-blue-700"> <div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>銀行調節表用於核對銀行對帳單餘額與內部現金簿餘額的差異。請準備好:</p> <p>銀行調節表用於核對銀行對帳單餘額與內部現金簿餘額的差異。請準備好:</p>
<ul class="list-disc pl-5 mt-2 space-y-1"> <ul class="list-disc pl-5 mt-2 space-y-1">
<li>銀行對帳單 (PDF/圖片檔)</li> <li>銀行對帳單 (PDF/圖片檔)</li>
@@ -40,89 +40,89 @@
@csrf @csrf
<!-- Basic Information --> <!-- Basic Information -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">基本資訊</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">基本資訊</h3>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Reconciliation Month --> <!-- Reconciliation Month -->
<div> <div>
<label for="reconciliation_month" class="block text-sm font-medium text-gray-700"> <label for="reconciliation_month" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
調節月份 <span class="text-red-500">*</span> 調節月份 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<input type="month" name="reconciliation_month" id="reconciliation_month" required <input type="month" name="reconciliation_month" id="reconciliation_month" required
value="{{ old('reconciliation_month', $month) }}" value="{{ old('reconciliation_month', $month) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('reconciliation_month') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100 @error('reconciliation_month') border-red-300 dark:border-red-700 @enderror">
@error('reconciliation_month') @error('reconciliation_month')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Bank Statement Date --> <!-- Bank Statement Date -->
<div> <div>
<label for="bank_statement_date" class="block text-sm font-medium text-gray-700"> <label for="bank_statement_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
對帳單日期 <span class="text-red-500">*</span> 對帳單日期 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<input type="date" name="bank_statement_date" id="bank_statement_date" required <input type="date" name="bank_statement_date" id="bank_statement_date" required
value="{{ old('bank_statement_date') }}" value="{{ old('bank_statement_date') }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('bank_statement_date') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100 @error('bank_statement_date') border-red-300 dark:border-red-700 @enderror">
@error('bank_statement_date') @error('bank_statement_date')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Bank Statement Balance --> <!-- Bank Statement Balance -->
<div> <div>
<label for="bank_statement_balance" class="block text-sm font-medium text-gray-700"> <label for="bank_statement_balance" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
銀行對帳單餘額 <span class="text-red-500">*</span> 銀行對帳單餘額 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<div class="relative mt-1 rounded-md shadow-sm"> <div class="relative mt-1 rounded-md shadow-sm">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500 sm:text-sm">NT$</span> <span class="text-gray-500 dark:text-gray-400 sm:text-sm">NT$</span>
</div> </div>
<input type="number" name="bank_statement_balance" id="bank_statement_balance" step="0.01" required <input type="number" name="bank_statement_balance" id="bank_statement_balance" step="0.01" required
value="{{ old('bank_statement_balance') }}" value="{{ old('bank_statement_balance') }}"
class="block w-full rounded-md border-gray-300 pl-12 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('bank_statement_balance') border-red-300 @enderror"> class="block w-full rounded-md border-gray-300 dark:border-gray-700 pl-12 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100 @error('bank_statement_balance') border-red-300 dark:border-red-700 @enderror">
</div> </div>
@error('bank_statement_balance') @error('bank_statement_balance')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- System Book Balance --> <!-- System Book Balance -->
<div> <div>
<label for="system_book_balance" class="block text-sm font-medium text-gray-700"> <label for="system_book_balance" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
系統帳面餘額 <span class="text-red-500">*</span> 系統帳面餘額 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<div class="relative mt-1 rounded-md shadow-sm"> <div class="relative mt-1 rounded-md shadow-sm">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500 sm:text-sm">NT$</span> <span class="text-gray-500 dark:text-gray-400 sm:text-sm">NT$</span>
</div> </div>
<input type="number" name="system_book_balance" id="system_book_balance" step="0.01" required <input type="number" name="system_book_balance" id="system_book_balance" step="0.01" required
value="{{ old('system_book_balance', $systemBalance) }}" value="{{ old('system_book_balance', $systemBalance) }}"
class="block w-full rounded-md border-gray-300 pl-12 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('system_book_balance') border-red-300 @enderror"> class="block w-full rounded-md border-gray-300 dark:border-gray-700 pl-12 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100 @error('system_book_balance') border-red-300 dark:border-red-700 @enderror">
</div> </div>
<p class="mt-1 text-xs text-gray-500">從現金簿自動帶入: NT$ {{ number_format($systemBalance, 2) }}</p> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">從現金簿自動帶入: NT$ {{ number_format($systemBalance, 2) }}</p>
@error('system_book_balance') @error('system_book_balance')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Bank Statement File --> <!-- Bank Statement File -->
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<label for="bank_statement_file" class="block text-sm font-medium text-gray-700"> <label for="bank_statement_file" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
銀行對帳單檔案 銀行對帳單檔案
</label> </label>
<input type="file" name="bank_statement_file" id="bank_statement_file" accept=".pdf,.jpg,.jpeg,.png" <input type="file" name="bank_statement_file" id="bank_statement_file" accept=".pdf,.jpg,.jpeg,.png"
class="mt-1 block w-full text-sm text-gray-500 class="mt-1 block w-full text-sm text-gray-500 dark:text-gray-400
file:mr-4 file:py-2 file:px-4 file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0 file:rounded-md file:border-0
file:text-sm file:font-semibold file:text-sm file:font-semibold
file:bg-indigo-50 file:text-indigo-700 file:bg-indigo-50 dark:file:bg-indigo-900/50 file:text-indigo-700 dark:file:text-indigo-300
hover:file:bg-indigo-100"> hover:file:bg-indigo-100 dark:hover:file:bg-indigo-800">
<p class="mt-1 text-xs text-gray-500">支援格式: PDF, JPG, PNG (最大 10MB)</p> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">支援格式: PDF, JPG, PNG (最大 10MB)</p>
@error('bank_statement_file') @error('bank_statement_file')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
</div> </div>
@@ -130,13 +130,13 @@
</div> </div>
<!-- Outstanding Checks --> <!-- Outstanding Checks -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">未兌現支票</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">未兌現支票</h3>
<div id="outstanding-checks-container" class="space-y-3"> <div id="outstanding-checks-container" class="space-y-3">
<!-- Template will be added by JavaScript --> <!-- Template will be added by JavaScript -->
</div> </div>
<button type="button" onclick="addOutstandingCheck()" class="mt-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <button type="button" onclick="addOutstandingCheck()" class="mt-3 inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -146,13 +146,13 @@
</div> </div>
<!-- Deposits in Transit --> <!-- Deposits in Transit -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">在途存款</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">在途存款</h3>
<div id="deposits-container" class="space-y-3"> <div id="deposits-container" class="space-y-3">
<!-- Template will be added by JavaScript --> <!-- Template will be added by JavaScript -->
</div> </div>
<button type="button" onclick="addDeposit()" class="mt-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <button type="button" onclick="addDeposit()" class="mt-3 inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -162,13 +162,13 @@
</div> </div>
<!-- Bank Charges --> <!-- Bank Charges -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">銀行手續費</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">銀行手續費</h3>
<div id="charges-container" class="space-y-3"> <div id="charges-container" class="space-y-3">
<!-- Template will be added by JavaScript --> <!-- Template will be added by JavaScript -->
</div> </div>
<button type="button" onclick="addCharge()" class="mt-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <button type="button" onclick="addCharge()" class="mt-3 inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
<svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="-ml-1 mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -178,20 +178,20 @@
</div> </div>
<!-- Notes --> <!-- Notes -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<label for="notes" class="block text-sm font-medium text-gray-700">備註</label> <label for="notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">備註</label>
<textarea name="notes" id="notes" rows="3" <textarea name="notes" id="notes" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">{{ old('notes') }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">{{ old('notes') }}</textarea>
</div> </div>
</div> </div>
<!-- Form Actions --> <!-- Form Actions -->
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<a href="{{ route('admin.bank-reconciliations.index') }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.bank-reconciliations.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
取消 取消
</a> </a>
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
製作調節表 製作調節表
</button> </button>
</div> </div>
@@ -208,22 +208,22 @@
function addOutstandingCheck() { function addOutstandingCheck() {
const container = document.getElementById('outstanding-checks-container'); const container = document.getElementById('outstanding-checks-container');
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'grid grid-cols-1 gap-4 sm:grid-cols-3 p-4 border border-gray-200 rounded-md'; div.className = 'grid grid-cols-1 gap-4 sm:grid-cols-3 p-4 border border-gray-200 dark:border-gray-700 rounded-md';
div.innerHTML = ` div.innerHTML = `
<div> <div>
<label class="block text-sm font-medium text-gray-700">支票號碼</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">支票號碼</label>
<input type="text" name="outstanding_checks[${checkIndex}][check_number]" <input type="text" name="outstanding_checks[${checkIndex}][check_number]"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700">金額 <span class="text-red-500">*</span></label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">金額 <span class="text-red-500 dark:text-red-400">*</span></label>
<input type="number" name="outstanding_checks[${checkIndex}][amount]" step="0.01" min="0" required <input type="number" name="outstanding_checks[${checkIndex}][amount]" step="0.01" min="0" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700">說明</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">說明</label>
<input type="text" name="outstanding_checks[${checkIndex}][description]" <input type="text" name="outstanding_checks[${checkIndex}][description]"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div> </div>
`; `;
container.appendChild(div); container.appendChild(div);
@@ -233,22 +233,22 @@
function addDeposit() { function addDeposit() {
const container = document.getElementById('deposits-container'); const container = document.getElementById('deposits-container');
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'grid grid-cols-1 gap-4 sm:grid-cols-3 p-4 border border-gray-200 rounded-md'; div.className = 'grid grid-cols-1 gap-4 sm:grid-cols-3 p-4 border border-gray-200 dark:border-gray-700 rounded-md';
div.innerHTML = ` div.innerHTML = `
<div> <div>
<label class="block text-sm font-medium text-gray-700">存款日期</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">存款日期</label>
<input type="date" name="deposits_in_transit[${depositIndex}][date]" <input type="date" name="deposits_in_transit[${depositIndex}][date]"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700">金額 <span class="text-red-500">*</span></label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">金額 <span class="text-red-500 dark:text-red-400">*</span></label>
<input type="number" name="deposits_in_transit[${depositIndex}][amount]" step="0.01" min="0" required <input type="number" name="deposits_in_transit[${depositIndex}][amount]" step="0.01" min="0" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700">說明</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">說明</label>
<input type="text" name="deposits_in_transit[${depositIndex}][description]" <input type="text" name="deposits_in_transit[${depositIndex}][description]"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div> </div>
`; `;
container.appendChild(div); container.appendChild(div);
@@ -258,18 +258,18 @@
function addCharge() { function addCharge() {
const container = document.getElementById('charges-container'); const container = document.getElementById('charges-container');
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'grid grid-cols-1 gap-4 sm:grid-cols-2 p-4 border border-gray-200 rounded-md'; div.className = 'grid grid-cols-1 gap-4 sm:grid-cols-2 p-4 border border-gray-200 dark:border-gray-700 rounded-md';
div.innerHTML = ` div.innerHTML = `
<div> <div>
<label class="block text-sm font-medium text-gray-700">金額 <span class="text-red-500">*</span></label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">金額 <span class="text-red-500 dark:text-red-400">*</span></label>
<input type="number" name="bank_charges[${chargeIndex}][amount]" step="0.01" min="0" required <input type="number" name="bank_charges[${chargeIndex}][amount]" step="0.01" min="0" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700">說明</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">說明</label>
<input type="text" name="bank_charges[${chargeIndex}][description]" <input type="text" name="bank_charges[${chargeIndex}][description]"
placeholder="例如: 轉帳手續費" placeholder="例如: 轉帳手續費"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div> </div>
`; `;
container.appendChild(div); container.appendChild(div);

View File

@@ -1,6 +1,6 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
銀行調節表 銀行調節表
</h2> </h2>
</x-slot> </x-slot>
@@ -8,21 +8,21 @@
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-4"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-4">
@if (session('status')) @if (session('status'))
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 dark:bg-green-900/30 p-4">
<p class="text-sm font-medium text-green-800">{{ session('status') }}</p> <p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div> </div>
@endif @endif
@if (session('error')) @if (session('error'))
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4">
<p class="text-sm font-medium text-red-800">{{ session('error') }}</p> <p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div> </div>
@endif @endif
<!-- Action Button --> <!-- Action Button -->
@can('prepare_bank_reconciliation') @can('prepare_bank_reconciliation')
<div class="flex justify-end"> <div class="flex justify-end">
<a href="{{ route('admin.bank-reconciliations.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.bank-reconciliations.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
<svg class="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -32,12 +32,12 @@
@endcan @endcan
<!-- Filters --> <!-- Filters -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<form method="GET" action="{{ route('admin.bank-reconciliations.index') }}" class="grid grid-cols-1 gap-4 sm:grid-cols-3"> <form method="GET" action="{{ route('admin.bank-reconciliations.index') }}" class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div> <div>
<label for="reconciliation_status" class="block text-sm font-medium text-gray-700">狀態</label> <label for="reconciliation_status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">狀態</label>
<select name="reconciliation_status" id="reconciliation_status" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> <select name="reconciliation_status" id="reconciliation_status" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部</option> <option value="">全部</option>
<option value="pending" {{ request('reconciliation_status') == 'pending' ? 'selected' : '' }}>待覆核</option> <option value="pending" {{ request('reconciliation_status') == 'pending' ? 'selected' : '' }}>待覆核</option>
<option value="completed" {{ request('reconciliation_status') == 'completed' ? 'selected' : '' }}>已完成</option> <option value="completed" {{ request('reconciliation_status') == 'completed' ? 'selected' : '' }}>已完成</option>
@@ -46,16 +46,16 @@
</div> </div>
<div> <div>
<label for="month" class="block text-sm font-medium text-gray-700">調節月份</label> <label for="month" class="block text-sm font-medium text-gray-700 dark:text-gray-300">調節月份</label>
<input type="month" name="month" id="month" value="{{ request('month') }}" <input type="month" name="month" id="month" value="{{ request('month') }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
篩選 篩選
</button> </button>
<a href="{{ route('admin.bank-reconciliations.index') }}" class="ml-2 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.bank-reconciliations.index') }}" class="ml-2 inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
清除 清除
</a> </a>
</div> </div>
@@ -64,71 +64,71 @@
</div> </div>
<!-- Reconciliations Table --> <!-- Reconciliations Table -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
調節月份 調節月份
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
銀行餘額 銀行餘額
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
帳面餘額 帳面餘額
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
差異金額 差異金額
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
狀態 狀態
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
製表人 製表人
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
<span class="sr-only">操作</span> <span class="sr-only">操作</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@forelse ($reconciliations as $reconciliation) @forelse ($reconciliations as $reconciliation)
<tr> <tr>
<td class="whitespace-nowrap px-4 py-4 text-sm font-medium text-gray-900"> <td class="whitespace-nowrap px-4 py-4 text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $reconciliation->reconciliation_month->format('Y年m月') }} {{ $reconciliation->reconciliation_month->format('Y年m月') }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-right text-gray-500"> <td class="whitespace-nowrap px-4 py-4 text-sm text-right text-gray-500 dark:text-gray-400">
NT$ {{ number_format($reconciliation->bank_statement_balance, 2) }} NT$ {{ number_format($reconciliation->bank_statement_balance, 2) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-right text-gray-500"> <td class="whitespace-nowrap px-4 py-4 text-sm text-right text-gray-500 dark:text-gray-400">
NT$ {{ number_format($reconciliation->system_book_balance, 2) }} NT$ {{ number_format($reconciliation->system_book_balance, 2) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-right {{ $reconciliation->discrepancy_amount > 0 ? 'text-red-600 font-semibold' : 'text-gray-500' }}"> <td class="whitespace-nowrap px-4 py-4 text-sm text-right {{ $reconciliation->discrepancy_amount > 0 ? 'text-red-600 dark:text-red-400 font-semibold' : 'text-gray-500 dark:text-gray-400' }}">
NT$ {{ number_format($reconciliation->discrepancy_amount, 2) }} NT$ {{ number_format($reconciliation->discrepancy_amount, 2) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm"> <td class="whitespace-nowrap px-4 py-4 text-sm">
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 <span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5
@if($reconciliation->reconciliation_status === 'completed') bg-green-100 text-green-800 @if($reconciliation->reconciliation_status === 'completed') bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200
@elseif($reconciliation->reconciliation_status === 'discrepancy') bg-red-100 text-red-800 @elseif($reconciliation->reconciliation_status === 'discrepancy') bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200
@else bg-yellow-100 text-yellow-800 @else bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200
@endif"> @endif">
{{ $reconciliation->getStatusText() }} {{ $reconciliation->getStatusText() }}
</span> </span>
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-4 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $reconciliation->preparedByCashier->name ?? 'N/A' }} {{ $reconciliation->preparedByCashier->name ?? 'N/A' }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-right text-sm font-medium"> <td class="whitespace-nowrap px-4 py-4 text-right text-sm font-medium">
<a href="{{ route('admin.bank-reconciliations.show', $reconciliation) }}" class="text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.bank-reconciliations.show', $reconciliation) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
查看 查看
</a> </a>
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="7" class="px-4 py-8 text-center text-sm text-gray-500"> <td colspan="7" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
沒有銀行調節表記錄 沒有銀行調節表記錄
</td> </td>
</tr> </tr>
@@ -144,16 +144,16 @@
</div> </div>
<!-- Help Info --> <!-- Help Info -->
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4"> <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-5 w-5 text-blue-400 dark:text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">關於銀行調節表</h3> <h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">關於銀行調節表</h3>
<div class="mt-2 text-sm text-blue-700"> <div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>銀行調節表用於核對銀行對帳單與內部現金簿的差異。建議每月定期製作,以確保帳務正確。</p> <p>銀行調節表用於核對銀行對帳單與內部現金簿的差異。建議每月定期製作,以確保帳務正確。</p>
<p class="mt-1">調節流程:出納製作 會計覆核 主管核准</p> <p class="mt-1">調節流程:出納製作 會計覆核 主管核准</p>
</div> </div>

View File

@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Create Budget') }} (úË) 新增預算 ()
</h2> </h2>
</x-slot> </x-slot>
@@ -9,13 +9,13 @@
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg dark:bg-gray-800"> <div class="bg-white shadow sm:rounded-lg dark:bg-gray-800">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<form method="POST" action="{{ route('admin.budgets.store') }}" class="space-y-6" aria-label="{{ __('Create budget form') }}"> <form method="POST" action="{{ route('admin.budgets.store') }}" class="space-y-6" aria-label="新增預算表單">
@csrf @csrf
<!-- Fiscal Year --> <!-- Fiscal Year -->
<div> <div>
<label for="fiscal_year" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="fiscal_year" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Fiscal Year') }} <span class="text-red-500" aria-label="{{ __('required') }}">*</span> 會計年度 <span class="text-red-500" aria-label="必填">*</span>
</label> </label>
<input type="number" <input type="number"
name="fiscal_year" name="fiscal_year"
@@ -27,7 +27,7 @@
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-offset-gray-800 @error('fiscal_year') border-red-300 dark:border-red-500 @enderror" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-offset-gray-800 @error('fiscal_year') border-red-300 dark:border-red-500 @enderror"
aria-describedby="fiscal_year_help @error('fiscal_year') fiscal_year_error @enderror"> aria-describedby="fiscal_year_help @error('fiscal_year') fiscal_year_error @enderror">
<p id="fiscal_year_help" class="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p id="fiscal_year_help" class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('The fiscal year this budget applies to') }} 此預算適用的會計年度
</p> </p>
@error('fiscal_year') @error('fiscal_year')
<p id="fiscal_year_error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{{ $message }}</p> <p id="fiscal_year_error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{{ $message }}</p>
@@ -37,7 +37,7 @@
<!-- Name --> <!-- Name -->
<div> <div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Budget Name') }} <span class="text-red-500" aria-label="{{ __('required') }}">*</span> 預算名稱 <span class="text-red-500" aria-label="必填">*</span>
</label> </label>
<input type="text" <input type="text"
name="name" name="name"
@@ -46,10 +46,10 @@
required required
maxlength="255" maxlength="255"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-offset-gray-800 @error('name') border-red-300 dark:border-red-500 @enderror" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-offset-gray-800 @error('name') border-red-300 dark:border-red-500 @enderror"
placeholder="{{ __('e.g., Annual Budget 2025') }}" placeholder="例如2025年度預算"
aria-describedby="name_help @error('name') name_error @enderror"> aria-describedby="name_help @error('name') name_error @enderror">
<p id="name_help" class="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p id="name_help" class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('Descriptive name for this budget') }} 此預算的描述性名稱
</p> </p>
@error('name') @error('name')
<p id="name_error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{{ $message }}</p> <p id="name_error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{{ $message }}</p>
@@ -59,19 +59,19 @@
<!-- Period Type --> <!-- Period Type -->
<div> <div>
<label for="period_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="period_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Period Type') }} <span class="text-red-500" aria-label="{{ __('required') }}">*</span> 期間類型 <span class="text-red-500" aria-label="必填">*</span>
</label> </label>
<select name="period_type" <select name="period_type"
id="period_type" id="period_type"
required required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-offset-gray-800 @error('period_type') border-red-300 dark:border-red-500 @enderror" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-offset-gray-800 @error('period_type') border-red-300 dark:border-red-500 @enderror"
aria-describedby="period_type_help @error('period_type') period_type_error @enderror"> aria-describedby="period_type_help @error('period_type') period_type_error @enderror">
<option value="annual" @selected(old('period_type', 'annual') === 'annual')>{{ __('Annual') }} ()</option> <option value="annual" @selected(old('period_type', 'annual') === 'annual')>年度 </option>
<option value="quarterly" @selected(old('period_type') === 'quarterly')>{{ __('Quarterly') }} ()</option> <option value="quarterly" @selected(old('period_type') === 'quarterly')>季度 </option>
<option value="monthly" @selected(old('period_type') === 'monthly')>{{ __('Monthly') }} (¦)</option> <option value="monthly" @selected(old('period_type') === 'monthly')>月度 ()</option>
</select> </select>
<p id="period_type_help" class="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p id="period_type_help" class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('Budget period duration') }} 預算期間長度
</p> </p>
@error('period_type') @error('period_type')
<p id="period_type_error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{{ $message }}</p> <p id="period_type_error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{{ $message }}</p>
@@ -82,7 +82,7 @@
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div> <div>
<label for="period_start" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="period_start" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Period Start Date') }} <span class="text-red-500" aria-label="{{ __('required') }}">*</span> 期間開始日期 <span class="text-red-500" aria-label="必填">*</span>
</label> </label>
<input type="date" <input type="date"
name="period_start" name="period_start"
@@ -98,7 +98,7 @@
<div> <div>
<label for="period_end" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="period_end" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Period End Date') }} <span class="text-red-500" aria-label="{{ __('required') }}">*</span> 期間結束日期 <span class="text-red-500" aria-label="必填">*</span>
</label> </label>
<input type="date" <input type="date"
name="period_end" name="period_end"
@@ -116,16 +116,16 @@
<!-- Notes --> <!-- Notes -->
<div> <div>
<label for="notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Notes') }} (;) 備註
</label> </label>
<textarea name="notes" <textarea name="notes"
id="notes" id="notes"
rows="3" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-offset-gray-800 @error('notes') border-red-300 dark:border-red-500 @enderror" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-offset-gray-800 @error('notes') border-red-300 dark:border-red-500 @enderror"
placeholder="{{ __('Optional notes about this budget...') }}" placeholder="關於此預算的選填備註..."
aria-describedby="notes_help @error('notes') notes_error @enderror">{{ old('notes') }}</textarea> aria-describedby="notes_help @error('notes') notes_error @enderror">{{ old('notes') }}</textarea>
<p id="notes_help" class="mt-1 text-sm text-gray-500 dark:text-gray-400"> <p id="notes_help" class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('Additional information about this budget (optional)') }} 關於此預算的額外資訊(選填)
</p> </p>
@error('notes') @error('notes')
<p id="notes_error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{{ $message }}</p> <p id="notes_error" class="mt-1 text-sm text-red-600 dark:text-red-400" role="alert">{{ $message }}</p>
@@ -136,11 +136,11 @@
<div class="flex items-center justify-end gap-x-4 border-t border-gray-200 pt-6 dark:border-gray-700"> <div class="flex items-center justify-end gap-x-4 border-t border-gray-200 pt-6 dark:border-gray-700">
<a href="{{ route('admin.budgets.index') }}" <a href="{{ route('admin.budgets.index') }}"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600 dark:focus:ring-offset-gray-800"> class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600 dark:focus:ring-offset-gray-800">
{{ __('Cancel') }} 取消
</a> </a>
<button type="submit" <button type="submit"
class="inline-flex justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus:ring-offset-gray-800"> class="inline-flex justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus:ring-offset-gray-800">
{{ __('Create Budget') }} 新增預算
</button> </button>
</div> </div>
</form> </form>
@@ -157,14 +157,14 @@
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200"> <h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
{{ __('Next Steps') }} 下一步
</h3> </h3>
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300"> <div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>{{ __('After creating the budget, you will be able to:') }}</p> <p>建立預算後,您將能夠:</p>
<ul class="list-disc pl-5 mt-2 space-y-1"> <ul class="list-disc pl-5 mt-2 space-y-1">
<li>{{ __('Add budget items for income and expense accounts') }}</li> <li>為收入和支出帳戶新增預算項目</li>
<li>{{ __('Submit the budget for chair approval') }}</li> <li>提交預算以供主席核准</li>
<li>{{ __('Activate the budget to start tracking actual amounts') }}</li> <li>啟用預算以開始追蹤實際金額</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Edit Budget') }} - {{ $budget->fiscal_year }} 編輯預算 - {{ $budget->fiscal_year }}
</h2> </h2>
</x-slot> </x-slot>
@@ -13,30 +13,30 @@
<!-- Basic Info --> <!-- Basic Info -->
<div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6"> <div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Basic Information') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">基本資訊</h3>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div> <div>
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Budget Name') }} *</label> <label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">預算名稱 *</label>
<input type="text" name="name" id="name" value="{{ old('name', $budget->name) }}" required <input type="text" name="name" id="name" value="{{ old('name', $budget->name) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
@error('name')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('name')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<div> <div>
<label for="period_start" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Period Start') }} *</label> <label for="period_start" class="block text-sm font-medium text-gray-700 dark:text-gray-300">期間開始 *</label>
<input type="date" name="period_start" id="period_start" value="{{ old('period_start', $budget->period_start->format('Y-m-d')) }}" required <input type="date" name="period_start" id="period_start" value="{{ old('period_start', $budget->period_start->format('Y-m-d')) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
</div> </div>
<div> <div>
<label for="period_end" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Period End') }} *</label> <label for="period_end" class="block text-sm font-medium text-gray-700 dark:text-gray-300">期間結束 *</label>
<input type="date" name="period_end" id="period_end" value="{{ old('period_end', $budget->period_end->format('Y-m-d')) }}" required <input type="date" name="period_end" id="period_end" value="{{ old('period_end', $budget->period_end->format('Y-m-d')) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
</div> </div>
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<label for="notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Notes') }}</label> <label for="notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">備註</label>
<textarea name="notes" id="notes" rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">{{ old('notes', $budget->notes) }}</textarea> <textarea name="notes" id="notes" rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">{{ old('notes', $budget->notes) }}</textarea>
</div> </div>
</div> </div>
@@ -45,25 +45,25 @@
<!-- Income Items --> <!-- Income Items -->
<div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6"> <div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ __('Income') }} (6e)</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">收入 </h3>
<button type="button" @click="addItem('income')" class="btn-secondary text-sm">+ {{ __('Add Income Item') }}</button> <button type="button" @click="addItem('income')" class="btn-secondary text-sm">+ 新增收入項目</button>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<template x-for="(item, index) in incomeItems" :key="index"> <template x-for="(item, index) in incomeItems" :key="index">
<div class="flex gap-4 items-start bg-gray-50 dark:bg-gray-900 p-4 rounded-md"> <div class="flex gap-4 items-start bg-gray-50 dark:bg-gray-900 p-4 rounded-md">
<div class="flex-1"> <div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Account') }}</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">帳戶</label>
<select :name="'budget_items[income_' + index + '][chart_of_account_id]'" x-model="item.account_id" required <select :name="'budget_items[income_' + index + '][chart_of_account_id]'" x-model="item.account_id" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="">{{ __('Select account...') }}</option> <option value="">選擇帳戶...</option>
@foreach($incomeAccounts as $account) @foreach($incomeAccounts as $account)
<option value="{{ $account->id }}">{{ $account->account_code }} - {{ $account->account_name_zh }}</option> <option value="{{ $account->id }}">{{ $account->account_code }} - {{ $account->account_name_zh }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="w-48"> <div class="w-48">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Amount') }}</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">金額</label>
<input type="number" :name="'budget_items[income_' + index + '][budgeted_amount]'" x-model="item.amount" step="0.01" min="0" required <input type="number" :name="'budget_items[income_' + index + '][budgeted_amount]'" x-model="item.amount" step="0.01" min="0" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
</div> </div>
@@ -75,7 +75,7 @@
</div> </div>
</template> </template>
<div x-show="incomeItems.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400"> <div x-show="incomeItems.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ __('No income items. Click "Add Income Item" to get started.') }} 無收入項目。點擊「新增收入項目」開始。
</div> </div>
</div> </div>
</div> </div>
@@ -83,25 +83,25 @@
<!-- Expense Items --> <!-- Expense Items -->
<div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6"> <div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ __('Expenses') }} (/ú)</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">支出 (/<EFBFBD>)</h3>
<button type="button" @click="addItem('expense')" class="btn-secondary text-sm">+ {{ __('Add Expense Item') }}</button> <button type="button" @click="addItem('expense')" class="btn-secondary text-sm">+ 新增支出項目</button>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
<template x-for="(item, index) in expenseItems" :key="index"> <template x-for="(item, index) in expenseItems" :key="index">
<div class="flex gap-4 items-start bg-gray-50 dark:bg-gray-900 p-4 rounded-md"> <div class="flex gap-4 items-start bg-gray-50 dark:bg-gray-900 p-4 rounded-md">
<div class="flex-1"> <div class="flex-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Account') }}</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">帳戶</label>
<select :name="'budget_items[expense_' + index + '][chart_of_account_id]'" x-model="item.account_id" required <select :name="'budget_items[expense_' + index + '][chart_of_account_id]'" x-model="item.account_id" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="">{{ __('Select account...') }}</option> <option value="">選擇帳戶...</option>
@foreach($expenseAccounts as $account) @foreach($expenseAccounts as $account)
<option value="{{ $account->id }}">{{ $account->account_code }} - {{ $account->account_name_zh }}</option> <option value="{{ $account->id }}">{{ $account->account_code }} - {{ $account->account_name_zh }}</option>
@endforeach @endforeach
</select> </select>
</div> </div>
<div class="w-48"> <div class="w-48">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Amount') }}</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">金額</label>
<input type="number" :name="'budget_items[expense_' + index + '][budgeted_amount]'" x-model="item.amount" step="0.01" min="0" required <input type="number" :name="'budget_items[expense_' + index + '][budgeted_amount]'" x-model="item.amount" step="0.01" min="0" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
</div> </div>
@@ -113,15 +113,15 @@
</div> </div>
</template> </template>
<div x-show="expenseItems.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400"> <div x-show="expenseItems.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
{{ __('No expense items. Click "Add Expense Item" to get started.') }} 無支出項目。點擊「新增支出項目」開始。
</div> </div>
</div> </div>
</div> </div>
<!-- Actions --> <!-- Actions -->
<div class="flex items-center justify-end gap-x-4"> <div class="flex items-center justify-end gap-x-4">
<a href="{{ route('admin.budgets.show', $budget) }}" class="btn-secondary">{{ __('Cancel') }}</a> <a href="{{ route('admin.budgets.show', $budget) }}" class="btn-secondary">取消</a>
<button type="submit" class="btn-primary">{{ __('Save Budget') }}</button> <button type="submit" class="btn-primary">儲存預算</button>
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Budgets') }} (—¡) 預算管理
</h2> </h2>
</x-slot> </x-slot>
@@ -31,34 +31,34 @@
<div class="sm:flex sm:items-center sm:justify-between mb-6"> <div class="sm:flex sm:items-center sm:justify-between mb-6">
<div> <div>
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100"> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
{{ __('Budget List') }} 預算列表
</h3> </h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400"> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{{ __('Manage annual budgets and track financial performance') }} 管理年度預算並追蹤財務績效
</p> </p>
</div> </div>
<div class="mt-4 sm:mt-0"> <div class="mt-4 sm:mt-0">
<a href="{{ route('admin.budgets.create') }}" <a href="{{ route('admin.budgets.create') }}"
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus:ring-offset-gray-800" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus:ring-offset-gray-800"
aria-label="{{ __('Create new budget') }}"> aria-label="新增預算">
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"> <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" /> <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
</svg> </svg>
{{ __('Create Budget') }} 新增預算
</a> </a>
</div> </div>
</div> </div>
<!-- Filters --> <!-- Filters -->
<form method="GET" action="{{ route('admin.budgets.index') }}" class="mb-6 space-y-4" role="search" aria-label="{{ __('Filter budgets') }}"> <form method="GET" action="{{ route('admin.budgets.index') }}" class="mb-6 space-y-4" role="search" aria-label="篩選預算">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div> <div>
<label for="fiscal_year" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="fiscal_year" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Fiscal Year') }} () 會計年度
</label> </label>
<select id="fiscal_year" name="fiscal_year" <select id="fiscal_year" name="fiscal_year"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="">{{ __('All Years') }}</option> <option value="">所有年度</option>
@foreach($fiscalYears as $year) @foreach($fiscalYears as $year)
<option value="{{ $year }}" @selected(request('fiscal_year') == $year)> <option value="{{ $year }}" @selected(request('fiscal_year') == $year)>
{{ $year }} {{ $year }}
@@ -69,23 +69,23 @@
<div> <div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Status') }} (ÀK) 狀態
</label> </label>
<select id="status" name="status" <select id="status" name="status"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="">{{ __('All Statuses') }}</option> <option value="">所有狀態</option>
<option value="draft" @selected(request('status') === 'draft')>{{ __('Draft') }}</option> <option value="draft" @selected(request('status') === 'draft')>草稿</option>
<option value="submitted" @selected(request('status') === 'submitted')>{{ __('Submitted') }}</option> <option value="submitted" @selected(request('status') === 'submitted')>已提交</option>
<option value="approved" @selected(request('status') === 'approved')>{{ __('Approved') }}</option> <option value="approved" @selected(request('status') === 'approved')>已核准</option>
<option value="active" @selected(request('status') === 'active')>{{ __('Active') }}</option> <option value="active" @selected(request('status') === 'active')>使用中</option>
<option value="closed" @selected(request('status') === 'closed')>{{ __('Closed') }}</option> <option value="closed" @selected(request('status') === 'closed')>已結案</option>
</select> </select>
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button type="submit" <button type="submit"
class="inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600 dark:focus:ring-offset-gray-800"> class="inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600 dark:focus:ring-offset-gray-800">
{{ __('Filter') }} 篩選
</button> </button>
</div> </div>
</div> </div>
@@ -95,27 +95,27 @@
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg dark:ring-gray-700"> <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg dark:ring-gray-700">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600"> <table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
<caption class="sr-only"> <caption class="sr-only">
{{ __('List of budgets showing fiscal year, name, period, and status') }} 預算列表,顯示會計年度、名稱、期間和狀態
</caption> </caption>
<thead class="bg-gray-50 dark:bg-gray-900"> <thead class="bg-gray-50 dark:bg-gray-900">
<tr> <tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6"> <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">
{{ __('Fiscal Year') }} 會計年度
</th> </th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"> <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ __('Name') }} 名稱
</th> </th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"> <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ __('Period') }} 期間
</th> </th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"> <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ __('Status') }} 狀態
</th> </th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100"> <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">
{{ __('Created By') }} 建立者
</th> </th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6"> <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">{{ __('Actions') }}</span> <span class="sr-only">操作</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -133,24 +133,24 @@
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm"> <td class="whitespace-nowrap px-3 py-4 text-sm">
@if($budget->status === 'draft') @if($budget->status === 'draft')
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200" role="status" aria-label="{{ __('Status: Draft') }}"> <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200" role="status" aria-label="狀態:草稿">
{{ __('Draft') }} 草稿
</span> </span>
@elseif($budget->status === 'submitted') @elseif($budget->status === 'submitted')
<span class="inline-flex items-center rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200" role="status" aria-label="{{ __('Status: Submitted') }}"> <span class="inline-flex items-center rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-medium text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200" role="status" aria-label="狀態:已提交">
{{ __('Submitted') }} 已提交
</span> </span>
@elseif($budget->status === 'approved') @elseif($budget->status === 'approved')
<span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200" role="status" aria-label="{{ __('Status: Approved') }}"> <span class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200" role="status" aria-label="狀態:已核准">
{{ __('Approved') }} 已核准
</span> </span>
@elseif($budget->status === 'active') @elseif($budget->status === 'active')
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200" role="status" aria-label="{{ __('Status: Active') }}"> <span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200" role="status" aria-label="狀態:使用中">
 {{ __('Active') }} 使用中
</span> </span>
@elseif($budget->status === 'closed') @elseif($budget->status === 'closed')
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200" role="status" aria-label="{{ __('Status: Closed') }}"> <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200" role="status" aria-label="狀態:已結案">
{{ __('Closed') }} 已結案
</span> </span>
@endif @endif
</td> </td>
@@ -160,15 +160,15 @@
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"> <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<a href="{{ route('admin.budgets.show', $budget) }}" <a href="{{ route('admin.budgets.show', $budget) }}"
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
aria-label="{{ __('View budget for fiscal year :year', ['year' => $budget->fiscal_year]) }}"> aria-label="檢視 {{ $budget->fiscal_year }} 年度預算">
{{ __('View') }} 檢視
</a> </a>
@if($budget->canBeEdited()) @if($budget->canBeEdited())
<span class="text-gray-300 dark:text-gray-600" aria-hidden="true"> | </span> <span class="text-gray-300 dark:text-gray-600" aria-hidden="true"> | </span>
<a href="{{ route('admin.budgets.edit', $budget) }}" <a href="{{ route('admin.budgets.edit', $budget) }}"
class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800"
aria-label="{{ __('Edit budget for fiscal year :year', ['year' => $budget->fiscal_year]) }}"> aria-label="編輯 {{ $budget->fiscal_year }} 年度預算">
{{ __('Edit') }} 編輯
</a> </a>
@endif @endif
</td> </td>
@@ -179,15 +179,15 @@
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
<p class="mt-2 text-sm font-semibold">{{ __('No budgets found') }}</p> <p class="mt-2 text-sm font-semibold">找不到預算</p>
<p class="mt-1 text-sm">{{ __('Get started by creating a new budget.') }}</p> <p class="mt-1 text-sm">開始建立新的預算。</p>
<div class="mt-6"> <div class="mt-6">
<a href="{{ route('admin.budgets.create') }}" <a href="{{ route('admin.budgets.create') }}"
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus:ring-offset-gray-800"> class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus:ring-offset-gray-800">
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true"> <svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" /> <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
</svg> </svg>
{{ __('Create Budget') }} 新增預算
</a> </a>
</div> </div>
</td> </td>

View File

@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Budget Details') }} - {{ $budget->fiscal_year }} 預算詳情 - {{ $budget->fiscal_year }}
</h2> </h2>
</x-slot> </x-slot>
@@ -24,23 +24,23 @@
</div> </div>
<div> <div>
@if($budget->status === 'active') @if($budget->status === 'active')
<span class="inline-flex rounded-full bg-green-100 px-3 py-1 text-sm font-medium text-green-800 dark:bg-green-900 dark:text-green-200"> {{ __('Active') }}</span> <span class="inline-flex rounded-full bg-green-100 px-3 py-1 text-sm font-medium text-green-800 dark:bg-green-900 dark:text-green-200"> 使用中</span>
@elseif($budget->status === 'approved') @elseif($budget->status === 'approved')
<span class="inline-flex rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">{{ __('Approved') }}</span> <span class="inline-flex rounded-full bg-blue-100 px-3 py-1 text-sm font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">已核准</span>
@else @else
<span class="inline-flex rounded-full bg-gray-100 px-3 py-1 text-sm font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200">{{ __(ucfirst($budget->status)) }}</span> <span class="inline-flex rounded-full bg-gray-100 px-3 py-1 text-sm font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200">{{ ucfirst($budget->status) }}</span>
@endif @endif
</div> </div>
</div> </div>
<div class="mt-6 flex gap-3"> <div class="mt-6 flex gap-3">
@if($budget->canBeEdited()) @if($budget->canBeEdited())
<a href="{{ route('admin.budgets.edit', $budget) }}" class="btn-secondary">{{ __('Edit') }}</a> <a href="{{ route('admin.budgets.edit', $budget) }}" class="btn-secondary">編輯</a>
@endif @endif
@if($budget->isDraft()) @if($budget->isDraft())
<form method="POST" action="{{ route('admin.budgets.submit', $budget) }}"> <form method="POST" action="{{ route('admin.budgets.submit', $budget) }}">
@csrf @csrf
<button type="submit" class="btn-primary">{{ __('Submit') }}</button> <button type="submit" class="btn-primary">提交</button>
</form> </form>
@endif @endif
</div> </div>
@@ -49,19 +49,19 @@
<!-- Summary Cards --> <!-- Summary Cards -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-4"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-4">
<div class="bg-white shadow rounded-lg dark:bg-gray-800 p-5"> <div class="bg-white shadow rounded-lg dark:bg-gray-800 p-5">
<dt class="text-sm text-gray-500 dark:text-gray-400">{{ __('Budgeted Income') }}</dt> <dt class="text-sm text-gray-500 dark:text-gray-400">預算收入</dt>
<dd class="text-2xl font-bold text-gray-900 dark:text-gray-100">NT$ {{ number_format($budget->total_budgeted_income) }}</dd> <dd class="text-2xl font-bold text-gray-900 dark:text-gray-100">NT$ {{ number_format($budget->total_budgeted_income) }}</dd>
</div> </div>
<div class="bg-white shadow rounded-lg dark:bg-gray-800 p-5"> <div class="bg-white shadow rounded-lg dark:bg-gray-800 p-5">
<dt class="text-sm text-gray-500 dark:text-gray-400">{{ __('Budgeted Expense') }}</dt> <dt class="text-sm text-gray-500 dark:text-gray-400">預算支出</dt>
<dd class="text-2xl font-bold text-gray-900 dark:text-gray-100">NT$ {{ number_format($budget->total_budgeted_expense) }}</dd> <dd class="text-2xl font-bold text-gray-900 dark:text-gray-100">NT$ {{ number_format($budget->total_budgeted_expense) }}</dd>
</div> </div>
<div class="bg-white shadow rounded-lg dark:bg-gray-800 p-5"> <div class="bg-white shadow rounded-lg dark:bg-gray-800 p-5">
<dt class="text-sm text-gray-500 dark:text-gray-400">{{ __('Actual Income') }}</dt> <dt class="text-sm text-gray-500 dark:text-gray-400">實際收入</dt>
<dd class="text-2xl font-bold text-gray-900 dark:text-gray-100">NT$ {{ number_format($budget->total_actual_income) }}</dd> <dd class="text-2xl font-bold text-gray-900 dark:text-gray-100">NT$ {{ number_format($budget->total_actual_income) }}</dd>
</div> </div>
<div class="bg-white shadow rounded-lg dark:bg-gray-800 p-5"> <div class="bg-white shadow rounded-lg dark:bg-gray-800 p-5">
<dt class="text-sm text-gray-500 dark:text-gray-400">{{ __('Actual Expense') }}</dt> <dt class="text-sm text-gray-500 dark:text-gray-400">實際支出</dt>
<dd class="text-2xl font-bold text-gray-900 dark:text-gray-100">NT$ {{ number_format($budget->total_actual_expense) }}</dd> <dd class="text-2xl font-bold text-gray-900 dark:text-gray-100">NT$ {{ number_format($budget->total_actual_expense) }}</dd>
</div> </div>
</div> </div>
@@ -69,14 +69,14 @@
<!-- Income Items --> <!-- Income Items -->
@if($incomeItems->count() > 0) @if($incomeItems->count() > 0)
<div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6"> <div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4">{{ __('Income') }} (6e)</h3> <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4">收入 </h3>
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600"> <table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
<thead class="bg-gray-50 dark:bg-gray-900"> <thead class="bg-gray-50 dark:bg-gray-900">
<tr> <tr>
<th scope="col" class="py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Account') }}</th> <th scope="col" class="py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">帳戶</th>
<th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Budgeted') }}</th> <th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">預算</th>
<th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Actual') }}</th> <th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">實際</th>
<th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Variance') }}</th> <th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">差異</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@@ -96,14 +96,14 @@
<!-- Expense Items --> <!-- Expense Items -->
@if($expenseItems->count() > 0) @if($expenseItems->count() > 0)
<div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6"> <div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4">{{ __('Expenses') }} (/ú)</h3> <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100 mb-4">支出 (/<EFBFBD>)</h3>
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600"> <table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
<thead class="bg-gray-50 dark:bg-gray-900"> <thead class="bg-gray-50 dark:bg-gray-900">
<tr> <tr>
<th scope="col" class="py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Account') }}</th> <th scope="col" class="py-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">帳戶</th>
<th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Budgeted') }}</th> <th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">預算</th>
<th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Actual') }}</th> <th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">實際</th>
<th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Utilization') }}</th> <th scope="col" class="px-3 py-3 text-right text-sm font-semibold text-gray-900 dark:text-gray-100">使用率</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">

View File

@@ -1,14 +1,14 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
現金簿餘額報表 現金簿餘額報表
</h2> </h2>
</x-slot> </x-slot>
<div class="py-6"> <div class="py-6">
<div class="mx-auto max-w-5xl sm:px-6 lg:px-8 space-y-6"> <div class="mx-auto max-w-5xl sm:px-6 lg:px-8 space-y-6">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900"> <div class="p-6 text-gray-900 dark:text-gray-100">
<h3 class="font-semibold mb-4">帳戶餘額</h3> <h3 class="font-semibold mb-4">帳戶餘額</h3>
<ul class="list-disc list-inside space-y-1"> <ul class="list-disc list-inside space-y-1">
@foreach($accounts as $account) @foreach($accounts as $account)
@@ -18,8 +18,8 @@
</div> </div>
</div> </div>
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900"> <div class="p-6 text-gray-900 dark:text-gray-100">
<h3 class="font-semibold mb-4">本月摘要</h3> <h3 class="font-semibold mb-4">本月摘要</h3>
<p>收入:{{ $monthlySummary['receipts'] ?? 0 }}</p> <p>收入:{{ $monthlySummary['receipts'] ?? 0 }}</p>
<p>支出:{{ $monthlySummary['payments'] ?? 0 }}</p> <p>支出:{{ $monthlySummary['payments'] ?? 0 }}</p>

View File

@@ -1,6 +1,6 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
記錄現金簿分錄 記錄現金簿分錄
</h2> </h2>
</x-slot> </x-slot>
@@ -8,32 +8,32 @@
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-4"> <div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-4">
@if (session('error')) @if (session('error'))
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
<p class="text-sm font-medium text-red-800">{{ session('error') }}</p> <p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div> </div>
@endif @endif
<!-- Related Finance Document Info (if applicable) --> <!-- Related Finance Document Info (if applicable) -->
@if($financeDocument) @if($financeDocument)
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4"> <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h3 class="text-sm font-medium text-blue-900 mb-2">關聯財務申請單</h3> <h3 class="text-sm font-medium text-blue-900 dark:text-blue-100 mb-2">關聯報銷申請單</h3>
<dl class="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-2 text-sm"> <dl class="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-2 text-sm">
<div> <div>
<dt class="font-medium text-blue-700">標題</dt> <dt class="font-medium text-blue-700 dark:text-blue-300">標題</dt>
<dd class="text-blue-900">{{ $financeDocument->title }}</dd> <dd class="text-blue-900 dark:text-blue-100">{{ $financeDocument->title }}</dd>
</div> </div>
<div> <div>
<dt class="font-medium text-blue-700">金額</dt> <dt class="font-medium text-blue-700 dark:text-blue-300">金額</dt>
<dd class="text-blue-900">NT$ {{ number_format($financeDocument->amount, 2) }}</dd> <dd class="text-blue-900 dark:text-blue-100">NT$ {{ number_format($financeDocument->amount, 2) }}</dd>
</div> </div>
@if($financeDocument->paymentOrder) @if($financeDocument->paymentOrder)
<div> <div>
<dt class="font-medium text-blue-700">付款單號</dt> <dt class="font-medium text-blue-700 dark:text-blue-300">付款單號</dt>
<dd class="text-blue-900 font-mono">{{ $financeDocument->paymentOrder->payment_order_number }}</dd> <dd class="text-blue-900 dark:text-blue-100 font-mono">{{ $financeDocument->paymentOrder->payment_order_number }}</dd>
</div> </div>
<div> <div>
<dt class="font-medium text-blue-700">付款方式</dt> <dt class="font-medium text-blue-700 dark:text-blue-300">付款方式</dt>
<dd class="text-blue-900">{{ $financeDocument->paymentOrder->getPaymentMethodText() }}</dd> <dd class="text-blue-900 dark:text-blue-100">{{ $financeDocument->paymentOrder->getPaymentMethodText() }}</dd>
</div> </div>
@endif @endif
</dl> </dl>
@@ -48,127 +48,127 @@
<input type="hidden" name="finance_document_id" value="{{ $financeDocument->id }}"> <input type="hidden" name="finance_document_id" value="{{ $financeDocument->id }}">
@endif @endif
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">分錄資訊</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">分錄資訊</h3>
<div class="grid grid-cols-1 gap-6"> <div class="grid grid-cols-1 gap-6">
<!-- Entry Date --> <!-- Entry Date -->
<div> <div>
<label for="entry_date" class="block text-sm font-medium text-gray-700"> <label for="entry_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
記帳日期 <span class="text-red-500">*</span> 記帳日期 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<input type="date" name="entry_date" id="entry_date" required <input type="date" name="entry_date" id="entry_date" required
value="{{ old('entry_date', now()->format('Y-m-d')) }}" value="{{ old('entry_date', now()->format('Y-m-d')) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('entry_date') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300 @error('entry_date') border-red-300 dark:border-red-700 @enderror">
@error('entry_date') @error('entry_date')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Entry Type --> <!-- Entry Type -->
<div> <div>
<label for="entry_type" class="block text-sm font-medium text-gray-700"> <label for="entry_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
類型 <span class="text-red-500">*</span> 類型 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<select name="entry_type" id="entry_type" required <select name="entry_type" id="entry_type" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('entry_type') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300 @error('entry_type') border-red-300 dark:border-red-700 @enderror">
<option value="">請選擇類型</option> <option value="">請選擇類型</option>
<option value="receipt" {{ old('entry_type') == 'receipt' ? 'selected' : '' }}>收入</option> <option value="receipt" {{ old('entry_type') == 'receipt' ? 'selected' : '' }}>收入</option>
<option value="payment" {{ old('entry_type', $financeDocument ? 'payment' : '') == 'payment' ? 'selected' : '' }}>支出</option> <option value="payment" {{ old('entry_type', $financeDocument ? 'payment' : '') == 'payment' ? 'selected' : '' }}>支出</option>
</select> </select>
@error('entry_type') @error('entry_type')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Payment Method --> <!-- Payment Method -->
<div> <div>
<label for="payment_method" class="block text-sm font-medium text-gray-700"> <label for="payment_method" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
付款方式 <span class="text-red-500">*</span> 付款方式 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<select name="payment_method" id="payment_method" required <select name="payment_method" id="payment_method" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('payment_method') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300 @error('payment_method') border-red-300 dark:border-red-700 @enderror">
<option value="">請選擇付款方式</option> <option value="">請選擇付款方式</option>
<option value="bank_transfer" {{ old('payment_method', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->payment_method : '') == 'bank_transfer' ? 'selected' : '' }}>銀行轉帳</option> <option value="bank_transfer" {{ old('payment_method', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->payment_method : '') == 'bank_transfer' ? 'selected' : '' }}>銀行轉帳</option>
<option value="check" {{ old('payment_method', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->payment_method : '') == 'check' ? 'selected' : '' }}>支票</option> <option value="check" {{ old('payment_method', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->payment_method : '') == 'check' ? 'selected' : '' }}>支票</option>
<option value="cash" {{ old('payment_method', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->payment_method : '') == 'cash' ? 'selected' : '' }}>現金</option> <option value="cash" {{ old('payment_method', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->payment_method : '') == 'cash' ? 'selected' : '' }}>現金</option>
</select> </select>
@error('payment_method') @error('payment_method')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Bank Account --> <!-- Bank Account -->
<div> <div>
<label for="bank_account" class="block text-sm font-medium text-gray-700"> <label for="bank_account" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
銀行帳戶 銀行帳戶
</label> </label>
<input type="text" name="bank_account" id="bank_account" <input type="text" name="bank_account" id="bank_account"
value="{{ old('bank_account', 'Main Account') }}" value="{{ old('bank_account', 'Main Account') }}"
placeholder="例如: Main Account, Petty Cash" placeholder="例如: Main Account, Petty Cash"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('bank_account') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300 @error('bank_account') border-red-300 dark:border-red-700 @enderror">
<p class="mt-1 text-xs text-gray-500">用於區分不同的現金/銀行帳戶</p> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">用於區分不同的現金/銀行帳戶</p>
@error('bank_account') @error('bank_account')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Amount --> <!-- Amount -->
<div> <div>
<label for="amount" class="block text-sm font-medium text-gray-700"> <label for="amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
金額 <span class="text-red-500">*</span> 金額 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<div class="relative mt-1 rounded-md shadow-sm"> <div class="relative mt-1 rounded-md shadow-sm">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500 sm:text-sm">NT$</span> <span class="text-gray-500 dark:text-gray-400 sm:text-sm">NT$</span>
</div> </div>
<input type="number" name="amount" id="amount" step="0.01" min="0.01" required <input type="number" name="amount" id="amount" step="0.01" min="0.01" required
value="{{ old('amount', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->payment_amount : '') }}" value="{{ old('amount', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->payment_amount : '') }}"
class="block w-full rounded-md border-gray-300 pl-12 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('amount') border-red-300 @enderror"> class="block w-full rounded-md border-gray-300 dark:border-gray-700 pl-12 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300 @error('amount') border-red-300 dark:border-red-700 @enderror">
</div> </div>
@error('amount') @error('amount')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Receipt Number --> <!-- Receipt Number -->
<div> <div>
<label for="receipt_number" class="block text-sm font-medium text-gray-700"> <label for="receipt_number" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
收據/憑證編號 收據/憑證編號
</label> </label>
<input type="text" name="receipt_number" id="receipt_number" <input type="text" name="receipt_number" id="receipt_number"
value="{{ old('receipt_number') }}" value="{{ old('receipt_number') }}"
placeholder="例如: RCP-2025-001" placeholder="例如: RCP-2025-001"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('receipt_number') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300 @error('receipt_number') border-red-300 dark:border-red-700 @enderror">
@error('receipt_number') @error('receipt_number')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Transaction Reference --> <!-- Transaction Reference -->
<div> <div>
<label for="transaction_reference" class="block text-sm font-medium text-gray-700"> <label for="transaction_reference" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
交易參考號 交易參考號
</label> </label>
<input type="text" name="transaction_reference" id="transaction_reference" <input type="text" name="transaction_reference" id="transaction_reference"
value="{{ old('transaction_reference', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->transaction_reference : '') }}" value="{{ old('transaction_reference', $financeDocument && $financeDocument->paymentOrder ? $financeDocument->paymentOrder->transaction_reference : '') }}"
placeholder="銀行交易編號或支票號碼" placeholder="銀行交易編號或支票號碼"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm @error('transaction_reference') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300 @error('transaction_reference') border-red-300 dark:border-red-700 @enderror">
@error('transaction_reference') @error('transaction_reference')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<!-- Notes --> <!-- Notes -->
<div> <div>
<label for="notes" class="block text-sm font-medium text-gray-700"> <label for="notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
備註 備註
</label> </label>
<textarea name="notes" id="notes" rows="3" <textarea name="notes" id="notes" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">{{ old('notes') }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">{{ old('notes') }}</textarea>
@error('notes') @error('notes')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
</div> </div>
@@ -176,16 +176,16 @@
</div> </div>
<!-- Help Text --> <!-- Help Text -->
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4"> <div class="bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-5 w-5 text-yellow-400 dark:text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">注意事項</h3> <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-300">注意事項</h3>
<div class="mt-2 text-sm text-yellow-700"> <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-400">
<ul class="list-disc pl-5 space-y-1"> <ul class="list-disc pl-5 space-y-1">
<li>提交後將自動計算交易前後餘額</li> <li>提交後將自動計算交易前後餘額</li>
<li>請確認金額和類型(收入/支出)正確</li> <li>請確認金額和類型(收入/支出)正確</li>
@@ -199,10 +199,10 @@
<!-- Form Actions --> <!-- Form Actions -->
<div class="flex justify-end space-x-3"> <div class="flex justify-end space-x-3">
<a href="{{ route('admin.cashier-ledger.index') }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.cashier-ledger.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
取消 取消
</a> </a>
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
記錄分錄 記錄分錄
</button> </button>
</div> </div>

View File

@@ -1,6 +1,6 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
出納現金簿 出納現金簿
</h2> </h2>
</x-slot> </x-slot>
@@ -8,34 +8,34 @@
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-4"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-4">
@if (session('status')) @if (session('status'))
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 dark:bg-green-900/50 p-4">
<p class="text-sm font-medium text-green-800">{{ session('status') }}</p> <p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div> </div>
@endif @endif
@if (session('error')) @if (session('error'))
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
<p class="text-sm font-medium text-red-800">{{ session('error') }}</p> <p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div> </div>
@endif @endif
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex justify-between"> <div class="flex justify-between">
<a href="{{ route('admin.cashier-ledger.balance-report') }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.cashier-ledger.balance-report') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="-ml-1 mr-2 h-5 w-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg> </svg>
餘額報表 餘額報表
</a> </a>
<div class="flex space-x-3"> <div class="flex space-x-3">
<a href="{{ route('admin.cashier-ledger.export', request()->all()) }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.cashier-ledger.export', request()->all()) }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="-ml-1 mr-2 h-5 w-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
匯出 CSV 匯出 CSV
</a> </a>
@can('record_cashier_ledger') @can('record_cashier_ledger')
<a href="{{ route('admin.cashier-ledger.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.cashier-ledger.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
<svg class="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -47,14 +47,14 @@
<!-- Current Balances Summary --> <!-- Current Balances Summary -->
@if(isset($balances) && $balances->isNotEmpty()) @if(isset($balances) && $balances->isNotEmpty())
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">當前餘額</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">當前餘額</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
@foreach($balances as $account => $balance) @foreach($balances as $account => $balance)
<div class="rounded-lg border border-gray-200 p-4"> <div class="rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<dt class="text-sm font-medium text-gray-500">{{ $account }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ $account }}</dt>
<dd class="mt-1 text-2xl font-semibold {{ $balance >= 0 ? 'text-green-600' : 'text-red-600' }}"> <dd class="mt-1 text-2xl font-semibold {{ $balance >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }}">
NT$ {{ number_format($balance, 2) }} NT$ {{ number_format($balance, 2) }}
</dd> </dd>
</div> </div>
@@ -65,12 +65,12 @@
@endif @endif
<!-- Filters --> <!-- Filters -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<form method="GET" action="{{ route('admin.cashier-ledger.index') }}" class="grid grid-cols-1 gap-4 sm:grid-cols-5"> <form method="GET" action="{{ route('admin.cashier-ledger.index') }}" class="grid grid-cols-1 gap-4 sm:grid-cols-5">
<div> <div>
<label for="entry_type" class="block text-sm font-medium text-gray-700">類型</label> <label for="entry_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">類型</label>
<select name="entry_type" id="entry_type" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> <select name="entry_type" id="entry_type" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部</option> <option value="">全部</option>
<option value="receipt" {{ request('entry_type') == 'receipt' ? 'selected' : '' }}>收入</option> <option value="receipt" {{ request('entry_type') == 'receipt' ? 'selected' : '' }}>收入</option>
<option value="payment" {{ request('entry_type') == 'payment' ? 'selected' : '' }}>支出</option> <option value="payment" {{ request('entry_type') == 'payment' ? 'selected' : '' }}>支出</option>
@@ -78,8 +78,8 @@
</div> </div>
<div> <div>
<label for="payment_method" class="block text-sm font-medium text-gray-700">付款方式</label> <label for="payment_method" class="block text-sm font-medium text-gray-700 dark:text-gray-300">付款方式</label>
<select name="payment_method" id="payment_method" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> <select name="payment_method" id="payment_method" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部</option> <option value="">全部</option>
<option value="bank_transfer" {{ request('payment_method') == 'bank_transfer' ? 'selected' : '' }}>銀行轉帳</option> <option value="bank_transfer" {{ request('payment_method') == 'bank_transfer' ? 'selected' : '' }}>銀行轉帳</option>
<option value="check" {{ request('payment_method') == 'check' ? 'selected' : '' }}>支票</option> <option value="check" {{ request('payment_method') == 'check' ? 'selected' : '' }}>支票</option>
@@ -88,22 +88,22 @@
</div> </div>
<div> <div>
<label for="date_from" class="block text-sm font-medium text-gray-700">開始日期</label> <label for="date_from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">開始日期</label>
<input type="date" name="date_from" id="date_from" value="{{ request('date_from') }}" <input type="date" name="date_from" id="date_from" value="{{ request('date_from') }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
</div> </div>
<div> <div>
<label for="date_to" class="block text-sm font-medium text-gray-700">結束日期</label> <label for="date_to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">結束日期</label>
<input type="date" name="date_to" id="date_to" value="{{ request('date_to') }}" <input type="date" name="date_to" id="date_to" value="{{ request('date_to') }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
</div> </div>
<div class="flex items-end"> <div class="flex items-end">
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
篩選 篩選
</button> </button>
<a href="{{ route('admin.cashier-ledger.index') }}" class="ml-2 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.cashier-ledger.index') }}" class="ml-2 inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
清除 清除
</a> </a>
</div> </div>
@@ -112,80 +112,80 @@
</div> </div>
<!-- Ledger Entries Table --> <!-- Ledger Entries Table -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
日期 日期
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
類型 類型
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
付款方式 付款方式
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
銀行帳戶 銀行帳戶
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
金額 金額
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
交易前餘額 交易前餘額
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
交易後餘額 交易後餘額
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
記錄人 記錄人
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
<span class="sr-only">操作</span> <span class="sr-only">操作</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@forelse ($entries as $entry) @forelse ($entries as $entry)
<tr class="{{ $entry->isReceipt() ? 'bg-green-50' : 'bg-red-50' }} bg-opacity-20"> <tr class="{{ $entry->isReceipt() ? 'bg-green-50 dark:bg-green-900/20' : 'bg-red-50 dark:bg-red-900/20' }}">
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-900"> <td class="whitespace-nowrap px-4 py-4 text-sm text-gray-900 dark:text-gray-100">
{{ $entry->entry_date->format('Y-m-d') }} {{ $entry->entry_date->format('Y-m-d') }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm"> <td class="whitespace-nowrap px-4 py-4 text-sm">
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 <span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5
{{ $entry->isReceipt() ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}"> {{ $entry->isReceipt() ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' : 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200' }}">
{{ $entry->getEntryTypeText() }} {{ $entry->getEntryTypeText() }}
</span> </span>
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-4 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $entry->getPaymentMethodText() }} {{ $entry->getPaymentMethodText() }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-4 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $entry->bank_account ?? 'N/A' }} {{ $entry->bank_account ?? 'N/A' }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-right font-medium {{ $entry->isReceipt() ? 'text-green-600' : 'text-red-600' }}"> <td class="whitespace-nowrap px-4 py-4 text-sm text-right font-medium {{ $entry->isReceipt() ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }}">
{{ $entry->isReceipt() ? '+' : '-' }} NT$ {{ number_format($entry->amount, 2) }} {{ $entry->isReceipt() ? '+' : '-' }} NT$ {{ number_format($entry->amount, 2) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-right text-gray-500"> <td class="whitespace-nowrap px-4 py-4 text-sm text-right text-gray-500 dark:text-gray-400">
NT$ {{ number_format($entry->balance_before, 2) }} NT$ {{ number_format($entry->balance_before, 2) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-right font-semibold text-gray-900"> <td class="whitespace-nowrap px-4 py-4 text-sm text-right font-semibold text-gray-900 dark:text-gray-100">
NT$ {{ number_format($entry->balance_after, 2) }} NT$ {{ number_format($entry->balance_after, 2) }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-4 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $entry->recordedByCashier->name ?? 'N/A' }} {{ $entry->recordedByCashier->name ?? 'N/A' }}
</td> </td>
<td class="whitespace-nowrap px-4 py-4 text-right text-sm font-medium"> <td class="whitespace-nowrap px-4 py-4 text-right text-sm font-medium">
<a href="{{ route('admin.cashier-ledger.show', $entry) }}" class="text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.cashier-ledger.show', $entry) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
查看 查看
</a> </a>
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="9" class="px-4 py-8 text-center text-sm text-gray-500"> <td colspan="9" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
沒有現金簿記錄 沒有現金簿記錄
</td> </td>
</tr> </tr>

View File

@@ -1,6 +1,6 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
現金簿分錄詳情 現金簿分錄詳情
</h2> </h2>
</x-slot> </x-slot>
@@ -8,63 +8,63 @@
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-4"> <div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-4">
<!-- Entry Info --> <!-- Entry Info -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium leading-6 text-gray-900">分錄資訊</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">分錄資訊</h3>
<span class="inline-flex rounded-full px-3 py-1 text-sm font-semibold <span class="inline-flex rounded-full px-3 py-1 text-sm font-semibold
{{ $entry->isReceipt() ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}"> {{ $entry->isReceipt() ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' : 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200' }}">
{{ $entry->getEntryTypeText() }} {{ $entry->getEntryTypeText() }}
</span> </span>
</div> </div>
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2"> <dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
<div> <div>
<dt class="text-sm font-medium text-gray-500">記帳日期</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">記帳日期</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $entry->entry_date->format('Y-m-d') }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $entry->entry_date->format('Y-m-d') }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">付款方式</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">付款方式</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $entry->getPaymentMethodText() }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $entry->getPaymentMethodText() }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">銀行帳戶</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">銀行帳戶</dt>
<dd class="mt-1 text-sm text-gray-900 font-mono">{{ $entry->bank_account ?? 'N/A' }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 font-mono">{{ $entry->bank_account ?? 'N/A' }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">金額</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">金額</dt>
<dd class="mt-1 text-lg font-semibold {{ $entry->isReceipt() ? 'text-green-600' : 'text-red-600' }}"> <dd class="mt-1 text-lg font-semibold {{ $entry->isReceipt() ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }}">
{{ $entry->isReceipt() ? '+' : '-' }} NT$ {{ number_format($entry->amount, 2) }} {{ $entry->isReceipt() ? '+' : '-' }} NT$ {{ number_format($entry->amount, 2) }}
</dd> </dd>
</div> </div>
@if($entry->receipt_number) @if($entry->receipt_number)
<div> <div>
<dt class="text-sm font-medium text-gray-500">收據/憑證編號</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">收據/憑證編號</dt>
<dd class="mt-1 text-sm text-gray-900 font-mono">{{ $entry->receipt_number }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 font-mono">{{ $entry->receipt_number }}</dd>
</div> </div>
@endif @endif
@if($entry->transaction_reference) @if($entry->transaction_reference)
<div> <div>
<dt class="text-sm font-medium text-gray-500">交易參考號</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">交易參考號</dt>
<dd class="mt-1 text-sm text-gray-900 font-mono">{{ $entry->transaction_reference }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 font-mono">{{ $entry->transaction_reference }}</dd>
</div> </div>
@endif @endif
<div> <div>
<dt class="text-sm font-medium text-gray-500">記錄人</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">記錄人</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $entry->recordedByCashier->name }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $entry->recordedByCashier->name }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">記錄時間</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">記錄時間</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $entry->recorded_at->format('Y-m-d H:i') }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $entry->recorded_at->format('Y-m-d H:i') }}</dd>
</div> </div>
@if($entry->notes) @if($entry->notes)
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">備註</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">備註</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $entry->notes }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $entry->notes }}</dd>
</div> </div>
@endif @endif
</dl> </dl>
@@ -72,36 +72,36 @@
</div> </div>
<!-- Balance Information --> <!-- Balance Information -->
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 shadow sm:rounded-lg border border-blue-200"> <div class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/30 dark:to-indigo-900/30 shadow sm:rounded-lg border border-blue-200 dark:border-blue-800">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">餘額變動</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">餘額變動</h3>
<div class="grid grid-cols-3 gap-4"> <div class="grid grid-cols-3 gap-4">
<div class="text-center p-4 bg-white rounded-lg shadow-sm"> <div class="text-center p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm">
<dt class="text-xs font-medium text-gray-500 uppercase">交易前餘額</dt> <dt class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">交易前餘額</dt>
<dd class="mt-2 text-2xl font-semibold text-gray-700"> <dd class="mt-2 text-2xl font-semibold text-gray-700 dark:text-gray-300">
{{ number_format($entry->balance_before, 2) }} {{ number_format($entry->balance_before, 2) }}
</dd> </dd>
</div> </div>
<div class="text-center p-4 bg-white rounded-lg shadow-sm flex items-center justify-center"> <div class="text-center p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm flex items-center justify-center">
<div> <div>
<svg class="h-8 w-8 mx-auto {{ $entry->isReceipt() ? 'text-green-500' : 'text-red-500' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-8 w-8 mx-auto {{ $entry->isReceipt() ? 'text-green-500 dark:text-green-400' : 'text-red-500 dark:text-red-400' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@if($entry->isReceipt()) @if($entry->isReceipt())
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
@else @else
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
@endif @endif
</svg> </svg>
<div class="mt-1 text-lg font-semibold {{ $entry->isReceipt() ? 'text-green-600' : 'text-red-600' }}"> <div class="mt-1 text-lg font-semibold {{ $entry->isReceipt() ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }}">
{{ $entry->isReceipt() ? '+' : '-' }} {{ number_format($entry->amount, 2) }} {{ $entry->isReceipt() ? '+' : '-' }} {{ number_format($entry->amount, 2) }}
</div> </div>
</div> </div>
</div> </div>
<div class="text-center p-4 bg-white rounded-lg shadow-sm"> <div class="text-center p-4 bg-white dark:bg-gray-700 rounded-lg shadow-sm">
<dt class="text-xs font-medium text-gray-500 uppercase">交易後餘額</dt> <dt class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">交易後餘額</dt>
<dd class="mt-2 text-2xl font-semibold {{ $entry->balance_after >= 0 ? 'text-green-600' : 'text-red-600' }}"> <dd class="mt-2 text-2xl font-semibold {{ $entry->balance_after >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }}">
{{ number_format($entry->balance_after, 2) }} {{ number_format($entry->balance_after, 2) }}
</dd> </dd>
</div> </div>
@@ -111,34 +111,34 @@
<!-- Related Finance Document --> <!-- Related Finance Document -->
@if($entry->financeDocument) @if($entry->financeDocument)
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">關聯財務申請單</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">關聯報銷申請單</h3>
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2"> <dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
<div> <div>
<dt class="text-sm font-medium text-gray-500">申請標題</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">申請標題</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $entry->financeDocument->title }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $entry->financeDocument->title }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">申請類型</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">申請類型</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $entry->financeDocument->getRequestTypeText() }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $entry->financeDocument->getRequestTypeText() }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">申請金額</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">申請金額</dt>
<dd class="mt-1 text-sm text-gray-900">NT$ {{ number_format($entry->financeDocument->amount, 2) }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">NT$ {{ number_format($entry->financeDocument->amount, 2) }}</dd>
</div> </div>
@if($entry->financeDocument->member) @if($entry->financeDocument->member)
<div> <div>
<dt class="text-sm font-medium text-gray-500">關聯會員</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">關聯會員</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $entry->financeDocument->member->full_name }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $entry->financeDocument->member->full_name }}</dd>
</div> </div>
@endif @endif
@if($entry->financeDocument->paymentOrder) @if($entry->financeDocument->paymentOrder)
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">付款單號</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">付款單號</dt>
<dd class="mt-1"> <dd class="mt-1">
<a href="{{ route('admin.payment-orders.show', $entry->financeDocument->paymentOrder) }}" class="text-indigo-600 hover:text-indigo-900 font-mono"> <a href="{{ route('admin.payment-orders.show', $entry->financeDocument->paymentOrder) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300 font-mono">
{{ $entry->financeDocument->paymentOrder->payment_order_number }} {{ $entry->financeDocument->paymentOrder->payment_order_number }}
</a> </a>
</dd> </dd>
@@ -146,7 +146,7 @@
@endif @endif
</dl> </dl>
<div class="mt-4"> <div class="mt-4">
<a href="{{ route('admin.finance.show', $entry->financeDocument) }}" class="text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.finance.show', $entry->financeDocument) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
查看完整申請單 查看完整申請單
</a> </a>
</div> </div>
@@ -156,13 +156,13 @@
<!-- Actions --> <!-- Actions -->
<div class="flex justify-between"> <div class="flex justify-between">
<a href="{{ route('admin.cashier-ledger.index') }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.cashier-ledger.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
返回列表 返回列表
</a> </a>
@if($entry->financeDocument) @if($entry->financeDocument)
<a href="{{ route('admin.finance.show', $entry->financeDocument) }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.finance.show', $entry->financeDocument) }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
查看財務申請單 查看報銷申請單
</a> </a>
@endif @endif
</div> </div>

View File

@@ -1,7 +1,7 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Admin Dashboard') }} 管理後台
</h2> </h2>
</x-slot> </x-slot>
@@ -10,18 +10,18 @@
<div class="space-y-6"> <div class="space-y-6">
{{-- My Pending Approvals Alert --}} {{-- My Pending Approvals Alert --}}
@if ($myPendingApprovals > 0) @if ($myPendingApprovals > 0)
<div class="rounded-md bg-yellow-50 p-4" role="alert" aria-live="polite"> <div class="rounded-md bg-yellow-50 dark:bg-yellow-900/50 p-4" role="alert" aria-live="polite">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400 dark:text-yellow-300" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<p class="text-sm font-medium text-yellow-800"> <p class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{{ __('You have :count finance document(s) waiting for your approval.', ['count' => $myPendingApprovals]) }} 您有 {{ $myPendingApprovals }} 個報銷單等待您的核准。
<a href="{{ route('admin.finance.index') }}" class="font-semibold underline hover:text-yellow-700"> <a href="{{ route('admin.finance.index') }}" class="font-semibold underline hover:text-yellow-700 dark:hover:text-yellow-100">
{{ __('View pending approvals') }} 查看待核准項目
</a> </a>
</p> </p>
</div> </div>
@@ -32,103 +32,103 @@
{{-- Stats Grid --}} {{-- Stats Grid --}}
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
{{-- Total Members --}} {{-- Total Members --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="truncate text-sm font-medium text-gray-500">{{ __('Total Members') }}</dt> <dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">會員總數</dt>
<dd class="text-2xl font-semibold text-gray-900">{{ number_format($totalMembers) }}</dd> <dd class="text-2xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($totalMembers) }}</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm"> <div class="text-sm">
<a href="{{ route('admin.members.index') }}" class="font-medium text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.members.index') }}" class="font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
{{ __('View all') }} 查看全部
</a> </a>
</div> </div>
</div> </div>
</div> </div>
{{-- Active Members --}} {{-- Active Members --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 text-green-400 dark:text-green-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="truncate text-sm font-medium text-gray-500">{{ __('Active Members') }}</dt> <dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">使用中會員</dt>
<dd class="text-2xl font-semibold text-green-600">{{ number_format($activeMembers) }}</dd> <dd class="text-2xl font-semibold text-green-600 dark:text-green-400">{{ number_format($activeMembers) }}</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm"> <div class="text-sm">
<a href="{{ route('admin.members.index', ['status' => 'active']) }}" class="font-medium text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.members.index', ['status' => 'active']) }}" class="font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
{{ __('View active') }} 查看使用中
</a> </a>
</div> </div>
</div> </div>
</div> </div>
{{-- Expired Members --}} {{-- Expired Members --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-red-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 text-red-400 dark:text-red-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="truncate text-sm font-medium text-gray-500">{{ __('Expired Members') }}</dt> <dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">已過期會員</dt>
<dd class="text-2xl font-semibold text-red-600">{{ number_format($expiredMembers) }}</dd> <dd class="text-2xl font-semibold text-red-600 dark:text-red-400">{{ number_format($expiredMembers) }}</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm"> <div class="text-sm">
<a href="{{ route('admin.members.index', ['status' => 'expired']) }}" class="font-medium text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.members.index', ['status' => 'expired']) }}" class="font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
{{ __('View expired') }} 查看已過期
</a> </a>
</div> </div>
</div> </div>
</div> </div>
{{-- Expiring Soon --}} {{-- Expiring Soon --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-yellow-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 text-yellow-400 dark:text-yellow-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="truncate text-sm font-medium text-gray-500">{{ __('Expiring in 30 Days') }}</dt> <dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">30天內到期</dt>
<dd class="text-2xl font-semibold text-yellow-600">{{ number_format($expiringSoon) }}</dd> <dd class="text-2xl font-semibold text-yellow-600 dark:text-yellow-400">{{ number_format($expiringSoon) }}</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm"> <div class="text-sm">
<span class="text-gray-500">{{ __('Renewal reminders needed') }}</span> <span class="text-gray-500 dark:text-gray-400">需要更新提醒</span>
</div> </div>
</div> </div>
</div> </div>
@@ -137,74 +137,74 @@
{{-- Revenue Stats --}} {{-- Revenue Stats --}}
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{{-- Total Revenue --}} {{-- Total Revenue --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="truncate text-sm font-medium text-gray-500">{{ __('Total Revenue') }}</dt> <dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">總收入</dt>
<dd class="text-2xl font-semibold text-gray-900">${{ number_format($totalRevenue, 2) }}</dd> <dd class="text-2xl font-semibold text-gray-900 dark:text-gray-100">${{ number_format($totalRevenue, 2) }}</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm text-gray-500"> <div class="text-sm text-gray-500 dark:text-gray-400">
{{ number_format($totalPayments) }} {{ __('total payments') }} {{ number_format($totalPayments) }} 總付款
</div> </div>
</div> </div>
</div> </div>
{{-- This Month Revenue --}} {{-- This Month Revenue --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 text-green-400 dark:text-green-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0115.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 013 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 00-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 01-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 003 15h-.75M15 10.5a3 3 0 11-6 0 3 3 0 016 0zm3 0h.008v.008H18V10.5zm-12 0h.008v.008H6V10.5z" />
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="truncate text-sm font-medium text-gray-500">{{ __('This Month') }}</dt> <dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">本月</dt>
<dd class="text-2xl font-semibold text-green-600">${{ number_format($revenueThisMonth, 2) }}</dd> <dd class="text-2xl font-semibold text-green-600 dark:text-green-400">${{ number_format($revenueThisMonth, 2) }}</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm text-gray-500"> <div class="text-sm text-gray-500 dark:text-gray-400">
{{ number_format($paymentsThisMonth) }} {{ __('payments this month') }} {{ number_format($paymentsThisMonth) }} 本月付款
</div> </div>
</div> </div>
</div> </div>
{{-- Pending Approvals --}} {{-- Pending Approvals --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-blue-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 text-blue-400 dark:text-blue-500" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="truncate text-sm font-medium text-gray-500">{{ __('Finance Documents') }}</dt> <dt class="truncate text-sm font-medium text-gray-500 dark:text-gray-400">報銷單</dt>
<dd class="text-2xl font-semibold text-blue-600">{{ number_format($pendingApprovals) }}</dd> <dd class="text-2xl font-semibold text-blue-600 dark:text-blue-400">{{ number_format($pendingApprovals) }}</dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<div class="text-sm"> <div class="text-sm">
<a href="{{ route('admin.finance.index') }}" class="font-medium text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.finance.index') }}" class="font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
{{ __('View pending') }} 查看待處理
</a> </a>
</div> </div>
</div> </div>
@@ -214,25 +214,25 @@
{{-- Recent Payments & Finance Stats --}} {{-- Recent Payments & Finance Stats --}}
<div class="grid grid-cols-1 gap-5 lg:grid-cols-2"> <div class="grid grid-cols-1 gap-5 lg:grid-cols-2">
{{-- Recent Payments --}} {{-- Recent Payments --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">{{ __('Recent Payments') }}</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">最近付款</h3>
<div class="mt-4 flow-root"> <div class="mt-4 flow-root">
@if ($recentPayments->count() > 0) @if ($recentPayments->count() > 0)
<ul role="list" class="-my-5 divide-y divide-gray-200"> <ul role="list" class="-my-5 divide-y divide-gray-200 dark:divide-gray-700">
@foreach ($recentPayments as $payment) @foreach ($recentPayments as $payment)
<li class="py-4"> <li class="py-4">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-gray-900"> <p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $payment->member?->full_name ?? __('N/A') }} {{ $payment->member?->full_name ?? __('N/A') }}
</p> </p>
<p class="truncate text-sm text-gray-500"> <p class="truncate text-sm text-gray-500 dark:text-gray-400">
{{ $payment->paid_at?->format('Y-m-d') ?? __('N/A') }} {{ $payment->paid_at?->format('Y-m-d') ?? __('N/A') }}
</p> </p>
</div> </div>
<div> <div>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800"> <span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:text-green-200">
${{ number_format($payment->amount, 2) }} ${{ number_format($payment->amount, 2) }}
</span> </span>
</div> </div>
@@ -241,43 +241,96 @@
@endforeach @endforeach
</ul> </ul>
@else @else
<p class="text-sm text-gray-500">{{ __('No recent payments.') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400">沒有最近的付款記錄。</p>
@endif @endif
</div> </div>
</div> </div>
</div> </div>
{{-- Finance Document Stats --}} {{-- Finance Document Stats --}}
<div class="overflow-hidden rounded-lg bg-white shadow"> <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">{{ __('Finance Document Status') }}</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">報銷單狀態</h3>
<div class="mt-6 space-y-4"> <div class="mt-6 space-y-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<span class="flex h-3 w-3 rounded-full bg-yellow-400"></span> <span class="flex h-3 w-3 rounded-full bg-yellow-400"></span>
<span class="ml-3 text-sm font-medium text-gray-900">{{ __('Pending Approval') }}</span> <span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-100">待核准</span>
</div> </div>
<span class="text-sm font-semibold text-gray-900">{{ number_format($pendingApprovals) }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ number_format($pendingApprovals) }}</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<span class="flex h-3 w-3 rounded-full bg-green-400"></span> <span class="flex h-3 w-3 rounded-full bg-green-400"></span>
<span class="ml-3 text-sm font-medium text-gray-900">{{ __('Fully Approved') }}</span> <span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-100">完全核准</span>
</div> </div>
<span class="text-sm font-semibold text-gray-900">{{ number_format($fullyApprovedDocs) }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ number_format($fullyApprovedDocs) }}</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center">
<span class="flex h-3 w-3 rounded-full bg-red-400"></span> <span class="flex h-3 w-3 rounded-full bg-red-400"></span>
<span class="ml-3 text-sm font-medium text-gray-900">{{ __('Rejected') }}</span> <span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-100">已拒絕</span>
</div> </div>
<span class="text-sm font-semibold text-gray-900">{{ number_format($rejectedDocs) }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ number_format($rejectedDocs) }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{-- Recent Announcements --}}
@if($recentAnnouncements->isNotEmpty())
<div class="mt-8">
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="border-b border-gray-200 dark:border-gray-700 px-4 py-5 sm:px-6">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">📢 最新公告</h3>
@can('view_announcements')
<a href="{{ route('admin.announcements.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
查看全部
</a>
@endcan
</div>
</div>
<ul role="list" class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($recentAnnouncements as $announcement)
<li class="px-4 py-4 sm:px-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center mb-1">
@if($announcement->is_pinned)
<span class="mr-2 text-blue-500" title="置頂公告">📌</span>
@endif
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">
{{ $announcement->title }}
</h4>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
{{ $announcement->getExcerpt(120) }}
</p>
<div class="mt-2 flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
<span>{{ $announcement->published_at?->diffForHumans() ?? $announcement->created_at->diffForHumans() }}</span>
<span></span>
<span>{{ $announcement->getAccessLevelLabel() }}</span>
@if($announcement->view_count > 0)
<span></span>
<span>👁 {{ $announcement->view_count }} 次瀏覽</span>
@endif
</div>
</div>
@can('view_announcements')
<a href="{{ route('admin.announcements.show', $announcement) }}" class="ml-4 flex-shrink-0 text-sm font-medium text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
查看
</a>
@endcan
</div>
</li>
@endforeach
</ul>
</div>
</div>
@endif
</div> </div>
</div> </div>
</div> </div>
</x-app-layout> </x-app-layout>

View File

@@ -1,95 +1,95 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
新增文件類別 新增文件類別
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-3xl sm:px-6 lg:px-8"> <div class="mx-auto max-w-3xl sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<form action="{{ route('admin.document-categories.store') }}" method="POST" class="p-6 space-y-6"> <form action="{{ route('admin.document-categories.store') }}" method="POST" class="p-6 space-y-6">
@csrf @csrf
<div> <div>
<label for="name" class="block text-sm font-medium text-gray-700"> <label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
類別名稱 <span class="text-red-500">*</span> 類別名稱 <span class="text-red-500">*</span>
</label> </label>
<input type="text" name="name" id="name" value="{{ old('name') }}" required <input type="text" name="name" id="name" value="{{ old('name') }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('name') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 @error('name') border-red-500 @enderror dark:bg-gray-700 dark:text-gray-100">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="slug" class="block text-sm font-medium text-gray-700"> <label for="slug" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
代碼 (URL slug) 代碼 (URL slug)
</label> </label>
<input type="text" name="slug" id="slug" value="{{ old('slug') }}" <input type="text" name="slug" id="slug" value="{{ old('slug') }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('slug') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 @error('slug') border-red-500 @enderror dark:bg-gray-700 dark:text-gray-100">
<p class="mt-1 text-sm text-gray-500">留空則自動產生</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">留空則自動產生</p>
@error('slug') @error('slug')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700"> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
說明 說明
</label> </label>
<textarea name="description" id="description" rows="3" <textarea name="description" id="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description') }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-700 dark:text-gray-100">{{ old('description') }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="icon" class="block text-sm font-medium text-gray-700"> <label for="icon" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
圖示 (emoji) 圖示 (emoji)
</label> </label>
<input type="text" name="icon" id="icon" value="{{ old('icon') }}" placeholder="📄" <input type="text" name="icon" id="icon" value="{{ old('icon') }}" placeholder="📄"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-700 dark:text-gray-100">
<p class="mt-1 text-sm text-gray-500">輸入 emoji例如📄 📝 📊 📋</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">輸入 emoji例如📄 📝 📊 📋</p>
@error('icon') @error('icon')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="default_access_level" class="block text-sm font-medium text-gray-700"> <label for="default_access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
預設存取權限 <span class="text-red-500">*</span> 預設存取權限 <span class="text-red-500">*</span>
</label> </label>
<select name="default_access_level" id="default_access_level" required <select name="default_access_level" id="default_access_level" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-700 dark:text-gray-100">
<option value="public" {{ old('default_access_level') === 'public' ? 'selected' : '' }}>公開 (任何人可查看)</option> <option value="public" {{ old('default_access_level') === 'public' ? 'selected' : '' }}>公開 (任何人可查看)</option>
<option value="members" {{ old('default_access_level') === 'members' ? 'selected' : '' }}>會員 (需登入且為會員)</option> <option value="members" {{ old('default_access_level') === 'members' ? 'selected' : '' }}>會員 (需登入且為會員)</option>
<option value="admin" {{ old('default_access_level') === 'admin' ? 'selected' : '' }}>管理員 (僅管理員可查看)</option> <option value="admin" {{ old('default_access_level') === 'admin' ? 'selected' : '' }}>管理員 (僅管理員可查看)</option>
<option value="board" {{ old('default_access_level') === 'board' ? 'selected' : '' }}>理事會 (僅理事會成員)</option> <option value="board" {{ old('default_access_level') === 'board' ? 'selected' : '' }}>理事會 (僅理事會成員)</option>
</select> </select>
@error('default_access_level') @error('default_access_level')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="sort_order" class="block text-sm font-medium text-gray-700"> <label for="sort_order" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
排序順序 排序順序
</label> </label>
<input type="number" name="sort_order" id="sort_order" value="{{ old('sort_order', 0) }}" <input type="number" name="sort_order" id="sort_order" value="{{ old('sort_order', 0) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-700 dark:text-gray-100">
<p class="mt-1 text-sm text-gray-500">數字越小越前面</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">數字越小越前面</p>
@error('sort_order') @error('sort_order')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="flex items-center justify-end space-x-4 pt-4"> <div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ route('admin.document-categories.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <a href="{{ route('admin.document-categories.index') }}" class="rounded-md border border-gray-300 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
取消 取消
</a> </a>
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"> <button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
建立類別 建立類別
</button> </button>
</div> </div>

View File

@@ -1,96 +1,54 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
編輯文件類別 編輯分類
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-3xl sm:px-6 lg:px-8"> <div class="mx-auto max-w-3xl sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<form action="{{ route('admin.document-categories.update', $documentCategory) }}" method="POST" class="p-6 space-y-6"> <form action="{{ route('admin.document-categories.update', $documentCategory) }}" method="POST" class="p-6 space-y-6">
@csrf @csrf
@method('PATCH') @method('PATCH')
<div> <div>
<label for="name" class="block text-sm font-medium text-gray-700"> <label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
名稱 <span class="text-red-500">*</span> 類名稱 <span class="text-red-500">*</span>
</label> </label>
<input type="text" name="name" id="name" value="{{ old('name', $documentCategory->name) }}" required <input type="text" name="name" id="name" value="{{ old('name', $documentCategory->name) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('name') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
@error('name') @error('name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="slug" class="block text-sm font-medium text-gray-700"> <label for="icon" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
代碼 (URL slug) 圖示(表情符號)
</label> </label>
<input type="text" name="slug" id="slug" value="{{ old('slug', $documentCategory->slug) }}" <input type="text" name="icon" id="icon" value="{{ old('icon', $documentCategory->icon) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('slug') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
@error('slug') <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">例如:📁, 📃, 📊</p>
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> @error('icon')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700"> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
說明 描述
</label> </label>
<textarea name="description" id="description" rows="3" <textarea name="description" id="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $documentCategory->description) }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-700 dark:text-gray-100">{{ old('description', $documentCategory->description) }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div class="flex items-center justify-end">
<label for="icon" class="block text-sm font-medium text-gray-700"> <button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
圖示 (emoji) 更新分類
</label>
<input type="text" name="icon" id="icon" value="{{ old('icon', $documentCategory->icon) }}" placeholder="📄"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-sm text-gray-500">輸入 emoji例如📄 📝 📊 📋</p>
@error('icon')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="default_access_level" class="block text-sm font-medium text-gray-700">
預設存取權限 <span class="text-red-500">*</span>
</label>
<select name="default_access_level" id="default_access_level" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option value="public" {{ old('default_access_level', $documentCategory->default_access_level) === 'public' ? 'selected' : '' }}>公開 (任何人可查看)</option>
<option value="members" {{ old('default_access_level', $documentCategory->default_access_level) === 'members' ? 'selected' : '' }}>會員 (需登入且為會員)</option>
<option value="admin" {{ old('default_access_level', $documentCategory->default_access_level) === 'admin' ? 'selected' : '' }}>管理員 (僅管理員可查看)</option>
<option value="board" {{ old('default_access_level', $documentCategory->default_access_level) === 'board' ? 'selected' : '' }}>理事會 (僅理事會成員)</option>
</select>
@error('default_access_level')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="sort_order" class="block text-sm font-medium text-gray-700">
排序順序
</label>
<input type="number" name="sort_order" id="sort_order" value="{{ old('sort_order', $documentCategory->sort_order) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<p class="mt-1 text-sm text-gray-500">數字越小越前面</p>
@error('sort_order')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ route('admin.document-categories.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50">
取消
</a>
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700">
更新類別
</button> </button>
</div> </div>
</form> </form>

View File

@@ -1,6 +1,6 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
文件類別管理 文件類別管理
</h2> </h2>
</x-slot> </x-slot>
@@ -8,23 +8,23 @@
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6">
@if (session('status')) @if (session('status'))
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 dark:bg-green-900/30 p-4">
<p class="text-sm font-medium text-green-800">{{ session('status') }}</p> <p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div> </div>
@endif @endif
@if (session('error')) @if (session('error'))
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4">
<p class="text-sm font-medium text-red-800">{{ session('error') }}</p> <p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div> </div>
@endif @endif
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<h3 class="text-lg font-medium text-gray-900">文件類別</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">文件類別</h3>
<p class="mt-1 text-sm text-gray-600">管理文件分類,設定預設存取權限</p> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">管理文件分類,設定預設存取權限</p>
</div> </div>
<a href="{{ route('admin.document-categories.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"> <a href="{{ route('admin.document-categories.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
<svg class="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -32,60 +32,60 @@
</a> </a>
</div> </div>
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">圖示</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">圖示</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">名稱</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">名稱</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">代碼</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">代碼</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">預設存取</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">預設存取</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">文件數量</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">文件數量</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">排序</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">排序</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">操作</th> <th scope="col" class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">操作</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@forelse ($categories as $category) @forelse ($categories as $category)
<tr> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4 whitespace-nowrap text-2xl"> <td class="px-6 py-4 whitespace-nowrap text-2xl">
{{ $category->getIconDisplay() }} {{ $category->getIconDisplay() }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">{{ $category->name }}</div> <div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $category->name }}</div>
@if($category->description) @if($category->description)
<div class="text-sm text-gray-500">{{ $category->description }}</div> <div class="text-sm text-gray-500 dark:text-gray-400">{{ $category->description }}</div>
@endif @endif
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<code class="px-2 py-1 bg-gray-100 rounded">{{ $category->slug }}</code> <code class="px-2 py-1 bg-gray-100 rounded dark:bg-gray-700 dark:text-gray-200">{{ $category->slug }}</code>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold <span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold
@if($category->default_access_level === 'public') bg-green-100 text-green-800 @if($category->default_access_level === 'public') bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200
@elseif($category->default_access_level === 'members') bg-blue-100 text-blue-800 @elseif($category->default_access_level === 'members') bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-200
@elseif($category->default_access_level === 'admin') bg-purple-100 text-purple-800 @elseif($category->default_access_level === 'admin') bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-200
@else bg-gray-100 text-gray-800 @else bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200
@endif"> @endif">
{{ $category->getAccessLevelLabel() }} {{ $category->getAccessLevelLabel() }}
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $category->active_documents_count }} {{ $category->active_documents_count }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $category->sort_order }} {{ $category->sort_order }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2"> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2">
<a href="{{ route('admin.document-categories.edit', $category) }}" class="text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.document-categories.edit', $category) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">
編輯 編輯
</a> </a>
<form action="{{ route('admin.document-categories.destroy', $category) }}" method="POST" class="inline" onsubmit="return confirm('確定要刪除此類別嗎?');"> <form action="{{ route('admin.document-categories.destroy', $category) }}" method="POST" class="inline" onsubmit="return confirm('確定要刪除此類別嗎?');">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900"> <button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
刪除 刪除
</button> </button>
</form> </form>
@@ -93,7 +93,7 @@
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="7" class="px-6 py-4 text-center text-sm text-gray-500"> <td colspan="7" class="px-6 py-4 text-center text-sm text-gray-500 dark:text-gray-400">
尚無類別資料 尚無類別資料
</td> </td>
</tr> </tr>

View File

@@ -1,22 +1,22 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
上傳文件 上傳文件
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-3xl sm:px-6 lg:px-8"> <div class="mx-auto max-w-3xl sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<form action="{{ route('admin.documents.store') }}" method="POST" enctype="multipart/form-data" class="p-6 space-y-6"> <form action="{{ route('admin.documents.store') }}" method="POST" enctype="multipart/form-data" class="p-6 space-y-6">
@csrf @csrf
<div> <div>
<label for="document_category_id" class="block text-sm font-medium text-gray-700"> <label for="document_category_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
文件類別 <span class="text-red-500">*</span> 文件類別 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<select name="document_category_id" id="document_category_id" required <select name="document_category_id" id="document_category_id" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('document_category_id') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300 @error('document_category_id') border-red-500 @enderror">
<option value="">請選擇類別</option> <option value="">請選擇類別</option>
@foreach($categories as $category) @foreach($categories as $category)
<option value="{{ $category->id }}" {{ old('document_category_id') == $category->id ? 'selected' : '' }}> <option value="{{ $category->id }}" {{ old('document_category_id') == $category->id ? 'selected' : '' }}>
@@ -25,97 +25,97 @@
@endforeach @endforeach
</select> </select>
@error('document_category_id') @error('document_category_id')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="title" class="block text-sm font-medium text-gray-700"> <label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
文件標題 <span class="text-red-500">*</span> 文件標題 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<input type="text" name="title" id="title" value="{{ old('title') }}" required <input type="text" name="title" id="title" value="{{ old('title') }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('title') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300 @error('title') border-red-500 @enderror">
@error('title') @error('title')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="document_number" class="block text-sm font-medium text-gray-700"> <label for="document_number" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
文件編號 文件編號
</label> </label>
<input type="text" name="document_number" id="document_number" value="{{ old('document_number') }}" <input type="text" name="document_number" id="document_number" value="{{ old('document_number') }}"
placeholder="例如BYL-2024-001" placeholder="例如BYL-2024-001"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('document_number') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300 @error('document_number') border-red-500 @enderror">
<p class="mt-1 text-sm text-gray-500">選填,用於正式文件編號</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">選填,用於正式文件編號</p>
@error('document_number') @error('document_number')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700"> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
文件說明 文件說明
</label> </label>
<textarea name="description" id="description" rows="3" <textarea name="description" id="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('description') border-red-500 @enderror">{{ old('description') }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300 @error('description') border-red-500 @enderror">{{ old('description') }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="access_level" class="block text-sm font-medium text-gray-700"> <label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
存取權限 <span class="text-red-500">*</span> 存取權限 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<select name="access_level" id="access_level" required <select name="access_level" id="access_level" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('access_level') border-red-500 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300 @error('access_level') border-red-500 @enderror">
<option value="public" {{ old('access_level') === 'public' ? 'selected' : '' }}>公開 (任何人可查看)</option> <option value="public" {{ old('access_level') === 'public' ? 'selected' : '' }}>公開 (任何人可查看)</option>
<option value="members" {{ old('access_level') === 'members' ? 'selected' : '' }}>會員 (需登入且為會員)</option> <option value="members" {{ old('access_level') === 'members' ? 'selected' : '' }}>會員 (需登入且為會員)</option>
<option value="admin" {{ old('access_level') === 'admin' ? 'selected' : '' }}>管理員 (僅管理員可查看)</option> <option value="admin" {{ old('access_level') === 'admin' ? 'selected' : '' }}>管理員 (僅管理員可查看)</option>
<option value="board" {{ old('access_level') === 'board' ? 'selected' : '' }}>理事會 (僅理事會成員)</option> <option value="board" {{ old('access_level') === 'board' ? 'selected' : '' }}>理事會 (僅理事會成員)</option>
</select> </select>
@error('access_level') @error('access_level')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="file" class="block text-sm font-medium text-gray-700"> <label for="file" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
上傳檔案 <span class="text-red-500">*</span> 上傳檔案 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<input type="file" name="file" id="file" required accept=".pdf,.doc,.docx,.xls,.xlsx,.txt" <input type="file" name="file" id="file" required accept=".pdf,.doc,.docx,.xls,.xlsx,.txt"
class="mt-1 block w-full text-sm text-gray-500 class="mt-1 block w-full text-sm text-gray-500 dark:text-gray-400
file:mr-4 file:py-2 file:px-4 file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0 file:rounded-md file:border-0
file:text-sm file:font-semibold file:text-sm file:font-semibold
file:bg-indigo-50 file:text-indigo-700 file:bg-indigo-50 dark:file:bg-indigo-900/50 file:text-indigo-700 dark:file:text-indigo-300
hover:file:bg-indigo-100 hover:file:bg-indigo-100 dark:hover:file:bg-indigo-800
@error('file') border-red-500 @enderror"> @error('file') border-red-500 @enderror">
<p class="mt-1 text-sm text-gray-500">支援格式PDF, Word, Excel, 文字檔,最大 10MB</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">支援格式PDF, Word, Excel, 文字檔,最大 10MB</p>
@error('file') @error('file')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="version_notes" class="block text-sm font-medium text-gray-700"> <label for="version_notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
版本說明 版本說明
</label> </label>
<textarea name="version_notes" id="version_notes" rows="2" placeholder="例如:初始版本" <textarea name="version_notes" id="version_notes" rows="2" placeholder="例如:初始版本"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('version_notes') }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300">{{ old('version_notes') }}</textarea>
<p class="mt-1 text-sm text-gray-500">說明此版本的內容或變更</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">說明此版本的內容或變更</p>
@error('version_notes') @error('version_notes')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="border-t border-gray-200 pt-4"> <div class="border-t border-gray-200 dark:border-gray-700 pt-4">
<div class="flex items-center justify-end space-x-4"> <div class="flex items-center justify-end space-x-4">
<a href="{{ route('admin.documents.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <a href="{{ route('admin.documents.index') }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
取消 取消
</a> </a>
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"> <button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
上傳文件 上傳文件
</button> </button>
</div> </div>

View File

@@ -1,23 +1,23 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
編輯文件資訊 編輯文件資訊
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-3xl sm:px-6 lg:px-8"> <div class="mx-auto max-w-3xl sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<form action="{{ route('admin.documents.update', $document) }}" method="POST" class="p-6 space-y-6"> <form action="{{ route('admin.documents.update', $document) }}" method="POST" class="p-6 space-y-6">
@csrf @csrf
@method('PATCH') @method('PATCH')
<div> <div>
<label for="document_category_id" class="block text-sm font-medium text-gray-700"> <label for="document_category_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
文件類別 <span class="text-red-500">*</span> 文件類別 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<select name="document_category_id" id="document_category_id" required <select name="document_category_id" id="document_category_id" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300">
@foreach($categories as $category) @foreach($categories as $category)
<option value="{{ $category->id }}" {{ old('document_category_id', $document->document_category_id) == $category->id ? 'selected' : '' }}> <option value="{{ $category->id }}" {{ old('document_category_id', $document->document_category_id) == $category->id ? 'selected' : '' }}>
{{ $category->icon }} {{ $category->name }} {{ $category->icon }} {{ $category->name }}
@@ -25,69 +25,69 @@
@endforeach @endforeach
</select> </select>
@error('document_category_id') @error('document_category_id')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="title" class="block text-sm font-medium text-gray-700"> <label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
文件標題 <span class="text-red-500">*</span> 文件標題 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<input type="text" name="title" id="title" value="{{ old('title', $document->title) }}" required <input type="text" name="title" id="title" value="{{ old('title', $document->title) }}" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300">
@error('title') @error('title')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="document_number" class="block text-sm font-medium text-gray-700"> <label for="document_number" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
文件編號 文件編號
</label> </label>
<input type="text" name="document_number" id="document_number" value="{{ old('document_number', $document->document_number) }}" <input type="text" name="document_number" id="document_number" value="{{ old('document_number', $document->document_number) }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300">
@error('document_number') @error('document_number')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700"> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
文件說明 文件說明
</label> </label>
<textarea name="description" id="description" rows="3" <textarea name="description" id="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">{{ old('description', $document->description) }}</textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300">{{ old('description', $document->description) }}</textarea>
@error('description') @error('description')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="access_level" class="block text-sm font-medium text-gray-700"> <label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
存取權限 <span class="text-red-500">*</span> 存取權限 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<select name="access_level" id="access_level" required <select name="access_level" id="access_level" required
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300">
<option value="public" {{ old('access_level', $document->access_level) === 'public' ? 'selected' : '' }}>公開 (任何人可查看)</option> <option value="public" {{ old('access_level', $document->access_level) === 'public' ? 'selected' : '' }}>公開 (任何人可查看)</option>
<option value="members" {{ old('access_level', $document->access_level) === 'members' ? 'selected' : '' }}>會員 (需登入且為會員)</option> <option value="members" {{ old('access_level', $document->access_level) === 'members' ? 'selected' : '' }}>會員 (需登入且為會員)</option>
<option value="admin" {{ old('access_level', $document->access_level) === 'admin' ? 'selected' : '' }}>管理員 (僅管理員可查看)</option> <option value="admin" {{ old('access_level', $document->access_level) === 'admin' ? 'selected' : '' }}>管理員 (僅管理員可查看)</option>
<option value="board" {{ old('access_level', $document->access_level) === 'board' ? 'selected' : '' }}>理事會 (僅理事會成員)</option> <option value="board" {{ old('access_level', $document->access_level) === 'board' ? 'selected' : '' }}>理事會 (僅理事會成員)</option>
</select> </select>
@error('access_level') @error('access_level')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p> <p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4"> <div class="bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-800 rounded-md p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400 dark:text-yellow-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">注意</h3> <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-300">注意</h3>
<div class="mt-2 text-sm text-yellow-700"> <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-400">
<p>此處僅更新文件資訊,不會變更檔案內容。如需更新檔案,請使用「上傳新版本」功能。</p> <p>此處僅更新文件資訊,不會變更檔案內容。如需更新檔案,請使用「上傳新版本」功能。</p>
</div> </div>
</div> </div>
@@ -95,10 +95,10 @@
</div> </div>
<div class="flex items-center justify-end space-x-4 pt-4"> <div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ route('admin.documents.show', $document) }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <a href="{{ route('admin.documents.show', $document) }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
取消 取消
</a> </a>
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"> <button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
更新資訊 更新資訊
</button> </button>
</div> </div>

View File

@@ -1,14 +1,14 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
文件管理 文件管理
</h2> </h2>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<a href="{{ route('admin.documents.statistics') }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <a href="{{ route('admin.documents.statistics') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
📊 統計分析 📊 統計分析
</a> </a>
<a href="{{ route('admin.documents.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700"> <a href="{{ route('admin.documents.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
+ 上傳文件 + 上傳文件
</a> </a>
</div> </div>
@@ -18,29 +18,29 @@
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6">
@if (session('status')) @if (session('status'))
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 dark:bg-green-900/50 p-4">
<p class="text-sm font-medium text-green-800">{{ session('status') }}</p> <p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div> </div>
@endif @endif
@if (session('error')) @if (session('error'))
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
<p class="text-sm font-medium text-red-800">{{ session('error') }}</p> <p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div> </div>
@endif @endif
<!-- Search and Filter --> <!-- Search and Filter -->
<div class="bg-white shadow sm:rounded-lg p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg p-6">
<form method="GET" action="{{ route('admin.documents.index') }}" class="space-y-4"> <form method="GET" action="{{ route('admin.documents.index') }}" class="space-y-4">
<div class="grid grid-cols-1 gap-4 md:grid-cols-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div> <div>
<label for="search" class="block text-sm font-medium text-gray-700">搜尋</label> <label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">搜尋</label>
<input type="text" name="search" id="search" value="{{ request('search') }}" placeholder="標題、文號..." <input type="text" name="search" id="search" value="{{ request('search') }}" placeholder="標題、文號..."
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
</div> </div>
<div> <div>
<label for="category" class="block text-sm font-medium text-gray-700">類別</label> <label for="category" class="block text-sm font-medium text-gray-700 dark:text-gray-300">類別</label>
<select name="category" id="category" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <select name="category" id="category" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部類別</option> <option value="">全部類別</option>
@foreach($categories as $cat) @foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ request('category') == $cat->id ? 'selected' : '' }}> <option value="{{ $cat->id }}" {{ request('category') == $cat->id ? 'selected' : '' }}>
@@ -50,8 +50,8 @@
</select> </select>
</div> </div>
<div> <div>
<label for="access_level" class="block text-sm font-medium text-gray-700">存取權限</label> <label for="access_level" class="block text-sm font-medium text-gray-700 dark:text-gray-300">存取權限</label>
<select name="access_level" id="access_level" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <select name="access_level" id="access_level" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部</option> <option value="">全部</option>
<option value="public" {{ request('access_level') === 'public' ? 'selected' : '' }}>公開</option> <option value="public" {{ request('access_level') === 'public' ? 'selected' : '' }}>公開</option>
<option value="members" {{ request('access_level') === 'members' ? 'selected' : '' }}>會員</option> <option value="members" {{ request('access_level') === 'members' ? 'selected' : '' }}>會員</option>
@@ -60,8 +60,8 @@
</select> </select>
</div> </div>
<div> <div>
<label for="status" class="block text-sm font-medium text-gray-700">狀態</label> <label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">狀態</label>
<select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"> <select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部</option> <option value="">全部</option>
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>啟用</option> <option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>啟用</option>
<option value="archived" {{ request('status') === 'archived' ? 'selected' : '' }}>封存</option> <option value="archived" {{ request('status') === 'archived' ? 'selected' : '' }}>封存</option>
@@ -69,10 +69,10 @@
</div> </div>
</div> </div>
<div class="flex justify-end space-x-2"> <div class="flex justify-end space-x-2">
<a href="{{ route('admin.documents.index') }}" class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <a href="{{ route('admin.documents.index') }}" class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
清除 清除
</a> </a>
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"> <button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
搜尋 搜尋
</button> </button>
</div> </div>
@@ -81,9 +81,9 @@
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div> <div>
<p class="text-sm text-gray-600"> {{ $documents->total() }} 個文件</p> <p class="text-sm text-gray-600 dark:text-gray-400"> {{ $documents->total() }} 個文件</p>
</div> </div>
<a href="{{ route('admin.documents.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"> <a href="{{ route('admin.documents.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
<svg class="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="-ml-1 mr-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -92,56 +92,56 @@
</div> </div>
<!-- Documents Table --> <!-- Documents Table -->
<div class="bg-white shadow overflow-hidden sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">文件</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">文件</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">類別</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">類別</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">存取</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">存取</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">版本</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">版本</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">統計</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">統計</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">狀態</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">狀態</th>
<th scope="col" class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500">操作</th> <th scope="col" class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">操作</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@forelse ($documents as $document) @forelse ($documents as $document)
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center"> <div class="flex items-center">
<div class="text-2xl mr-3"> <div class="text-2xl mr-3">
{{ $document->currentVersion?->getFileIcon() ?? '📄' }} {{ $document->currentVersion?->getFileIcon() ?? '📄' }}
</div> </div>
<div> <div>
<div class="text-sm font-medium text-gray-900">{{ $document->title }}</div> <div class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $document->title }}</div>
@if($document->document_number) @if($document->document_number)
<div class="text-xs text-gray-500">{{ $document->document_number }}</div> <div class="text-xs text-gray-500 dark:text-gray-400">{{ $document->document_number }}</div>
@endif @endif
@if($document->description) @if($document->description)
<div class="text-xs text-gray-500 mt-1">{{ Str::limit($document->description, 60) }}</div> <div class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ Str::limit($document->description, 60) }}</div>
@endif @endif
</div> </div>
</div> </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $document->category->icon }} {{ $document->category->name }} {{ $document->category->icon }} {{ $document->category->name }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold <span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold
@if($document->access_level === 'public') bg-green-100 text-green-800 @if($document->access_level === 'public') bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200
@elseif($document->access_level === 'members') bg-blue-100 text-blue-800 @elseif($document->access_level === 'members') bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200
@elseif($document->access_level === 'admin') bg-purple-100 text-purple-800 @elseif($document->access_level === 'admin') bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200
@else bg-gray-100 text-gray-800 @else bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300
@endif"> @endif">
{{ $document->getAccessLevelLabel() }} {{ $document->getAccessLevelLabel() }}
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<div>v{{ $document->currentVersion?->version_number ?? '—' }}</div> <div>v{{ $document->currentVersion?->version_number ?? '—' }}</div>
<div class="text-xs text-gray-400"> {{ $document->version_count }} </div> <div class="text-xs text-gray-400"> {{ $document->version_count }} </div>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<span title="檢視次數">👁️ {{ $document->view_count }}</span> <span title="檢視次數">👁️ {{ $document->view_count }}</span>
<span title="下載次數">⬇️ {{ $document->download_count }}</span> <span title="下載次數">⬇️ {{ $document->download_count }}</span>
@@ -149,12 +149,12 @@
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold <span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold
{{ $document->status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}"> {{ $document->status === 'active' ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300' }}">
{{ $document->getStatusLabel() }} {{ $document->getStatusLabel() }}
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('admin.documents.show', $document) }}" class="text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.documents.show', $document) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
查看 查看
</a> </a>
</td> </td>
@@ -162,12 +162,12 @@
@empty @empty
<tr> <tr>
<td colspan="7" class="px-6 py-12 text-center"> <td colspan="7" class="px-6 py-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
<p class="mt-2 text-sm text-gray-500">尚無文件</p> <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">尚無文件</p>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<a href="{{ route('admin.documents.create') }}" class="text-indigo-600 hover:text-indigo-900">上傳第一個文件</a> <a href="{{ route('admin.documents.create') }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">上傳第一個文件</a>
</p> </p>
</td> </td>
</tr> </tr>
@@ -176,7 +176,7 @@
</table> </table>
@if($documents->hasPages()) @if($documents->hasPages())
<div class="px-6 py-4 border-t border-gray-200"> <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
{{ $documents->links() }} {{ $documents->links() }}
</div> </div>
@endif @endif

View File

@@ -1,24 +1,24 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ $document->title }} {{ $document->title }}
</h2> </h2>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<a href="{{ route('admin.documents.edit', $document) }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <a href="{{ route('admin.documents.edit', $document) }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
編輯資訊 編輯資訊
</a> </a>
@if($document->status === 'active') @if($document->status === 'active')
<form action="{{ route('admin.documents.archive', $document) }}" method="POST" class="inline"> <form action="{{ route('admin.documents.archive', $document) }}" method="POST" class="inline">
@csrf @csrf
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
封存 封存
</button> </button>
</form> </form>
@else @else
<form action="{{ route('admin.documents.restore', $document) }}" method="POST" class="inline"> <form action="{{ route('admin.documents.restore', $document) }}" method="POST" class="inline">
@csrf @csrf
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> <button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
恢復 恢復
</button> </button>
</form> </form>
@@ -30,88 +30,94 @@
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6">
@if (session('status')) @if (session('status'))
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 dark:bg-green-900/50 p-4">
<p class="text-sm font-medium text-green-800">{{ session('status') }}</p> <p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div> </div>
@endif @endif
@if (session('error')) @if (session('error'))
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/50 p-4">
<p class="text-sm font-medium text-red-800">{{ session('error') }}</p> <p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div> </div>
@endif @endif
<!-- Document Info --> <!-- Document Info -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-6 py-5"> <div class="px-6 py-5">
<h3 class="text-lg font-medium leading-6 text-gray-900">文件資訊</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">文件資訊</h3>
</div> </div>
<div class="border-t border-gray-200 px-6 py-5"> <div class="border-t border-gray-200 dark:border-gray-700 px-6 py-5">
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2"> <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div> <div>
<dt class="text-sm font-medium text-gray-500">類別</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">類別</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->category->icon }} {{ $document->category->name }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $document->category->icon }} {{ $document->category->name }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">文件編號</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">文件編號</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->document_number ?? '—' }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $document->document_number ?? '—' }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">存取權限</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">存取權限</dt>
<dd class="mt-1"> <dd class="mt-1">
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold <span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold
@if($document->access_level === 'public') bg-green-100 text-green-800 @if($document->access_level === 'public') bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200
@elseif($document->access_level === 'members') bg-blue-100 text-blue-800 @elseif($document->access_level === 'members') bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200
@elseif($document->access_level === 'admin') bg-purple-100 text-purple-800 @elseif($document->access_level === 'admin') bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200
@else bg-gray-100 text-gray-800 @else bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300
@endif"> @endif">
{{ $document->getAccessLevelLabel() }} {{ $document->getAccessLevelLabel() }}
</span> </span>
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">狀態</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">狀態</dt>
<dd class="mt-1"> <dd class="mt-1">
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold <span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold
{{ $document->status === 'active' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}"> {{ $document->status === 'active' ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300' }}">
{{ $document->getStatusLabel() }} {{ $document->getStatusLabel() }}
</span> </span>
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">當前版本</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">當前版本</dt>
<dd class="mt-1 text-sm text-gray-900">v{{ $document->currentVersion->version_number }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
@if($document->currentVersion)
v{{ $document->currentVersion->version_number }}
@else
<span class="text-gray-400">尚無版本</span>
@endif
</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">總版本數</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">總版本數</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->version_count }} 個版本</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $document->version_count }} 個版本</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">檢視 / 下載次數</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">檢視 / 下載次數</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->view_count }} / {{ $document->download_count }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $document->view_count }} / {{ $document->download_count }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">公開連結</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">公開連結</dt>
<dd class="mt-1 text-sm"> <dd class="mt-1 text-sm">
<a href="{{ $document->getPublicUrl() }}" target="_blank" class="text-indigo-600 hover:text-indigo-900"> <a href="{{ $document->getPublicUrl() }}" target="_blank" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
{{ $document->getPublicUrl() }} {{ $document->getPublicUrl() }}
</a> </a>
</dd> </dd>
</div> </div>
@if($document->description) @if($document->description)
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">說明</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">說明</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->description }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $document->description }}</dd>
</div> </div>
@endif @endif
<div> <div>
<dt class="text-sm font-medium text-gray-500">建立者</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">建立者</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->createdBy->name }} · {{ $document->created_at->format('Y-m-d H:i') }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $document->createdBy->name }} · {{ $document->created_at->format('Y-m-d H:i') }}</dd>
</div> </div>
@if($document->lastUpdatedBy) @if($document->lastUpdatedBy)
<div> <div>
<dt class="text-sm font-medium text-gray-500">最後更新</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">最後更新</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->lastUpdatedBy->name }} · {{ $document->updated_at->format('Y-m-d H:i') }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $document->lastUpdatedBy->name }} · {{ $document->updated_at->format('Y-m-d H:i') }}</dd>
</div> </div>
@endif @endif
</dl> </dl>
@@ -119,31 +125,31 @@
</div> </div>
<!-- Upload New Version --> <!-- Upload New Version -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-6 py-5"> <div class="px-6 py-5">
<h3 class="text-lg font-medium leading-6 text-gray-900">上傳新版本</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">上傳新版本</h3>
</div> </div>
<div class="border-t border-gray-200 px-6 py-5"> <div class="border-t border-gray-200 dark:border-gray-700 px-6 py-5">
<form action="{{ route('admin.documents.upload-version', $document) }}" method="POST" enctype="multipart/form-data" class="space-y-4"> <form action="{{ route('admin.documents.upload-version', $document) }}" method="POST" enctype="multipart/form-data" class="space-y-4">
@csrf @csrf
<div> <div>
<label for="file" class="block text-sm font-medium text-gray-700">選擇檔案 <span class="text-red-500">*</span></label> <label for="file" class="block text-sm font-medium text-gray-700 dark:text-gray-300">選擇檔案 <span class="text-red-500 dark:text-red-400">*</span></label>
<input type="file" name="file" id="file" required <input type="file" name="file" id="file" required
class="mt-1 block w-full text-sm text-gray-500 class="mt-1 block w-full text-sm text-gray-500 dark:text-gray-400
file:mr-4 file:py-2 file:px-4 file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0 file:rounded-md file:border-0
file:text-sm file:font-semibold file:text-sm file:font-semibold
file:bg-indigo-50 file:text-indigo-700 file:bg-indigo-50 dark:file:bg-indigo-900/50 file:text-indigo-700 dark:file:text-indigo-300
hover:file:bg-indigo-100"> hover:file:bg-indigo-100 dark:hover:file:bg-indigo-800">
<p class="mt-1 text-sm text-gray-500">最大 10MB</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">最大 10MB</p>
</div> </div>
<div> <div>
<label for="version_notes" class="block text-sm font-medium text-gray-700">版本說明 <span class="text-red-500">*</span></label> <label for="version_notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">版本說明 <span class="text-red-500 dark:text-red-400">*</span></label>
<textarea name="version_notes" id="version_notes" rows="2" required placeholder="說明此版本的變更內容" <textarea name="version_notes" id="version_notes" rows="2" required placeholder="說明此版本的變更內容"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"></textarea> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-900 dark:text-gray-300"></textarea>
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
<button type="submit" class="rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"> <button type="submit" class="rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600">
上傳新版本 上傳新版本
</button> </button>
</div> </div>
@@ -152,13 +158,13 @@
</div> </div>
<!-- Version History --> <!-- Version History -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-6 py-5"> <div class="px-6 py-5">
<h3 class="text-lg font-medium leading-6 text-gray-900">版本歷史</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">版本歷史</h3>
<p class="mt-1 text-sm text-gray-600">所有版本永久保留,無法刪除</p> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">所有版本永久保留,無法刪除</p>
</div> </div>
<div class="border-t border-gray-200"> <div class="border-t border-gray-200 dark:border-gray-700">
<ul class="divide-y divide-gray-200"> <ul class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($versionHistory as $history) @foreach($versionHistory as $history)
@php $version = $history['version']; @endphp @php $version = $history['version']; @endphp
<li class="px-6 py-5"> <li class="px-6 py-5">
@@ -168,15 +174,15 @@
<span class="text-2xl">{{ $version->getFileIcon() }}</span> <span class="text-2xl">{{ $version->getFileIcon() }}</span>
<div> <div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="text-sm font-medium text-gray-900">版本 {{ $version->version_number }}</span> <span class="text-sm font-medium text-gray-900 dark:text-gray-100">版本 {{ $version->version_number }}</span>
@if($version->is_current) @if($version->is_current)
<span class="inline-flex rounded-full bg-green-100 px-2 py-1 text-xs font-semibold text-green-800"> <span class="inline-flex rounded-full bg-green-100 dark:bg-green-900 px-2 py-1 text-xs font-semibold text-green-800 dark:text-green-200">
當前版本 當前版本
</span> </span>
@endif @endif
</div> </div>
<div class="mt-1 text-sm text-gray-900">{{ $version->original_filename }}</div> <div class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $version->original_filename }}</div>
<div class="mt-1 flex items-center space-x-4 text-xs text-gray-500"> <div class="mt-1 flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
<span>{{ $version->getFileSizeHuman() }}</span> <span>{{ $version->getFileSizeHuman() }}</span>
<span>{{ $version->uploadedBy->name }}</span> <span>{{ $version->uploadedBy->name }}</span>
<span>{{ $version->uploaded_at->format('Y-m-d H:i') }}</span> <span>{{ $version->uploaded_at->format('Y-m-d H:i') }}</span>
@@ -185,17 +191,17 @@
@endif @endif
</div> </div>
@if($version->version_notes) @if($version->version_notes)
<div class="mt-2 text-sm text-gray-600"> <div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<span class="font-medium">變更說明:</span>{{ $version->version_notes }} <span class="font-medium">變更說明:</span>{{ $version->version_notes }}
</div> </div>
@endif @endif
<div class="mt-2 flex items-center space-x-2 text-xs text-gray-500"> <div class="mt-2 flex items-center space-x-2 text-xs text-gray-500 dark:text-gray-400">
<span>檔案雜湊:</span> <span>檔案雜湊:</span>
<code class="px-2 py-1 bg-gray-100 rounded">{{ substr($version->file_hash, 0, 16) }}...</code> <code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">{{ substr($version->file_hash, 0, 16) }}...</code>
@if($version->verifyIntegrity()) @if($version->verifyIntegrity())
<span class="text-green-600"> 完整</span> <span class="text-green-600 dark:text-green-400"> 完整</span>
@else @else
<span class="text-red-600"> 損壞</span> <span class="text-red-600 dark:text-red-400"> 損壞</span>
@endif @endif
</div> </div>
</div> </div>
@@ -203,13 +209,13 @@
</div> </div>
<div class="ml-6 flex flex-col space-y-2"> <div class="ml-6 flex flex-col space-y-2">
<a href="{{ route('admin.documents.download-version', [$document, $version]) }}" <a href="{{ route('admin.documents.download-version', [$document, $version]) }}"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"> class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
下載 下載
</a> </a>
@if(!$version->is_current) @if(!$version->is_current)
<form action="{{ route('admin.documents.promote-version', [$document, $version]) }}" method="POST"> <form action="{{ route('admin.documents.promote-version', [$document, $version]) }}" method="POST">
@csrf @csrf
<button type="submit" class="w-full inline-flex items-center justify-center rounded-md border border-indigo-300 bg-indigo-50 px-3 py-2 text-sm font-medium text-indigo-700 hover:bg-indigo-100" <button type="submit" class="w-full inline-flex items-center justify-center rounded-md border border-indigo-300 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/50 px-3 py-2 text-sm font-medium text-indigo-700 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-800"
onclick="return confirm('確定要將此版本設為當前版本嗎?');"> onclick="return confirm('確定要將此版本設為當前版本嗎?');">
設為當前 設為當前
</button> </button>
@@ -224,48 +230,48 @@
</div> </div>
<!-- Access Logs --> <!-- Access Logs -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-6 py-5"> <div class="px-6 py-5">
<h3 class="text-lg font-medium leading-6 text-gray-900">存取記錄</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">存取記錄</h3>
<p class="mt-1 text-sm text-gray-600">最近 20 </p> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">最近 20 </p>
</div> </div>
<div class="border-t border-gray-200"> <div class="border-t border-gray-200 dark:border-gray-700">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">時間</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">時間</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">使用者</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">使用者</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">動作</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">動作</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">IP</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">IP</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">瀏覽器</th> <th scope="col" class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">瀏覽器</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@forelse($document->accessLogs->take(20) as $log) @forelse($document->accessLogs->take(20) as $log)
<tr> <tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $log->accessed_at->format('Y-m-d H:i:s') }} {{ $log->accessed_at->format('Y-m-d H:i:s') }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
{{ $log->getUserDisplay() }} {{ $log->getUserDisplay() }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm"> <td class="px-6 py-4 whitespace-nowrap text-sm">
<span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold <span class="inline-flex rounded-full px-2 py-1 text-xs font-semibold
{{ $log->action === 'view' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800' }}"> {{ $log->action === 'view' ? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200' : 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' }}">
{{ $log->getActionLabel() }} {{ $log->getActionLabel() }}
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $log->ip_address }} {{ $log->ip_address }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $log->getBrowser() }} {{ $log->getBrowser() }}
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500"> <td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500 dark:text-gray-400">
尚無存取記錄 尚無存取記錄
</td> </td>
</tr> </tr>

View File

@@ -1,27 +1,26 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('New Finance Document') }} 新增報銷單
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-3xl sm:px-6 lg:px-8"> <div class="mx-auto max-w-3xl sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<form method="POST" action="{{ route('admin.finance.store') }}" enctype="multipart/form-data" class="space-y-6" aria-label="{{ __('Finance document submission form') }}"> <form method="POST" action="{{ route('admin.finance.store') }}" enctype="multipart/form-data" class="space-y-6" aria-label="報銷單提交表單">
@csrf @csrf
<div> <div>
<label for="member_id" class="block text-sm font-medium text-gray-700"> <label for="member_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Member (optional)') }} 會員(選填)
</label> </label>
<select <select
name="member_id" name="member_id"
id="member_id" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
> >
<option value="">{{ __('Not linked to a member') }}</option> <option value="">未連結會員</option>
@foreach ($members as $member) @foreach ($members as $member)
<option value="{{ $member->id }}" @selected(old('member_id') == $member->id)> <option value="{{ $member->id }}" @selected(old('member_id') == $member->id)>
{{ $member->full_name }} {{ $member->full_name }}
@@ -29,83 +28,89 @@
@endforeach @endforeach
</select> </select>
@error('member_id') @error('member_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p> <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="title" class="block text-sm font-medium text-gray-700"> <label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Title') }} 標題
</label> </label>
<input <input
type="text" type="text"
name="title" name="title"
id="title" id="title"
value="{{ old('title') }}" value="{{ old('title') }}"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
required required
> >
@error('title') @error('title')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p> <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="amount" class="block text-sm font-medium text-gray-700"> <label for="amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Amount') }} 金額 <span class="text-red-500">*</span>
</label> </label>
<input <div class="relative mt-1">
type="number" <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
step="0.01" <span class="text-gray-500 dark:text-gray-400 sm:text-sm">NT$</span>
name="amount" </div>
id="amount" <input
value="{{ old('amount') }}" type="number"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" step="0.01"
> name="amount"
id="amount"
value="{{ old('amount') }}"
class="block w-full rounded-md border-gray-300 dark:border-gray-700 pl-12 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
required
>
</div>
@error('amount') @error('amount')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p> <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700"> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Description') }} 描述
</label> </label>
<textarea <textarea
name="description" name="description"
id="description" id="description"
rows="4" rows="4"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
>{{ old('description') }}</textarea> >{{ old('description') }}</textarea>
@error('description') @error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p> <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
</div> </div>
<div> <div>
<label for="attachment" class="block text-sm font-medium text-gray-700"> <label for="attachment" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Attachment (optional)') }} 附件(選填)
</label> </label>
<input <input
type="file" type="file"
name="attachment" name="attachment"
id="attachment" id="attachment"
class="mt-1 block w-full text-sm text-gray-900 border border-gray-300 rounded-md cursor-pointer focus:outline-none focus:ring-indigo-500 focus:border-indigo-500" class="mt-1 block w-full text-sm text-gray-900 dark:text-gray-300 border border-gray-300 dark:border-gray-700 rounded-md cursor-pointer focus:outline-none focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:border-indigo-500 dark:bg-gray-900"
> >
@error('attachment') @error('attachment')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p> <p class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror @enderror
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('Max file size: 10MB') }} 最大檔案大小10MB
</p> </p>
</div> </div>
<div class="flex justify-end gap-3"> <div class="flex justify-end gap-3">
<a href="{{ route('admin.finance.index') }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.finance.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
{{ __('Cancel') }} 取消
</a> </a>
<button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <button type="submit" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white dark:text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
{{ __('Submit Document') }} 提交文件
</button> </button>
</div> </div>
</form> </form>

View File

@@ -1,82 +1,179 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Finance Documents') }} 報銷申請單
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-4"> <div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-4">
@if (session('status')) @if (session('status'))
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 dark:bg-green-900/30 p-4">
<p class="text-sm font-medium text-green-800">{{ session('status') }}</p> <p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div> </div>
@endif @endif
<div class="flex justify-end"> <div class="flex justify-between items-center">
<a href="{{ route('admin.finance.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">報銷單列表</h3>
{{ __('New Document') }} <a href="{{ route('admin.finance.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
新增報銷單
</a> </a>
</div> </div>
<div class="bg-white shadow sm:rounded-lg"> {{-- Filters --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<form method="GET" action="{{ route('admin.finance.index') }}" class="grid grid-cols-1 gap-4 sm:grid-cols-4">
{{-- 審核狀態篩選 --}}
<div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">審核狀態</label>
<select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部狀態</option>
<option value="pending" @selected(request('status') == 'pending')>待審核</option>
<option value="approved_secretary" @selected(request('status') == 'approved_secretary')>秘書長已核准</option>
<option value="approved_chair" @selected(request('status') == 'approved_chair')>理事長已核准</option>
<option value="approved_board" @selected(request('status') == 'approved_board')>董理事會已核准</option>
<option value="rejected" @selected(request('status') == 'rejected')>已駁回</option>
</select>
</div>
{{-- 工作流程階段篩選 --}}
<div>
<label for="workflow_stage" class="block text-sm font-medium text-gray-700 dark:text-gray-300">工作流程階段</label>
<select name="workflow_stage" id="workflow_stage" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部階段</option>
<option value="approval" @selected(request('workflow_stage') == 'approval')>審核階段</option>
<option value="payment" @selected(request('workflow_stage') == 'payment')>出帳階段</option>
<option value="recording" @selected(request('workflow_stage') == 'recording')>入帳階段</option>
<option value="completed" @selected(request('workflow_stage') == 'completed')>已完成</option>
</select>
</div>
{{-- 金額級別篩選 --}}
<div>
<label for="amount_tier" class="block text-sm font-medium text-gray-700 dark:text-gray-300">金額級別</label>
<select name="amount_tier" id="amount_tier" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部級別</option>
<option value="small" @selected(request('amount_tier') == 'small')>小額 (< 5,000)</option>
<option value="medium" @selected(request('amount_tier') == 'medium')>中額 (5,000-50,000)</option>
<option value="large" @selected(request('amount_tier') == 'large')>大額 (> 50,000)</option>
</select>
</div>
{{-- 篩選按鈕 --}}
<div class="flex items-end space-x-2">
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
篩選
</button>
<a href="{{ route('admin.finance.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
重設
</a>
</div>
</form>
</div>
</div>
{{-- Table --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200" role="table"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700" role="table">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
{{ __('Title') }} 標題
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
{{ __('Member') }} 申請人
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
{{ __('Amount') }} 金額
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
{{ __('Status') }} 審核狀態
</th> </th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
{{ __('Submitted At') }} 工作流程
</th> </th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"> <th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
<span class="sr-only">{{ __('View') }}</span> 提交時間
</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
<span class="sr-only">操作</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@forelse ($documents as $document) @forelse ($documents as $document)
<tr> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900"> <td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ $document->title }} <div class="font-medium">{{ $document->title }}</div>
</td> </td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900"> <td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ $document->member?->full_name ?? __('N/A') }} {{ $document->submittedBy?->name ?? '不適用' }}
</td> </td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900"> <td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
@if (! is_null($document->amount)) <div>NT$ {{ number_format($document->amount, 2) }}</div>
{{ number_format($document->amount, 2) }} <div class="text-xs text-gray-500 dark:text-gray-400">{{ $document->getAmountTierText() }}</div>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
@if ($document->isRejected())
<span class="inline-flex rounded-full bg-red-100 dark:bg-red-900 px-2 text-xs font-semibold leading-5 text-red-800 dark:text-red-200">
{{ $document->status_label }}
</span>
@elseif ($document->isApprovalComplete())
<span class="inline-flex rounded-full bg-green-100 dark:bg-green-900 px-2 text-xs font-semibold leading-5 text-green-800 dark:text-green-200">
{{ $document->status_label }}
</span>
@else @else
{{ __('N/A') }} <span class="inline-flex rounded-full bg-yellow-100 dark:bg-yellow-900 px-2 text-xs font-semibold leading-5 text-yellow-800 dark:text-yellow-200">
{{ $document->status_label }}
</span>
@endif @endif
</td> </td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900"> <td class="whitespace-nowrap px-4 py-3 text-sm">
{{ ucfirst($document->status) }} @if ($document->isRejected())
<span class="inline-flex rounded-full bg-red-100 dark:bg-red-900 px-2 text-xs font-semibold leading-5 text-red-800 dark:text-red-200">
已駁回
</span>
@elseif ($document->isRecordingComplete())
<span class="inline-flex rounded-full bg-green-100 dark:bg-green-900 px-2 text-xs font-semibold leading-5 text-green-800 dark:text-green-200">
已完成
</span>
@elseif ($document->isDisbursementComplete())
<span class="inline-flex rounded-full bg-blue-100 dark:bg-blue-900 px-2 text-xs font-semibold leading-5 text-blue-800 dark:text-blue-200">
入帳階段
</span>
@elseif ($document->isApprovalComplete())
<span class="inline-flex rounded-full bg-purple-100 dark:bg-purple-900 px-2 text-xs font-semibold leading-5 text-purple-800 dark:text-purple-200">
出帳階段
</span>
@else
<span class="inline-flex rounded-full bg-gray-100 dark:bg-gray-700 px-2 text-xs font-semibold leading-5 text-gray-800 dark:text-gray-200">
審核階段
</span>
@endif
</td> </td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900"> <td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ optional($document->submitted_at)->toDateString() }} {{ optional($document->submitted_at)->format('Y-m-d') }}
</td> </td>
<td class="whitespace-nowrap px-4 py-3 text-right text-sm"> <td class="whitespace-nowrap px-4 py-3 text-right text-sm">
<a href="{{ route('admin.finance.show', $document) }}" class="text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.finance.show', $document) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
{{ __('View') }} 檢視
</a> </a>
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="6" class="px-4 py-4 text-sm text-gray-500"> <td colspan="7" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
{{ __('No finance documents found.') }} <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="mt-2 text-sm font-semibold">找不到報銷申請單</p>
<p class="mt-1 text-sm">點選「新增報銷單」開始建立。</p>
</td> </td>
</tr> </tr>
@endforelse @endforelse
@@ -92,4 +189,3 @@
</div> </div>
</div> </div>
</x-app-layout> </x-app-layout>

View File

@@ -1,11 +1,11 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<h2 class="text-xl font-semibold leading-tight text-gray-800"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Finance Document Details') }} 報銷申請單詳情
</h2> </h2>
<a href="{{ route('admin.finance.index') }}" class="text-sm text-indigo-600 hover:text-indigo-900"> <a href="{{ route('admin.finance.index') }}" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
&larr; {{ __('Back to list') }} &larr; 返回列表
</a> </a>
</div> </div>
</x-slot> </x-slot>
@@ -14,39 +14,82 @@
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-6"> <div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-6">
{{-- Status Message --}} {{-- Status Message --}}
@if (session('status')) @if (session('status'))
<div class="rounded-md bg-green-50 p-4" role="status" aria-live="polite"> <div class="rounded-md bg-green-50 dark:bg-green-900/30 p-4" role="status" aria-live="polite">
<p class="text-sm font-medium text-green-800"> <p class="text-sm font-medium text-green-800 dark:text-green-200">
{{ session('status') }} {{ session('status') }}
</p> </p>
</div> </div>
@endif @endif
{{-- Document Details --}} {{-- Workflow Stage Overview --}}
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4"> <div class="flex items-center justify-between mb-4">
{{ __('Document Information') }} <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
工作流程階段
</h3>
<span class="inline-flex rounded-full px-3 py-1 text-sm font-semibold
@if ($document->isRejected())
bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200
@elseif ($document->isRecordingComplete())
bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200
@else
bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200
@endif
">
{{ $document->workflow_stage_label }}
</span>
</div>
{{-- Stage Progress Bar --}}
<div class="flex items-center space-x-2">
{{-- 審核階段 --}}
<div class="flex-1 text-center">
<div class="h-2 rounded-full {{ $document->isApprovalComplete() || $document->isRejected() ? 'bg-green-500' : 'bg-gray-200 dark:bg-gray-700' }}"></div>
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1">審核</span>
</div>
<div class="w-4 text-gray-400"></div>
{{-- 出帳階段 --}}
<div class="flex-1 text-center">
<div class="h-2 rounded-full {{ $document->isDisbursementComplete() ? 'bg-green-500' : ($document->isApprovalComplete() && !$document->isRejected() ? 'bg-yellow-500' : 'bg-gray-200 dark:bg-gray-700') }}"></div>
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1">出帳</span>
</div>
<div class="w-4 text-gray-400"></div>
{{-- 入帳階段 --}}
<div class="flex-1 text-center">
<div class="h-2 rounded-full {{ $document->isRecordingComplete() ? 'bg-green-500' : ($document->isDisbursementComplete() ? 'bg-yellow-500' : 'bg-gray-200 dark:bg-gray-700') }}"></div>
<span class="text-xs text-gray-500 dark:text-gray-400 mt-1">入帳</span>
</div>
</div>
</div>
</div>
{{-- Document Details --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">
報銷單資訊
</h3> </h3>
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2"> <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div> <div>
<dt class="text-sm font-medium text-gray-500">{{ __('Title') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">標題</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $document->title }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $document->title }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">{{ __('Status') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">審核狀態</dt>
<dd class="mt-1"> <dd class="mt-1">
@if ($document->isRejected()) @if ($document->isRejected())
<span class="inline-flex rounded-full bg-red-100 px-2 text-xs font-semibold leading-5 text-red-800"> <span class="inline-flex rounded-full bg-red-100 dark:bg-red-900 px-2 text-xs font-semibold leading-5 text-red-800 dark:text-red-200">
{{ $document->status_label }} {{ $document->status_label }}
</span> </span>
@elseif ($document->isFullyApproved()) @elseif ($document->isApprovalComplete())
<span class="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800"> <span class="inline-flex rounded-full bg-green-100 dark:bg-green-900 px-2 text-xs font-semibold leading-5 text-green-800 dark:text-green-200">
{{ $document->status_label }} {{ $document->status_label }}
</span> </span>
@else @else
<span class="inline-flex rounded-full bg-yellow-100 px-2 text-xs font-semibold leading-5 text-yellow-800"> <span class="inline-flex rounded-full bg-yellow-100 dark:bg-yellow-900 px-2 text-xs font-semibold leading-5 text-yellow-800 dark:text-yellow-200">
{{ $document->status_label }} {{ $document->status_label }}
</span> </span>
@endif @endif
@@ -54,52 +97,47 @@
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">{{ __('Member') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">金額</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
@if ($document->member) NT$ {{ number_format($document->amount, 2) }}
<a href="{{ route('admin.members.show', $document->member) }}" class="text-indigo-600 hover:text-indigo-900"> <span class="text-xs text-gray-500 dark:text-gray-400 ml-1">({{ $document->getAmountTierText() }})</span>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">申請人</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $document->submittedBy?->name ?? '不適用' }}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">提交時間</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $document->submitted_at?->format('Y-m-d H:i:s') ?? '不適用' }}
</dd>
</div>
@if ($document->member)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">關聯會員</dt>
<dd class="mt-1 text-sm">
<a href="{{ route('admin.members.show', $document->member) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
{{ $document->member->full_name }} {{ $document->member->full_name }}
</a> </a>
@else </dd>
<span class="text-gray-500">{{ __('Not linked to a member') }}</span> </div>
@endif @endif
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">{{ __('Amount') }}</dt>
<dd class="mt-1 text-sm text-gray-900">
@if (!is_null($document->amount))
${{ number_format($document->amount, 2) }}
@else
<span class="text-gray-500">{{ __('N/A') }}</span>
@endif
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">{{ __('Submitted by') }}</dt>
<dd class="mt-1 text-sm text-gray-900">
{{ $document->submittedBy?->name ?? __('N/A') }}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">{{ __('Submitted at') }}</dt>
<dd class="mt-1 text-sm text-gray-900">
{{ $document->submitted_at?->format('Y-m-d H:i:s') ?? __('N/A') }}
</dd>
</div>
@if ($document->attachment_path) @if ($document->attachment_path)
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">{{ __('Attachment') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">附件</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm">
<a href="{{ route('admin.finance.download', $document) }}" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"> <a href="{{ route('admin.finance.download', $document) }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg> </svg>
{{ __('Download Attachment') }} 下載附件
</a> </a>
</dd> </dd>
</div> </div>
@@ -107,57 +145,59 @@
@if ($document->description) @if ($document->description)
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">{{ __('Description') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">描述</dt>
<dd class="mt-1 whitespace-pre-line text-sm text-gray-900"> <dd class="mt-1 whitespace-pre-line text-sm text-gray-900 dark:text-gray-100">{{ $document->description }}</dd>
{{ $document->description }}
</dd>
</div> </div>
@endif @endif
</dl> </dl>
</div> </div>
</div> </div>
{{-- Approval Timeline --}} {{-- Approval Timeline (新工作流程) --}}
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4"> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">
{{ __('Approval Timeline') }} 審核時程
</h3> </h3>
<div class="flow-root"> <div class="flow-root">
<ul role="list" class="-mb-8"> <ul role="list" class="-mb-8">
{{-- Cashier Approval --}} {{-- 秘書長審核 (第一階段) --}}
<li> <li>
<div class="relative pb-8"> <div class="relative pb-8">
<span class="absolute left-4 top-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true"></span> <span class="absolute left-4 top-4 -ml-px h-full w-0.5 bg-gray-200 dark:bg-gray-700" aria-hidden="true"></span>
<div class="relative flex space-x-3"> <div class="relative flex space-x-3">
<div> <div>
@if ($document->cashier_approved_at) @if ($document->secretary_approved_at)
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-green-500 ring-8 ring-white"> <span class="flex h-8 w-8 items-center justify-center rounded-full bg-green-500 ring-8 ring-white dark:ring-gray-800">
<svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg> </svg>
</span> </span>
@elseif ($document->status === 'pending')
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-500 ring-8 ring-white dark:ring-gray-800">
<span class="h-2.5 w-2.5 rounded-full bg-white"></span>
</span>
@else @else
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-400 ring-8 ring-white"> <span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-400 dark:bg-gray-600 ring-8 ring-white dark:ring-gray-800">
<span class="h-2.5 w-2.5 rounded-full bg-white"></span> <span class="h-2.5 w-2.5 rounded-full bg-white"></span>
</span> </span>
@endif @endif
</div> </div>
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div> <div>
<p class="text-sm text-gray-900"> <p class="text-sm text-gray-900 dark:text-gray-100">
{{ __('Cashier Approval') }} 秘書長核准
@if ($document->cashier_approved_at) @if ($document->secretary_approved_at)
<span class="font-medium text-gray-900">{{ $document->approvedByCashier?->name }}</span> <span class="font-medium">{{ $document->approvedBySecretary?->name }}</span>
@endif @endif
</p> </p>
</div> </div>
<div class="whitespace-nowrap text-right text-sm text-gray-500"> <div class="whitespace-nowrap text-right text-sm text-gray-500 dark:text-gray-400">
@if ($document->cashier_approved_at) @if ($document->secretary_approved_at)
{{ $document->cashier_approved_at->format('Y-m-d H:i') }} {{ $document->secretary_approved_at->format('Y-m-d H:i') }}
@else @else
{{ __('Pending') }} 待處理
@endif @endif
</div> </div>
</div> </div>
@@ -165,90 +205,102 @@
</div> </div>
</li> </li>
{{-- Accountant Approval --}} {{-- 理事長審核 (第二階段:中額以上) --}}
<li> @if (in_array($document->amount_tier, ['medium', 'large']))
<div class="relative pb-8"> <li>
<span class="absolute left-4 top-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true"></span> <div class="relative pb-8">
<div class="relative flex space-x-3"> <span class="absolute left-4 top-4 -ml-px h-full w-0.5 bg-gray-200 dark:bg-gray-700" aria-hidden="true"></span>
<div> <div class="relative flex space-x-3">
@if ($document->accountant_approved_at)
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-green-500 ring-8 ring-white">
<svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
@else
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-400 ring-8 ring-white">
<span class="h-2.5 w-2.5 rounded-full bg-white"></span>
</span>
@endif
</div>
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div> <div>
<p class="text-sm text-gray-900">
{{ __('Accountant Approval') }}
@if ($document->accountant_approved_at)
<span class="font-medium text-gray-900">{{ $document->approvedByAccountant?->name }}</span>
@endif
</p>
</div>
<div class="whitespace-nowrap text-right text-sm text-gray-500">
@if ($document->accountant_approved_at)
{{ $document->accountant_approved_at->format('Y-m-d H:i') }}
@else
{{ __('Pending') }}
@endif
</div>
</div>
</div>
</div>
</li>
{{-- Chair Approval --}}
<li>
<div class="relative pb-8">
<div class="relative flex space-x-3">
<div>
@if ($document->chair_approved_at)
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-green-500 ring-8 ring-white">
<svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
@else
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-400 ring-8 ring-white">
<span class="h-2.5 w-2.5 rounded-full bg-white"></span>
</span>
@endif
</div>
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p class="text-sm text-gray-900">
{{ __('Chair Approval') }}
@if ($document->chair_approved_at)
<span class="font-medium text-gray-900">{{ $document->approvedByChair?->name }}</span>
@endif
</p>
</div>
<div class="whitespace-nowrap text-right text-sm text-gray-500">
@if ($document->chair_approved_at) @if ($document->chair_approved_at)
{{ $document->chair_approved_at->format('Y-m-d H:i') }} <span class="flex h-8 w-8 items-center justify-center rounded-full bg-green-500 ring-8 ring-white dark:ring-gray-800">
<svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
@elseif ($document->status === 'approved_secretary')
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-500 ring-8 ring-white dark:ring-gray-800">
<span class="h-2.5 w-2.5 rounded-full bg-white"></span>
</span>
@else @else
{{ __('Pending') }} <span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-400 dark:bg-gray-600 ring-8 ring-white dark:ring-gray-800">
<span class="h-2.5 w-2.5 rounded-full bg-white"></span>
</span>
@endif @endif
</div> </div>
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p class="text-sm text-gray-900 dark:text-gray-100">
理事長核准
@if ($document->chair_approved_at)
<span class="font-medium">{{ $document->approvedByChair?->name }}</span>
@endif
</p>
</div>
<div class="whitespace-nowrap text-right text-sm text-gray-500 dark:text-gray-400">
@if ($document->chair_approved_at)
{{ $document->chair_approved_at->format('Y-m-d H:i') }}
@else
待處理
@endif
</div>
</div>
</div> </div>
</div> </div>
</div> </li>
</li> @endif
{{-- Rejection Info --}} {{-- 董理事會審核 (第三階段:大額) --}}
@if ($document->amount_tier === 'large')
<li>
<div class="relative pb-8">
<div class="relative flex space-x-3">
<div>
@if ($document->board_meeting_approved_at)
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-green-500 ring-8 ring-white dark:ring-gray-800">
<svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
@elseif ($document->status === 'approved_chair')
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-yellow-500 ring-8 ring-white dark:ring-gray-800">
<span class="h-2.5 w-2.5 rounded-full bg-white"></span>
</span>
@else
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-400 dark:bg-gray-600 ring-8 ring-white dark:ring-gray-800">
<span class="h-2.5 w-2.5 rounded-full bg-white"></span>
</span>
@endif
</div>
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p class="text-sm text-gray-900 dark:text-gray-100">
董理事會核准
@if ($document->board_meeting_approved_at)
<span class="font-medium">{{ $document->approvedByBoardMeeting?->title ?? '理事會決議' }}</span>
@endif
</p>
</div>
<div class="whitespace-nowrap text-right text-sm text-gray-500 dark:text-gray-400">
@if ($document->board_meeting_approved_at)
{{ $document->board_meeting_approved_at->format('Y-m-d H:i') }}
@else
待處理
@endif
</div>
</div>
</div>
</div>
</li>
@endif
{{-- 駁回資訊 --}}
@if ($document->isRejected()) @if ($document->isRejected())
<li> <li>
<div class="relative"> <div class="relative">
<div class="relative flex space-x-3"> <div class="relative flex space-x-3">
<div> <div>
<span class="flex h-8 w-8 items-center justify-center rounded-full bg-red-500 ring-8 ring-white"> <span class="flex h-8 w-8 items-center justify-center rounded-full bg-red-500 ring-8 ring-white dark:ring-gray-800">
<svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg> </svg>
@@ -256,17 +308,17 @@
</div> </div>
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5"> <div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div class="flex-1"> <div class="flex-1">
<p class="text-sm text-gray-900"> <p class="text-sm text-gray-900 dark:text-gray-100">
{{ __('Rejected by') }} 駁回者
<span class="font-medium text-gray-900">{{ $document->rejectedBy?->name }}</span> <span class="font-medium">{{ $document->rejectedBy?->name }}</span>
</p> </p>
@if ($document->rejection_reason) @if ($document->rejection_reason)
<p class="mt-2 text-sm text-red-600"> <p class="mt-2 text-sm text-red-600 dark:text-red-400">
<strong>{{ __('Reason:') }}</strong> {{ $document->rejection_reason }} <strong>原因:</strong> {{ $document->rejection_reason }}
</p> </p>
@endif @endif
</div> </div>
<div class="whitespace-nowrap text-right text-sm text-gray-500"> <div class="whitespace-nowrap text-right text-sm text-gray-500 dark:text-gray-400">
{{ $document->rejected_at?->format('Y-m-d H:i') }} {{ $document->rejected_at?->format('Y-m-d H:i') }}
</div> </div>
</div> </div>
@@ -279,23 +331,150 @@
</div> </div>
</div> </div>
{{-- Approval Actions --}} {{-- 出帳確認區塊 --}}
@if (!$document->isRejected() && !$document->isFullyApproved()) @if ($document->isApprovalComplete() && !$document->isRejected())
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4"> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">
{{ __('Actions') }} 出帳確認
<span class="ml-2 text-sm font-normal text-gray-500 dark:text-gray-400">(需申請人與出納雙重確認)</span>
</h3>
<div class="space-y-4">
{{-- 申請人確認狀態 --}}
<div class="flex items-center justify-between p-4 rounded-lg {{ $document->requester_confirmed_at ? 'bg-green-50 dark:bg-green-900/30' : 'bg-gray-50 dark:bg-gray-700' }}">
<div class="flex items-center">
@if ($document->requester_confirmed_at)
<svg class="h-5 w-5 text-green-500 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
@else
<span class="h-5 w-5 rounded-full border-2 border-gray-300 dark:border-gray-500 mr-3"></span>
@endif
<div>
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">申請人確認領款</p>
@if ($document->requester_confirmed_at)
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ $document->requesterConfirmedBy?->name }} - {{ $document->requester_confirmed_at->format('Y-m-d H:i') }}
</p>
@endif
</div>
</div>
@if (!$document->requester_confirmed_at && $document->canRequesterConfirmDisbursement(auth()->user()))
<form method="POST" action="{{ route('admin.finance.confirm-disbursement', $document) }}">
@csrf
<button type="submit" class="inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700">
確認已領款
</button>
</form>
@endif
</div>
{{-- 出納確認狀態 --}}
<div class="flex items-center justify-between p-4 rounded-lg {{ $document->cashier_confirmed_at ? 'bg-green-50 dark:bg-green-900/30' : 'bg-gray-50 dark:bg-gray-700' }}">
<div class="flex items-center">
@if ($document->cashier_confirmed_at)
<svg class="h-5 w-5 text-green-500 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
@else
<span class="h-5 w-5 rounded-full border-2 border-gray-300 dark:border-gray-500 mr-3"></span>
@endif
<div>
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">出納確認出帳</p>
@if ($document->cashier_confirmed_at)
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ $document->cashierConfirmedBy?->name }} - {{ $document->cashier_confirmed_at->format('Y-m-d H:i') }}
</p>
@endif
</div>
</div>
@if (!$document->cashier_confirmed_at && $document->canCashierConfirmDisbursement() && (auth()->user()->hasRole('finance_cashier') || auth()->user()->hasRole('admin')))
<form method="POST" action="{{ route('admin.finance.confirm-disbursement', $document) }}">
@csrf
<button type="submit" class="inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700">
確認已出帳
</button>
</form>
@endif
</div>
{{-- 出帳狀態摘要 --}}
<div class="text-center text-sm {{ $document->isDisbursementComplete() ? 'text-green-600 dark:text-green-400' : 'text-gray-500 dark:text-gray-400' }}">
@if ($document->isDisbursementComplete())
出帳確認完成
@else
{{ $document->disbursement_status_label }}
@endif
</div>
</div>
</div>
</div>
@endif
{{-- 入帳確認區塊 --}}
@if ($document->isDisbursementComplete())
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">
入帳確認
</h3>
<div class="flex items-center justify-between p-4 rounded-lg {{ $document->accountant_recorded_at ? 'bg-green-50 dark:bg-green-900/30' : 'bg-gray-50 dark:bg-gray-700' }}">
<div class="flex items-center">
@if ($document->accountant_recorded_at)
<svg class="h-5 w-5 text-green-500 mr-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
@else
<span class="h-5 w-5 rounded-full border-2 border-gray-300 dark:border-gray-500 mr-3"></span>
@endif
<div>
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">會計確認入帳</p>
@if ($document->accountant_recorded_at)
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ $document->accountantRecordedBy?->name }} - {{ $document->accountant_recorded_at->format('Y-m-d H:i') }}
</p>
@endif
</div>
</div>
@if ($document->canAccountantConfirmRecording() && (auth()->user()->hasRole('finance_accountant') || auth()->user()->hasRole('admin')))
<form method="POST" action="{{ route('admin.finance.confirm-recording', $document) }}">
@csrf
<button type="submit" class="inline-flex items-center rounded-md bg-green-600 px-3 py-2 text-sm font-medium text-white hover:bg-green-700">
確認入帳
</button>
</form>
@endif
</div>
</div>
</div>
@endif
{{-- Approval Actions (審核中才顯示) --}}
@if (!$document->isRejected() && !$document->isApprovalComplete())
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100 mb-4">
審核操作
</h3> </h3>
<div class="flex gap-3"> <div class="flex gap-3">
{{-- Approve Button --}} {{-- Approve Button --}}
@php @php
$canApprove = false; $canApprove = false;
if (auth()->user()->hasRole('cashier') && $document->canBeApprovedByCashier()) { $isAdmin = auth()->user()->hasRole('admin');
$isSecretary = auth()->user()->hasRole('secretary_general');
$isChair = auth()->user()->hasRole('finance_chair');
$isBoardMember = auth()->user()->hasRole('finance_board_member');
if ($isAdmin && !$document->isApprovalComplete() && !$document->isRejected()) {
$canApprove = true; $canApprove = true;
} elseif (auth()->user()->hasRole('accountant') && $document->canBeApprovedByAccountant()) { } elseif ($isSecretary && $document->canBeApprovedBySecretary(auth()->user())) {
$canApprove = true; $canApprove = true;
} elseif (auth()->user()->hasRole('chair') && $document->canBeApprovedByChair()) { } elseif ($isChair && $document->canBeApprovedByChair(auth()->user())) {
$canApprove = true;
} elseif ($isBoardMember && $document->canBeApprovedByBoard(auth()->user())) {
$canApprove = true; $canApprove = true;
} }
@endphp @endphp
@@ -307,18 +486,21 @@
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg> </svg>
{{ __('Approve') }} 核准
@if ($isAdmin)
<span class="ml-1 text-xs opacity-75">(管理員)</span>
@endif
</button> </button>
</form> </form>
@endif @endif
{{-- Reject Button (show for cashier, accountant, chair) --}} {{-- Reject Button --}}
@if (auth()->user()->hasRole('cashier') || auth()->user()->hasRole('accountant') || auth()->user()->hasRole('chair')) @if (auth()->user()->hasRole('admin') || auth()->user()->hasRole('secretary_general') || auth()->user()->hasRole('finance_chair'))
<button type="button" onclick="document.getElementById('rejectModal').classList.remove('hidden')" class="inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"> <button type="button" onclick="document.getElementById('rejectModal').classList.remove('hidden')" class="inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
{{ __('Reject') }} 駁回
</button> </button>
@endif @endif
</div> </div>
@@ -331,43 +513,43 @@
{{-- Rejection Modal --}} {{-- Rejection Modal --}}
<div id="rejectModal" class="fixed inset-0 z-10 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <div id="rejectModal" class="fixed inset-0 z-10 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0"> <div class="flex min-h-screen items-end justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" onclick="document.getElementById('rejectModal').classList.add('hidden')"></div> <div class="fixed inset-0 bg-gray-500 bg-opacity-75 dark:bg-gray-900 dark:bg-opacity-75 transition-opacity" onclick="document.getElementById('rejectModal').classList.add('hidden')"></div>
<div class="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle"> <div class="inline-block transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle">
<form method="POST" action="{{ route('admin.finance.reject', $document) }}"> <form method="POST" action="{{ route('admin.finance.reject', $document) }}">
@csrf @csrf
<div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4"> <div class="bg-white dark:bg-gray-800 px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> <div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 text-red-600 dark:text-red-200" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg> </svg>
</div> </div>
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left flex-1"> <div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left flex-1">
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title"> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100" id="modal-title">
{{ __('Reject Document') }} 駁回報銷單
</h3> </h3>
<div class="mt-4"> <div class="mt-4">
<label for="rejection_reason" class="block text-sm font-medium text-gray-700"> <label for="rejection_reason" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Rejection Reason') }} 駁回原因
</label> </label>
<textarea <textarea
name="rejection_reason" name="rejection_reason"
id="rejection_reason" id="rejection_reason"
rows="4" rows="4"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
required required
></textarea> ></textarea>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 gap-3"> <div class="bg-gray-50 dark:bg-gray-700 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 gap-3">
<button type="submit" class="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:w-auto"> <button type="submit" class="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:w-auto">
{{ __('Reject') }} 駁回
</button> </button>
<button type="button" onclick="document.getElementById('rejectModal').classList.add('hidden')" class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"> <button type="button" onclick="document.getElementById('rejectModal').classList.add('hidden')" class="mt-3 inline-flex w-full justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 sm:mt-0 sm:w-auto">
{{ __('Cancel') }} 取消
</button> </button>
</div> </div>
</form> </form>

View File

@@ -0,0 +1,270 @@
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
總分類帳
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6">
<!-- Filter Form -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<form method="GET" action="{{ route('admin.general-ledger.index') }}" class="space-y-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-4">
<!-- Account Selection -->
<div class="sm:col-span-2">
<label for="account_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
科目
</label>
<select name="account_id" id="account_id"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
<option value="">請選擇科目</option>
@foreach($accounts as $account)
<option value="{{ $account->id }}" @selected(request('account_id') == $account->id)>
{{ $account->account_code }} - {{ $account->account_name_zh }}
</option>
@endforeach
</select>
</div>
<!-- Start Date -->
<div>
<label for="start_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
開始日期
</label>
<input type="date" name="start_date" id="start_date" value="{{ $startDate }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div>
<!-- End Date -->
<div>
<label for="end_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
結束日期
</label>
<input type="date" name="end_date" id="end_date" value="{{ $endDate }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-gray-100">
</div>
</div>
<div class="flex gap-4">
<button type="submit" class="inline-flex justify-center rounded-md bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
查詢
</button>
<a href="{{ route('admin.general-ledger.index') }}" class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-4 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
清除
</a>
</div>
</form>
</div>
</div>
@if($selectedAccount)
<!-- Account Info -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{{ $selectedAccount->account_code }} - {{ $selectedAccount->account_name_zh }}
<span class="text-sm text-gray-500 dark:text-gray-400">
({{ $selectedAccount->account_name_en }})
</span>
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
類型:
@switch($selectedAccount->account_type)
@case('asset') 資產 @break
@case('liability') 負債 @break
@case('net_asset') 淨資產/基金 @break
@case('income') 收入 @break
@case('expense') 支出 @break
@default {{ $selectedAccount->account_type }}
@endswitch
</p>
</div>
</div>
<!-- Summary Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<!-- Opening Balance -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg border-l-4 border-gray-400">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
期初餘額
</dt>
<dd class="mt-1 text-2xl font-semibold {{ $openingBalance >= 0 ? 'text-gray-900 dark:text-gray-100' : 'text-red-600 dark:text-red-400' }}">
NT$ {{ number_format($openingBalance, 2) }}
</dd>
</div>
</div>
<!-- Debit Total -->
<div class="bg-blue-50 dark:bg-blue-900/30 shadow sm:rounded-lg border-l-4 border-blue-400">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-blue-800 dark:text-blue-200">
借方總計
</dt>
<dd class="mt-1 text-2xl font-semibold text-blue-900 dark:text-blue-100">
NT$ {{ number_format($debitTotal, 2) }}
</dd>
</div>
</div>
<!-- Credit Total -->
<div class="bg-purple-50 dark:bg-purple-900/30 shadow sm:rounded-lg border-l-4 border-purple-400">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-purple-800 dark:text-purple-200">
貸方總計
</dt>
<dd class="mt-1 text-2xl font-semibold text-purple-900 dark:text-purple-100">
NT$ {{ number_format($creditTotal, 2) }}
</dd>
</div>
</div>
<!-- Closing Balance -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg border-l-4 {{ $closingBalance >= 0 ? 'border-green-400' : 'border-red-400' }}">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
期末餘額
</dt>
<dd class="mt-1 text-2xl font-semibold {{ $closingBalance >= 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400' }}">
NT$ {{ number_format($closingBalance, 2) }}
</dd>
</div>
</div>
</div>
<!-- Entries Table -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h4 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
分錄明細
</h4>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
日期
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
憑證
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
說明
</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-blue-600 dark:text-blue-400">
借方
</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-purple-600 dark:text-purple-400">
貸方
</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
餘額
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
<!-- Opening Balance Row -->
<tr class="bg-gray-50 dark:bg-gray-700">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ \Carbon\Carbon::parse($startDate)->format('Y/m/d') }}
</td>
<td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400" colspan="2">
<em>期初餘額</em>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
-
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
-
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right font-medium {{ $openingBalance >= 0 ? 'text-gray-900 dark:text-gray-100' : 'text-red-600 dark:text-red-400' }}">
{{ number_format($openingBalance, 2) }}
</td>
</tr>
@php $runningBalance = $openingBalance; @endphp
@forelse($entries as $entry)
@php
if (in_array($selectedAccount->account_type, ['asset', 'expense'])) {
$runningBalance += ($entry->entry_type === 'debit' ? $entry->amount : -$entry->amount);
} else {
$runningBalance += ($entry->entry_type === 'credit' ? $entry->amount : -$entry->amount);
}
@endphp
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ $entry->entry_date->format('Y/m/d') }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
@if($entry->financeDocument)
<a href="{{ route('admin.finance.show', $entry->financeDocument) }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">
#{{ $entry->finance_document_id }}
</a>
@else
<span class="text-gray-400">-</span>
@endif
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 max-w-xs truncate">
{{ $entry->description ?? '-' }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right text-blue-600 dark:text-blue-400">
{{ $entry->entry_type === 'debit' ? number_format($entry->amount, 2) : '' }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right text-purple-600 dark:text-purple-400">
{{ $entry->entry_type === 'credit' ? number_format($entry->amount, 2) : '' }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right font-medium {{ $runningBalance >= 0 ? 'text-gray-900 dark:text-gray-100' : 'text-red-600 dark:text-red-400' }}">
{{ number_format($runningBalance, 2) }}
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
此期間無分錄
</td>
</tr>
@endforelse
<!-- Totals Row -->
@if($entries && $entries->count() > 0)
<tr class="bg-gray-100 dark:bg-gray-700 font-semibold">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100" colspan="3">
本期合計
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right text-blue-700 dark:text-blue-300">
{{ number_format($debitTotal, 2) }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right text-purple-700 dark:text-purple-300">
{{ number_format($creditTotal, 2) }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-right {{ $closingBalance >= 0 ? 'text-green-700 dark:text-green-300' : 'text-red-700 dark:text-red-300' }}">
{{ number_format($closingBalance, 2) }}
</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
@else
<!-- No Account Selected -->
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-12 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
請選擇科目
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
從上方下拉選單選擇科目以查看分錄明細
</p>
</div>
</div>
@endif
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,272 @@
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
新增收入記錄
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-3xl sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<form method="POST" action="{{ route('admin.incomes.store') }}" enctype="multipart/form-data">
@csrf
<div class="space-y-6">
{{-- 基本資訊 --}}
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">基本資訊</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">輸入收入的基本資料。</p>
</div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
{{-- 標題 --}}
<div class="sm:col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
標題 <span class="text-red-500">*</span>
</label>
<input type="text" name="title" id="title" value="{{ old('title') }}" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="例如2024年度會費收入">
@error('title')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 收入日期 --}}
<div>
<label for="income_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
收入日期 <span class="text-red-500">*</span>
</label>
<input type="date" name="income_date" id="income_date" value="{{ old('income_date', date('Y-m-d')) }}" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
@error('income_date')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 金額 --}}
<div>
<label for="amount" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
金額 (NT$) <span class="text-red-500">*</span>
</label>
<input type="number" name="amount" id="amount" value="{{ old('amount') }}" required
step="0.01" min="0.01"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="0.00">
@error('amount')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 收入類型 --}}
<div>
<label for="income_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
收入類型 <span class="text-red-500">*</span>
</label>
<select name="income_type" id="income_type" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">請選擇收入類型</option>
<option value="membership_fee" @selected(old('income_type') == 'membership_fee')>會費</option>
<option value="entrance_fee" @selected(old('income_type') == 'entrance_fee')>入會費</option>
<option value="donation" @selected(old('income_type') == 'donation')>捐款</option>
<option value="activity" @selected(old('income_type') == 'activity')>活動收入</option>
<option value="grant" @selected(old('income_type') == 'grant')>補助款</option>
<option value="interest" @selected(old('income_type') == 'interest')>利息收入</option>
<option value="other" @selected(old('income_type') == 'other')>其他</option>
</select>
@error('income_type')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 會計科目 --}}
<div>
<label for="chart_of_account_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
會計科目 <span class="text-red-500">*</span>
</label>
<select name="chart_of_account_id" id="chart_of_account_id" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">請選擇會計科目</option>
@foreach ($chartOfAccounts as $account)
<option value="{{ $account->id }}" @selected(old('chart_of_account_id') == $account->id)>
{{ $account->account_code }} - {{ $account->account_name_zh }}
</option>
@endforeach
</select>
@error('chart_of_account_id')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
</div>
<hr class="border-gray-200 dark:border-gray-700">
{{-- 付款資訊 --}}
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">付款資訊</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">記錄付款方式與相關資訊。</p>
</div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
{{-- 付款方式 --}}
<div>
<label for="payment_method" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
付款方式 <span class="text-red-500">*</span>
</label>
<select name="payment_method" id="payment_method" required
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">請選擇付款方式</option>
<option value="cash" @selected(old('payment_method') == 'cash')>現金</option>
<option value="bank_transfer" @selected(old('payment_method') == 'bank_transfer')>銀行轉帳</option>
<option value="check" @selected(old('payment_method') == 'check')>支票</option>
</select>
@error('payment_method')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 銀行帳號 --}}
<div>
<label for="bank_account" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
銀行帳號
</label>
<input type="text" name="bank_account" id="bank_account" value="{{ old('bank_account') }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="例如012-12345678">
@error('bank_account')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 付款人姓名 --}}
<div>
<label for="payer_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
付款人姓名
</label>
<input type="text" name="payer_name" id="payer_name" value="{{ old('payer_name') }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="付款人姓名">
@error('payer_name')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 關聯會員 --}}
<div>
<label for="member_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
關聯會員
</label>
<select name="member_id" id="member_id"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">不關聯會員</option>
@foreach ($members as $member)
<option value="{{ $member->id }}" @selected(old('member_id', $selectedMember?->id) == $member->id)>
{{ $member->full_name }}
</option>
@endforeach
</select>
@error('member_id')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 收據編號 --}}
<div>
<label for="receipt_number" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
收據編號
</label>
<input type="text" name="receipt_number" id="receipt_number" value="{{ old('receipt_number') }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="收據編號">
@error('receipt_number')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 交易參考號 --}}
<div>
<label for="transaction_reference" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
交易參考號
</label>
<input type="text" name="transaction_reference" id="transaction_reference" value="{{ old('transaction_reference') }}"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="銀行交易參考號">
@error('transaction_reference')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
</div>
<hr class="border-gray-200 dark:border-gray-700">
{{-- 說明與備註 --}}
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">說明與備註</h3>
</div>
<div class="grid grid-cols-1 gap-6">
{{-- 說明 --}}
<div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
說明
</label>
<textarea name="description" id="description" rows="3"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="收入來源說明">{{ old('description') }}</textarea>
@error('description')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 備註 --}}
<div>
<label for="notes" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
內部備註
</label>
<textarea name="notes" id="notes" rows="2"
class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300"
placeholder="內部備註(僅管理員可見)">{{ old('notes') }}</textarea>
@error('notes')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
{{-- 附件 --}}
<div>
<label for="attachment" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
附件
</label>
<input type="file" name="attachment" id="attachment"
class="mt-1 block w-full text-sm text-gray-500 dark:text-gray-400
file:mr-4 file:py-2 file:px-4
file:rounded-md file:border-0
file:text-sm file:font-semibold
file:bg-indigo-50 file:text-indigo-700
dark:file:bg-indigo-900 dark:file:text-indigo-300
hover:file:bg-indigo-100 dark:hover:file:bg-indigo-800">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">支援 PDF、圖片等格式最大 10MB</p>
@error('attachment')
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>
</div>
{{-- 提交按鈕 --}}
<div class="flex justify-end space-x-3 pt-6">
<a href="{{ route('admin.incomes.index') }}"
class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
取消
</a>
<button type="submit"
class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
建立收入記錄
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,234 @@
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
收入管理
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-4">
@if (session('status'))
<div class="rounded-md bg-green-50 dark:bg-green-900/30 p-4">
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div>
@endif
@if (session('error'))
<div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4">
<p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div>
@endif
{{-- Statistics Cards --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">待確認收入</dt>
<dd class="mt-1 text-2xl font-semibold text-yellow-600 dark:text-yellow-400">
{{ $statistics['pending_count'] }}
</dd>
<dd class="text-sm text-gray-500 dark:text-gray-400">
NT$ {{ number_format($statistics['pending_amount'], 2) }}
</dd>
</div>
</div>
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">已確認收入</dt>
<dd class="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400">
{{ $statistics['confirmed_count'] }}
</dd>
<dd class="text-sm text-gray-500 dark:text-gray-400">
NT$ {{ number_format($statistics['confirmed_amount'], 2) }}
</dd>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">收入記錄列表</h3>
<div class="flex space-x-2">
<a href="{{ route('admin.incomes.statistics') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
統計報表
</a>
@can('record_income')
<a href="{{ route('admin.incomes.create') }}" class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
新增收入
</a>
@endcan
</div>
</div>
{{-- Filters --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<form method="GET" action="{{ route('admin.incomes.index') }}" class="grid grid-cols-1 gap-4 sm:grid-cols-5">
{{-- 狀態篩選 --}}
<div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">狀態</label>
<select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部狀態</option>
<option value="pending" @selected(request('status') == 'pending')>待確認</option>
<option value="confirmed" @selected(request('status') == 'confirmed')>已確認</option>
<option value="cancelled" @selected(request('status') == 'cancelled')>已取消</option>
</select>
</div>
{{-- 收入類型篩選 --}}
<div>
<label for="income_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">收入類型</label>
<select name="income_type" id="income_type" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部類型</option>
<option value="membership_fee" @selected(request('income_type') == 'membership_fee')>會費</option>
<option value="entrance_fee" @selected(request('income_type') == 'entrance_fee')>入會費</option>
<option value="donation" @selected(request('income_type') == 'donation')>捐款</option>
<option value="activity" @selected(request('income_type') == 'activity')>活動收入</option>
<option value="grant" @selected(request('income_type') == 'grant')>補助款</option>
<option value="interest" @selected(request('income_type') == 'interest')>利息收入</option>
<option value="other" @selected(request('income_type') == 'other')>其他</option>
</select>
</div>
{{-- 付款方式篩選 --}}
<div>
<label for="payment_method" class="block text-sm font-medium text-gray-700 dark:text-gray-300">付款方式</label>
<select name="payment_method" id="payment_method" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
<option value="">全部方式</option>
<option value="cash" @selected(request('payment_method') == 'cash')>現金</option>
<option value="bank_transfer" @selected(request('payment_method') == 'bank_transfer')>銀行轉帳</option>
<option value="check" @selected(request('payment_method') == 'check')>支票</option>
</select>
</div>
{{-- 日期範圍 --}}
<div>
<label for="date_from" class="block text-sm font-medium text-gray-700 dark:text-gray-300">起始日期</label>
<input type="date" name="date_from" id="date_from" value="{{ request('date_from') }}" class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-900 dark:text-gray-300">
</div>
{{-- 篩選按鈕 --}}
<div class="flex items-end space-x-2">
<button type="submit" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
篩選
</button>
<a href="{{ route('admin.incomes.index') }}" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
重設
</a>
</div>
</form>
</div>
</div>
{{-- Table --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700" role="table">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
收入編號
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
標題
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
收入類型
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
金額
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
付款方式
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
狀態
</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
日期
</th>
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
<span class="sr-only">操作</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@forelse ($incomes as $income)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<div class="font-mono text-xs">{{ $income->income_number }}</div>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<div class="font-medium">{{ $income->title }}</div>
@if ($income->member)
<div class="text-xs text-gray-500 dark:text-gray-400">
會員:{{ $income->member->full_name }}
</div>
@elseif ($income->payer_name)
<div class="text-xs text-gray-500 dark:text-gray-400">
付款人:{{ $income->payer_name }}
</div>
@endif
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ $income->getIncomeTypeText() }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
<span class="font-semibold">NT$ {{ number_format($income->amount, 2) }}</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ $income->getPaymentMethodText() }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
@if ($income->isCancelled())
<span class="inline-flex rounded-full bg-red-100 dark:bg-red-900 px-2 text-xs font-semibold leading-5 text-red-800 dark:text-red-200">
{{ $income->getStatusText() }}
</span>
@elseif ($income->isConfirmed())
<span class="inline-flex rounded-full bg-green-100 dark:bg-green-900 px-2 text-xs font-semibold leading-5 text-green-800 dark:text-green-200">
{{ $income->getStatusText() }}
</span>
@else
<span class="inline-flex rounded-full bg-yellow-100 dark:bg-yellow-900 px-2 text-xs font-semibold leading-5 text-yellow-800 dark:text-yellow-200">
{{ $income->getStatusText() }}
</span>
@endif
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{{ $income->income_date->format('Y-m-d') }}
</td>
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
<a href="{{ route('admin.incomes.show', $income) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
檢視
</a>
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="mt-2 text-sm font-semibold">尚無收入記錄</p>
<p class="mt-1 text-sm">點選「新增收入」開始建立。</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">
{{ $incomes->links() }}
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,329 @@
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
收入詳情 - {{ $income->income_number }}
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-4xl sm:px-6 lg:px-8 space-y-6">
@if (session('status'))
<div class="rounded-md bg-green-50 dark:bg-green-900/30 p-4">
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div>
@endif
@if (session('error'))
<div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4">
<p class="text-sm font-medium text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div>
@endif
{{-- 狀態卡片 --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
{{ $income->title }}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
收入編號:{{ $income->income_number }}
</p>
</div>
<div>
@if ($income->isCancelled())
<span class="inline-flex items-center rounded-full bg-red-100 dark:bg-red-900 px-3 py-1 text-sm font-medium text-red-800 dark:text-red-200">
{{ $income->getStatusText() }}
</span>
@elseif ($income->isConfirmed())
<span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900 px-3 py-1 text-sm font-medium text-green-800 dark:text-green-200">
{{ $income->getStatusText() }}
</span>
@else
<span class="inline-flex items-center rounded-full bg-yellow-100 dark:bg-yellow-900 px-3 py-1 text-sm font-medium text-yellow-800 dark:text-yellow-200">
{{ $income->getStatusText() }}
</span>
@endif
</div>
</div>
{{-- 操作按鈕 --}}
<div class="mt-6 flex space-x-3">
@if ($income->canBeConfirmed())
@can('confirm_income')
<form method="POST" action="{{ route('admin.incomes.confirm', $income) }}" class="inline">
@csrf
<button type="submit" onclick="return confirm('確定要確認此收入嗎?將會自動產生出納日記帳和會計分錄。')"
class="inline-flex items-center rounded-md border border-transparent bg-green-600 dark:bg-green-500 px-4 py-2 text-sm font-medium text-white hover:bg-green-700 dark:hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
確認收入
</button>
</form>
@endcan
@endif
@if ($income->canBeCancelled())
@can('cancel_income')
<form method="POST" action="{{ route('admin.incomes.cancel', $income) }}" class="inline">
@csrf
<button type="submit" onclick="return confirm('確定要取消此收入嗎?')"
class="inline-flex items-center rounded-md border border-red-300 dark:border-red-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
取消收入
</button>
</form>
@endcan
@endif
@if ($income->attachment_path)
<a href="{{ route('admin.incomes.download', $income) }}"
class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
下載附件
</a>
@endif
</div>
</div>
</div>
{{-- 金額與日期資訊 --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h4 class="text-base font-medium text-gray-900 dark:text-gray-100 mb-4">金額與日期</h4>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">金額</dt>
<dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">
NT$ {{ number_format($income->amount, 2) }}
</dd>
</div>
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">收入日期</dt>
<dd class="mt-1 text-lg font-medium text-gray-900 dark:text-gray-100">
{{ $income->income_date->format('Y-m-d') }}
</dd>
</div>
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">收入類型</dt>
<dd class="mt-1 text-lg font-medium text-gray-900 dark:text-gray-100">
{{ $income->getIncomeTypeText() }}
</dd>
</div>
</dl>
</div>
</div>
{{-- 會計資訊 --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h4 class="text-base font-medium text-gray-900 dark:text-gray-100 mb-4">會計資訊</h4>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">會計科目</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
@if ($income->chartOfAccount)
{{ $income->chartOfAccount->account_code }} - {{ $income->chartOfAccount->account_name_zh }}
@else
<span class="text-gray-400">未指定</span>
@endif
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">付款方式</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $income->getPaymentMethodText() }}
</dd>
</div>
@if ($income->bank_account)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">銀行帳號</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $income->bank_account }}
</dd>
</div>
@endif
@if ($income->receipt_number)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">收據編號</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $income->receipt_number }}
</dd>
</div>
@endif
@if ($income->transaction_reference)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">交易參考號</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $income->transaction_reference }}
</dd>
</div>
@endif
</dl>
</div>
</div>
{{-- 付款人資訊 --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h4 class="text-base font-medium text-gray-900 dark:text-gray-100 mb-4">付款人資訊</h4>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
@if ($income->member)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">關聯會員</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<a href="{{ route('admin.members.show', $income->member) }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">
{{ $income->member->full_name }}
</a>
</dd>
</div>
@endif
@if ($income->payer_name)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">付款人姓名</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $income->payer_name }}
</dd>
</div>
@endif
@if (!$income->member && !$income->payer_name)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">付款人</dt>
<dd class="mt-1 text-sm text-gray-400">未記錄</dd>
</div>
@endif
</dl>
</div>
</div>
{{-- 說明與備註 --}}
@if ($income->description || $income->notes)
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h4 class="text-base font-medium text-gray-900 dark:text-gray-100 mb-4">說明與備註</h4>
<dl class="space-y-4">
@if ($income->description)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">說明</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap">{{ $income->description }}</dd>
</div>
@endif
@if ($income->notes)
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">內部備註</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100 whitespace-pre-wrap">{{ $income->notes }}</dd>
</div>
@endif
</dl>
</div>
</div>
@endif
{{-- 處理記錄 --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h4 class="text-base font-medium text-gray-900 dark:text-gray-100 mb-4">處理記錄</h4>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">出納記錄人</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $income->recordedByCashier?->name ?? '不適用' }}
</dd>
@if ($income->recorded_at)
<dd class="text-xs text-gray-500 dark:text-gray-400">
{{ $income->recorded_at->format('Y-m-d H:i') }}
</dd>
@endif
</div>
@if ($income->isConfirmed())
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">會計確認人</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ $income->confirmedByAccountant?->name ?? '不適用' }}
</dd>
@if ($income->confirmed_at)
<dd class="text-xs text-gray-500 dark:text-gray-400">
{{ $income->confirmed_at->format('Y-m-d H:i') }}
</dd>
@endif
</div>
@endif
</dl>
</div>
</div>
{{-- 關聯記錄 --}}
@if ($income->isConfirmed() && ($income->cashierLedgerEntry || $income->accountingEntries->count() > 0))
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h4 class="text-base font-medium text-gray-900 dark:text-gray-100 mb-4">關聯記錄</h4>
@if ($income->cashierLedgerEntry)
<div class="mb-4">
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">出納日記帳</h5>
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
<a href="{{ route('admin.cashier-ledger.show', $income->cashierLedgerEntry) }}" class="text-indigo-600 dark:text-indigo-400 hover:underline">
查看出納日記帳記錄
</a>
</div>
</div>
@endif
@if ($income->accountingEntries->count() > 0)
<div>
<h5 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">會計分錄</h5>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-700">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300">會計科目</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-300">借方</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-300">貸方</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach ($income->accountingEntries as $entry)
<tr>
<td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
{{ $entry->chartOfAccount->account_code }} - {{ $entry->chartOfAccount->account_name_zh }}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-gray-100">
@if ($entry->entry_type === 'debit')
NT$ {{ number_format($entry->amount, 2) }}
@endif
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-gray-100">
@if ($entry->entry_type === 'credit')
NT$ {{ number_format($entry->amount, 2) }}
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
</div>
</div>
@endif
{{-- 返回按鈕 --}}
<div class="flex justify-start">
<a href="{{ route('admin.incomes.index') }}"
class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600">
<svg class="mr-2 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
返回列表
</a>
</div>
</div>
</div>
</x-app-layout>

Some files were not shown because too many files have changed in this diff Show More