Files
Gbanyan 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

372 lines
25 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ $issue->issue_number }} - {{ $issue->title }}
</h2>
</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 p-4 dark:bg-green-900/30 border-l-4 border-green-400" role="status">
<p class="text-sm text-green-800 dark:text-green-200">{{ session('status') }}</p>
</div>
@endif
@if (session('error'))
<div class="rounded-md bg-red-50 p-4 dark:bg-red-900/30 border-l-4 border-red-400" role="alert">
<p class="text-sm text-red-800 dark:text-red-200">{{ session('error') }}</p>
</div>
@endif
<!-- Main Details -->
<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-start sm:justify-between mb-6">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $issue->title }}</h3>
<x-issue.status-badge :status="$issue->status" :label="$issue->status_label" />
<x-issue.priority-badge :priority="$issue->priority" :label="$issue->priority_label" />
</div>
<div class="flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
<span>{{ $issue->issue_type_label }}</span>
<span>"</span>
<span>建立者 {{ $issue->creator->name }}</span>
<span>"</span>
<span>{{ $issue->created_at->diffForHumans() }}</span>
</div>
</div>
<div class="mt-4 sm:mt-0 flex gap-2">
@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>
@endif
@if(Auth::user()->hasRole('admin'))
<form method="POST" action="{{ route('admin.issues.destroy', $issue) }}" onsubmit="return confirm('您確定嗎?')">
@csrf
@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>
</form>
@endif
</div>
</div>
<!-- Labels -->
@if($issue->labels->count() > 0)
<div class="mb-6 flex gap-2 flex-wrap">
@foreach($issue->labels as $label)
<span class="inline-flex items-center rounded-full px-3 py-1 text-sm font-medium"
style="background-color: {{ $label->color }}; color: {{ $label->text_color }}">
{{ $label->name }}
</span>
@endforeach
</div>
@endif
<!-- Description -->
<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">描述</h4>
<div class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $issue->description ?: __('No description provided') }}</div>
</div>
<!-- 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">
<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">{{ $issue->assignee?->name ?? __('Unassigned') }}</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">{{ $issue->reviewer?->name ?? __('None') }}</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">
@if($issue->due_date)
<span class="{{ $issue->is_overdue ? 'text-red-600 dark:text-red-400 font-semibold' : '' }}">
{{ $issue->due_date->format('Y-m-d') }}
@if($issue->is_overdue)
(逾期 {{ abs($issue->days_until_due) }} )
@elseif($issue->days_until_due !== null && $issue->days_until_due >= 0)
({{ $issue->days_until_due }} 天剩餘)
@endif
</span>
@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">
{{ number_format($issue->actual_hours, 1) }}h
@if($issue->estimated_hours)
/ {{ number_format($issue->estimated_hours, 1) }}h 預估
@endif
</dd>
</div>
@if($issue->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">{{ $issue->member->full_name }}</dd>
</div>
@endif
@if($issue->parentIssue)
<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.issues.show', $issue->parentIssue) }}" class="text-indigo-600 hover:underline dark:text-indigo-400">
{{ $issue->parentIssue->issue_number }} - {{ $issue->parentIssue->title }}
</a>
</dd>
</div>
@endif
</dl>
<!-- Sub-tasks -->
@if($issue->subTasks->count() > 0)
<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">子任務 ({{ $issue->subTasks->count() }})</h4>
<ul class="space-y-2">
@foreach($issue->subTasks as $subTask)
<li class="flex items-center gap-2">
<x-issue.status-badge :status="$subTask->status" />
<a href="{{ route('admin.issues.show', $subTask) }}" class="text-indigo-600 hover:underline dark:text-indigo-400">
{{ $subTask->issue_number }} - {{ $subTask->title }}
</a>
</li>
@endforeach
</ul>
</div>
@endif
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Left Column: Comments, Attachments, Time Logs -->
<div class="lg:col-span-2 space-y-6">
<!-- Workflow Actions -->
@if(!$issue->isClosed())
<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>
<div class="flex flex-wrap gap-2">
<!-- Update Status -->
<form method="POST" action="{{ route('admin.issues.update-status', $issue) }}" class="inline-flex gap-2">
@csrf
@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">
<option value="new" @selected($issue->status === 'new')></option>
<option value="assigned" @selected($issue->status === 'assigned')>已指派</option>
<option value="in_progress" @selected($issue->status === 'in_progress')>進行中</option>
<option value="review" @selected($issue->status === 'review')>審查</option>
<option value="closed" @selected($issue->status === 'closed')>已結案</option>
</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>
</form>
<!-- Assign -->
<form method="POST" action="{{ route('admin.issues.assign', $issue) }}" class="inline-flex gap-2">
@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">
<option value="">未指派</option>
@foreach($users as $user)
<option value="{{ $user->id }}" @selected($issue->assigned_to_user_id == $user->id)>{{ $user->name }}</option>
@endforeach
</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>
</form>
</div>
</div>
@endif
<!-- Comments -->
<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">
留言 ({{ $issue->comments->count() }})
</h3>
<!-- Comments List -->
<div class="space-y-4 mb-6">
@forelse($issue->comments as $comment)
<div class="border-l-2 {{ $comment->is_internal ? 'border-orange-400 bg-orange-50 dark:bg-orange-900/20' : 'border-gray-300 dark:border-gray-600' }} pl-4 py-2">
<div class="flex items-center gap-2 mb-1">
<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>
@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">內部</span>
@endif
</div>
<p class="text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ $comment->comment_text }}</p>
</div>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">尚無留言</p>
@endforelse
</div>
<!-- Add Comment Form -->
<form method="POST" action="{{ route('admin.issues.comments.store', $issue) }}" class="border-t border-gray-200 dark:border-gray-700 pt-4">
@csrf
<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"
placeholder="新增留言..."></textarea>
<div class="flex items-center justify-between">
<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">
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">內部留言</span>
</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>
</div>
</form>
</div>
<!-- Attachments -->
<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">
附件 ({{ $issue->attachments->count() }})
</h3>
<div class="space-y-2 mb-6">
@forelse($issue->attachments as $attachment)
<div class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-900 rounded">
<div class="flex items-center gap-2">
<svg class="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z" clip-rule="evenodd"/>
</svg>
<div>
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ $attachment->file_name }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $attachment->file_size_human }} " {{ $attachment->user->name }} " {{ $attachment->created_at->diffForHumans() }}</p>
</div>
</div>
<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">下載</a>
@if(Auth::user()->hasRole('admin'))
<form method="POST" action="{{ route('admin.issues.attachments.destroy', $attachment) }}" onsubmit="return confirm('刪除此附件?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900 dark:text-red-400 text-sm">刪除</button>
</form>
@endif
</div>
</div>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">無附件</p>
@endforelse
</div>
<!-- Upload Form -->
<form method="POST" action="{{ route('admin.issues.attachments.store', $issue) }}" enctype="multipart/form-data" class="border-t border-gray-200 dark:border-gray-700 pt-4">
@csrf
<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">
<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>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">最大大小10MB</p>
</form>
</div>
<!-- Time Logs -->
<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">
時間追蹤 ({{ number_format($issue->total_time_logged, 1) }}h total)
</h3>
<div class="space-y-2 mb-6">
@forelse($issue->timeLogs->sortByDesc('logged_at') as $timeLog)
<div class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-900 rounded">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-gray-100">{{ number_format($timeLog->hours, 2) }}h - {{ $timeLog->user->name }}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{{ $timeLog->logged_at->format('Y-m-d') }} - {{ $timeLog->description }}</p>
</div>
</div>
@empty
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">尚未記錄時間</p>
@endforelse
</div>
<!-- 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">
@csrf
<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">
<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">
<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">
<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>
</form>
</div>
</div>
<!-- Right Column: Timeline & Watchers -->
<div class="space-y-6">
<!-- Timeline -->
<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>
<x-issue.timeline :issue="$issue" />
<div class="mt-4">
<div class="flex justify-between text-sm mb-1">
<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>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div class="bg-indigo-600 dark:bg-indigo-500 h-2 rounded-full transition-all" style="width: {{ $issue->progress_percentage }}%"></div>
</div>
</div>
</div>
<!-- Watchers -->
<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">
觀察者 ({{ $issue->watchers->count() }})
</h3>
<ul class="space-y-2 mb-4">
@foreach($issue->watchers as $watcher)
<li class="flex items-center justify-between">
<span class="text-sm text-gray-900 dark:text-gray-100">{{ $watcher->name }}</span>
@if($watcher->id !== $issue->created_by_user_id)
<form method="POST" action="{{ route('admin.issues.watchers.destroy', $issue) }}">
@csrf
@method('DELETE')
<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">移除</button>
</form>
@endif
</li>
@endforeach
</ul>
<!-- Add Watcher -->
<form method="POST" action="{{ route('admin.issues.watchers.store', $issue) }}" class="border-t border-gray-200 dark:border-gray-700 pt-4">
@csrf
<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">
<option value="">新增觀察者...</option>
@foreach($users->whereNotIn('id', $issue->watchers->pluck('id')) as $user)
<option value="{{ $user->id }}">{{ $user->name }}</option>
@endforeach
</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>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</x-app-layout>