Compare commits

..

3 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
209 changed files with 19493 additions and 3048 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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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,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('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

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,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">
{{ __('Audit Logs') }} 稽核日誌
</h2> </h2>
</x-slot> </x-slot>
@@ -9,18 +9,18 @@
<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 dark:bg-gray-800 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.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 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="user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('User') }} 使用者
</label> </label>
<select <select
name="user_id" name="user_id"
id="user_id" 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" 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 Users') }}</option> <option value="">所有使用者</option>
@foreach ($users as $user) @foreach ($users as $user)
<option value="{{ $user->id }}" @selected(request('user_id') == $user->id)> <option value="{{ $user->id }}" @selected(request('user_id') == $user->id)>
{{ $user->name }} {{ $user->name }}
@@ -31,14 +31,14 @@
<div> <div>
<label for="event" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="event" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Event') }} 事件
</label> </label>
<select <select
name="event" name="event"
id="event" id="event"
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" 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 Events') }}</option> <option value="">所有事件</option>
@foreach ($actions as $action) @foreach ($actions as $action)
<option value="{{ $action }}" @selected(request('action') == $action)> <option value="{{ $action }}" @selected(request('action') == $action)>
{{ ucfirst($action) }} {{ ucfirst($action) }}
@@ -49,14 +49,14 @@
<div> <div>
<label for="auditable_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="auditable_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Model Type') }} 模型類型
</label> </label>
<select <select
name="auditable_type" name="auditable_type"
id="auditable_type" 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" 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 Types') }}</option> <option value="">所有類型</option>
@foreach ($auditableTypes as $type) @foreach ($auditableTypes as $type)
<option value="{{ $type }}" @selected(request('auditable_type') == $type)> <option value="{{ $type }}" @selected(request('auditable_type') == $type)>
{{ class_basename($type) }} {{ class_basename($type) }}
@@ -67,7 +67,7 @@
<div class="flex items-end"> <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 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">
{{ __('Filter') }} 篩選
</button> </button>
</div> </div>
</div> </div>
@@ -78,22 +78,22 @@
<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">
{{ __('User') }} 使用者
</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">
{{ __('Event') }} 事件
</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">
{{ __('Model') }} 模型
</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">
{{ __('Details') }} 詳情
</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">
{{ __('IP Address') }} IP位址
</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">
{{ __('Date') }} 日期
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -120,7 +120,7 @@
<div>{{ $log->description ?: '—' }}</div> <div>{{ $log->description ?: '—' }}</div>
@if(!empty($log->metadata)) @if(!empty($log->metadata))
<details class="cursor-pointer group mt-1"> <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">{{ __('Metadata') }}</summary> <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"> <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> <pre class="whitespace-pre-wrap text-gray-800 dark:text-gray-200">{{ json_encode($log->metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
</div> </div>
@@ -137,7 +137,7 @@
@empty @empty
<tr> <tr>
<td colspan="6" class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center"> <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

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>

View File

@@ -0,0 +1,216 @@
<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">
{{-- 篩選器 --}}
<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.statistics') }}" class="flex items-end space-x-4">
<div>
<label for="year" class="block text-sm font-medium text-gray-700 dark:text-gray-300">年度</label>
<select name="year" id="year" 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">
@for ($y = date('Y'); $y >= date('Y') - 5; $y--)
<option value="{{ $y }}" @selected($year == $y)>{{ $y }}</option>
@endfor
</select>
</div>
<div>
<label for="month" class="block text-sm font-medium text-gray-700 dark:text-gray-300">月份</label>
<select name="month" id="month" 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>
@for ($m = 1; $m <= 12; $m++)
<option value="{{ $m }}" @selected($month == $m)>{{ $m }}</option>
@endfor
</select>
</div>
<div class="flex space-x-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">
查詢
</button>
<a href="{{ route('admin.incomes.export', ['date_from' => $year . '-01-01', 'date_to' => $year . '-12-31']) }}"
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>
匯出 CSV
</a>
</div>
</form>
</div>
</div>
{{-- 總計 --}}
<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 mb-4">
{{ $year }}{{ $month ? $month . '月' : '' }}收入總計
</h3>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="bg-indigo-50 dark:bg-indigo-900/30 rounded-lg p-4">
<dt class="text-sm font-medium text-indigo-600 dark:text-indigo-400">總收入金額</dt>
<dd class="mt-1 text-3xl font-semibold text-indigo-900 dark:text-indigo-100">
NT$ {{ number_format($total['amount'], 2) }}
</dd>
</div>
<div class="bg-green-50 dark:bg-green-900/30 rounded-lg p-4">
<dt class="text-sm font-medium text-green-600 dark:text-green-400">收入筆數</dt>
<dd class="mt-1 text-3xl font-semibold text-green-900 dark:text-green-100">
{{ number_format($total['count']) }}
</dd>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
{{-- 依收入類型統計 --}}
<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 mb-4">依收入類型統計</h3>
@if ($byType->count() > 0)
<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 ($byType as $item)
<tr>
<td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
{{ \App\Models\Income::getIncomeTypeLabel($item->income_type) }}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-gray-100">
{{ number_format($item->count) }}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-gray-100">
NT$ {{ number_format($item->total_amount, 2) }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-sm text-gray-500 dark:text-gray-400">此期間無收入資料</p>
@endif
</div>
</div>
{{-- 依會計科目統計 --}}
<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 mb-4">依會計科目統計</h3>
@if ($byAccount->count() > 0)
<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 ($byAccount as $item)
<tr>
<td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
@if ($item->chartOfAccount)
{{ $item->chartOfAccount->account_code }} - {{ $item->chartOfAccount->account_name_zh }}
@else
未分類
@endif
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-gray-100">
{{ number_format($item->count) }}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-gray-100">
NT$ {{ number_format($item->total_amount, 2) }}
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@else
<p class="text-sm text-gray-500 dark:text-gray-400">此期間無收入資料</p>
@endif
</div>
</div>
</div>
{{-- 依月份統計 --}}
@if (!$month)
<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 mb-4">{{ $year }}年月度收入趨勢</h3>
@if ($byMonth->count() > 0)
<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>
<th class="px-4 py-2 text-left 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">
@php
$maxAmount = $byMonth->max('total_amount') ?: 1;
@endphp
@for ($m = 1; $m <= 12; $m++)
@php
$monthData = $byMonth->firstWhere('month', $m);
$amount = $monthData->total_amount ?? 0;
$count = $monthData->count ?? 0;
$percentage = ($amount / $maxAmount) * 100;
@endphp
<tr>
<td class="px-4 py-2 text-sm text-gray-900 dark:text-gray-100">
{{ $m }}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-gray-100">
{{ number_format($count) }}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-gray-100">
NT$ {{ number_format($amount, 2) }}
</td>
<td class="px-4 py-2">
<div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2">
<div class="bg-indigo-600 dark:bg-indigo-500 h-2 rounded-full" style="width: {{ $percentage }}%"></div>
</div>
</td>
</tr>
@endfor
</tbody>
</table>
</div>
@else
<p class="text-sm text-gray-500 dark:text-gray-400">此年度無收入資料</p>
@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>

View File

@@ -1,59 +1,57 @@
<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 Label') }} 新增標籤
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-2xl 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 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">
<form method="POST" action="{{ route('admin.issue-labels.store') }}" class="space-y-6"> <form method="POST" action="{{ route('admin.issue-labels.store') }}" class="space-y-6">
@csrf @csrf
<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">
{{ __('Name') }} <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 maxlength="255" <input type="text" name="name" id="name" value="{{ old('name') }}" required 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 @error('name') border-red-300 @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 @error('name') border-red-300 @enderror"
placeholder="{{ __('e.g., urgent, bug, enhancement') }}"> placeholder="例如:錯誤、功能、增強">
@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="color" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="color" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Color') }} <span class="text-red-500">*</span> 顏色 <span class="text-red-500">*</span>
</label> </label>
<div class="mt-1 flex gap-2"> <div class="mt-1 flex items-center gap-2">
<input type="color" name="color" id="color" value="{{ old('color', '#6B7280') }}" required <input type="color" name="color" id="color" value="{{ old('color', '#3b82f6') }}" required
class="h-10 w-20 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600"> class="h-10 w-20 rounded-md border-gray-300 dark:border-gray-600 p-1">
<input type="text" id="color-text" value="{{ old('color', '#6B7280') }}" maxlength="7" <input type="text" name="color_text" id="color_text" value="{{ old('color', '#3b82f6') }}" required maxlength="7"
class="flex-1 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="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"
placeholder="#000000"> placeholder="#RRGGBB">
</div> </div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Choose a color for this label') }}</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">標籤的十六進位色碼</p>
@error('color')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('color')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="text_color" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Description') }} 文字顏色
</label> </label>
<textarea name="description" id="description" rows="3" maxlength="500" <select name="text_color" id="text_color"
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">
placeholder="{{ __('Optional description...') }}">{{ old('description') }}</textarea> <option value="#ffffff" @selected(old('text_color') == '#ffffff')>White</option>
@error('description')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror <option value="#000000" @selected(old('text_color') == '#000000')>Black</option>
</select>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">對比的文字顏色</p>
@error('text_color')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<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 pt-6">
<a href="{{ route('admin.issue-labels.index') }}" <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="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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600"> 新增標籤
{{ __('Cancel') }}
</a>
<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 dark:bg-indigo-500 dark:hover:bg-indigo-400">
{{ __('Create Label') }}
</button> </button>
</div> </div>
</form> </form>
@@ -62,19 +60,17 @@
</div> </div>
<script> <script>
// Sync color picker and text input const colorInput = document.getElementById('color');
const colorPicker = document.getElementById('color'); const colorTextInput = document.getElementById('color_text');
const colorText = document.getElementById('color-text');
colorPicker.addEventListener('input', (e) => { colorInput.addEventListener('input', (e) => {
colorText.value = e.target.value.toUpperCase(); colorTextInput.value = e.target.value;
}); });
colorText.addEventListener('input', (e) => { colorTextInput.addEventListener('input', (e) => {
const value = e.target.value; if (e.target.value.match(/^#[0-9A-Fa-f]{6}$/)) {
if (/^#[0-9A-Fa-f]{6}$/.test(value)) { colorInput.value = e.target.value;
colorPicker.value = value;
} }
}); });
</script> </script>
</x-app-layout> </x-app-layout>

View File

@@ -1,12 +1,12 @@
<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 Label') }} - {{ $issueLabel->name }} 編輯標籤
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-2xl 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 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">
<form method="POST" action="{{ route('admin.issue-labels.update', $issueLabel) }}" class="space-y-6"> <form method="POST" action="{{ route('admin.issue-labels.update', $issueLabel) }}" class="space-y-6">
@csrf @csrf
@@ -14,43 +14,45 @@
<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">
{{ __('Name') }} <span class="text-red-500">*</span> 名稱 <span class="text-red-500">*</span>
</label> </label>
<input type="text" name="name" id="name" value="{{ old('name', $issueLabel->name) }}" required maxlength="255" <input type="text" name="name" id="name" value="{{ old('name', $issueLabel->name) }}" required 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 @error('name') border-red-300 @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 @error('name') border-red-300 @enderror"
placeholder="例如:錯誤、功能、增強">
@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="color" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="color" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Color') }} <span class="text-red-500">*</span> 顏色 <span class="text-red-500">*</span>
</label> </label>
<div class="mt-1 flex gap-2"> <div class="mt-1 flex items-center gap-2">
<input type="color" name="color" id="color" value="{{ old('color', $issueLabel->color) }}" required <input type="color" name="color" id="color" value="{{ old('color', $issueLabel->color) }}" required
class="h-10 w-20 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-600"> class="h-10 w-20 rounded-md border-gray-300 dark:border-gray-600 p-1">
<input type="text" id="color-text" value="{{ old('color', $issueLabel->color) }}" maxlength="7" <input type="text" name="color_text" id="color_text" value="{{ old('color', $issueLabel->color) }}" required maxlength="7"
class="flex-1 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="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"
placeholder="#RRGGBB">
</div> </div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">標籤的十六進位色碼</p>
@error('color')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('color')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="text_color" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Description') }} 文字顏色
</label> </label>
<textarea name="description" id="description" rows="3" maxlength="500" <select name="text_color" id="text_color"
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('description', $issueLabel->description) }}</textarea> 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('description')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror <option value="#ffffff" @selected(old('text_color', $issueLabel->text_color) == '#ffffff')>White</option>
<option value="#000000" @selected(old('text_color', $issueLabel->text_color) == '#000000')>Black</option>
</select>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">對比的文字顏色</p>
@error('text_color')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<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 pt-6">
<a href="{{ route('admin.issue-labels.index') }}" <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="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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600"> 更新標籤
{{ __('Cancel') }}
</a>
<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 dark:bg-indigo-500 dark:hover:bg-indigo-400">
{{ __('Update Label') }}
</button> </button>
</div> </div>
</form> </form>
@@ -59,19 +61,17 @@
</div> </div>
<script> <script>
// Sync color picker and text input const colorInput = document.getElementById('color');
const colorPicker = document.getElementById('color'); const colorTextInput = document.getElementById('color_text');
const colorText = document.getElementById('color-text');
colorPicker.addEventListener('input', (e) => { colorInput.addEventListener('input', (e) => {
colorText.value = e.target.value.toUpperCase(); colorTextInput.value = e.target.value;
}); });
colorText.addEventListener('input', (e) => { colorTextInput.addEventListener('input', (e) => {
const value = e.target.value; if (e.target.value.match(/^#[0-9A-Fa-f]{6}$/)) {
if (/^#[0-9A-Fa-f]{6}$/.test(value)) { colorInput.value = e.target.value;
colorPicker.value = value;
} }
}); });
</script> </script>
</x-app-layout> </x-app-layout>

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 dark:text-gray-200"> <h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Issue Labels') }} (標籤管理) 任務標籤
</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">
@if (session('status')) @if (session('status'))
<div class="mb-4 rounded-md bg-green-50 p-4 dark:bg-green-900/30 border-l-4 border-green-400" role="status"> <div class="mb-4 rounded-md bg-green-50 p-4 dark:bg-green-900/30 border-l-4 border-green-400 dark:border-green-500" role="status">
<p class="text-sm text-green-800 dark:text-green-200">{{ session('status') }}</p> <p class="text-sm text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div> </div>
@endif @endif
@@ -16,13 +16,13 @@
<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="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 text-gray-900 dark:text-gray-100">{{ __('Manage Labels') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">標籤</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{{ __('Create and manage labels for categorizing issues') }}</p> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">管理用於分類任務的標籤</p>
</div> </div>
<div class="mt-4 sm:mt-0"> <div class="mt-4 sm:mt-0">
<a href="{{ route('admin.issue-labels.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 dark:bg-indigo-500 dark:hover:bg-indigo-400"> <a href="{{ route('admin.issue-labels.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 dark:bg-indigo-500 dark:hover:bg-indigo-400 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-0.5 mr-1.5 h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><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 class="-ml-0.5 mr-1.5 h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><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>
{{ __('Create Label') }} 新增標籤
</a> </a>
</div> </div>
</div> </div>
@@ -31,42 +31,42 @@
<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.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Label') }}</th> <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">名稱</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Description') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Issues') }}</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 scope="col" class="relative py-3.5 pl-3 pr-4"><span class="sr-only">{{ __('Actions') }}</span></th> <th scope="col" class="relative py-3.5 pl-3 pr-4"><span class="sr-only">操作</span></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800"> <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800">
@forelse($labels as $label) @forelse($labels as $label)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700"> <tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm"> <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium">
<span class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium" <span class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium" style="background-color: {{ $label->color }}20; color: {{ $label->color }}">
style="background-color: {{ $label->color }}; color: {{ $label->text_color }}">
{{ $label->name }} {{ $label->name }}
</span> </span>
</td> </td>
<td class="px-3 py-4 text-sm text-gray-900 dark:text-gray-100"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $label->description ?? '—' }} <div class="flex items-center gap-2">
<div class="w-4 h-4 rounded-full border border-gray-200 dark:border-gray-600" style="background-color: {{ $label->color }}"></div>
{{ $label->color }}
</div>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-900 dark:text-gray-100"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{{ $label->issues_count }} {{ $label->issues_count }} 任務
</td> </td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium"> <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium">
<a href="{{ route('admin.issue-labels.edit', $label) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-4">{{ __('Edit') }}</a> <a href="{{ route('admin.issue-labels.edit', $label) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">編輯</a>
@if($label->issues_count === 0) <form method="POST" action="{{ route('admin.issue-labels.destroy', $label) }}" class="inline" onsubmit="return confirm('刪除此標籤?')">
<form method="POST" action="{{ route('admin.issue-labels.destroy', $label) }}" class="inline" onsubmit="return confirm('{{ __('Delete this label?') }}')"> @csrf
@csrf @method('DELETE')
@method('DELETE') <button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 ml-2">刪除</button>
<button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">{{ __('Delete') }}</button> </form>
</form>
@endif
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="4" class="px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400"> <td colspan="4" class="px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<p>{{ __('No labels found') }}</p> 找不到標籤
</td> </td>
</tr> </tr>
@endforelse @endforelse
@@ -76,4 +76,4 @@
</div> </div>
</div> </div>
</div> </div>
</x-app-layout> </x-app-layout>

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">
{{ __('Task Reports & Analytics') }} 任務報告與分析
</h2> </h2>
</x-slot> </x-slot>
@@ -13,20 +13,20 @@
<form method="GET" action="{{ route('admin.issue-reports.index') }}" class="flex flex-wrap gap-4 items-end"> <form method="GET" action="{{ route('admin.issue-reports.index') }}" class="flex flex-wrap gap-4 items-end">
<div class="flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
<label for="start_date" 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 dark:text-gray-300">
{{ __('Start Date') }} 開始日期
</label> </label>
<input type="date" name="start_date" id="start_date" value="{{ $startDate->format('Y-m-d') }}" <input type="date" name="start_date" id="start_date" value="{{ $startDate->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 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="flex-1 min-w-[200px]"> <div class="flex-1 min-w-[200px]">
<label for="end_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="end_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('End Date') }} 結束日期
</label> </label>
<input type="date" name="end_date" id="end_date" value="{{ $endDate->format('Y-m-d') }}" <input type="date" name="end_date" id="end_date" value="{{ $endDate->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 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>
<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 dark:bg-indigo-500 dark:hover:bg-indigo-400"> <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 dark:bg-indigo-500 dark:hover:bg-indigo-400">
{{ __('Apply Filter') }} 套用篩選
</button> </button>
</form> </form>
</div> </div>
@@ -34,19 +34,19 @@
{{-- Summary Statistics --}} {{-- Summary Statistics --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 border-l-4 border-blue-400"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 border-l-4 border-blue-400">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Total Tasks') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">總任務數</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['total_issues']) }}</dd> <dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['total_issues']) }}</dd>
</div> </div>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 border-l-4 border-green-400"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 border-l-4 border-green-400">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Open Tasks') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">開啟的任務</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['open_issues']) }}</dd> <dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['open_issues']) }}</dd>
</div> </div>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 border-l-4 border-gray-400"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 border-l-4 border-gray-400">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Closed Tasks') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">已結案任務</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['closed_issues']) }}</dd> <dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['closed_issues']) }}</dd>
</div> </div>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 border-l-4 border-red-400"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 border-l-4 border-red-400">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Overdue Tasks') }}</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">逾期任務</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['overdue_issues']) }}</dd> <dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['overdue_issues']) }}</dd>
</div> </div>
</div> </div>
@@ -55,7 +55,7 @@
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- Tasks by Status --}} {{-- Tasks by Status --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Tasks by Status') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">依狀態的任務</h3>
<div class="space-y-2"> <div class="space-y-2">
@foreach(['new', 'assigned', 'in_progress', 'review', 'closed'] as $status) @foreach(['new', 'assigned', 'in_progress', 'review', 'closed'] as $status)
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -74,7 +74,7 @@
{{-- Tasks by Priority --}} {{-- Tasks by Priority --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Tasks by Priority') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">依優先級的任務</h3>
<div class="space-y-2"> <div class="space-y-2">
@foreach(['low', 'medium', 'high', 'urgent'] as $priority) @foreach(['low', 'medium', 'high', 'urgent'] as $priority)
@php @php
@@ -96,7 +96,7 @@
{{-- Tasks by Type --}} {{-- Tasks by Type --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Tasks by Type') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">依類型的任務</h3>
<div class="space-y-2"> <div class="space-y-2">
@foreach(['work_item', 'project_task', 'maintenance', 'member_request'] as $type) @foreach(['work_item', 'project_task', 'maintenance', 'member_request'] as $type)
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -117,22 +117,22 @@
{{-- Time Tracking Metrics --}} {{-- Time Tracking Metrics --}}
@if($timeTrackingMetrics && $timeTrackingMetrics->total_estimated > 0) @if($timeTrackingMetrics && $timeTrackingMetrics->total_estimated > 0)
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Time Tracking Metrics') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">時間追蹤指標</h3>
<div class="grid grid-cols-1 sm:grid-cols-4 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-4 gap-4">
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Total Estimated Hours') }}</dt> <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">{{ number_format($timeTrackingMetrics->total_estimated, 1) }}</dd> <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($timeTrackingMetrics->total_estimated, 1) }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Total Actual Hours') }}</dt> <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">{{ number_format($timeTrackingMetrics->total_actual, 1) }}</dd> <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($timeTrackingMetrics->total_actual, 1) }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Avg Estimated Hours') }}</dt> <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">{{ number_format($timeTrackingMetrics->avg_estimated, 1) }}</dd> <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($timeTrackingMetrics->avg_estimated, 1) }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Avg Actual Hours') }}</dt> <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">{{ number_format($timeTrackingMetrics->avg_actual, 1) }}</dd> <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($timeTrackingMetrics->avg_actual, 1) }}</dd>
</div> </div>
</div> </div>
@@ -142,7 +142,7 @@
@endphp @endphp
<div class="mt-4"> <div class="mt-4">
<p class="text-sm text-gray-600 dark:text-gray-400"> <p class="text-sm text-gray-600 dark:text-gray-400">
{{ __('Variance') }}: 差異:
<span class="font-semibold {{ $variance > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400' }}"> <span class="font-semibold {{ $variance > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400' }}">
{{ $variance > 0 ? '+' : '' }}{{ number_format($variance, 1) }} hours ({{ number_format($variancePercentage, 1) }}%) {{ $variance > 0 ? '+' : '' }}{{ number_format($variance, 1) }} hours ({{ number_format($variancePercentage, 1) }}%)
</span> </span>
@@ -154,24 +154,24 @@
{{-- Average Resolution Time --}} {{-- Average Resolution Time --}}
@if($avgResolutionTime) @if($avgResolutionTime)
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">{{ __('Average Resolution Time') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">平均解決時間</h3>
<p class="text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($avgResolutionTime, 1) }} {{ __('days') }}</p> <p class="text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($avgResolutionTime, 1) }} </p>
</div> </div>
@endif @endif
{{-- Assignee Performance --}} {{-- Assignee Performance --}}
@if($assigneePerformance->isNotEmpty()) @if($assigneePerformance->isNotEmpty())
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Assignee Performance (Top 10)') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">指派對象表現前10名</h3>
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg"> <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<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.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Assignee') }}</th> <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">指派對象</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Total Assigned') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Completed') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Overdue') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Completion Rate') }}</th> <th scope="col" class="px-3 py-3.5 text-left 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 bg-white dark:bg-gray-800"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
@@ -210,7 +210,7 @@
{{-- Top Labels Used --}} {{-- Top Labels Used --}}
@if($topLabels->isNotEmpty()) @if($topLabels->isNotEmpty())
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Top Labels Used') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">最常用標籤</h3>
<div class="space-y-3"> <div class="space-y-3">
@foreach($topLabels as $label) @foreach($topLabels as $label)
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
@@ -220,7 +220,7 @@
{{ $label->name }} {{ $label->name }}
</span> </span>
</div> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $label->usage_count }} {{ __('uses') }}</span> <span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $label->usage_count }} 使用</span>
</div> </div>
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700"> <div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
@php @php
@@ -236,16 +236,16 @@
{{-- Recent Tasks --}} {{-- Recent Tasks --}}
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Recent Tasks') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">最近的任務</h3>
<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">
<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">{{ __('Task') }}</th> <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">任務</th>
<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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">狀態</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Priority') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Assignee') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Created') }}</th> <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">已建立</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800"> <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800">

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 Task') }} (建立任務) 新增任務 (建立任務)
</h2> </h2>
</x-slot> </x-slot>
@@ -14,22 +14,22 @@
<!-- Title --> <!-- Title -->
<div> <div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Title') }} <span class="text-red-500">*</span> 標題 <span class="text-red-500">*</span>
</label> </label>
<input type="text" name="title" id="title" value="{{ old('title') }}" required maxlength="255" <input type="text" name="title" id="title" value="{{ old('title') }}" required 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 @error('title') border-red-300 @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 @error('title') border-red-300 @enderror"
placeholder="{{ __('Brief summary of the task') }}"> placeholder="任務簡述">
@error('title')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('title')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<!-- Description --> <!-- Description -->
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Description') }} 描述
</label> </label>
<textarea name="description" id="description" rows="5" <textarea name="description" id="description" rows="5"
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"
placeholder="{{ __('Detailed description of the task...') }}">{{ old('description') }}</textarea> placeholder="詳細描述此任務...">{{ old('description') }}</textarea>
@error('description')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('description')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
@@ -37,30 +37,30 @@
<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="issue_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="issue_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Task Type') }} <span class="text-red-500">*</span> 任務類型 <span class="text-red-500">*</span>
</label> </label>
<select name="issue_type" id="issue_type" required <select name="issue_type" id="issue_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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 @error('issue_type') border-red-300 @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 @error('issue_type') border-red-300 @enderror">
<option value="">{{ __('Select type...') }}</option> <option value="">選擇類型...</option>
<option value="work_item" @selected(old('issue_type') === 'work_item')>{{ __('Work Item') }}</option> <option value="work_item" @selected(old('issue_type') === 'work_item')>工作項目</option>
<option value="project_task" @selected(old('issue_type') === 'project_task')>{{ __('Project Task') }}</option> <option value="project_task" @selected(old('issue_type') === 'project_task')>專案任務</option>
<option value="maintenance" @selected(old('issue_type') === 'maintenance')>{{ __('Maintenance') }}</option> <option value="maintenance" @selected(old('issue_type') === 'maintenance')>維護</option>
<option value="member_request" @selected(old('issue_type') === 'member_request')>{{ __('Member Request') }}</option> <option value="member_request" @selected(old('issue_type') === 'member_request')>會員請求</option>
</select> </select>
@error('issue_type')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('issue_type')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<div> <div>
<label for="priority" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="priority" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Priority') }} <span class="text-red-500">*</span> 優先級 <span class="text-red-500">*</span>
</label> </label>
<select name="priority" id="priority" required <select name="priority" id="priority" 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 @error('priority') border-red-300 @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 @error('priority') border-red-300 @enderror">
<option value="">{{ __('Select priority...') }}</option> <option value="">選擇優先級...</option>
<option value="low" @selected(old('priority') === 'low')>{{ __('Low') }} </option> <option value="low" @selected(old('priority') === 'low')> </option>
<option value="medium" @selected(old('priority', 'medium') === 'medium')>{{ __('Medium') }} </option> <option value="medium" @selected(old('priority', 'medium') === 'medium')> </option>
<option value="high" @selected(old('priority') === 'high')>{{ __('High') }} </option> <option value="high" @selected(old('priority') === 'high')> </option>
<option value="urgent" @selected(old('priority') === 'urgent')>{{ __('Urgent') }} </option> <option value="urgent" @selected(old('priority') === 'urgent')>緊急 </option>
</select> </select>
@error('priority')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('priority')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
@@ -70,21 +70,21 @@
<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="assigned_to_user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="assigned_to_user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Assign To') }} 指派給
</label> </label>
<select name="assigned_to_user_id" id="assigned_to_user_id" <select name="assigned_to_user_id" id="assigned_to_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 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="">{{ __('Unassigned') }}</option> <option value="">未指派</option>
@foreach($users as $user) @foreach($users as $user)
<option value="{{ $user->id }}" @selected(old('assigned_to_user_id') == $user->id)>{{ $user->name }}</option> <option value="{{ $user->id }}" @selected(old('assigned_to_user_id') == $user->id)>{{ $user->name }}</option>
@endforeach @endforeach
</select> </select>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Optional: Assign to a team member') }}</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">選填:指派給團隊成員</p>
</div> </div>
<div> <div>
<label for="due_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="due_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Due Date') }} 截止日期
</label> </label>
<input type="date" name="due_date" id="due_date" value="{{ old('due_date') }}" <input type="date" name="due_date" id="due_date" value="{{ old('due_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 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">
@@ -95,50 +95,50 @@
<!-- Estimated Hours --> <!-- Estimated Hours -->
<div> <div>
<label for="estimated_hours" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="estimated_hours" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Estimated Hours') }} 預估時數
</label> </label>
<input type="number" name="estimated_hours" id="estimated_hours" value="{{ old('estimated_hours') }}" step="0.5" min="0" <input type="number" name="estimated_hours" id="estimated_hours" value="{{ old('estimated_hours') }}" step="0.5" min="0"
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"
placeholder="0.0"> placeholder="0.0">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Estimated time to complete this task') }}</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">完成此任務的預估時間</p>
</div> </div>
<!-- Member (for member requests) --> <!-- Member (for member requests) -->
<div> <div>
<label for="member_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="member_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Related Member') }} 相關會員
</label> </label>
<select name="member_id" id="member_id" <select name="member_id" id="member_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 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="">{{ __('None') }}</option> <option value=""></option>
@foreach($members as $member) @foreach($members as $member)
<option value="{{ $member->id }}" @selected(old('member_id') == $member->id)>{{ $member->full_name }}</option> <option value="{{ $member->id }}" @selected(old('member_id') == $member->id)>{{ $member->full_name }}</option>
@endforeach @endforeach
</select> </select>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Link to a member for member requests') }}</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">連結至會員(用於會員請求)</p>
</div> </div>
<!-- Parent Task (for sub-tasks) --> <!-- Parent Task (for sub-tasks) -->
<div> <div>
<label for="parent_issue_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="parent_issue_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Parent Task') }} 父任務
</label> </label>
<select name="parent_issue_id" id="parent_issue_id" <select name="parent_issue_id" id="parent_issue_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 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="">{{ __('None (top-level task)') }}</option> <option value="">無(頂層任務)</option>
@foreach($openIssues as $parentIssue) @foreach($openIssues as $parentIssue)
<option value="{{ $parentIssue->id }}" @selected(old('parent_issue_id') == $parentIssue->id)> <option value="{{ $parentIssue->id }}" @selected(old('parent_issue_id') == $parentIssue->id)>
{{ $parentIssue->issue_number }} - {{ $parentIssue->title }} {{ $parentIssue->issue_number }} - {{ $parentIssue->title }}
</option> </option>
@endforeach @endforeach
</select> </select>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Make this a sub-task of another task') }}</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">將此設為另一個任務的子任務</p>
</div> </div>
<!-- Labels --> <!-- Labels -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ __('Labels') }} 標籤
</label> </label>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2"> <div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
@foreach($labels as $label) @foreach($labels as $label)
@@ -153,18 +153,18 @@
</label> </label>
@endforeach @endforeach
</div> </div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ __('Select one or more labels to categorize this task') }}</p> <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">選擇一個或多個標籤來分類此任務</p>
</div> </div>
<!-- Actions --> <!-- Actions -->
<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.issues.index') }}" <a href="{{ route('admin.issues.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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600"> 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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600">
{{ __('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 Task') }} 新增任務
</button> </button>
</div> </div>
</form> </form>
@@ -179,14 +179,14 @@
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">{{ __('Creating Tasks') }}</h3> <h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">建立任務</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">
<ul class="list-disc pl-5 space-y-1"> <ul class="list-disc pl-5 space-y-1">
<li>{{ __('Use work items for general tasks and todos') }}</li> <li>使用工作項目處理一般任務和待辦事項</li>
<li>{{ __('Project tasks are for specific project milestones') }}</li> <li>專案任務用於特定的專案里程碑</li>
<li>{{ __('Member requests track inquiries or requests from members') }}</li> <li>會員請求追蹤來自會員的查詢或請求</li>
<li>{{ __('Assign tasks to team members to track responsibility') }}</li> <li>將任務指派給團隊成員以追蹤責任歸屬</li>
<li>{{ __('Use labels to categorize and filter tasks easily') }}</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 Issue') }} - {{ $issue->issue_number }} 編輯任務 - {{ $issue->issue_number }}
</h2> </h2>
</x-slot> </x-slot>
@@ -15,22 +15,22 @@
<!-- Title --> <!-- Title -->
<div> <div>
<label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="title" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Title') }} <span class="text-red-500">*</span> 標題 <span class="text-red-500">*</span>
</label> </label>
<input type="text" name="title" id="title" value="{{ old('title', $issue->title) }}" required maxlength="255" <input type="text" name="title" id="title" value="{{ old('title', $issue->title) }}" required 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 @error('title') border-red-300 @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 @error('title') border-red-300 @enderror"
placeholder="{{ __('Brief summary of the issue') }}"> placeholder="任務簡述">
@error('title')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('title')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<!-- Description --> <!-- Description -->
<div> <div>
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Description') }} 描述
</label> </label>
<textarea name="description" id="description" rows="5" <textarea name="description" id="description" rows="5"
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"
placeholder="{{ __('Detailed description of the issue...') }}">{{ old('description', $issue->description) }}</textarea> placeholder="詳細描述此任務...">{{ old('description', $issue->description) }}</textarea>
@error('description')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('description')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
@@ -38,30 +38,30 @@
<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="issue_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="issue_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Issue Type') }} <span class="text-red-500">*</span> 任務類型 <span class="text-red-500">*</span>
</label> </label>
<select name="issue_type" id="issue_type" required <select name="issue_type" id="issue_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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 @error('issue_type') border-red-300 @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 @error('issue_type') border-red-300 @enderror">
<option value="">{{ __('Select type...') }}</option> <option value="">選擇類型...</option>
<option value="work_item" @selected(old('issue_type', $issue->issue_type) === 'work_item')>{{ __('Work Item') }}</option> <option value="work_item" @selected(old('issue_type', $issue->issue_type) === 'work_item')>工作項目</option>
<option value="project_task" @selected(old('issue_type', $issue->issue_type) === 'project_task')>{{ __('Project Task') }}</option> <option value="project_task" @selected(old('issue_type', $issue->issue_type) === 'project_task')>專案任務</option>
<option value="maintenance" @selected(old('issue_type', $issue->issue_type) === 'maintenance')>{{ __('Maintenance') }}</option> <option value="maintenance" @selected(old('issue_type', $issue->issue_type) === 'maintenance')>維護</option>
<option value="member_request" @selected(old('issue_type', $issue->issue_type) === 'member_request')>{{ __('Member Request') }}</option> <option value="member_request" @selected(old('issue_type', $issue->issue_type) === 'member_request')>會員請求</option>
</select> </select>
@error('issue_type')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('issue_type')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
<div> <div>
<label for="priority" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="priority" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Priority') }} <span class="text-red-500">*</span> 優先級 <span class="text-red-500">*</span>
</label> </label>
<select name="priority" id="priority" required <select name="priority" id="priority" 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 @error('priority') border-red-300 @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 @error('priority') border-red-300 @enderror">
<option value="">{{ __('Select priority...') }}</option> <option value="">選擇優先級...</option>
<option value="low" @selected(old('priority', $issue->priority) === 'low')>{{ __('Low') }} </option> <option value="low" @selected(old('priority', $issue->priority) === 'low')> </option>
<option value="medium" @selected(old('priority', $issue->priority) === 'medium')>{{ __('Medium') }} </option> <option value="medium" @selected(old('priority', $issue->priority) === 'medium')> </option>
<option value="high" @selected(old('priority', $issue->priority) === 'high')>{{ __('High') }} </option> <option value="high" @selected(old('priority', $issue->priority) === 'high')> </option>
<option value="urgent" @selected(old('priority', $issue->priority) === 'urgent')>{{ __('Urgent') }} </option> <option value="urgent" @selected(old('priority', $issue->priority) === 'urgent')>緊急 </option>
</select> </select>
@error('priority')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('priority')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
@@ -71,11 +71,11 @@
<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="assigned_to_user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="assigned_to_user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Assign To') }} 指派給
</label> </label>
<select name="assigned_to_user_id" id="assigned_to_user_id" <select name="assigned_to_user_id" id="assigned_to_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 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="">{{ __('Unassigned') }}</option> <option value="">未指派</option>
@foreach($users as $user) @foreach($users as $user)
<option value="{{ $user->id }}" @selected(old('assigned_to_user_id', $issue->assigned_to_user_id) == $user->id)>{{ $user->name }}</option> <option value="{{ $user->id }}" @selected(old('assigned_to_user_id', $issue->assigned_to_user_id) == $user->id)>{{ $user->name }}</option>
@endforeach @endforeach
@@ -84,11 +84,11 @@
<div> <div>
<label for="reviewer_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="reviewer_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Reviewer') }} 審查者
</label> </label>
<select name="reviewer_id" id="reviewer_id" <select name="reviewer_id" id="reviewer_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 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="">{{ __('None') }}</option> <option value=""></option>
@foreach($users as $user) @foreach($users as $user)
<option value="{{ $user->id }}" @selected(old('reviewer_id', $issue->reviewer_id) == $user->id)>{{ $user->name }}</option> <option value="{{ $user->id }}" @selected(old('reviewer_id', $issue->reviewer_id) == $user->id)>{{ $user->name }}</option>
@endforeach @endforeach
@@ -100,7 +100,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="due_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="due_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Due Date') }} 截止日期
</label> </label>
<input type="date" name="due_date" id="due_date" value="{{ old('due_date', $issue->due_date?->format('Y-m-d')) }}" <input type="date" name="due_date" id="due_date" value="{{ old('due_date', $issue->due_date?->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 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">
@@ -109,7 +109,7 @@
<div> <div>
<label for="estimated_hours" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="estimated_hours" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Estimated Hours') }} 預估時數
</label> </label>
<input type="number" name="estimated_hours" id="estimated_hours" value="{{ old('estimated_hours', $issue->estimated_hours) }}" step="0.5" min="0" <input type="number" name="estimated_hours" id="estimated_hours" value="{{ old('estimated_hours', $issue->estimated_hours) }}" step="0.5" min="0"
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">
@@ -119,11 +119,11 @@
<!-- Member (for member requests) --> <!-- Member (for member requests) -->
<div> <div>
<label for="member_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="member_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Related Member') }} 相關會員
</label> </label>
<select name="member_id" id="member_id" <select name="member_id" id="member_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 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="">{{ __('None') }}</option> <option value=""></option>
@foreach($members as $member) @foreach($members as $member)
<option value="{{ $member->id }}" @selected(old('member_id', $issue->member_id) == $member->id)>{{ $member->full_name }}</option> <option value="{{ $member->id }}" @selected(old('member_id', $issue->member_id) == $member->id)>{{ $member->full_name }}</option>
@endforeach @endforeach
@@ -133,11 +133,11 @@
<!-- Parent Issue (for sub-tasks) --> <!-- Parent Issue (for sub-tasks) -->
<div> <div>
<label for="parent_issue_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="parent_issue_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Parent Issue') }} 父任務
</label> </label>
<select name="parent_issue_id" id="parent_issue_id" <select name="parent_issue_id" id="parent_issue_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 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="">{{ __('None (top-level issue)') }}</option> <option value="">無(頂層任務)</option>
@foreach($openIssues as $parentIssue) @foreach($openIssues as $parentIssue)
<option value="{{ $parentIssue->id }}" @selected(old('parent_issue_id', $issue->parent_issue_id) == $parentIssue->id)> <option value="{{ $parentIssue->id }}" @selected(old('parent_issue_id', $issue->parent_issue_id) == $parentIssue->id)>
{{ $parentIssue->issue_number }} - {{ $parentIssue->title }} {{ $parentIssue->issue_number }} - {{ $parentIssue->title }}
@@ -149,7 +149,7 @@
<!-- Labels --> <!-- Labels -->
<div> <div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{{ __('Labels') }} 標籤
</label> </label>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2"> <div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
@foreach($labels as $label) @foreach($labels as $label)
@@ -170,11 +170,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.issues.show', $issue) }}" <a href="{{ route('admin.issues.show', $issue) }}"
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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600"> 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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600">
{{ __('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">
{{ __('Update Issue') }} 更新任務
</button> </button>
</div> </div>
</form> </form>

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">
{{ __('Tasks') }} (任務追蹤) 任務管理 (任務追蹤)
</h2> </h2>
</x-slot> </x-slot>
@@ -23,13 +23,13 @@
<!-- Header --> <!-- Header -->
<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 text-gray-900 dark:text-gray-100">{{ __('Task Tracker') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">任務追蹤器</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{{ __('Manage work items, tasks, and member requests') }}</p> <p class="mt-1 text-sm text-gray-600 dark:text-gray-400">管理工作項目、任務和會員請求</p>
</div> </div>
<div class="mt-4 sm:mt-0"> <div class="mt-4 sm:mt-0">
<a href="{{ route('admin.issues.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"> <a href="{{ route('admin.issues.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">
<svg class="-ml-0.5 mr-1.5 h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><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 class="-ml-0.5 mr-1.5 h-5 w-5" fill="currentColor" viewBox="0 0 20 20"><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>
{{ __('Create Task') }} 新增任務
</a> </a>
</div> </div>
</div> </div>
@@ -37,19 +37,19 @@
<!-- Summary Stats --> <!-- Summary Stats -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-4 mb-6"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-4 mb-6">
<div class="bg-blue-50 dark:bg-blue-900/30 rounded-lg p-4 border-l-4 border-blue-400"> <div class="bg-blue-50 dark:bg-blue-900/30 rounded-lg p-4 border-l-4 border-blue-400">
<dt class="text-sm font-medium text-blue-800 dark:text-blue-200">{{ __('Total Open') }}</dt> <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">{{ $stats['total_open'] }}</dd> <dd class="mt-1 text-2xl font-semibold text-blue-900 dark:text-blue-100">{{ $stats['total_open'] }}</dd>
</div> </div>
<div class="bg-purple-50 dark:bg-purple-900/30 rounded-lg p-4 border-l-4 border-purple-400"> <div class="bg-purple-50 dark:bg-purple-900/30 rounded-lg p-4 border-l-4 border-purple-400">
<dt class="text-sm font-medium text-purple-800 dark:text-purple-200">{{ __('Assigned to Me') }}</dt> <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">{{ $stats['assigned_to_me'] }}</dd> <dd class="mt-1 text-2xl font-semibold text-purple-900 dark:text-purple-100">{{ $stats['assigned_to_me'] }}</dd>
</div> </div>
<div class="bg-red-50 dark:bg-red-900/30 rounded-lg p-4 border-l-4 border-red-400"> <div class="bg-red-50 dark:bg-red-900/30 rounded-lg p-4 border-l-4 border-red-400">
<dt class="text-sm font-medium text-red-800 dark:text-red-200">{{ __('Overdue') }}</dt> <dt class="text-sm font-medium text-red-800 dark:text-red-200">逾期</dt>
<dd class="mt-1 text-2xl font-semibold text-red-900 dark:text-red-100">{{ $stats['overdue'] }}</dd> <dd class="mt-1 text-2xl font-semibold text-red-900 dark:text-red-100">{{ $stats['overdue'] }}</dd>
</div> </div>
<div class="bg-orange-50 dark:bg-orange-900/30 rounded-lg p-4 border-l-4 border-orange-400"> <div class="bg-orange-50 dark:bg-orange-900/30 rounded-lg p-4 border-l-4 border-orange-400">
<dt class="text-sm font-medium text-orange-800 dark:text-orange-200">{{ __('High Priority') }}</dt> <dt class="text-sm font-medium text-orange-800 dark:text-orange-200">高優先級</dt>
<dd class="mt-1 text-2xl font-semibold text-orange-900 dark:text-orange-100">{{ $stats['high_priority'] }}</dd> <dd class="mt-1 text-2xl font-semibold text-orange-900 dark:text-orange-100">{{ $stats['high_priority'] }}</dd>
</div> </div>
</div> </div>
@@ -58,40 +58,40 @@
<form method="GET" class="mb-6 space-y-4" role="search"> <form method="GET" class="mb-6 space-y-4" role="search">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-4"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-4">
<div> <div>
<label for="issue_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Type') }}</label> <label for="issue_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">類型</label>
<select name="issue_type" id="issue_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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> <select name="issue_type" id="issue_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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="">{{ __('All Types') }}</option> <option value="">所有類型</option>
<option value="work_item" @selected(request('issue_type') === 'work_item')>{{ __('Work Item') }}</option> <option value="work_item" @selected(request('issue_type') === 'work_item')>工作項目</option>
<option value="project_task" @selected(request('issue_type') === 'project_task')>{{ __('Project Task') }}</option> <option value="project_task" @selected(request('issue_type') === 'project_task')>專案任務</option>
<option value="maintenance" @selected(request('issue_type') === 'maintenance')>{{ __('Maintenance') }}</option> <option value="maintenance" @selected(request('issue_type') === 'maintenance')>維護</option>
<option value="member_request" @selected(request('issue_type') === 'member_request')>{{ __('Member Request') }}</option> <option value="member_request" @selected(request('issue_type') === 'member_request')>會員請求</option>
</select> </select>
</div> </div>
<div> <div>
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Status') }}</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 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> <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 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="new" @selected(request('status') === 'new')>{{ __('New') }}</option> <option value="new" @selected(request('status') === 'new')></option>
<option value="assigned" @selected(request('status') === 'assigned')>{{ __('Assigned') }}</option> <option value="assigned" @selected(request('status') === 'assigned')>已指派</option>
<option value="in_progress" @selected(request('status') === 'in_progress')>{{ __('In Progress') }}</option> <option value="in_progress" @selected(request('status') === 'in_progress')>進行中</option>
<option value="review" @selected(request('status') === 'review')>{{ __('Review') }}</option> <option value="review" @selected(request('status') === 'review')>審查</option>
<option value="closed" @selected(request('status') === 'closed')>{{ __('Closed') }}</option> <option value="closed" @selected(request('status') === 'closed')>已結案</option>
</select> </select>
</div> </div>
<div> <div>
<label for="priority" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Priority') }}</label> <label for="priority" class="block text-sm font-medium text-gray-700 dark:text-gray-300">優先級</label>
<select name="priority" id="priority" 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"> <select name="priority" id="priority" 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 Priorities') }}</option> <option value="">所有優先級</option>
<option value="low" @selected(request('priority') === 'low')>{{ __('Low') }}</option> <option value="low" @selected(request('priority') === 'low')></option>
<option value="medium" @selected(request('priority') === 'medium')>{{ __('Medium') }}</option> <option value="medium" @selected(request('priority') === 'medium')></option>
<option value="high" @selected(request('priority') === 'high')>{{ __('High') }}</option> <option value="high" @selected(request('priority') === 'high')></option>
<option value="urgent" @selected(request('priority') === 'urgent')>{{ __('Urgent') }}</option> <option value="urgent" @selected(request('priority') === 'urgent')>緊急</option>
</select> </select>
</div> </div>
<div> <div>
<label for="assigned_to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Assignee') }}</label> <label for="assigned_to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">指派對象</label>
<select name="assigned_to" id="assigned_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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> <select name="assigned_to" id="assigned_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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="">{{ __('All Assignees') }}</option> <option value="">所有指派對象</option>
@foreach($users as $user) @foreach($users as $user)
<option value="{{ $user->id }}" @selected(request('assigned_to') == $user->id)>{{ $user->name }}</option> <option value="{{ $user->id }}" @selected(request('assigned_to') == $user->id)>{{ $user->name }}</option>
@endforeach @endforeach
@@ -100,14 +100,14 @@
</div> </div>
<div class="flex gap-4"> <div class="flex gap-4">
<div class="flex-1"> <div class="flex-1">
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Search') }}</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="{{ __('Task number, title, or 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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> <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 sm:text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
</div> </div>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<button type="submit" class="inline-flex 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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600">{{ __('Filter') }}</button> <button type="submit" class="inline-flex 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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600 dark:hover:bg-gray-600">篩選</button>
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" name="show_closed" value="1" @checked(request('show_closed') === '1') class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600"> <input type="checkbox" name="show_closed" value="1" @checked(request('show_closed') === '1') class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600">
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ __('Show closed') }}</span> <span class="ml-2 text-sm text-gray-700 dark:text-gray-300">顯示已結案</span>
</label> </label>
</div> </div>
</div> </div>
@@ -116,16 +116,16 @@
<!-- Issues Table --> <!-- Issues Table -->
<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">{{ __('List of tasks with their current status and assignment') }}</caption> <caption class="sr-only">任務列表及其目前狀態與指派</caption>
<thead class="bg-gray-50 dark:bg-gray-700"> <thead class="bg-gray-50 dark:bg-gray-700">
<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">{{ __('Task') }}</th> <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">任務</th>
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Type') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Status') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Priority') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Assignee') }}</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 scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">{{ __('Due Date') }}</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 scope="col" class="relative py-3.5 pl-3 pr-4"><span class="sr-only">{{ __('Actions') }}</span></th> <th scope="col" class="relative py-3.5 pl-3 pr-4"><span class="sr-only">操作</span></th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800"> <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800">
@@ -163,7 +163,7 @@
<span class="{{ $issue->is_overdue ? 'text-red-600 dark:text-red-400 font-semibold' : '' }}"> <span class="{{ $issue->is_overdue ? 'text-red-600 dark:text-red-400 font-semibold' : '' }}">
{{ $issue->due_date->format('Y-m-d') }} {{ $issue->due_date->format('Y-m-d') }}
@if($issue->is_overdue) @if($issue->is_overdue)
<span class="text-xs">({{ __('Overdue') }})</span> <span class="text-xs">(逾期)</span>
@endif @endif
</span> </span>
@else @else
@@ -171,16 +171,16 @@
@endif @endif
</td> </td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium"> <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium">
<a href="{{ route('admin.issues.show', $issue) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">{{ __('View') }}</a> <a href="{{ route('admin.issues.show', $issue) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">檢視</a>
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="7" class="px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400"> <td colspan="7" class="px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<p>{{ __('No tasks found') }}</p> <p>找不到任務</p>
<div class="mt-4"> <div class="mt-4">
<a href="{{ route('admin.issues.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 dark:bg-indigo-500 dark:hover:bg-indigo-400"> <a href="{{ route('admin.issues.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 dark:bg-indigo-500 dark:hover:bg-indigo-400">
+ {{ __('Create First Task') }} + 新增第一個任務
</a> </a>
</div> </div>
</td> </td>

