Compare commits

..

12 Commits

Author SHA1 Message Date
bcff65cf67 Fix audit logs and issue reports pages, rename Issues to Tasks
修復稽核日誌與任務報表頁面,並將「問題」改名為「任務」

## Changes 變更內容

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

518
lang/zh_TW.json Normal file
View File

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

5
package-lock.json generated
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

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

View File

@@ -1,7 +1,7 @@
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Issue Reports & Analytics') }}
{{ __('Task Reports & Analytics') }}
</h2>
</x-slot>
@@ -9,7 +9,7 @@
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-6">
{{-- Date Range Filter --}}
<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">
<form method="GET" action="{{ route('admin.issue-reports.index') }}" class="flex flex-wrap gap-4 items-end">
<div class="flex-1 min-w-[200px]">
<label for="start_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -34,35 +34,35 @@
{{-- Summary Statistics --}}
<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">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Total Issues') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Total Tasks') }}</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['total_issues']) }}</dd>
</div>
<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 Issues') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Open Tasks') }}</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['open_issues']) }}</dd>
</div>
<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 Issues') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Closed Tasks') }}</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['closed_issues']) }}</dd>
</div>
<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 Issues') }}</dt>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ __('Overdue Tasks') }}</dt>
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($stats['overdue_issues']) }}</dd>
</div>
</div>
{{-- Charts Section --}}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- Issues by Status --}}
<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">{{ __('Issues by Status') }}</h3>
{{-- Tasks by Status --}}
<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>
<div class="space-y-2">
@foreach(['new', 'assigned', 'in_progress', 'review', 'closed'] as $status)
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ ucfirst(str_replace('_', ' ', $status)) }}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $issuesByStatus[$status] ?? 0 }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
@php
$percentage = $stats['total_issues'] > 0 ? (($issuesByStatus[$status] ?? 0) / $stats['total_issues']) * 100 : 0;
@endphp
@@ -72,9 +72,9 @@
</div>
</div>
{{-- Issues by Priority --}}
<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">{{ __('Issues by Priority') }}</h3>
{{-- Tasks by Priority --}}
<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>
<div class="space-y-2">
@foreach(['low', 'medium', 'high', 'urgent'] as $priority)
@php
@@ -84,7 +84,7 @@
<span class="text-sm text-gray-600 dark:text-gray-400">{{ ucfirst($priority) }}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $issuesByPriority[$priority] ?? 0 }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
@php
$percentage = $stats['total_issues'] > 0 ? (($issuesByPriority[$priority] ?? 0) / $stats['total_issues']) * 100 : 0;
@endphp
@@ -94,16 +94,16 @@
</div>
</div>
{{-- Issues by Type --}}
<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">{{ __('Issues by Type') }}</h3>
{{-- Tasks by Type --}}
<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>
<div class="space-y-2">
@foreach(['work_item', 'project_task', 'maintenance', 'member_request'] as $type)
<div class="flex items-center justify-between">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ ucfirst(str_replace('_', ' ', $type)) }}</span>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $issuesByType[$type] ?? 0 }}</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
@php
$percentage = $stats['total_issues'] > 0 ? (($issuesByType[$type] ?? 0) / $stats['total_issues']) * 100 : 0;
@endphp
@@ -116,7 +116,7 @@
{{-- Time Tracking Metrics --}}
@if($timeTrackingMetrics && $timeTrackingMetrics->total_estimated > 0)
<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">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Time Tracking Metrics') }}</h3>
<div class="grid grid-cols-1 sm:grid-cols-4 gap-4">
<div>
@@ -153,7 +153,7 @@
{{-- Average Resolution Time --}}
@if($avgResolutionTime)
<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">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">{{ __('Average Resolution Time') }}</h3>
<p class="text-3xl font-semibold text-gray-900 dark:text-gray-100">{{ number_format($avgResolutionTime, 1) }} {{ __('days') }}</p>
</div>
@@ -161,9 +161,9 @@
{{-- Assignee Performance --}}
@if($assigneePerformance->isNotEmpty())
<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">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Assignee Performance (Top 10)') }}</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">
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
<thead class="bg-gray-50 dark:bg-gray-900">
<tr>
@@ -174,7 +174,7 @@
<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>
</tr>
</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 dark:divide-gray-700 bg-white dark:bg-gray-800">
@foreach($assigneePerformance as $user)
<tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100">
@@ -209,7 +209,7 @@
{{-- Top Labels Used --}}
@if($topLabels->isNotEmpty())
<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">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">{{ __('Top Labels Used') }}</h3>
<div class="space-y-3">
@foreach($topLabels as $label)
@@ -234,14 +234,14 @@
</div>
@endif
{{-- Recent Issues --}}
<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">{{ __('Recent Issues') }}</h3>
{{-- Recent Tasks --}}
<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>
<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">
<thead class="bg-gray-50 dark:bg-gray-900">
<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">{{ __('Issue') }}</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">{{ __('Task') }}</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">{{ __('Priority') }}</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>
@@ -277,4 +277,4 @@
</div>
</div>
</x-app-layout>
</x-app-layout>