View File

@@ -31,23 +31,23 @@
<div class="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400"> <div class="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
<span>{{ $issue->issue_type_label }}</span> <span>{{ $issue->issue_type_label }}</span>
<span>"</span> <span>"</span>
<span>{{ __('Created by') }} {{ $issue->creator->name }}</span> <span>建立者 {{ $issue->creator->name }}</span>
<span>"</span> <span>"</span>
<span>{{ $issue->created_at->diffForHumans() }}</span> <span>{{ $issue->created_at->diffForHumans() }}</span>
</div> </div>
</div> </div>
<div class="mt-4 sm:mt-0 flex gap-2"> <div class="mt-4 sm:mt-0 flex gap-2">
@if(!$issue->isClosed() || Auth::user()->is_admin) @if(!$issue->isClosed() || Auth::user()->hasRole('admin'))
<a href="{{ route('admin.issues.edit', $issue) }}" class="inline-flex items-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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600"> <a href="{{ route('admin.issues.edit', $issue) }}" class="inline-flex items-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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600">
{{ __('Edit') }} 編輯
</a> </a>
@endif @endif
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole('admin'))
<form method="POST" action="{{ route('admin.issues.destroy', $issue) }}" onsubmit="return confirm('{{ __('Are you sure?') }}')"> <form method="POST" action="{{ route('admin.issues.destroy', $issue) }}" onsubmit="return confirm('您確定嗎?')">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 dark:bg-red-500 dark:hover:bg-red-400"> <button type="submit" class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 dark:bg-red-500 dark:hover:bg-red-400">
{{ __('Delete') }} 刪除
</button> </button>
</form> </form>
@endif @endif
@@ -68,55 +68,55 @@
<!-- Description --> <!-- Description -->
<div class="prose dark:prose-invert max-w-none mb-6"> <div class="prose dark:prose-invert max-w-none mb-6">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">{{ __('Description') }}</h4> <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">描述</h4>
<div class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $issue->description ?: __('No description provided') }}</div> <div class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $issue->description ?: __('No description provided') }}</div>
</div> </div>
<!-- Details Grid --> <!-- Details Grid -->
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2 border-t border-gray-200 dark:border-gray-700 pt-6"> <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2 border-t border-gray-200 dark:border-gray-700 pt-6">
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Assigned To') }}</dt> <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">{{ $issue->assignee?->name ?? __('Unassigned') }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $issue->assignee?->name ?? __('Unassigned') }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Reviewer') }}</dt> <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">{{ $issue->reviewer?->name ?? __('None') }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $issue->reviewer?->name ?? __('None') }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Due Date') }}</dt> <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"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
@if($issue->due_date) @if($issue->due_date)
<span class="{{ $issue->is_overdue ? 'text-red-600 dark:text-red-400 font-semibold' : '' }}"> <span class="{{ $issue->is_overdue ? 'text-red-600 dark:text-red-400 font-semibold' : '' }}">
{{ $issue->due_date->format('Y-m-d') }} {{ $issue->due_date->format('Y-m-d') }}
@if($issue->is_overdue) @if($issue->is_overdue)
({{ __('Overdue by :days days', ['days' => abs($issue->days_until_due)]) }}) (逾期 {{ abs($issue->days_until_due) }} )
@elseif($issue->days_until_due !== null && $issue->days_until_due >= 0) @elseif($issue->days_until_due !== null && $issue->days_until_due >= 0)
({{ __(':days days left', ['days' => $issue->days_until_due]) }}) ({{ $issue->days_until_due }} 天剩餘)
@endif @endif
</span> </span>
@else @else
<span class="text-gray-400">{{ __('No due date') }}</span> <span class="text-gray-400">無截止日期</span>
@endif @endif
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Time Tracking') }}</dt> <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"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
{{ number_format($issue->actual_hours, 1) }}h {{ number_format($issue->actual_hours, 1) }}h
@if($issue->estimated_hours) @if($issue->estimated_hours)
/ {{ number_format($issue->estimated_hours, 1) }}h {{ __('estimated') }} / {{ number_format($issue->estimated_hours, 1) }}h 預估
@endif @endif
</dd> </dd>
</div> </div>
@if($issue->member) @if($issue->member)
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Related 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 dark:text-gray-100">{{ $issue->member->full_name }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $issue->member->full_name }}</dd>
</div> </div>
@endif @endif
@if($issue->parentIssue) @if($issue->parentIssue)
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Parent Issue') }}</dt> <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"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<a href="{{ route('admin.issues.show', $issue->parentIssue) }}" class="text-indigo-600 hover:underline dark:text-indigo-400"> <a href="{{ route('admin.issues.show', $issue->parentIssue) }}" class="text-indigo-600 hover:underline dark:text-indigo-400">
{{ $issue->parentIssue->issue_number }} - {{ $issue->parentIssue->title }} {{ $issue->parentIssue->issue_number }} - {{ $issue->parentIssue->title }}
@@ -129,7 +129,7 @@
<!-- Sub-tasks --> <!-- Sub-tasks -->
@if($issue->subTasks->count() > 0) @if($issue->subTasks->count() > 0)
<div class="mt-6 border-t border-gray-200 dark:border-gray-700 pt-6"> <div class="mt-6 border-t border-gray-200 dark:border-gray-700 pt-6">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">{{ __('Sub-tasks') }} ({{ $issue->subTasks->count() }})</h4> <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">子任務 ({{ $issue->subTasks->count() }})</h4>
<ul class="space-y-2"> <ul class="space-y-2">
@foreach($issue->subTasks as $subTask) @foreach($issue->subTasks as $subTask)
<li class="flex items-center gap-2"> <li class="flex items-center gap-2">
@@ -150,21 +150,21 @@
<!-- Workflow Actions --> <!-- Workflow Actions -->
@if(!$issue->isClosed()) @if(!$issue->isClosed())
<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">{{ __('Actions') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">操作</h3>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<!-- Update Status --> <!-- Update Status -->
<form method="POST" action="{{ route('admin.issues.update-status', $issue) }}" class="inline-flex gap-2"> <form method="POST" action="{{ route('admin.issues.update-status', $issue) }}" class="inline-flex gap-2">
@csrf @csrf
@method('PATCH') @method('PATCH')
<select name="status" class="rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> <select name="status" class="rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="new" @selected($issue->status === 'new')>{{ __('New') }}</option> <option value="new" @selected($issue->status === 'new')></option>
<option value="assigned" @selected($issue->status === 'assigned')>{{ __('Assigned') }}</option> <option value="assigned" @selected($issue->status === 'assigned')>已指派</option>
<option value="in_progress" @selected($issue->status === 'in_progress')>{{ __('In Progress') }}</option> <option value="in_progress" @selected($issue->status === 'in_progress')>進行中</option>
<option value="review" @selected($issue->status === 'review')>{{ __('Review') }}</option> <option value="review" @selected($issue->status === 'review')>審查</option>
<option value="closed" @selected($issue->status === 'closed')>{{ __('Closed') }}</option> <option value="closed" @selected($issue->status === 'closed')>已結案</option>
</select> </select>
<button type="submit" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"> <button type="submit" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400">
{{ __('Update Status') }} 更新狀態
</button> </button>
</form> </form>
@@ -172,13 +172,13 @@
<form method="POST" action="{{ route('admin.issues.assign', $issue) }}" class="inline-flex gap-2"> <form method="POST" action="{{ route('admin.issues.assign', $issue) }}" class="inline-flex gap-2">
@csrf @csrf
<select name="assigned_to_user_id" class="rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> <select name="assigned_to_user_id" class="rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="">{{ __('Unassigned') }}</option> <option value="">未指派</option>
@foreach($users as $user) @foreach($users as $user)
<option value="{{ $user->id }}" @selected($issue->assigned_to_user_id == $user->id)>{{ $user->name }}</option> <option value="{{ $user->id }}" @selected($issue->assigned_to_user_id == $user->id)>{{ $user->name }}</option>
@endforeach @endforeach
</select> </select>
<button type="submit" class="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600"> <button type="submit" class="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600">
{{ __('Assign') }} 指派
</button> </button>
</form> </form>
</div> </div>
@@ -188,7 +188,7 @@
<!-- Comments --> <!-- Comments -->
<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"> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
{{ __('Comments') }} ({{ $issue->comments->count() }}) 留言 ({{ $issue->comments->count() }})
</h3> </h3>
<!-- Comments List --> <!-- Comments List -->
@@ -199,13 +199,13 @@
<span class="font-medium text-sm text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</span> <span class="font-medium text-sm text-gray-900 dark:text-gray-100">{{ $comment->user->name }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ $comment->created_at->diffForHumans() }}</span> <span class="text-xs text-gray-500 dark:text-gray-400">{{ $comment->created_at->diffForHumans() }}</span>
@if($comment->is_internal) @if($comment->is_internal)
<span class="text-xs bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 px-2 py-0.5 rounded">{{ __('Internal') }}</span> <span class="text-xs bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 px-2 py-0.5 rounded">內部</span>
@endif @endif
</div> </div>
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $comment->comment_text }}</p> <p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $comment->comment_text }}</p>
</div> </div>
@empty @empty
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">{{ __('No comments yet') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">尚無留言</p>
@endforelse @endforelse
</div> </div>
@@ -214,14 +214,14 @@
@csrf @csrf
<textarea name="comment_text" rows="3" required <textarea name="comment_text" rows="3" required
class="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 mb-2" class="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 mb-2"
placeholder="{{ __('Add a comment...') }}"></textarea> placeholder="新增留言..."></textarea>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<label class="inline-flex items-center"> <label class="inline-flex items-center">
<input type="checkbox" name="is_internal" value="1" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600"> <input type="checkbox" name="is_internal" value="1" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:bg-gray-700 dark:border-gray-600">
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">{{ __('Internal comment') }}</span> <span class="ml-2 text-sm text-gray-700 dark:text-gray-300">內部留言</span>
</label> </label>
<button type="submit" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500"> <button type="submit" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500">
{{ __('Add Comment') }} 新增留言
</button> </button>
</div> </div>
</form> </form>
@@ -230,7 +230,7 @@
<!-- Attachments --> <!-- Attachments -->
<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"> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
{{ __('Attachments') }} ({{ $issue->attachments->count() }}) 附件 ({{ $issue->attachments->count() }})
</h3> </h3>
<div class="space-y-2 mb-6"> <div class="space-y-2 mb-6">
@@ -246,18 +246,18 @@
</div> </div>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<a href="{{ route('admin.issues.attachments.download', $attachment) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 text-sm">{{ __('Download') }}</a> <a href="{{ route('admin.issues.attachments.download', $attachment) }}" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 text-sm">下載</a>
@if(Auth::user()->is_admin) @if(Auth::user()->hasRole('admin'))
<form method="POST" action="{{ route('admin.issues.attachments.destroy', $attachment) }}" onsubmit="return confirm('{{ __('Delete this attachment?') }}')"> <form method="POST" action="{{ route('admin.issues.attachments.destroy', $attachment) }}" onsubmit="return confirm('刪除此附件?')">
@csrf @csrf
@method('DELETE') @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 text-sm">{{ __('Delete') }}</button> <button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 text-sm">刪除</button>
</form> </form>
@endif @endif
</div> </div>
</div> </div>
@empty @empty
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">{{ __('No attachments') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">無附件</p>
@endforelse @endforelse
</div> </div>
@@ -267,17 +267,17 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input type="file" name="file" required class="block w-full text-sm text-gray-900 dark:text-gray-100 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 hover:file:bg-indigo-100 dark:file:bg-indigo-900 dark:file:text-indigo-200"> <input type="file" name="file" required class="block w-full text-sm text-gray-900 dark:text-gray-100 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 hover:file:bg-indigo-100 dark:file:bg-indigo-900 dark:file:text-indigo-200">
<button type="submit" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500"> <button type="submit" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500">
{{ __('Upload') }} 上傳
</button> </button>
</div> </div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ __('Max size: 10MB') }}</p> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">最大大小10MB</p>
</form> </form>
</div> </div>
<!-- Time Logs --> <!-- Time Logs -->
<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"> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
{{ __('Time Tracking') }} ({{ number_format($issue->total_time_logged, 1) }}h total) 時間追蹤 ({{ number_format($issue->total_time_logged, 1) }}h total)
</h3> </h3>
<div class="space-y-2 mb-6"> <div class="space-y-2 mb-6">
@@ -289,21 +289,21 @@
</div> </div>
</div> </div>
@empty @empty
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">{{ __('No time logged yet') }}</p> <p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">尚未記錄時間</p>
@endforelse @endforelse
</div> </div>
<!-- Log Time Form --> <!-- Log Time Form -->
<form method="POST" action="{{ route('admin.issues.time-logs.store', $issue) }}" class="border-t border-gray-200 dark:border-gray-700 pt-4 grid grid-cols-2 gap-2"> <form method="POST" action="{{ route('admin.issues.time-logs.store', $issue) }}" class="border-t border-gray-200 dark:border-gray-700 pt-4 grid grid-cols-2 gap-2">
@csrf @csrf
<input type="number" name="hours" step="0.25" min="0.25" placeholder="{{ __('Hours') }}" required <input type="number" name="hours" step="0.25" min="0.25" placeholder="時數" required
class="rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<input type="date" name="logged_at" value="{{ now()->format('Y-m-d') }}" required <input type="date" name="logged_at" value="{{ now()->format('Y-m-d') }}" required
class="rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<input type="text" name="description" placeholder="{{ __('What did you do?') }}" <input type="text" name="description" placeholder="您做了什麼?"
class="col-span-2 rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> class="col-span-2 rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<button type="submit" class="col-span-2 inline-flex justify-center items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500"> <button type="submit" class="col-span-2 inline-flex justify-center items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500">
{{ __('Log Time') }} 記錄時間
</button> </button>
</form> </form>
</div> </div>
@@ -313,11 +313,11 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Timeline --> <!-- Timeline -->
<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">{{ __('Progress') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">進度</h3>
<x-issue.timeline :issue="$issue" /> <x-issue.timeline :issue="$issue" />
<div class="mt-4"> <div class="mt-4">
<div class="flex justify-between text-sm mb-1"> <div class="flex justify-between text-sm mb-1">
<span class="text-gray-700 dark:text-gray-300">{{ __('Completion') }}</span> <span class="text-gray-700 dark:text-gray-300">完成度</span>
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $issue->progress_percentage }}%</span> <span class="font-medium text-gray-900 dark:text-gray-100">{{ $issue->progress_percentage }}%</span>
</div> </div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2"> <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
@@ -329,7 +329,7 @@
<!-- Watchers --> <!-- Watchers -->
<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"> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
{{ __('Watchers') }} ({{ $issue->watchers->count() }}) 觀察者 ({{ $issue->watchers->count() }})
</h3> </h3>
<ul class="space-y-2 mb-4"> <ul class="space-y-2 mb-4">
@@ -341,7 +341,7 @@
@csrf @csrf
@method('DELETE') @method('DELETE')
<input type="hidden" name="user_id" value="{{ $watcher->id }}"> <input type="hidden" name="user_id" value="{{ $watcher->id }}">
<button type="submit" class="text-xs text-red-600 hover:text-red-900 dark:text-red-400">{{ __('Remove') }}</button> <button type="submit" class="text-xs text-red-600 hover:text-red-900 dark:text-red-400">移除</button>
</form> </form>
@endif @endif
</li> </li>
@@ -353,13 +353,13 @@
@csrf @csrf
<div class="flex gap-2"> <div class="flex gap-2">
<select name="user_id" required class="flex-1 rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100"> <select name="user_id" required class="flex-1 rounded-md border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100">
<option value="">{{ __('Add watcher...') }}</option> <option value="">新增觀察者...</option>
@foreach($users->whereNotIn('id', $issue->watchers->pluck('id')) as $user) @foreach($users->whereNotIn('id', $issue->watchers->pluck('id')) as $user)
<option value="{{ $user->id }}">{{ $user->name }}</option> <option value="{{ $user->id }}">{{ $user->name }}</option>
@endforeach @endforeach
</select> </select>
<button type="submit" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500"> <button type="submit" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500">
{{ __('Add') }} 新增
</button> </button>
</div> </div>
</form> </form>

View File

@@ -1,13 +1,13 @@
<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">
{{ __('Activate Membership') }} - {{ $member->full_name }} 啟用會員資格 - {{ $member->full_name }}
</h2> </h2>
</x-slot> </x-slot>
<div class="py-12"> <div class="py-12">
<div class="mx-auto max-w-2xl sm:px-6 lg:px-8"> <div class="mx-auto max-w-2xl sm:px-6 lg:px-8">
<div class="bg-white shadow sm:rounded-lg dark:bg-gray-800 px-4 py-5 sm:p-6"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg px-4 py-5 sm:p-6">
@if($approvedPayment) @if($approvedPayment)
{{-- Approved Payment Info --}} {{-- Approved Payment Info --}}
@@ -19,12 +19,12 @@
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-green-800 dark:text-green-200">{{ __('Payment Approved') }}</h3> <h3 class="text-sm font-medium text-green-800 dark:text-green-200">付款已核准</h3>
<div class="mt-2 text-sm text-green-700 dark:text-green-300"> <div class="mt-2 text-sm text-green-700 dark:text-green-300">
<p>{{ __('Amount') }}: TWD {{ number_format($approvedPayment->amount, 0) }}</p> <p>金額: TWD {{ number_format($approvedPayment->amount, 0) }}</p>
<p>{{ __('Payment Date') }}: {{ $approvedPayment->paid_at->format('Y-m-d') }}</p> <p>付款日期: {{ $approvedPayment->paid_at->format('Y-m-d') }}</p>
<p>{{ __('Payment Method') }}: {{ $approvedPayment->payment_method_label }}</p> <p>付款方式: {{ $approvedPayment->payment_method_label }}</p>
<p>{{ __('Approved on') }}: {{ $approvedPayment->chair_verified_at->format('Y-m-d H:i') }}</p> <p>核准日期: {{ $approvedPayment->chair_verified_at->format('Y-m-d H:i') }}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -33,18 +33,18 @@
{{-- Member Info --}} {{-- Member Info --}}
<div class="mb-6"> <div class="mb-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">{{ __('Member Information') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">會員資訊</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 dark:text-gray-400">{{ __('Full Name') }}</dt> <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">{{ $member->full_name }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $member->full_name }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Email') }}</dt> <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">{{ $member->email }}</dd> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $member->email }}</dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Current Status') }}</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 items-center rounded-full px-2 py-1 text-xs font-medium {{ $member->membership_status_badge }}"> <span class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium {{ $member->membership_status_badge }}">
{{ $member->membership_status_label }} {{ $member->membership_status_label }}
@@ -59,19 +59,19 @@
@csrf @csrf
<div class="border-t border-gray-200 dark:border-gray-700 pt-6"> <div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Membership Activation Details') }}</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">會員啟用詳情</h3>
{{-- Membership Type --}} {{-- Membership Type --}}
<div> <div>
<label for="membership_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="membership_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Membership Type') }} <span class="text-red-500">*</span> 會員類型 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<select name="membership_type" id="membership_type" required <select name="membership_type" id="membership_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 dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100 @error('membership_type') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm @error('membership_type') border-red-300 @enderror">
<option value="regular" {{ old('membership_type', 'regular') == 'regular' ? 'selected' : '' }}>{{ __('Regular Member (一般會員)') }}</option> <option value="regular" {{ old('membership_type', 'regular') == 'regular' ? 'selected' : '' }}>一般會員</option>
<option value="student" {{ old('membership_type') == 'student' ? 'selected' : '' }}>{{ __('Student Member (學生會員)') }}</option> <option value="student" {{ old('membership_type') == 'student' ? 'selected' : '' }}>學生會員</option>
<option value="honorary" {{ old('membership_type') == 'honorary' ? 'selected' : '' }}>{{ __('Honorary Member (榮譽會員)') }}</option> <option value="honorary" {{ old('membership_type') == 'honorary' ? 'selected' : '' }}>榮譽會員</option>
<option value="lifetime" {{ old('membership_type') == 'lifetime' ? 'selected' : '' }}>{{ __('Lifetime Member (終身會員)') }}</option> <option value="lifetime" {{ old('membership_type') == 'lifetime' ? 'selected' : '' }}>終身會員</option>
</select> </select>
@error('membership_type')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('membership_type')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
@@ -79,23 +79,23 @@
{{-- Start Date --}} {{-- Start Date --}}
<div class="mt-4"> <div class="mt-4">
<label for="membership_started_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="membership_started_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Membership Start Date') }} <span class="text-red-500">*</span> 會員資格開始日 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<input type="date" name="membership_started_at" id="membership_started_at" <input type="date" name="membership_started_at" id="membership_started_at"
value="{{ old('membership_started_at', today()->format('Y-m-d')) }}" required value="{{ old('membership_started_at', today()->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 @error('membership_started_at') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm @error('membership_started_at') border-red-300 @enderror">
@error('membership_started_at')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('membership_started_at')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
{{-- End Date --}} {{-- End Date --}}
<div class="mt-4"> <div class="mt-4">
<label for="membership_expires_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300"> <label for="membership_expires_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Membership Expiry Date') }} <span class="text-red-500">*</span> 會員資格到期日 <span class="text-red-500 dark:text-red-400">*</span>
</label> </label>
<input type="date" name="membership_expires_at" id="membership_expires_at" <input type="date" name="membership_expires_at" id="membership_expires_at"
value="{{ old('membership_expires_at', today()->addYear()->format('Y-m-d')) }}" required value="{{ old('membership_expires_at', today()->addYear()->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 @error('membership_expires_at') border-red-300 @enderror"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 sm:text-sm @error('membership_expires_at') border-red-300 @enderror">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ __('Default: One year from start date') }}</p> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">預設:開始日期後一年</p>
@error('membership_expires_at')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror @error('membership_expires_at')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div> </div>
</div> </div>
@@ -110,21 +110,21 @@
</div> </div>
<div class="ml-3"> <div class="ml-3">
<p class="text-sm text-blue-700 dark:text-blue-300"> <p class="text-sm text-blue-700 dark:text-blue-300">
{{ __('After activation, the member will receive a confirmation email and gain access to member-only resources.') }} 啟用後,會員將收到確認電子郵件並獲得會員專屬資源的存取權限。
</p> </p>
</div> </div>
</div> </div>
</div> </div>
{{-- Submit Buttons --}} {{-- Submit Buttons --}}
<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 dark:border-gray-700 pt-6">
<a href="{{ route('admin.members.show', $member) }}" <a href="{{ route('admin.members.show', $member) }}"
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 dark:bg-gray-700 dark:text-gray-100 dark:ring-gray-600"> class="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">
{{ __('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 dark:bg-indigo-500 dark:hover:bg-indigo-400"> class="inline-flex justify-center rounded-md bg-indigo-600 dark:bg-indigo-500 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 dark:hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
{{ __('Activate Membership') }} 啟用會員資格
</button> </button>
</div> </div>
</form> </form>
@@ -168,4 +168,4 @@
} }
}); });
</script> </script>
</x-app-layout> </x-app-layout>