View File

@@ -1,7 +1,7 @@
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Create Issue') }} (建立問題)
{{ __('Create Task') }} (建立任務)
</h2>
</x-slot>
@@ -18,7 +18,7 @@
</label>
<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"
placeholder="{{ __('Brief summary of the issue') }}">
placeholder="{{ __('Brief summary of the task') }}">
@error('title')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div>
@@ -29,15 +29,15 @@
</label>
<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"
placeholder="{{ __('Detailed description of the issue...') }}">{{ old('description') }}</textarea>
placeholder="{{ __('Detailed description of the task...') }}">{{ old('description') }}</textarea>
@error('description')<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>@enderror
</div>
<!-- Issue Type and Priority -->
<!-- Task Type and Priority -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<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>
{{ __('Task Type') }} <span class="text-red-500">*</span>
</label>
<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">
@@ -100,7 +100,7 @@
<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"
placeholder="0.0">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Estimated time to complete this issue') }}</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Estimated time to complete this task') }}</p>
</div>
<!-- Member (for member requests) -->
@@ -118,21 +118,21 @@
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Link to a member for member requests') }}</p>
</div>
<!-- Parent Issue (for sub-tasks) -->
<!-- Parent Task (for sub-tasks) -->
<div>
<label for="parent_issue_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
{{ __('Parent Issue') }}
{{ __('Parent Task') }}
</label>
<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">
<option value="">{{ __('None (top-level issue)') }}</option>
<option value="">{{ __('None (top-level task)') }}</option>
@foreach($openIssues as $parentIssue)
<option value="{{ $parentIssue->id }}" @selected(old('parent_issue_id') == $parentIssue->id)>
{{ $parentIssue->issue_number }} - {{ $parentIssue->title }}
</option>
@endforeach
</select>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Make this a sub-task of another issue') }}</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ __('Make this a sub-task of another task') }}</p>
</div>
<!-- Labels -->
@@ -153,7 +153,7 @@
</label>
@endforeach
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ __('Select one or more labels to categorize this issue') }}</p>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ __('Select one or more labels to categorize this task') }}</p>
</div>
<!-- Actions -->
@@ -164,7 +164,7 @@
</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 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 Issue') }}
{{ __('Create Task') }}
</button>
</div>
</form>
@@ -179,14 +179,14 @@
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">{{ __('Creating Issues') }}</h3>
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">{{ __('Creating Tasks') }}</h3>
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<ul class="list-disc pl-5 space-y-1">
<li>{{ __('Use work items for general tasks and todos') }}</li>
<li>{{ __('Project tasks are for specific project milestones') }}</li>
<li>{{ __('Member requests track inquiries or requests from members') }}</li>
<li>{{ __('Assign issues to team members to track responsibility') }}</li>
<li>{{ __('Use labels to categorize and filter issues easily') }}</li>
<li>{{ __('Assign tasks to team members to track responsibility') }}</li>
<li>{{ __('Use labels to categorize and filter tasks easily') }}</li>
</ul>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Issues') }} (問題追蹤)
{{ __('Tasks') }} (任務追蹤)
</h2>
</x-slot>
@@ -23,13 +23,13 @@
<!-- Header -->
<div class="sm:flex sm:items-center sm:justify-between mb-6">
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ __('Issue Tracker') }}</h3>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ __('Task Tracker') }}</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{{ __('Manage work items, tasks, and member requests') }}</p>
</div>
<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">
<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 Issue') }}
{{ __('Create Task') }}
</a>
</div>
</div>
@@ -101,7 +101,7 @@
<div class="flex gap-4">
<div class="flex-1">
<label for="search" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ __('Search') }}</label>
<input type="text" name="search" id="search" value="{{ request('search') }}" placeholder="{{ __('Issue 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="{{ __('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">
</div>
<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>
@@ -116,10 +116,10 @@
<!-- Issues Table -->
<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">
<caption class="sr-only">{{ __('List of issues with their current status and assignment') }}</caption>
<thead class="bg-gray-50 dark:bg-gray-900">
<caption class="sr-only">{{ __('List of tasks with their current status and assignment') }}</caption>
<thead class="bg-gray-50 dark:bg-gray-700">
<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">{{ __('Issue') }}</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">{{ __('Task') }}</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">{{ __('Status') }}</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>
@@ -177,10 +177,10 @@
@empty
<tr>
<td colspan="7" class="px-3 py-8 text-center text-sm text-gray-500 dark:text-gray-400">
<p>{{ __('No issues found') }}</p>
<p>{{ __('No tasks found') }}</p>
<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">
+ {{ __('Create First Issue') }}
+ {{ __('Create First Task') }}
</a>
</div>
</td>

View File

@@ -1,3 +1,8 @@
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
</svg>
@props(['alt' => '台灣尤塞氏症暨視聽弱協會'])
<img
src="{{ asset('images/usher-logo-long.png') }}"
alt="{{ $alt }}"
loading="lazy"
{{ $attributes }}
>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 180 B

View File

@@ -1,4 +1,4 @@
@props(['active'])
@props(['active' => false])
@php
$classes = ($active ?? false)
@@ -6,6 +6,6 @@ $classes = ($active ?? false)
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
<a {{ $attributes->merge(['class' => $classes]) }} @if($active) aria-current="page" @endif>
{{ $slot }}
</a>

View File

@@ -1,4 +1,4 @@
@props(['active'])
@props(['active' => false])
@php
$classes = ($active ?? false)
@@ -6,6 +6,6 @@ $classes = ($active ?? false)
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
<a {{ $attributes->merge(['class' => $classes]) }} @if($active) aria-current="page" @endif>
{{ $slot }}
</a>

View File

@@ -1,65 +1,256 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
<div class="flex items-center justify-between">
<div>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
台灣尤塞氏症暨視聽弱協會|工作桌面
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">會員服務、財務簽核、文件與公告都在這裡。</p>
</div>
<div class="hidden sm:flex items-center gap-3">
<a href="{{ route('member.dashboard') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md bg-blue-50 dark:bg-blue-900 text-blue-700 dark:text-blue-200 hover:bg-blue-100 dark:hover:bg-blue-800 border border-blue-200 dark:border-blue-700">
我的會籍/繳費
</a>
@can('create_finance_documents')
<a href="{{ route('admin.finance.create') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md bg-emerald-50 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-200 hover:bg-emerald-100 dark:hover:bg-emerald-800 border border-emerald-200 dark:border-emerald-700">
建立財務申請
</a>
@endcan
</div>
</div>
</x-slot>
<div class="py-12">
<div class="py-10">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<!-- Welcome Message -->
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<h3 class="text-lg font-medium text-gray-900">歡迎回來,{{ Auth::user()->name }}</h3>
<p class="mt-1 text-sm text-gray-600">這是您的個人儀表板</p>
<!-- Primary cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg border border-gray-100 dark:border-gray-700 p-5">
<div class="flex items-start justify-between">
<div>
<div class="text-sm font-semibold text-gray-700 dark:text-gray-300">會員狀態</div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">查看會籍期限、繳費紀錄、下載收據</p>
</div>
<span class="text-2xl">🎫</span>
</div>
<div class="mt-4 flex gap-3">
<a href="{{ route('member.dashboard') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700">
前往會員專區
</a>
<a href="{{ route('member.payments.create') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
自助繳費
</a>
</div>
</div>
@if(Auth::user()->can('create_finance_documents') || Auth::user()->can('view_finance_documents'))
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg border border-gray-100 dark:border-gray-700 p-5">
<div class="flex items-start justify-between">
<div>
<div class="text-sm font-semibold text-gray-700 dark:text-gray-300">財務申請/審核</div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">申請、審核、付款、對帳全流程</p>
</div>
<span class="text-2xl">💼</span>
</div>
<div class="mt-4 flex gap-3">
@can('create_finance_documents')
<a href="{{ route('admin.finance.create') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md bg-emerald-600 text-white hover:bg-emerald-700">
新增申請
</a>
@endcan
@can('view_finance_documents')
<a href="{{ route('admin.finance.index') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
查看案件列表
</a>
@endcan
</div>
</div>
@endif
@if(Auth::user()->hasRole(['admin', 'membership_manager']) || Auth::user()->can('view_audit_logs'))
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg border border-gray-100 dark:border-gray-700 p-5">
<div class="flex items-start justify-between">
<div>
<div class="text-sm font-semibold text-gray-700 dark:text-gray-300">管理/維運</div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">會員匯入、角色權限、審計與任務追蹤</p>
</div>
<span class="text-2xl">🛡️</span>
</div>
<div class="mt-4 flex gap-3 flex-wrap">
@hasrole('admin|membership_manager')
<a href="{{ route('admin.members.index') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
會員管理
</a>
@endhasrole
@role('admin')
<a href="{{ route('admin.roles.index') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
角色與權限
</a>
<a href="{{ route('admin.audit.index') }}" class="inline-flex items-center px-3 py-2 text-sm font-medium rounded-md border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
審計日誌
</a>
@endrole
</div>
</div>
@endif
</div>
<!-- Recent Documents Widget -->
@if($recentDocuments->isNotEmpty())
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="px-6 py-5 border-b border-gray-200">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">最新文件</h3>
<a href="{{ route('documents.index') }}" class="text-sm text-indigo-600 hover:text-indigo-900">
查看全部
</a>
</div>
</div>
<div class="divide-y divide-gray-200">
@foreach($recentDocuments as $document)
<div class="px-6 py-4 hover:bg-gray-50 transition">
<div class="flex items-start">
<div class="flex-shrink-0 text-3xl mr-4">
{{ $document->currentVersion?->getFileIcon() ?? '📄' }}
</div>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900">
<a href="{{ route('documents.public.show', $document->public_uuid) }}" class="hover:text-indigo-600">
{{ $document->title }}
</a>
</h4>
@if($document->description)
<p class="mt-1 text-sm text-gray-500 line-clamp-1">{{ $document->description }}</p>
@endif
<div class="mt-2 flex items-center space-x-4 text-xs text-gray-500">
<span>{{ $document->category->icon }} {{ $document->category->name }}</span>
<span>📅 {{ $document->created_at->format('Y-m-d') }}</span>
<span>📏 {{ $document->currentVersion?->getFileSizeHuman() }}</span>
</div>
</div>
<div class="ml-4 flex-shrink-0">
<a href="{{ route('documents.public.download', $document->public_uuid) }}"
class="inline-flex items-center px-3 py-1.5 border border-gray-300 rounded-md text-xs font-medium text-gray-700 bg-white hover:bg-gray-50">
下載
</a>
</div>
</div>
</div>
@endforeach
<!-- To-do by role (all roles see the buckets) -->
@if(Auth::user()->hasRole(['admin', 'finance_cashier', 'finance_accountant', 'finance_chair']) || Auth::user()->can('create_finance_documents'))
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg border border-gray-100 dark:border-gray-700 p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">待辦總覽</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">依職責挑選你需要處理的事項。</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
@can('create_finance_documents')
<div class="p-4 rounded-lg border border-gray-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-700">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">申請人 / 會員</h4><span>📝</span>
</div>
<ul class="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.finance.create') }}">建立財務申請</a></li>
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.finance.index') }}">查看我的申請進度</a></li>
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('member.dashboard') }}">查看會籍與繳費紀錄</a></li>
</ul>
</div>
@endcan
@hasrole('finance_cashier|admin')
<div class="p-4 rounded-lg border border-gray-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-700">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">出納</h4><span>💰</span>
</div>
<ul class="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.finance.index') }}">待出納審核的申請</a></li>
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.payment-orders.index') }}">待覆核/執行的付款單</a></li>
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.cashier-ledger.index') }}">填寫現金簿/匯出報表</a></li>
</ul>
</div>
@endhasrole
@hasrole('finance_accountant|admin')
<div class="p-4 rounded-lg border border-gray-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-700">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">會計</h4><span>📊</span>
</div>
<ul class="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.finance.index') }}">待會計審核的申請</a></li>
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.payment-orders.index') }}">製作/更新付款單</a></li>
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.transactions.index') }}">建立交易分錄</a></li>
</ul>
</div>
@endhasrole
@hasrole('finance_chair|admin')
<div class="p-4 rounded-lg border border-gray-100 dark:border-gray-700 bg-slate-50 dark:bg-gray-700">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200">理事長/理事</h4><span></span>
</div>
<ul class="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.finance.index') }}">待核准的中額/大額申請</a></li>
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.bank-reconciliations.index') }}">待核准的銀行調節表</a></li>
<li><a class="hover:text-blue-600 dark:hover:text-blue-400" href="{{ route('admin.roles.index') }}">角色/權限檢視</a></li>
</ul>
</div>
@endhasrole
</div>
</div>
@endif
<!-- Issues / Documents -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
@if(Auth::user() && (Auth::user()->hasRole(['admin', 'membership_manager', 'finance_accountant', 'staff']) || Auth::user()->canAny(['view_finance_documents', 'view_accounting_transactions', 'manage_system_settings'])))
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg border border-gray-100 dark:border-gray-700 p-6">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">任務追蹤</h3>
<a href="{{ route('admin.issues.create') }}" class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">建立任務</a>
</div>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">內部項目管理、工作追蹤與協作工具。</p>
<div class="mt-3 flex gap-3 text-sm">
<a href="{{ route('admin.issues.index') }}" class="inline-flex items-center px-3 py-2 rounded-md border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">查看全部</a>
<a href="{{ route('admin.issue-reports.index') }}" class="inline-flex items-center px-3 py-2 rounded-md border border-gray-200 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">統計報表</a>
</div>
</div>
@endif
@if($recentDocuments->isNotEmpty())
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg border border-gray-100 dark:border-gray-700">
<div class="px-6 py-5 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">最新文件/公告</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">協會文件、公告與外部發佈。</p>
</div>
<a href="{{ route('documents.index') }}" class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">查看全部 </a>
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($recentDocuments as $document)
<div class="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition">
<div class="flex items-start">
<div class="flex-shrink-0 text-3xl mr-4">
{{ $document->currentVersion?->getFileIcon() ?? '📄' }}
</div>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 dark:text-gray-100">
<a href="{{ route('documents.public.show', $document->public_uuid) }}" class="hover:text-blue-600 dark:hover:text-blue-400">
{{ $document->title }}
</a>
</h4>
@if($document->description)
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400 line-clamp-1">{{ $document->description }}</p>
@endif
<div class="mt-2 flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
<span>{{ $document->category->icon }} {{ $document->category->name }}</span>
<span>📅 {{ $document->created_at->format('Y-m-d') }}</span>
<span>📏 {{ $document->currentVersion?->getFileSizeHuman() }}</span>
</div>
</div>
<div class="ml-4 flex-shrink-0">
<a href="{{ route('documents.public.download', $document->public_uuid) }}"
class="inline-flex items-center px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-md text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
下載
</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endif
<!-- Recent Announcements -->
@if(isset($recentAnnouncements) && $recentAnnouncements->isNotEmpty())
<div class="bg-white dark:bg-gray-800 shadow-sm sm:rounded-lg border border-gray-100 dark:border-gray-700">
<div class="border-b border-gray-100 dark:border-gray-700 px-6 py-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">📢 最新公告</h3>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
@foreach($recentAnnouncements as $announcement)
<div class="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div class="flex items-start">
@if($announcement->is_pinned)
<span class="mr-2 text-blue-500 flex-shrink-0" title="置頂公告">📌</span>
@endif
<div class="flex-1 min-w-0">
<h4 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1">
{{ $announcement->title }}
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">
{{ $announcement->getExcerpt(120) }}
</p>
<div class="flex items-center space-x-3 text-xs text-gray-500 dark:text-gray-400">
<span>{{ $announcement->published_at?->diffForHumans() ?? $announcement->created_at->diffForHumans() }}</span>
@if($announcement->view_count > 0)
<span></span>
<span>👁 {{ $announcement->view_count }} </span>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endif
</div>
</div>
</div>
</x-app-layout>

View File

@@ -25,7 +25,7 @@ A finance document has been approved by both cashier and accountant, and is now
This document includes an attachment for review.
@endif
<x-mail::button :url="route('finance.show', $document)">
<x-mail::button :url="route('admin.finance.show', $document)">
Review Document
</x-mail::button>

View File

@@ -24,7 +24,7 @@ A finance document has been approved by the cashier and is now awaiting your rev
This document includes an attachment for review.
@endif
<x-mail::button :url="route('finance.show', $document)">
<x-mail::button :url="route('admin.finance.show', $document)">
Review Document
</x-mail::button>

View File

@@ -21,7 +21,7 @@ Great news! Your finance document has been fully approved by all required approv
{{ $document->description }}
@endif
<x-mail::button :url="route('finance.show', $document)">
<x-mail::button :url="route('admin.finance.show', $document)">
View Document
</x-mail::button>

View File

@@ -20,7 +20,7 @@ Your finance document has been rejected.
{{ $document->rejection_reason }}
@endif
<x-mail::button :url="route('finance.show', $document)">
<x-mail::button :url="route('admin.finance.show', $document)">
View Document
</x-mail::button>

View File

@@ -15,6 +15,9 @@
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased bg-gray-50 text-slate-900 transition-colors duration-300 dark:bg-slate-900 dark:text-slate-100">
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-white focus:text-black focus:outline-none focus:ring-2 focus:ring-indigo-500">
{{ __('Skip to main content') }}
</a>
<div class="min-h-screen">
@include('layouts.navigation')
@@ -28,7 +31,7 @@
@endif
<!-- Page Content -->
<main class="bg-gray-50 dark:bg-slate-900">
<main id="main-content" class="bg-gray-50 dark:bg-slate-900">
{{ $slot }}
</main>
</div>

View File

@@ -15,14 +15,17 @@
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-white focus:text-black focus:outline-none focus:ring-2 focus:ring-indigo-500">
{{ __('Skip to main content') }}
</a>
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
<x-application-logo class="h-auto w-48 fill-current text-gray-500" />
</a>
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
<div id="main-content" class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>

View File

@@ -24,61 +24,82 @@
{{ __('Documents') }}
</x-nav-link>
@if(Auth::user() && (Auth::user()->is_admin || Auth::user()->hasRole('admin')))
<x-nav-link :href="route('admin.members.index')" :active="request()->routeIs('admin.members.*')">
{{ __('Admin: Members') }}
</x-nav-link>
<x-nav-link :href="route('admin.roles.index')" :active="request()->routeIs('admin.roles.*')">
{{ __('Admin: Roles') }}
</x-nav-link>
<x-nav-link :href="route('admin.finance.index')" :active="request()->routeIs('admin.finance.*')">
{{ __('Admin: Finance') }}
</x-nav-link>
<x-nav-link :href="route('admin.budgets.index')" :active="request()->routeIs('admin.budgets.*')">
{{ __('Admin: Budgets') }}
</x-nav-link>
<x-nav-link :href="route('admin.transactions.index')" :active="request()->routeIs('admin.transactions.*')">
{{ __('Admin: Transactions') }}
</x-nav-link>
<x-nav-link :href="route('admin.issues.index')" :active="request()->routeIs('admin.issues.*')">
{{ __('Admin: Issues') }}
</x-nav-link>
<x-nav-link :href="route('admin.audit.index')" :active="request()->routeIs('admin.audit.*')">
{{ __('Admin: Audit Logs') }}
</x-nav-link>
<x-nav-link :href="route('admin.document-categories.index')" :active="request()->routeIs('admin.document-categories.*')">
{{ __('Admin: Document Categories') }}
</x-nav-link>
<x-nav-link :href="route('admin.documents.index')" :active="request()->routeIs('admin.documents.*')">
{{ __('Admin: Documents') }}
</x-nav-link>
@can('manage_system_settings')
<x-nav-link :href="route('admin.settings.general')" :active="request()->routeIs('admin.settings.*')">
{{ __('Admin: System Settings') }}
</x-nav-link>
@endcan
@if(Auth::user() && (Auth::user()->hasRole(['admin', 'membership_manager', 'finance_accountant', 'staff']) || Auth::user()->canAny(['view_finance_documents', 'view_accounting_transactions', 'manage_system_settings'])))
<div class="hidden sm:flex sm:items-center">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="@if(request()->routeIs('admin.*')) inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-slate-100 focus:outline-none focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out @else inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-slate-400 hover:text-gray-700 dark:hover:text-slate-300 hover:border-gray-300 dark:hover:border-slate-600 focus:outline-none focus:text-gray-700 dark:focus:text-slate-300 focus:border-gray-300 dark:focus:border-slate-600 transition duration-150 ease-in-out @endif">
<div>{{ __('Management') }}</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
@can('view_announcements')
<x-dropdown-link :href="route('admin.announcements.index')">
{{ __('Admin: Announcements') }}
</x-dropdown-link>
@endcan
@hasrole('admin|membership_manager')
<x-dropdown-link :href="route('admin.members.index')">
{{ __('Admin: Members') }}
</x-dropdown-link>
@endhasrole
@role('admin')
<x-dropdown-link :href="route('admin.roles.index')">
{{ __('Admin: Roles') }}
</x-dropdown-link>
@endrole
@can('view_finance_documents')
<x-dropdown-link :href="route('admin.finance.index')">
{{ __('Admin: Finance') }}
</x-dropdown-link>
@endcan
@hasrole('admin|finance_accountant')
<x-dropdown-link :href="route('admin.budgets.index')">
{{ __('Admin: Budgets') }}
</x-dropdown-link>
@endhasrole
@can('view_accounting_transactions')
<x-dropdown-link :href="route('admin.transactions.index')">
{{ __('Admin: Transactions') }}
</x-dropdown-link>
@endcan
<x-dropdown-link :href="route('admin.issues.index')">
{{ __('Admin: Tasks') }}
</x-dropdown-link>
@role('admin')
<x-dropdown-link :href="route('admin.audit.index')">
{{ __('Admin: Audit Logs') }}
</x-dropdown-link>
<x-dropdown-link :href="route('admin.document-categories.index')">
{{ __('Admin: Document Categories') }}
</x-dropdown-link>
@endrole
@hasrole('admin|staff')
<x-dropdown-link :href="route('admin.documents.index')">
{{ __('Admin: Documents') }}
</x-dropdown-link>
@endhasrole
@can('manage_system_settings')
<x-dropdown-link :href="route('admin.settings.general')">
{{ __('Admin: System Settings') }}
</x-dropdown-link>
@endcan
</x-slot>
</x-dropdown>
</div>
@endif
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6 space-x-3">
<button
type="button"
@click="$root.toggle()"
class="inline-flex items-center px-3 py-2 rounded-md text-sm font-medium border border-transparent bg-gray-100 text-gray-600 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700"
aria-label="Toggle theme"
>
<span x-show="$root.isDark" class="flex items-center space-x-2">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M17.293 13.293a1 1 0 00-1.414 0l-.586.586a6 6 0 01-8.486-8.486l.586-.586A1 1 0 006.172 3H6a1 1 0 00-.707.293l-.586.586a8 8 0 1011.314 11.314l.586-.586a1 1 0 000-1.414l-.314-.314z"/></svg>
<span class="sr-only">Switch to light</span>
</span>
<span x-show="!$root.isDark" class="flex items-center space-x-2">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 3a1 1 0 011 1v1a1 1 0 11-2 0V4a1 1 0 011-1zm0 8a3 3 0 100-6 3 3 0 000 6zm5.657-6.657a1 1 0 010 1.414l-.707.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 9a1 1 0 110 2h-1a1 1 0 110-2h1zM5 9a1 1 0 100 2H4a1 1 0 100-2h1zm10.657 6.657a1 1 0 00-1.414 0l-.707.707a1 1 0 001.414 1.414l.707-.707a1 1 0 000-1.414zM10 16a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zm-5.657-.343a1 1 0 010-1.414l.707-.707a1 1 0 011.414 1.414l-.707.707a1 1 0 01-1.414 0z"/></svg>
<span class="sr-only">Switch to dark</span>
</span>
</button>
<div class="hidden sm:flex sm:items-center sm:ms-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150 dark:bg-slate-800 dark:text-slate-100 dark:hover:text-white">
@@ -113,7 +134,7 @@
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out dark:text-slate-500 dark:hover:text-slate-400 dark:hover:bg-slate-800 dark:focus:bg-slate-800 dark:focus:text-slate-400" aria-label="{{ __('Main menu') }}">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
@@ -138,34 +159,58 @@
{{ __('Documents') }}
</x-responsive-nav-link>
@if(Auth::user() && (Auth::user()->is_admin || Auth::user()->hasRole('admin')))
@if(Auth::user() && (Auth::user()->hasRole(['admin', 'membership_manager', 'finance_accountant', 'staff']) || Auth::user()->canAny(['view_finance_documents', 'view_accounting_transactions', 'manage_system_settings'])))
<div class="pt-2 pb-1 border-t border-gray-200 dark:border-gray-700 mt-2">
<div class="px-4 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{{ __('Management') }}
</div>
</div>
@can('view_announcements')
<x-responsive-nav-link :href="route('admin.announcements.index')" :active="request()->routeIs('admin.announcements.*')">
{{ __('Admin: Announcements') }}
</x-responsive-nav-link>
@endcan
@hasrole('admin|membership_manager')
<x-responsive-nav-link :href="route('admin.members.index')" :active="request()->routeIs('admin.members.*')">
{{ __('Admin: Members') }}
</x-responsive-nav-link>
@endhasrole
@role('admin')
<x-responsive-nav-link :href="route('admin.roles.index')" :active="request()->routeIs('admin.roles.*')">
{{ __('Admin: Roles') }}
</x-responsive-nav-link>
@endrole
@can('view_finance_documents')
<x-responsive-nav-link :href="route('admin.finance.index')" :active="request()->routeIs('admin.finance.*')">
{{ __('Admin: Finance') }}
</x-responsive-nav-link>
@endcan
@hasrole('admin|finance_accountant')
<x-responsive-nav-link :href="route('admin.budgets.index')" :active="request()->routeIs('admin.budgets.*')">
{{ __('Admin: Budgets') }}
</x-responsive-nav-link>
@endhasrole
@can('view_accounting_transactions')
<x-responsive-nav-link :href="route('admin.transactions.index')" :active="request()->routeIs('admin.transactions.*')">
{{ __('Admin: Transactions') }}
</x-responsive-nav-link>
@endcan
<x-responsive-nav-link :href="route('admin.issues.index')" :active="request()->routeIs('admin.issues.*')">
{{ __('Admin: Issues') }}
{{ __('Admin: Tasks') }}
</x-responsive-nav-link>
@role('admin')
<x-responsive-nav-link :href="route('admin.audit.index')" :active="request()->routeIs('admin.audit.*')">
{{ __('Admin: Audit Logs') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('admin.document-categories.index')" :active="request()->routeIs('admin.document-categories.*')">
{{ __('Admin: Document Categories') }}
</x-responsive-nav-link>
@endrole
@hasrole('admin|staff')
<x-responsive-nav-link :href="route('admin.documents.index')" :active="request()->routeIs('admin.documents.*')">
{{ __('Admin: Documents') }}
</x-responsive-nav-link>
@endhasrole
@can('manage_system_settings')
<x-responsive-nav-link :href="route('admin.settings.general')" :active="request()->routeIs('admin.settings.*')">
{{ __('Admin: System Settings') }}
@@ -175,10 +220,10 @@
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
<div class="font-medium text-base text-gray-800 dark:text-slate-200">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500 dark:text-slate-400">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">

View File

@@ -0,0 +1,121 @@
<x-app-layout>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
{{ __('Complete Your Membership Profile') }}
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800">
<div class="p-6 text-gray-900 dark:text-gray-100">
<div class="mb-6">
{{ __('To access the member area, please provide your membership details. This information is required for our records.') }}
</div>
<form method="POST" action="{{ route('member.profile.store') }}" class="space-y-6">
@csrf
{{-- Basic Information --}}
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">{{ __('Basic Information') }}</h3>
<!-- Full Name -->
<div>
<x-input-label for="full_name" :value="__('Full Name')" />
<x-text-input id="full_name" class="block mt-1 w-full" type="text" name="full_name" :value="old('full_name', Auth::user()->name)" required autofocus />
<x-input-error :messages="$errors->get('full_name')" class="mt-2" />
</div>
<!-- Phone -->
<div class="mt-4">
<x-input-label for="phone" :value="__('Phone')" />
<x-text-input id="phone" class="block mt-1 w-full" type="text" name="phone" :value="old('phone')" />
<x-input-error :messages="$errors->get('phone')" class="mt-2" />
</div>
<!-- National ID (Optional) -->
<div class="mt-4">
<x-input-label for="national_id" :value="__('National ID (Optional)')" />
<x-text-input id="national_id" class="block mt-1 w-full" type="text" name="national_id" :value="old('national_id')" maxlength="20" />
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ __('Your national ID will be encrypted for security.') }}</p>
<x-input-error :messages="$errors->get('national_id')" class="mt-2" />
</div>
</div>
{{-- Address Information --}}
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">{{ __('Address') }}</h3>
<!-- Address Line 1 -->
<div>
<x-input-label for="address_line_1" :value="__('Address Line 1')" />
<x-text-input id="address_line_1" class="block mt-1 w-full" type="text" name="address_line_1" :value="old('address_line_1')" />
<x-input-error :messages="$errors->get('address_line_1')" class="mt-2" />
</div>
<!-- Address Line 2 -->
<div class="mt-4">
<x-input-label for="address_line_2" :value="__('Address Line 2')" />
<x-text-input id="address_line_2" class="block mt-1 w-full" type="text" name="address_line_2" :value="old('address_line_2')" />
<x-input-error :messages="$errors->get('address_line_2')" class="mt-2" />
</div>
<div class="grid grid-cols-2 gap-4 mt-4">
<!-- City -->
<div>
<x-input-label for="city" :value="__('City')" />
<x-text-input id="city" class="block mt-1 w-full" type="text" name="city" :value="old('city')" />
<x-input-error :messages="$errors->get('city')" class="mt-2" />
</div>
<!-- Postal Code -->
<div>
<x-input-label for="postal_code" :value="__('Postal Code')" />
<x-text-input id="postal_code" class="block mt-1 w-full" type="text" name="postal_code" :value="old('postal_code')" />
<x-input-error :messages="$errors->get('postal_code')" class="mt-2" />
</div>
</div>
</div>
{{-- Emergency Contact --}}
<div class="border-b border-gray-200 dark:border-gray-700 pb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-3">{{ __('Emergency Contact') }}</h3>
<!-- Emergency Contact Name -->
<div>
<x-input-label for="emergency_contact_name" :value="__('Emergency Contact Name')" />
<x-text-input id="emergency_contact_name" class="block mt-1 w-full" type="text" name="emergency_contact_name" :value="old('emergency_contact_name')" />
<x-input-error :messages="$errors->get('emergency_contact_name')" class="mt-2" />
</div>
<!-- Emergency Contact Phone -->
<div class="mt-4">
<x-input-label for="emergency_contact_phone" :value="__('Emergency Contact Phone')" />
<x-text-input id="emergency_contact_phone" class="block mt-1 w-full" type="text" name="emergency_contact_phone" :value="old('emergency_contact_phone')" />
<x-input-error :messages="$errors->get('emergency_contact_phone')" class="mt-2" />
</div>
</div>
{{-- Terms and Conditions --}}
<div class="mt-4">
<label class="inline-flex items-center">
<input type="checkbox" name="terms_accepted" value="1" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" {{ old('terms_accepted') ? 'checked' : '' }} required>
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">
{{ __('I accept the terms and conditions and agree to submit payment for membership activation.') }}
</span>
</label>
<x-input-error :messages="$errors->get('terms_accepted')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-6">
<x-primary-button>
{{ __('Complete Profile') }}
</x-primary-button>
</div>
</form>
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -197,15 +197,10 @@
<td class="whitespace-nowrap px-4 py-3 text-sm">
@if($payment->isRejected())
<button type="button"
onclick="showRejectionReason{{ $payment->id }}()"
@click="alert('{{ __('Rejection Reason') }}:\n\n' + {{ json_encode($payment->rejection_reason) }})"
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
{{ __('View Reason') }}
</button>
<script>
function showRejectionReason{{ $payment->id }}() {
alert('{{ __("Rejection Reason") }}:\n\n{{ addslashes($payment->rejection_reason) }}');
}
</script>
@elseif($payment->isPending() || $payment->isApprovedByCashier() || $payment->isApprovedByAccountant())
<div class="text-xs text-gray-500 dark:text-gray-400">
@if($payment->verifiedByCashier)

File diff suppressed because one or more lines are too long

View File

@@ -82,6 +82,10 @@ Route::middleware('auth')->group(function () {
// Member Payment Submission Routes
Route::get('/member/submit-payment', [MemberPaymentController::class, 'create'])->name('member.payments.create');
Route::post('/member/payments', [MemberPaymentController::class, 'store'])->name('member.payments.store');
Route::get('/create-member-profile', [MemberDashboardController::class, 'createProfile'])->name('member.profile.create');
Route::post('/create-member-profile', [MemberDashboardController::class, 'storeProfile'])->name('member.profile.store');
Route::get('/my-membership', [MemberDashboardController::class, 'show'])
->name('member.dashboard');

30
start-usher.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
# Start Laravel HTTP server for Usher stack
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"
PORT=8000
HOST=0.0.0.0
LOG_FILE="${ROOT_DIR}/storage/logs/serve.log"
# Basic sanity checks
if [ ! -f "artisan" ]; then
echo "artisan not found. Run this script from project root." >&2
exit 1
fi
mkdir -p "$(dirname "$LOG_FILE")"
# If already running, skip
if pgrep -f "artisan serve --host ${HOST} --port ${PORT}" >/dev/null; then
echo "Laravel dev server already running on ${HOST}:${PORT}."
exit 0
fi
echo "Starting Laravel dev server on ${HOST}:${PORT}..."
nohup php artisan serve --host "${HOST}" --port "${PORT}" >>"$LOG_FILE" 2>&1 &
PID=$!
echo "Started (PID: $PID). Logs: $LOG_FILE"

18
stop-usher.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
# Stop Laravel HTTP server for Usher stack
HOST=0.0.0.0
PORT=8000
PIDS=$(pgrep -f "artisan serve --host ${HOST} --port ${PORT}" || true)
if [ -z "$PIDS" ]; then
echo "No running Laravel dev server found on ${HOST}:${PORT}."
exit 0
fi
echo "Stopping Laravel dev server (PIDs: $PIDS)..."
echo "$PIDS" | xargs kill
echo "Stopped."