View File

@@ -1,236 +1,236 @@
<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">
{{ __('Create new member') }} 新增會員
</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">
@if (session('status')) @if (session('status'))
<div class="mb-4 rounded-md bg-green-50 p-4" role="status" aria-live="polite"> <div class="mb-4 rounded-md bg-green-50 dark:bg-green-900/50 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
<form method="POST" action="{{ route('admin.members.store') }}" class="space-y-6" aria-label="{{ __('Create member form') }}"> <form method="POST" action="{{ route('admin.members.store') }}" class="space-y-6" aria-label="新增會員表單">
@csrf @csrf
<div> <div>
<label for="full_name" class="block text-sm font-medium text-gray-700"> <label for="full_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Full name') }} 全名
</label> </label>
<input <input
type="text" type="text"
name="full_name" name="full_name"
id="full_name" id="full_name"
value="{{ old('full_name') }}" value="{{ old('full_name') }}"
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('full_name') @error('full_name')
<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="email" class="block text-sm font-medium text-gray-700"> <label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Email') }} 電子郵件
</label> </label>
<input <input
type="email" type="email"
name="email" name="email"
id="email" id="email"
value="{{ old('email') }}" value="{{ old('email') }}"
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('email') @error('email')
<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">
{{ __('An activation email will be sent to this address.') }} 啟用電子郵件將發送至此地址。
</p> </p>
</div> </div>
<div> <div>
<label for="national_id" class="block text-sm font-medium text-gray-700"> <label for="national_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('National ID') }} 身分證號
</label> </label>
<input <input
type="text" type="text"
name="national_id" name="national_id"
id="national_id" id="national_id"
value="{{ old('national_id') }}" value="{{ old('national_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" 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"
autocomplete="off" autocomplete="off"
> >
@error('national_id') @error('national_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
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ __('Will be stored encrypted for security.') }} 將加密儲存以確保安全。
</p> </p>
</div> </div>
<div> <div>
<label for="phone" class="block text-sm font-medium text-gray-700"> <label for="phone" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Phone') }} 電話
</label> </label>
<input <input
type="text" type="text"
name="phone" name="phone"
id="phone" id="phone"
value="{{ old('phone') }}" value="{{ old('phone') }}"
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"
> >
@error('phone') @error('phone')
<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 class="grid gap-6 sm:grid-cols-2"> <div class="grid gap-6 sm:grid-cols-2">
<div> <div>
<label for="membership_started_at" class="block text-sm font-medium text-gray-700"> <label for="membership_started_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Membership start date') }} 會員資格開始日
</label> </label>
<input <input
type="date" type="date"
name="membership_started_at" name="membership_started_at"
id="membership_started_at" id="membership_started_at"
value="{{ old('membership_started_at') }}" value="{{ old('membership_started_at') }}"
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"
> >
@error('membership_started_at') @error('membership_started_at')
<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="membership_expires_at" class="block text-sm font-medium text-gray-700"> <label for="membership_expires_at" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Membership expiry date') }} 會員資格到期日
</label> </label>
<input <input
type="date" type="date"
name="membership_expires_at" name="membership_expires_at"
id="membership_expires_at" id="membership_expires_at"
value="{{ old('membership_expires_at') }}" value="{{ old('membership_expires_at') }}"
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"
> >
@error('membership_expires_at') @error('membership_expires_at')
<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>
<div> <div>
<label for="address_line_1" class="block text-sm font-medium text-gray-700"> <label for="address_line_1" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Address Line 1') }} 地址第1行
</label> </label>
<input <input
type="text" type="text"
name="address_line_1" name="address_line_1"
id="address_line_1" id="address_line_1"
value="{{ old('address_line_1') }}" value="{{ old('address_line_1') }}"
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"
> >
@error('address_line_1') @error('address_line_1')
<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="address_line_2" class="block text-sm font-medium text-gray-700"> <label for="address_line_2" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Address Line 2') }} 地址第2行
</label> </label>
<input <input
type="text" type="text"
name="address_line_2" name="address_line_2"
id="address_line_2" id="address_line_2"
value="{{ old('address_line_2') }}" value="{{ old('address_line_2') }}"
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"
> >
@error('address_line_2') @error('address_line_2')
<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 class="grid gap-6 sm:grid-cols-2"> <div class="grid gap-6 sm:grid-cols-2">
<div> <div>
<label for="city" class="block text-sm font-medium text-gray-700"> <label for="city" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('City') }} 城市
</label> </label>
<input <input
type="text" type="text"
name="city" name="city"
id="city" id="city"
value="{{ old('city') }}" value="{{ old('city') }}"
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"
> >
@error('city') @error('city')
<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="postal_code" class="block text-sm font-medium text-gray-700"> <label for="postal_code" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Postal Code') }} 郵遞區號
</label> </label>
<input <input
type="text" type="text"
name="postal_code" name="postal_code"
id="postal_code" id="postal_code"
value="{{ old('postal_code') }}" value="{{ old('postal_code') }}"
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"
> >
@error('postal_code') @error('postal_code')
<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>
<div> <div>
<label for="emergency_contact_name" class="block text-sm font-medium text-gray-700"> <label for="emergency_contact_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Emergency Contact Name') }} 緊急聯絡人姓名
</label> </label>
<input <input
type="text" type="text"
name="emergency_contact_name" name="emergency_contact_name"
id="emergency_contact_name" id="emergency_contact_name"
value="{{ old('emergency_contact_name') }}" value="{{ old('emergency_contact_name') }}"
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"
> >
@error('emergency_contact_name') @error('emergency_contact_name')
<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="emergency_contact_phone" class="block text-sm font-medium text-gray-700"> <label for="emergency_contact_phone" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Emergency Contact Phone') }} 緊急聯絡人電話
</label> </label>
<input <input
type="text" type="text"
name="emergency_contact_phone" name="emergency_contact_phone"
id="emergency_contact_phone" id="emergency_contact_phone"
value="{{ old('emergency_contact_phone') }}" value="{{ old('emergency_contact_phone') }}"
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"
> >
@error('emergency_contact_phone') @error('emergency_contact_phone')
<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 class="flex justify-end gap-3"> <div class="flex justify-end gap-3">
<a href="{{ route('admin.members.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.members.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">
{{ __('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 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">
{{ __('Create member') }} 新增會員
</button> </button>
</div> </div>
</form> </form>

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