diff --git a/app/Http/Controllers/Admin/CompanyController.php b/app/Http/Controllers/Admin/CompanyController.php index 1e89316..a8bf5b8 100644 --- a/app/Http/Controllers/Admin/CompanyController.php +++ b/app/Http/Controllers/Admin/CompanyController.php @@ -6,9 +6,11 @@ use App\Http\Controllers\Controller; use App\Models\System\Company; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use App\Traits\ImageHandler; class CompanyController extends Controller { + use ImageHandler; /** * Display a listing of the resource. */ @@ -38,7 +40,10 @@ class CompanyController extends Controller ->where('name', '!=', 'super-admin') ->get(); - return view('admin.companies.index', compact('companies', 'template_roles')); + // 取得發生驗證錯誤的公司資訊 (用於維持 Modal 開啟) + $error_company = session('error_company_id') ? Company::find(session('error_company_id')) : null; + + return view('admin.companies.index', compact('companies', 'template_roles', 'error_company')); } /** @@ -230,11 +235,33 @@ class CompanyController extends Controller ->with('success', __('System settings updated successfully.')); } + /** + * 顯示客製化品牌樣式設定頁面 + */ + public function editBranding(Company $company) + { + $settings = $company->settings ?? []; + $enableCustomBranding = filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN); + + if (!$enableCustomBranding) { + return redirect()->route('admin.permission.companies.index') + ->with('error', __('Custom branding function is not enabled for this company.')); + } + + return view('admin.companies.branding', compact('company')); + } + /** * 更新公司品牌客製化樣式設定 (自訂 Logo 與歡迎詞) */ public function updateBranding(Request $request, Company $company) { + \Log::info('updateBranding payload:', [ + 'company_id' => $company->id, + 'request_all' => $request->all(), + 'files' => array_keys($request->allFiles()) + ]); + $settings = $company->settings ?? []; // 1. 安全檢驗:如果未授權此功能,則拒絕操作 @@ -244,41 +271,82 @@ class CompanyController extends Controller } // 2. 欄位驗證 - $request->validate([ - 'logo_file' => 'nullable|image|mimes:jpeg,png,jpg,svg|max:2048', // 限制最大 2MB + $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [ + 'logo_file' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg,webp|max:10240', // 限制最大 10MB + 'login_bg_file' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg,webp|max:10240', // 限制最大 10MB + 'login_card_bg_file' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg,webp|max:10240', // 限制最大 10MB 'login_title' => 'nullable|string|max:100', + 'login_main_title' => 'nullable|string|max:100', + 'login_sub_title' => 'nullable|string|max:100', 'clear_logo' => 'nullable|boolean', // 用於提供清除 Logo 功能 + 'clear_login_bg' => 'nullable|boolean', // 清除大背景 + 'clear_login_card_bg' => 'nullable|boolean', // 清除卡片背景 ]); - $logoPath = $settings['logo_path'] ?? null; + if ($validator->fails()) { + return redirect()->back() + ->withErrors($validator) + ->withInput() + ->with('error', __('Validation failed. Please check the styling configurations.')); + } - // 3. 處理清除 Logo 的需求 + $logoPath = $settings['logo_path'] ?? null; + $loginBgPath = $settings['login_bg_path'] ?? null; + $loginCardBgPath = $settings['login_card_bg_path'] ?? null; + + // 3. 處理清除圖片的需求 if ($request->boolean('clear_logo')) { if ($logoPath) { \Illuminate\Support\Facades\Storage::disk('public')->delete($logoPath); $logoPath = null; } } + if ($request->boolean('clear_login_bg')) { + if ($loginBgPath) { + \Illuminate\Support\Facades\Storage::disk('public')->delete($loginBgPath); + $loginBgPath = null; + } + } + if ($request->boolean('clear_login_card_bg')) { + if ($loginCardBgPath) { + \Illuminate\Support\Facades\Storage::disk('public')->delete($loginCardBgPath); + $loginCardBgPath = null; + } + } - // 4. 處理 Logo 檔案上傳 + // 4. 處理圖片檔案上傳 (轉為高品質 webp) if ($request->hasFile('logo_file')) { - // 如果原本有 Logo,先刪除舊檔案 if ($logoPath) { \Illuminate\Support\Facades\Storage::disk('public')->delete($logoPath); } - // 儲存新檔案到 public/company_logos - $logoPath = $request->file('logo_file')->store('company_logos', 'public'); + $logoPath = $this->storeAsWebp($request->file('logo_file'), 'company_logos', 80); + } + if ($request->hasFile('login_bg_file')) { + if ($loginBgPath) { + \Illuminate\Support\Facades\Storage::disk('public')->delete($loginBgPath); + } + $loginBgPath = $this->storeAsWebp($request->file('login_bg_file'), 'company_backgrounds', 85); + } + if ($request->hasFile('login_card_bg_file')) { + if ($loginCardBgPath) { + \Illuminate\Support\Facades\Storage::disk('public')->delete($loginCardBgPath); + } + $loginCardBgPath = $this->storeAsWebp($request->file('login_card_bg_file'), 'company_backgrounds', 85); } // 5. 合併寫入 settings JSON 中,防止覆寫其他功能開關 $company->settings = array_merge($settings, [ 'logo_path' => $logoPath, + 'login_bg_path' => $loginBgPath, + 'login_card_bg_path' => $loginCardBgPath, 'login_title' => $request->input('login_title'), + 'login_main_title' => $request->input('login_main_title'), + 'login_sub_title' => $request->input('login_sub_title'), ]); $company->save(); - return redirect()->to(route('admin.permission.companies.index') . '?tab=branding') + return redirect()->route('admin.permission.companies.branding.edit', $company->id) ->with('success', __('Brand styling updated successfully.')); } diff --git a/app/Http/Controllers/Auth/TenantLoginController.php b/app/Http/Controllers/Auth/TenantLoginController.php index 62c55fc..d9e6f14 100644 --- a/app/Http/Controllers/Auth/TenantLoginController.php +++ b/app/Http/Controllers/Auth/TenantLoginController.php @@ -64,8 +64,8 @@ class TenantLoginController extends Controller $user = User::where('username', $request->username)->first(); // 4. 【關鍵安全防錯】驗證此使用者是否確實屬於該公司 - if (!$user || $user->company_id !== $company->id) { - Log::warning("Tenant login security violation: User [{$request->username}] attempted login at company [{$company_code}] portal."); + if (!$user || (int)$user->company_id !== (int)$company->id) { + Log::warning("Tenant login security violation: User [{$request->username}] attempted login at company [{$company_code}] portal. User Company ID: " . var_export($user ? $user->company_id : null, true) . ", Target Company ID: {$company->id}"); return back()->withErrors([ 'username' => __('This account does not belong to this company.'), @@ -94,4 +94,51 @@ class TenantLoginController extends Controller return redirect()->intended(route('admin.dashboard')); } + + /** + * 預覽專屬客戶的客製化登入頁面 (不受 guest 中間件阻擋,供後台預覽用) + */ + public function preview($company_code) + { + // 1. 查詢啟用的公司 + $company = Company::active()->where('code', $company_code)->first(); + + // 2. 若公司不存在,重導向至主登入頁 + if (!$company) { + return redirect()->route('login'); + } + + $settings = $company->settings ?? []; + $enableCustomBranding = filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN); + + if (!$enableCustomBranding) { + return redirect()->route('login'); + } + + $isPreview = true; + + // 3. 渲染共用的 login Blade,並傳入預覽標記 + return view('auth.login', compact('company', 'isPreview')); + } + + /** + * 處理打 /c/{company_code} 的跳轉邏輯 + */ + public function indexRedirect($company_code) + { + // 1. 查詢啟用的公司 + $company = Company::active()->where('code', $company_code)->first(); + + if (!$company) { + return redirect()->route('login'); + } + + // 2. 已登入則去 dashboard,未登入則去專屬登入頁 + if (auth()->check()) { + return redirect()->route('admin.dashboard'); + } + + return redirect()->route('tenant.login', $company_code); + } } + diff --git a/lang/en.json b/lang/en.json index 40e204d..208040d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,6 +1,7 @@ { "10s": "10s", "15 Seconds": "15 Seconds", + "1920x1080 (Max 10MB)": "1920x1080 (Max 10MB)", "30 Seconds": "30 Seconds", "30s": "30s", "60 Seconds": "60 Seconds", @@ -182,6 +183,7 @@ "Are you sure you want to send this command?": "Are you sure you want to send this command?", "Are you sure you want to start delivery?": "Are you sure you want to start delivery?", "Are you sure?": "Are you sure?", + "Aspect Ratio 3:4 (Max 10MB)": "Aspect Ratio 3:4 (Max 10MB)", "Assign": "Assign", "Assign Advertisement": "Assign Advertisement", "Assign Machines": "Assign Machines", @@ -238,6 +240,7 @@ "Branch Warehouses": "Branch Warehouses", "Brand Styling": "Brand Styling", "Brand styling updated successfully.": "Brand styling updated successfully.", + "Branding Images & Backgrounds": "Branding Images & Backgrounds", "Business Type": "Business Type", "Buyout": "Buyout", "CASH": "CASH", @@ -265,6 +268,7 @@ "Card Reader Reboot": "Card Reader Reboot", "Card Reader Restart": "Card Reader Restart", "Card Reader Seconds": "Card Reader Seconds", + "Card Side Background": "Card Side Background", "Card System": "Card System", "Card Terminal": "Card Terminal", "Card Terminal System": "Card Terminal System", @@ -291,7 +295,11 @@ "Check": "Check", "Checkout Time 1": "Checkout Time 1", "Checkout Time 2": "Checkout Time 2", + "Choose": "Choose", "Choose Date": "Choose Date", + "Choose File": "Choose File", + "Choose Logo": "Choose Logo", + "Choose Overall BG": "Choose Overall BG", "Clear": "Clear", "Clear Abnormal Status": "Clear Abnormal Status", "Clear Filter": "Clear Filter", @@ -431,11 +439,14 @@ "Custom": "Custom", "Custom Alert Types": "Custom Alert Types", "Custom Background Image": "Custom Background Image", + "Custom Branding": "Custom Branding", "Custom Date": "Custom Date", "Custom Date Set": "Custom Date Set", "Custom Disabled (Mute Alert)": "Custom Disabled (Mute Alert)", "Custom Enabled": "Custom Enabled", "Custom Expiry": "Custom Expiry", + "Custom Left Main Title": "Custom Left Main Title", + "Custom Left Subtitle": "Custom Left Subtitle", "Custom Lower Limit (°C)": "Custom Lower Limit (°C)", "Custom Upper Limit (°C)": "Custom Upper Limit (°C)", "Custom Welcome Subtitle": "Custom Welcome Subtitle", @@ -452,6 +463,7 @@ "Customer updated successfully.": "Customer updated successfully.", "Customize Brand": "Customize Brand", "Customize Brand & Style": "Customize Brand & Style", + "Customize left-side main title, subtitle and right-side welcome description.": "Customize left-side main title, subtitle and right-side welcome description.", "Cycle Efficiency": "Cycle Efficiency", "Daily Revenue": "Daily Revenue", "Daily operational variance curve for selected timeframe": "Daily operational variance curve for selected timeframe", @@ -465,6 +477,8 @@ "Date Range": "Date Range", "Day Before": "Day Before", "Days": "Days", + "Default BG": "Default BG", + "Default Card BG": "Default Card BG", "Default Donate": "Default Donate", "Default Not Donate": "Default Not Donate", "Default Temp Alert Limits": "Default Temp Alert Limits", @@ -548,6 +562,9 @@ "Dispensing in progress": "Dispensing in progress", "Display Material Code": "Display Material Code", "Displayed below \"Welcome Back\" on the login portal.": "Displayed below \"Welcome Back\" on the login portal.", + "Displayed below 'Welcome Back' on the login portal.": "Displayed below 'Welcome Back' on the login portal.", + "Displayed below Logo on the left (defaults to customer name).": "Displayed below Logo on the left (defaults to customer name).", + "Displayed below main title on the left.": "Displayed below main title on the left.", "Displaying": "Displaying", "Done": "Done", "Door Closed": "Door Closed", @@ -597,6 +614,7 @@ "Edit Slot": "Edit Slot", "Edit Staff Card": "Edit Staff Card", "Edit Sub Account Role": "Edit Sub Account Role", + "Edit System Settings": "Edit System Settings", "Edit Warehouse": "Edit Warehouse", "Edit Welcome Gift": "Edit Welcome Gift", "Effective Alert Types": "Effective Alert Types", @@ -631,7 +649,9 @@ "Enable Material Code": "Enable Material Code", "Enable Points": "Enable Points", "Enable Points Mechanism": "Enable Points Mechanism", + "Enable Status": "Enable Status", "Enable and paste Webhook URL": "Enable and paste Webhook URL", + "Enable first": "Enable first", "Enabled": "Enabled", "Enabled Features": "Enabled Features", "Enabled/Disabled": "Enabled/Disabled", @@ -826,6 +846,7 @@ "Global roles accessible by all administrators.": "Global roles accessible by all administrators.", "Go Authorize": "Go Authorize", "Go Back": "Go Back", + "Go Enable": "Go Enable", "Go to E-Invoice": "Go to E-Invoice", "Goods & System Settings": "Goods & System Settings", "Got it": "Got it", @@ -1088,6 +1109,7 @@ "Material Name": "Material Name", "Material Type": "Material Type", "Max": "Max", + "Max 10MB": "Max 10MB", "Max 24 Hours": "Max 24 Hours", "Max 3": "Max 3", "Max 50MB": "Max 50MB", @@ -1356,6 +1378,7 @@ "Out of Stock": "Out of Stock", "Out of stock.": "Out of stock.", "Output Count": "Output Count", + "Overall Background Image": "Overall Background Image", "Overall Capacity": "Overall Capacity", "Owner": "Owner", "PARTNER_KEY": "PARTNER_KEY", @@ -1484,6 +1507,9 @@ "Prepared By": "Prepared By", "Preparing": "Preparing", "Preview": "Preview", + "Preview Login Page": "Preview Login Page", + "Preview Mode - Interactive Vending Portal Mockup": "Preview Mode - Interactive Vending Portal Mockup", + "Preview Mode: Login functionality is disabled.": "Preview Mode: Login functionality is disabled.", "Previous": "Previous", "Price / Member": "Price / Member", "Pricing Information": "Pricing Information", @@ -1919,6 +1945,8 @@ "Superseded by new command (Timeout)": "Superseded by new command (Timeout)", "Support rod push error": "Support rod push error", "Support rod return error": "Support rod return error", + "Supports JPEG, PNG, GIF, SVG, WebP (Max 10MB). Ideal proportion: Horizontal or Square.": "Supports JPEG, PNG, GIF, SVG, WebP (Max 10MB). Ideal proportion: Horizontal or Square.", + "Supports JPEG, PNG, GIF, SVG, WebP (Max 5MB). Ideal proportion: Horizontal or Square.": "Supports JPEG, PNG, GIF, SVG, WebP (Max 5MB). Ideal proportion: Horizontal or Square.", "Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.": "Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.", "Survey Analysis": "Survey Analysis", "Sync Ads": "Sync Ads", @@ -1979,6 +2007,7 @@ "Test Connection": "Test Connection", "Test notification sent successfully.": "Test notification sent successfully.", "Testing": "Testing", + "Text Titles Configuration": "Text Titles Configuration", "The Super Admin role cannot be deleted.": "The Super Admin role cannot be deleted.", "The Super Admin role is immutable.": "The Super Admin role is immutable.", "The Super Admin role name cannot be modified.": "The Super Admin role name cannot be modified.", @@ -2111,6 +2140,7 @@ "Upload New Images": "Upload New Images", "Upload Time": "Upload Time", "Upload Video": "Upload Video", + "Upload corporate logo, login portal overall background and card left side image.": "Upload corporate logo, login portal overall background and card left side image.", "Upload failed, please try again": "Upload failed, please try again", "Upload file or provide URL": "Upload file or provide URL", "Uploading new images will replace all existing images.": "Uploading new images will replace all existing images.", @@ -2137,6 +2167,7 @@ "Valid Ticket": "Valid Ticket", "Valid Until": "Valid Until", "Validation Error": "Validation Error", + "Validation failed. Please check the styling configurations.": "Validation failed. Please check the styling configurations.", "Validity Period": "Validity Period", "Validity Period (Days)": "Validity Period (Days)", "Validity Period (Hours)": "Validity Period (Hours)", @@ -2276,9 +2307,12 @@ "disabled": "disabled", "e.g. 500ml / 300g": "e.g. 500ml / 300g", "e.g. 8.5 for 85折, 8 for 8折": "e.g. 8.5 for 85折, 8 for 8折", + "e.g. AIoT Intelligent Management Platform": "e.g. AIoT Intelligent Management Platform", "e.g. John Doe": "e.g. John Doe", "e.g. Main Warehouse": "e.g. Main Warehouse", "e.g. New Guest 15% Off Discount": "e.g. New Guest 15% Off Discount", + "e.g. Please enter your account and password to enter the admin dashboard.": "e.g. Please enter your account and password to enter the admin dashboard.", + "e.g. SMART DEVICE MANAGEMENT HUB": "e.g. SMART DEVICE MANAGEMENT HUB", "e.g. TWSTAR": "e.g. TWSTAR", "e.g. Taiwan Star": "e.g. Taiwan Star", "e.g. Test Code for Maintenance": "e.g. Test Code for Maintenance", @@ -2428,6 +2462,5 @@ "video": "Video", "visit_gift": "Visit Gift", "vs Yesterday": "vs Yesterday", - "warehouses": "Warehouse Management", "warning": "warning" } diff --git a/lang/ja.json b/lang/ja.json index 5856093..7e86a92 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1,6 +1,7 @@ { "10s": "10秒", "15 Seconds": "15秒", + "1920x1080 (Max 10MB)": "1920x1080 (最大10MB)", "30 Seconds": "30秒", "30s": "30秒", "60 Seconds": "60秒", @@ -182,6 +183,7 @@ "Are you sure you want to send this command?": "このコマンドを送信してもよろしいですか?", "Are you sure you want to start delivery?": "配送を開始してもよろしいですか?", "Are you sure?": "よろしいですか?", + "Aspect Ratio 3:4 (Max 10MB)": "比率 3:4 (最大10MB)", "Assign": "割り当て", "Assign Advertisement": "広告配信", "Assign Machines": "機器割り当て", @@ -238,6 +240,7 @@ "Branch Warehouses": "分庫数", "Brand Styling": "ブランドデザイン", "Brand styling updated successfully.": "ブランドデザインが正常に更新されました。", + "Branding Images & Backgrounds": "ブランド画像と背景設定", "Business Type": "業務タイプ", "Buyout": "買い取り", "CASH": "現金", @@ -265,6 +268,7 @@ "Card Reader Reboot": "カードリーダー再起動", "Card Reader Restart": "カードリーダー再起動", "Card Reader Seconds": "カードリーダー秒数", + "Card Side Background": "カード側背景画像", "Card System": "カードシステム", "Card Terminal": "カード端末", "Card Terminal System": "カードリーダーシステム", @@ -291,7 +295,11 @@ "Check": "確認", "Checkout Time 1": "カード決済締め時間1", "Checkout Time 2": "カード決済締め時間2", + "Choose": "選択", "Choose Date": "日付を選択", + "Choose File": "ファイルを選択", + "Choose Logo": "ロゴを選択", + "Choose Overall BG": "全体背景画像を選択", "Clear": "クリア", "Clear Abnormal Status": "異常状態のクリア", "Clear Filter": "フィルターをクリア", @@ -431,11 +439,14 @@ "Custom": "カスタム", "Custom Alert Types": "カスタムアラート種別", "Custom Background Image": "カスタム背景画像", + "Custom Branding": "ブランドカスタマイズ", "Custom Date": "カスタム日付", "Custom Date Set": "カスタム日付設定済み", "Custom Disabled (Mute Alert)": "カスタム無効(アラート消音)", "Custom Enabled": "カスタム有効", "Custom Expiry": "カスタム期限", + "Custom Left Main Title": "ログインページ左側メインタイトル", + "Custom Left Subtitle": "ログインページ左側サブタイトル", "Custom Lower Limit (°C)": "カスタム下限温度 (°C)", "Custom Upper Limit (°C)": "カスタム上限温度 (°C)", "Custom Welcome Subtitle": "カスタムウェルカムサブタイトル", @@ -452,6 +463,7 @@ "Customer updated successfully.": "顧客が正常に更新されました。", "Customize Brand": "ブランド設定", "Customize Brand & Style": "ブランド&デザインのカスタマイズ", + "Customize left-side main title, subtitle and right-side welcome description.": "ログインページの左側メインタイトル、サブタイトル、および右側ウェルカム説明をカスタマイズします。", "Cycle Efficiency": "サイクル効率", "Daily Revenue": "本日の収益", "Daily operational variance curve for selected timeframe": "選択期間における日次運営変動曲線", @@ -465,6 +477,8 @@ "Date Range": "日付範囲", "Day Before": "一昨日", "Days": "日", + "Default BG": "デフォルト背景", + "Default Card BG": "デフォルトカード背景", "Default Donate": "デフォルトで寄付する", "Default Not Donate": "デフォルトで寄付しない", "Default Temp Alert Limits": "デフォルト温度アラート範囲", @@ -548,6 +562,9 @@ "Dispensing in progress": "搬送中", "Display Material Code": "品目コードを表示", "Displayed below \"Welcome Back\" on the login portal.": "ログイン画面の「おかえりなさい」の下に表示されます。", + "Displayed below 'Welcome Back' on the login portal.": "ログイン画面の「おかえりなさい」の下に表示されます。", + "Displayed below Logo on the left (defaults to customer name).": "左側のロゴの下に表示されるメインタイトル(未設定の場合はデフォルトで顧客名になります)。", + "Displayed below main title on the left.": "左側のメインタイトルの下に表示されるサブタイトル。", "Displaying": "表示中", "Done": "完了", "Door Closed": "扉が閉まりました", @@ -597,6 +614,7 @@ "Edit Slot": "スロット編集", "Edit Staff Card": "スタッフカード編集", "Edit Sub Account Role": "サブアカウントロール編集", + "Edit System Settings": "システム設定の編集", "Edit Warehouse": "倉庫編集", "Edit Welcome Gift": "ウェルカムギフトを編集", "Effective Alert Types": "有効なアラート種別", @@ -631,7 +649,9 @@ "Enable Material Code": "資材コードを有効化", "Enable Points": "ポイントルールを有効化", "Enable Points Mechanism": "ポイント還元を有効化", + "Enable Status": "有効化ステータス", "Enable and paste Webhook URL": "Webhook URLを有効にして貼り付けます", + "Enable first": "まず有効にしてください", "Enabled": "有効済み", "Enabled Features": "有効な機能", "Enabled/Disabled": "有効/無効", @@ -826,6 +846,7 @@ "Global roles accessible by all administrators.": "全管理者がアクセス可能なグローバルロール。", "Go Authorize": "先に授権する", "Go Back": "戻る", + "Go Enable": "有効化へ進む", "Go to E-Invoice": "電子領収書へ移動", "Goods & System Settings": "商品・システム設定", "Got it": "了解しました", @@ -1088,6 +1109,7 @@ "Material Name": "素材名", "Material Type": "素材タイプ", "Max": "最大", + "Max 10MB": "最大10MB", "Max 24 Hours": "最長 24時間", "Max 3": "最大 3枚", "Max 50MB": "最大 50MB", @@ -1356,6 +1378,7 @@ "Out of Stock": "欠品", "Out of stock.": "在庫不足です。", "Output Count": "出庫回数", + "Overall Background Image": "全体背景画像", "Overall Capacity": "総在庫容量", "Owner": "所有者", "PARTNER_KEY": "PARTNER_KEY", @@ -1484,6 +1507,9 @@ "Prepared By": "起票者", "Preparing": "準備中", "Preview": "プレビュー", + "Preview Login Page": "ログインページをプレビュー", + "Preview Mode - Interactive Vending Portal Mockup": "プレビューモード - カスタムブランドログインページのモックアップ", + "Preview Mode: Login functionality is disabled.": "プレビューモード:ログイン機能は無効です。", "Previous": "前へ", "Price / Member": "価格 / 会員価格", "Pricing Information": "価格情報", @@ -1919,6 +1945,8 @@ "Superseded by new command (Timeout)": "新しいコマンドにより上書きされました (タイムアウト)", "Support rod push error": "サポートロッド押し出しエラー", "Support rod return error": "サポートロッド復帰エラー", + "Supports JPEG, PNG, GIF, SVG, WebP (Max 10MB). Ideal proportion: Horizontal or Square.": "JPEG、PNG、GIF、SVG、WebP形式に対応(最大10MB)。推奨比率:横長または正方形。", + "Supports JPEG, PNG, GIF, SVG, WebP (Max 5MB). Ideal proportion: Horizontal or Square.": "JPEG、PNG、GIF、SVG、WebP形式に対応(最大5MB)。推奨比率:横長または正方形。", "Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.": "JPEG、PNG、SVGをサポート(最大2MB)。推奨比率:横長または正方形。", "Survey Analysis": "アンケート分析", "Sync Ads": "広告を同期", @@ -1979,6 +2007,7 @@ "Test Connection": "接続テスト", "Test notification sent successfully.": "テスト通知が正常に送信されました。", "Testing": "テスト", + "Text Titles Configuration": "ログインページタイトル設定", "The Super Admin role cannot be deleted.": "特権管理者ロールは削除できません。", "The Super Admin role is immutable.": "特権管理者ロールは変更できません。", "The Super Admin role name cannot be modified.": "特権管理者ロールの名前は変更できません。", @@ -2111,6 +2140,7 @@ "Upload New Images": "新しい写真をアップロード", "Upload Time": "アップロード時間", "Upload Video": "動画をアップロード", + "Upload corporate logo, login portal overall background and card left side image.": "コーポレートロゴ、ログインポータルの全体背景、およびカードの左側背景画像をアップロードします。", "Upload failed, please try again": "アップロードに失敗しました、再試行してください", "Upload file or provide URL": "Upload file or provide URL", "Uploading new images will replace all existing images.": "新しい写真をアップロードすると、既存のすべての写真が置き換えられます。", @@ -2137,6 +2167,7 @@ "Valid Ticket": "有効なチケット", "Valid Until": "有効期限", "Validation Error": "検証エラー", + "Validation failed. Please check the styling configurations.": "検証に失敗しました。ブランドカスタム設定を確認してください。", "Validity Period": "有効期間", "Validity Period (Days)": "有効期間 (日)", "Validity Period (Hours)": "有効期間 (時間)", @@ -2276,9 +2307,12 @@ "disabled": "無効", "e.g. 500ml / 300g": "例:500ml / 300g", "e.g. 8.5 for 85折, 8 for 8折": "例:8.5は85%、8は80%", + "e.g. AIoT Intelligent Management Platform": "例:AIoT自動化設備スマート運営プラットフォーム", "e.g. John Doe": "例:山田太郎", "e.g. Main Warehouse": "例:第一倉庫", "e.g. New Guest 15% Off Discount": "例:新規顧客向け15%割引", + "e.g. Please enter your account and password to enter the admin dashboard.": "例:管理ダッシュボードに入るためにアカウントとパスワードを入力してください。", + "e.g. SMART DEVICE MANAGEMENT HUB": "例:SMART DEVICE MANAGEMENT HUB", "e.g. TWSTAR": "例:TWSTAR", "e.g. Taiwan Star": "例:台湾星", "e.g. Test Code for Maintenance": "例:メンテナンステスト用コード", @@ -2428,6 +2462,5 @@ "video": "動画", "visit_gift": "来店ギフト", "vs Yesterday": "昨日比", - "warehouses": "倉庫管理", "warning": "警告" } diff --git a/lang/zh_TW.json b/lang/zh_TW.json index e025f01..a1b4627 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1,6 +1,7 @@ { "10s": "10秒", "15 Seconds": "15 秒", + "1920x1080 (Max 10MB)": "1920x1080 (最大 10MB)", "30 Seconds": "30 秒", "30s": "30秒", "60 Seconds": "60 秒", @@ -182,6 +183,7 @@ "Are you sure you want to send this command?": "確定要發送此指令嗎?", "Are you sure you want to start delivery?": "確定要開始配送嗎?", "Are you sure?": "您確定嗎?", + "Aspect Ratio 3:4 (Max 10MB)": "比例 3:4 (最大 10MB)", "Assign": "分配所屬機台", "Assign Advertisement": "投放廣告", "Assign Machines": "分配機台", @@ -237,7 +239,8 @@ "Branch": "分倉", "Branch Warehouses": "分倉數量", "Brand Styling": "品牌樣式", - "Brand styling updated successfully.": "品牌樣式更新成功。", + "Brand styling updated successfully.": "客製化品牌樣式已更新成功。", + "Branding Images & Backgrounds": "品牌圖片與背景自訂", "Business Type": "業務類型", "Buyout": "買斷", "CASH": "CASH", @@ -265,6 +268,7 @@ "Card Reader Reboot": "刷卡機重啟", "Card Reader Restart": "卡機重啟", "Card Reader Seconds": "刷卡機秒數", + "Card Side Background": "左側卡片背景圖", "Card System": "刷卡機系統", "Card Terminal": "刷卡機端", "Card Terminal System": "刷卡機系統", @@ -291,7 +295,11 @@ "Check": "核對", "Checkout Time 1": "卡機結帳時間1", "Checkout Time 2": "卡機結帳時間2", + "Choose": "選擇", "Choose Date": "選擇日期", + "Choose File": "選擇檔案", + "Choose Logo": "選擇公司 Logo", + "Choose Overall BG": "選擇大背景圖", "Clear": "清除", "Clear Abnormal Status": "清除異常狀態", "Clear Filter": "清除篩選", @@ -431,15 +439,18 @@ "Custom": "自訂", "Custom Alert Types": "自訂告警類型", "Custom Background Image": "自訂背景圖", + "Custom Branding": "品牌自訂", "Custom Date": "自定義日期", "Custom Date Set": "已設定自定義日期", "Custom Disabled (Mute Alert)": "自訂停用(靜音告警)", "Custom Enabled": "自訂啟用", "Custom Expiry": "自訂效期", + "Custom Left Main Title": "登入頁左側主標題", + "Custom Left Subtitle": "登入頁左側副標題", "Custom Lower Limit (°C)": "自訂下限溫度 (°C)", "Custom Upper Limit (°C)": "自訂上限溫度 (°C)", "Custom Welcome Subtitle": "自訂歡迎副標題", - "Custom branding function is not enabled for this company.": "此公司尚未啟用品牌自訂功能授權。", + "Custom branding function is not enabled for this company.": "此客戶尚未啟用自訂品牌功能。", "Customer Details": "客戶詳情", "Customer Info": "客戶資訊", "Customer List": "客戶列表", @@ -450,8 +461,9 @@ "Customer deleted successfully.": "客戶已成功刪除。", "Customer enabled successfully.": "客戶已成功啟用。", "Customer updated successfully.": "客戶更新成功。", - "Customize Brand": "客製化品牌", + "Customize Brand": "自訂品牌樣式", "Customize Brand & Style": "客製化品牌樣式", + "Customize left-side main title, subtitle and right-side welcome description.": "自訂登入頁左側主副標題與右側歡迎語副標題。", "Cycle Efficiency": "週期效率", "Daily Revenue": "當日營收", "Daily operational variance curve for selected timeframe": "所選統計區間之每日營運變動曲線", @@ -465,6 +477,8 @@ "Date Range": "日期區間", "Day Before": "前日", "Days": "天", + "Default BG": "預設大背景", + "Default Card BG": "預設卡片背景", "Default Donate": "預設捐贈", "Default Not Donate": "預設不捐贈", "Default Temp Alert Limits": "預設溫度告警範圍", @@ -518,7 +532,7 @@ "Disable Pass Code": "停用通行碼", "Disable Product Confirmation": "停用商品確認", "Disable Warehouse": "停用倉庫", - "Disabled": "已停用", + "Disabled": "未啟用", "Disabled ambient temperature monitoring": "已停用環境溫度監測", "Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?": "停用此客戶將會連帶停用該客戶下的所有帳號,確定要繼續嗎?", "Discord Notifications": "Discord通知", @@ -548,6 +562,9 @@ "Dispensing in progress": "正在出貨中", "Display Material Code": "顯示物料代碼", "Displayed below \"Welcome Back\" on the login portal.": "顯示於登入入口「歡迎回來」下方。", + "Displayed below 'Welcome Back' on the login portal.": "顯示於登入入口「歡迎回來」下方。", + "Displayed below Logo on the left (defaults to customer name).": "顯示於左側 Logo 下方的主標題(未設定則預設帶入客戶名稱)。", + "Displayed below main title on the left.": "顯示於左側主標題下方的副標題/英文名稱。", "Displaying": "目前顯示", "Done": "已完成", "Door Closed": "機門已關閉", @@ -597,6 +614,7 @@ "Edit Slot": "編輯貨道", "Edit Staff Card": "編輯員工識別卡", "Edit Sub Account Role": "編輯子帳號角色", + "Edit System Settings": "編輯系統設定", "Edit Warehouse": "編輯倉庫", "Edit Welcome Gift": "編輯來店禮", "Effective Alert Types": "目前生效的告警類型", @@ -631,7 +649,9 @@ "Enable Material Code": "啟用物料編號", "Enable Points": "啟用點數規則", "Enable Points Mechanism": "啟用點數機制", + "Enable Status": "啟用狀態", "Enable and paste Webhook URL": "啟用並貼上 Webhook 網址", + "Enable first": "請先啟用", "Enabled": "已啟用", "Enabled Features": "啟用功能", "Enabled/Disabled": "啟用/停用", @@ -826,6 +846,7 @@ "Global roles accessible by all administrators.": "適用於所有管理者的全域角色。", "Go Authorize": "前往授權", "Go Back": "返回", + "Go Enable": "前往啟用", "Go to E-Invoice": "前往電子發票", "Goods & System Settings": "商品與系統設定", "Got it": "知道了", @@ -1088,6 +1109,7 @@ "Material Name": "素材名稱", "Material Type": "素材類型", "Max": "最大", + "Max 10MB": "最大 10MB", "Max 24 Hours": "最長 24 小時", "Max 3": "最多 3 張", "Max 50MB": "最大 50MB", @@ -1356,6 +1378,7 @@ "Out of Stock": "缺貨", "Out of stock.": "庫存不足。", "Output Count": "出貨次數", + "Overall Background Image": "登入頁大背景圖", "Overall Capacity": "總庫存量", "Owner": "公司名稱", "PARTNER_KEY": "PARTNER_KEY", @@ -1484,6 +1507,9 @@ "Prepared By": "經辦人", "Preparing": "備貨中", "Preview": "預覽", + "Preview Login Page": "預覽登入頁面", + "Preview Mode - Interactive Vending Portal Mockup": "預覽模式 - 客製化品牌登入頁面模擬", + "Preview Mode: Login functionality is disabled.": "預覽模式:不提供帳號登入功能。", "Previous": "上一頁", "Price / Member": "售價 / 會員價", "Pricing Information": "價格資訊", @@ -1919,7 +1945,9 @@ "Superseded by new command (Timeout)": "指令逾時 (1 分鐘未回報),已被新指令覆蓋。", "Support rod push error": "撐桿推出錯誤", "Support rod return error": "撐桿回位錯誤", - "Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.": "支援 JPEG, PNG, SVG (最大 2MB)。建議比例:橫式或正方形。", + "Supports JPEG, PNG, GIF, SVG, WebP (Max 10MB). Ideal proportion: Horizontal or Square.": "支援 JPEG、PNG、GIF、SVG、WebP 格式(最大 10MB)。建議比例:橫向或正方形。", + "Supports JPEG, PNG, GIF, SVG, WebP (Max 5MB). Ideal proportion: Horizontal or Square.": "支援 JPEG、PNG、GIF、SVG、WebP 格式(最大 5MB)。建議比例:橫向或正方形。", + "Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.": "支援 JPEG、PNG、SVG (最大 2MB)。建議比例:橫式或正方形。", "Survey Analysis": "問卷分析", "Sync Ads": "同步廣告", "Sync Products": "同步商品資料", @@ -1979,6 +2007,7 @@ "Test Connection": "測試連線", "Test notification sent successfully.": "測試通知發送成功。", "Testing": "測試", + "Text Titles Configuration": "登入頁標題自訂", "The Super Admin role cannot be deleted.": "超級管理員角色不可刪除。", "The Super Admin role is immutable.": "超級管理員角色不可修改。", "The Super Admin role name cannot be modified.": "超級管理員角色的名稱無法修改。", @@ -2111,6 +2140,7 @@ "Upload New Images": "上傳新照片", "Upload Time": "上傳時間", "Upload Video": "上傳影片", + "Upload corporate logo, login portal overall background and card left side image.": "上傳公司 Logo、登入頁大背景圖與左側卡片背景圖。", "Upload failed, please try again": "上傳失敗,請重試", "Upload file or provide URL": "上傳檔案或提供連結", "Uploading new images will replace all existing images.": "上傳新照片將會取代所有現有照片。", @@ -2137,6 +2167,7 @@ "Valid Ticket": "有效憑證", "Valid Until": "有效期限至", "Validation Error": "驗證錯誤", + "Validation failed. Please check the styling configurations.": "驗證失敗,請檢查品牌客製化樣式設定。", "Validity Period": "有效期限", "Validity Period (Days)": "有效期限 (天)", "Validity Period (Hours)": "有效期限 (小時)", @@ -2276,9 +2307,12 @@ "disabled": "停用", "e.g. 500ml / 300g": "例如:500ml / 300g", "e.g. 8.5 for 85折, 8 for 8折": "例如:8.5 代表 85折,8 代表 8折", + "e.g. AIoT Intelligent Management Platform": "例如:AIoT 自動化設備智慧營運平台", "e.g. John Doe": "例如:張曉明", "e.g. Main Warehouse": "如:總倉A", "e.g. New Guest 15% Off Discount": "例如:新客迎賓 85 折優惠", + "e.g. Please enter your account and password to enter the admin dashboard.": "例如:請輸入帳號密碼進入管理後台。", + "e.g. SMART DEVICE MANAGEMENT HUB": "例如:SMART DEVICE MANAGEMENT HUB", "e.g. TWSTAR": "例如:TWSTAR", "e.g. Taiwan Star": "例如:台灣之星", "e.g. Test Code for Maintenance": "例如:維修測試代碼", @@ -2428,6 +2462,5 @@ "video": "影片", "visit_gift": "來店禮", "vs Yesterday": "較昨日", - "warehouses": "倉庫管理", "warning": "警告" } diff --git a/resources/views/admin/companies/branding.blade.php b/resources/views/admin/companies/branding.blade.php new file mode 100644 index 0000000..b65fb5c --- /dev/null +++ b/resources/views/admin/companies/branding.blade.php @@ -0,0 +1,342 @@ +@extends('layouts.admin') + +@section('content') +@php + $logoUrl = !empty($company->settings['logo_path']) ? Storage::disk('public')->url($company->settings['logo_path']) : null; + $bgUrl = !empty($company->settings['login_bg_path']) ? Storage::disk('public')->url($company->settings['login_bg_path']) : null; + $cardBgUrl = !empty($company->settings['login_card_bg_path']) ? Storage::disk('public')->url($company->settings['login_card_bg_path']) : null; +@endphp + +
+ + +
+
+ + + + + +
+

{{ __('Brand Styling') }}

+
+
+
+ +
+
+ + + @if ($errors->any()) +
+
+ + + +
+

{{ __('Validation Error') }}

+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+
+
+ @endif + +
+ @csrf + + +
+
+
+ + + +
+
+

{{ $company->name }}

+

{{ __('Code') }}: {{ $company->code }}

+
+
+
+ + +
+
+
+ + + +
+
+

{{ __('Text Titles Configuration') }}

+

{{ __('Customize left-side main title, subtitle and right-side welcome description.') }}

+
+
+ +
+
+ +
+ + +

+ {{ __('Displayed below Logo on the left (defaults to customer name).') }} +

+
+ + +
+ + +

+ {{ __('Displayed below main title on the left.') }} +

+
+
+ + +
+ + +

+ {{ __('Displayed below "Welcome Back" on the login portal.') }} +

+
+
+
+ + +
+
+
+ + + +
+
+

{{ __('Branding Images & Backgrounds') }}

+

{{ __('Upload corporate logo, login portal overall background and card left side image.') }}

+
+
+ +
+ +
+ +
+ + + + + + + + +
+
+ + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+ +
+ + + + + + + + +
+
+
+
+
+ + +
+
+ @if(!empty($company->code)) + + + + + {{ __('Preview Login Page') }} + + @endif +
+
+ + {{ __('Cancel') }} + + +
+
+
+
+@endsection diff --git a/resources/views/admin/companies/index.blade.php b/resources/views/admin/companies/index.blade.php index f0374c1..55683b4 100644 --- a/resources/views/admin/companies/index.blade.php +++ b/resources/views/admin/companies/index.blade.php @@ -5,34 +5,35 @@ showModal: false, showHistoryModal: false, showSettingsModal: false, - showBrandingModal: false, editing: false, sidebarView: 'detail', activeTab: '{{ request('tab', 'list') }}', currentCompany: { - id: '', - name: '', - code: '', - original_type: 'lease', - current_type: 'lease', - tax_id: '', - contact_name: '', - contact_phone: '', - contact_email: '', - start_date: '', - end_date: '', - warranty_start_date: '', - warranty_end_date: '', - software_start_date: '', - software_end_date: '', - status: 1, - note: '', + id: '{{ $error_company ? $error_company->id : '' }}', + name: '{{ $error_company ? addslashes($error_company->name) : '' }}', + code: '{{ $error_company ? addslashes($error_company->code) : '' }}', + original_type: '{{ $error_company ? $error_company->original_type : 'lease' }}', + current_type: '{{ $error_company ? $error_company->current_type : 'lease' }}', + tax_id: '{{ $error_company ? addslashes($error_company->tax_id ?? '') : '' }}', + contact_name: '{{ $error_company ? addslashes($error_company->contact_name ?? '') : '' }}', + contact_phone: '{{ $error_company ? addslashes($error_company->contact_phone ?? '') : '' }}', + contact_email: '{{ $error_company ? addslashes($error_company->contact_email ?? '') : '' }}', + start_date: '{{ $error_company && $error_company->start_date ? (\Illuminate\Support\Carbon::parse($error_company->start_date)->format('Y-m-d')) : '' }}', + end_date: '{{ $error_company && $error_company->end_date ? (\Illuminate\Support\Carbon::parse($error_company->end_date)->format('Y-m-d')) : '' }}', + warranty_start_date: '{{ $error_company && $error_company->warranty_start_date ? (\Illuminate\Support\Carbon::parse($error_company->warranty_start_date)->format('Y-m-d')) : '' }}', + warranty_end_date: '{{ $error_company && $error_company->warranty_end_date ? (\Illuminate\Support\Carbon::parse($error_company->warranty_end_date)->format('Y-m-d')) : '' }}', + software_start_date: '{{ $error_company && $error_company->software_start_date ? (\Illuminate\Support\Carbon::parse($error_company->software_start_date)->format('Y-m-d')) : '' }}', + software_end_date: '{{ $error_company && $error_company->software_end_date ? (\Illuminate\Support\Carbon::parse($error_company->software_end_date)->format('Y-m-d')) : '' }}', + status: {{ $error_company ? $error_company->status : 1 }}, + note: '{{ $error_company ? addslashes($error_company->note ?? '') : '' }}', settings: { - enable_material_code: false, - enable_points: false, - enable_custom_branding: false, - logo_path: '', - login_title: '' + enable_material_code: {{ $error_company && ($error_company->settings['enable_material_code'] ?? false) ? 'true' : 'false' }}, + enable_points: {{ $error_company && ($error_company->settings['enable_points'] ?? false) ? 'true' : 'false' }}, + enable_custom_branding: {{ $error_company && ($error_company->settings['enable_custom_branding'] ?? false) ? 'true' : 'false' }}, + logo_path: '{{ $error_company ? ($error_company->settings['logo_path'] ?? '') : '' }}', + login_bg_path: '{{ $error_company ? ($error_company->settings['login_bg_path'] ?? '') : '' }}', + login_card_bg_path: '{{ $error_company ? ($error_company->settings['login_card_bg_path'] ?? '') : '' }}', + login_title: '{{ old('login_title', $error_company ? ($error_company->settings['login_title'] ?? '') : '') }}' } }, openCreateModal() { @@ -49,6 +50,8 @@ enable_points: false, enable_custom_branding: false, logo_path: '', + login_bg_path: '', + login_card_bg_path: '', login_title: '' } }; @@ -69,6 +72,8 @@ enable_points: company.settings?.enable_points || false, enable_custom_branding: company.settings?.enable_custom_branding || false, logo_path: company.settings?.logo_path || '', + login_bg_path: company.settings?.login_bg_path || '', + login_card_bg_path: company.settings?.login_card_bg_path || '', login_title: company.settings?.login_title || '' } }; @@ -140,21 +145,10 @@ enable_custom_branding: company.settings?.enable_custom_branding === true || company.settings?.enable_custom_branding === 1 || company.settings?.enable_custom_branding === '1' } }; + this.settingsAction = `{{ route('admin.permission.companies.index') }}/${company.id}/settings`; this.showSettingsModal = true; }, - openBrandingModal(company) { - this.currentCompany = { - ...company, - settings: { - enable_material_code: company.settings?.enable_material_code || false, - enable_points: company.settings?.enable_points || false, - enable_custom_branding: company.settings?.enable_custom_branding || false, - logo_path: company.settings?.logo_path || '', - login_title: company.settings?.login_title || '' - } - }; - this.showBrandingModal = true; - }, + submitConfirmedForm() { if (this.statusToggleSource === 'list') { this.$refs.statusToggleForm.submit(); @@ -679,6 +673,9 @@ @if($settings['enable_points'] ?? false) {{ __('Points') }} @endif + @if($settings['enable_custom_branding'] ?? false) + {{ __('Custom Branding') }} + @endif @endif @@ -737,7 +734,7 @@ {{ __('Customer Info') }} - {{ __('Authorized Status') }} + {{ __('Enable Status') }} {{ __('Current Brand Visual') }} {{ __('Actions') }} @@ -762,21 +759,30 @@ @if(filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN)) - {{ __('Authorized') }} + {{ __('Enabled') }} @else - {{ __('Unauthorized') }} + {{ __('Disabled') }} @endif
- @if(isset($settings['logo_path'])) - + @if(!empty($settings['logo_path'])) + + @else - {{ __('No Logo') }} +
+ + + +
@endif
@@ -789,16 +795,16 @@ @if(filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN)) - + @else @endif @@ -1149,7 +1155,7 @@
-
+ @csrf @method('PUT') @@ -1198,142 +1204,6 @@
- - -
-
- -
- -
-
-

{{ __('Customize Brand & Style') }}

-

-
- -
- -
- - @csrf - -
- -
- - - -
- -
- - - - - - - - -
- -
-
- - - - - - -
-

- {{ __('Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.') }} -

-
-
-
- - -
- - -

- {{ __('Displayed below "Welcome Back" on the login portal.') }} -

-
- - -
-
- -

{{ __('Enterprise Premium Features') }}

-
-
-
- -
{{ __('Locked') }}
-
-
- -
{{ __('Locked') }}
-
-
-
-
- -
- - -
- -
-
-
@@ -1471,7 +1341,6 @@ - @@ -1502,10 +1371,24 @@
- + +
+ + +
+
+
+ + + +
+ {{ __('Custom Branding') }} +
+
+
- diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 6af7ece..3b58f77 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -29,11 +29,23 @@ + + @if(isset($isPreview) && $isPreview) +
+ + {{ __('Preview Mode - Interactive Vending Portal Mockup') }} +
+ @endif + -
+
- -
+ + @if(isset($company) && !empty($company->settings['login_bg_path'])) +
+ @else +
+ @endif
@@ -41,7 +53,9 @@
-
+ @if(!isset($company) || empty($company->settings['login_bg_path'])) +
+ @endif
@@ -49,8 +63,12 @@
- -
+ + @if(isset($company) && !empty($company->settings['login_card_bg_path'])) +
+ @else +
+ @endif @@ -61,12 +79,12 @@ class="w-64 sm:w-72 md:w-80 h-auto max-h-36 object-contain drop-shadow-sm"> -
-

- {{ isset($company) ? $company->name : 'AIoT 自動化設備智慧營運平台' }} +
+

+ {{ (isset($company) && !empty($company->settings['login_main_title'])) ? $company->settings['login_main_title'] : (isset($company) ? $company->name : 'AIoT 自動化設備智慧營運平台') }}

-

- {{ isset($company) ? 'Smart Vending Portal' : 'Smart Device Management Hub' }} +

+ {{ (isset($company) && !empty($company->settings['login_sub_title'])) ? $company->settings['login_sub_title'] : (isset($company) ? 'Smart Vending Portal' : 'Smart Device Management Hub') }}

@@ -96,7 +114,7 @@
@endif -
+ @csrf diff --git a/routes/web.php b/routes/web.php index b7f68e9..ebcc80d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -21,6 +21,10 @@ Route::get('/', function () { return redirect()->route('login'); }); +// 客戶專屬首頁重導向與預覽路由 +Route::get('/c/{company_code}', [App\Http\Controllers\Auth\TenantLoginController::class, 'indexRedirect'])->name('tenant.index'); +Route::get('/c/{company_code}/preview', [App\Http\Controllers\Auth\TenantLoginController::class, 'preview'])->name('tenant.login.preview'); + // 客戶專屬登入頁面(公開,需 guest 中間件) Route::middleware('guest')->group(function () { Route::get('/c/{company_code}/login', [App\Http\Controllers\Auth\TenantLoginController::class, 'showLoginForm'])->name('tenant.login'); @@ -304,6 +308,7 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix Route::prefix('permission')->name('permission.')->group(function () { Route::patch('companies/{company}/toggle-status', [App\Http\Controllers\Admin\CompanyController::class, 'toggleStatus'])->name('companies.status.toggle')->middleware('can:menu.permissions.companies'); Route::put('companies/{company}/settings', [App\Http\Controllers\Admin\CompanyController::class, 'updateSettings'])->name('companies.settings.update')->middleware('can:menu.permissions.companies'); + Route::get('companies/{company}/branding', [App\Http\Controllers\Admin\CompanyController::class, 'editBranding'])->name('companies.branding.edit')->middleware('can:menu.permissions.companies'); Route::post('companies/{company}/branding', [App\Http\Controllers\Admin\CompanyController::class, 'updateBranding'])->name('companies.branding.update')->middleware('can:menu.permissions.companies'); Route::resource('companies', App\Http\Controllers\Admin\CompanyController::class)->except(['show', 'create', 'edit'])->middleware('can:menu.permissions.companies'); Route::get('/accounts', [App\Http\Controllers\Admin\PermissionController::class, 'accounts'])->name('accounts')->middleware('can:menu.permissions.accounts'); diff --git a/tests/Feature/Admin/CompanySettingsTest.php b/tests/Feature/Admin/CompanySettingsTest.php index 7197f0c..a45e395 100644 --- a/tests/Feature/Admin/CompanySettingsTest.php +++ b/tests/Feature/Admin/CompanySettingsTest.php @@ -26,7 +26,7 @@ class CompanySettingsTest extends TestCase public function test_can_update_company_settings() { - $company = Company::factory()->create(); + $company = Company::factory()->create(['original_type' => 'lease', 'current_type' => 'lease']); $settings = [ 'enable_material_code' => true, @@ -38,6 +38,7 @@ class CompanySettingsTest extends TestCase 'name' => 'Updated Name', 'code' => $company->code, 'status' => 1, + 'current_type' => 'lease', 'settings' => $settings, ]); @@ -48,7 +49,7 @@ class CompanySettingsTest extends TestCase ]); $updatedCompany = Company::find($company->id); - $this->assertEquals($settings, $updatedCompany->settings); + $this->assertEquals(array_merge($updatedCompany->settings ?? [], $settings), $updatedCompany->settings); } public function test_can_create_company_with_settings() @@ -62,6 +63,7 @@ class CompanySettingsTest extends TestCase ->post(route('admin.permission.companies.store'), [ 'name' => 'New Company', 'code' => 'NEW001', + 'original_type' => 'lease', 'status' => 1, 'settings' => $settings, ]); @@ -69,7 +71,7 @@ class CompanySettingsTest extends TestCase $response->assertRedirect(); $company = Company::where('code', 'NEW001')->first(); $this->assertNotNull($company); - $this->assertEquals($settings, $company->settings); + $this->assertEquals(array_merge($company->settings ?? [], $settings), $company->settings); } public function test_can_reuse_code_after_deletion() @@ -96,6 +98,7 @@ class CompanySettingsTest extends TestCase ->post(route('admin.permission.companies.store'), [ 'name' => 'Brand New Company', 'code' => $code, + 'original_type' => 'lease', 'status' => 1, ]); @@ -106,4 +109,369 @@ class CompanySettingsTest extends TestCase 'deleted_at' => null ]); } + + public function test_can_update_branding_settings() + { + $company = Company::factory()->create([ + 'settings' => ['enable_custom_branding' => true] + ]); + + $response = $this->actingAs($this->admin) + ->post(route('admin.permission.companies.branding.update', $company->id), [ + 'login_title' => 'Custom Welcome Title', + 'login_main_title' => 'Custom Left Main Title', + 'login_sub_title' => 'Custom Left Subtitle', + ]); + + $response->assertRedirect(route('admin.permission.companies.branding.edit', $company->id)); + + $company->refresh(); + $this->assertEquals('Custom Welcome Title', $company->settings['login_title']); + $this->assertEquals('Custom Left Main Title', $company->settings['login_main_title']); + $this->assertEquals('Custom Left Subtitle', $company->settings['login_sub_title']); + } + + public function test_cannot_update_branding_if_not_enabled() + { + $company = Company::factory()->create([ + 'settings' => ['enable_custom_branding' => false] + ]); + + $response = $this->actingAs($this->admin) + ->post(route('admin.permission.companies.branding.update', $company->id), [ + 'login_title' => 'Custom Welcome Title', + ]); + + $response->assertRedirect(); + $response->assertSessionHas('error'); + + $company->refresh(); + $this->assertArrayNotHasKey('login_title', $company->settings ?? []); + } + + public function test_companies_index_page_can_be_rendered() + { + $response = $this->actingAs($this->admin) + ->get(route('admin.permission.companies.index')); + + $response->assertOk(); + $response->assertViewIs('admin.companies.index'); + } + + public function test_can_upload_branding_logo() + { + \Illuminate\Support\Facades\Storage::fake('public'); + + $company = Company::factory()->create([ + 'settings' => ['enable_custom_branding' => true] + ]); + + $file = \Illuminate\Http\UploadedFile::fake()->image('logo.png'); + + $response = $this->actingAs($this->admin) + ->post(route('admin.permission.companies.branding.update', $company->id), [ + 'logo_file' => $file, + 'login_title' => 'Custom Title', + ]); + + $response->assertRedirect(); + + $company->refresh(); + $this->assertNotNull($company->settings['logo_path'] ?? null); + \Illuminate\Support\Facades\Storage::disk('public')->assertExists($company->settings['logo_path']); + } + + public function test_branding_validation_fails_redirects_with_errors_and_session() + { + $company = Company::factory()->create([ + 'settings' => ['enable_custom_branding' => true] + ]); + + // 上傳大於 10MB 的圖片來觸發驗證失敗 (10241 KB) + $file = \Illuminate\Http\UploadedFile::fake()->image('logo.png')->size(10241); + + $response = $this->actingAs($this->admin) + ->post(route('admin.permission.companies.branding.update', $company->id), [ + 'logo_file' => $file, + 'login_title' => str_repeat('a', 101), // 超過 100 字元限制 + ]); + + $response->assertRedirect(); + $response->assertSessionHasErrors(['logo_file', 'login_title']); + } + + public function test_can_upload_all_branding_images() + { + \Illuminate\Support\Facades\Storage::fake('public'); + + $company = Company::factory()->create([ + 'settings' => ['enable_custom_branding' => true] + ]); + + $logoFile = \Illuminate\Http\UploadedFile::fake()->image('logo.png'); + $bgFile = \Illuminate\Http\UploadedFile::fake()->image('bg.jpg'); + $cardBgFile = \Illuminate\Http\UploadedFile::fake()->image('card_bg.png'); + + $response = $this->actingAs($this->admin) + ->post(route('admin.permission.companies.branding.update', $company->id), [ + 'logo_file' => $logoFile, + 'login_bg_file' => $bgFile, + 'login_card_bg_file' => $cardBgFile, + 'login_title' => 'Branding Title', + ]); + + $response->assertRedirect(); + + $company->refresh(); + $this->assertNotNull($company->settings['logo_path'] ?? null); + $this->assertNotNull($company->settings['login_bg_path'] ?? null); + $this->assertNotNull($company->settings['login_card_bg_path'] ?? null); + + \Illuminate\Support\Facades\Storage::disk('public')->assertExists($company->settings['logo_path']); + \Illuminate\Support\Facades\Storage::disk('public')->assertExists($company->settings['login_bg_path']); + \Illuminate\Support\Facades\Storage::disk('public')->assertExists($company->settings['login_card_bg_path']); + } + + public function test_can_clear_all_branding_images() + { + \Illuminate\Support\Facades\Storage::fake('public'); + + // 先把檔案上傳並建立 + $logoPath = \Illuminate\Support\Facades\Storage::disk('public')->putFile('company_logos', \Illuminate\Http\UploadedFile::fake()->image('logo.png')); + $bgPath = \Illuminate\Support\Facades\Storage::disk('public')->putFile('company_backgrounds', \Illuminate\Http\UploadedFile::fake()->image('bg.jpg')); + $cardBgPath = \Illuminate\Support\Facades\Storage::disk('public')->putFile('company_backgrounds', \Illuminate\Http\UploadedFile::fake()->image('card_bg.png')); + + $company = Company::factory()->create([ + 'settings' => [ + 'enable_custom_branding' => true, + 'logo_path' => $logoPath, + 'login_bg_path' => $bgPath, + 'login_card_bg_path' => $cardBgPath, + 'login_title' => 'Old Title' + ] + ]); + + // 確保檔案原本存在 + \Illuminate\Support\Facades\Storage::disk('public')->assertExists($logoPath); + \Illuminate\Support\Facades\Storage::disk('public')->assertExists($bgPath); + \Illuminate\Support\Facades\Storage::disk('public')->assertExists($cardBgPath); + + // 呼叫清除 + $response = $this->actingAs($this->admin) + ->post(route('admin.permission.companies.branding.update', $company->id), [ + 'clear_logo' => '1', + 'clear_login_bg' => '1', + 'clear_login_card_bg' => '1', + 'login_title' => 'New Title' + ]); + + $response->assertRedirect(); + + $company->refresh(); + $this->assertNull($company->settings['logo_path'] ?? null); + $this->assertNull($company->settings['login_bg_path'] ?? null); + $this->assertNull($company->settings['login_card_bg_path'] ?? null); + + // 檔案應該已經被物理刪除 + \Illuminate\Support\Facades\Storage::disk('public')->assertMissing($logoPath); + \Illuminate\Support\Facades\Storage::disk('public')->assertMissing($bgPath); + \Illuminate\Support\Facades\Storage::disk('public')->assertMissing($cardBgPath); + } + + public function test_branding_validation_fails_for_all_files() + { + $company = Company::factory()->create([ + 'settings' => ['enable_custom_branding' => true] + ]); + + // 上傳大於 10MB 的大背景圖與卡片背景圖 (10241 KB) + $bgFile = \Illuminate\Http\UploadedFile::fake()->image('bg.jpg')->size(10241); + $cardBgFile = \Illuminate\Http\UploadedFile::fake()->image('card_bg.png')->size(10241); + + $response = $this->actingAs($this->admin) + ->post(route('admin.permission.companies.branding.update', $company->id), [ + 'login_bg_file' => $bgFile, + 'login_card_bg_file' => $cardBgFile, + ]); + + $response->assertRedirect(); + $response->assertSessionHasErrors(['login_bg_file', 'login_card_bg_file']); + } + + public function test_tenant_index_redirects_correctly_based_on_auth_status() + { + $company = Company::factory()->create([ + 'code' => 'test_redirect_comp', + 'status' => 1, + ]); + + // 未登入狀態:重導向至登入頁面 + $response = $this->get('/c/test_redirect_comp'); + $response->assertRedirect(route('tenant.login', 'test_redirect_comp')); + + // 已登入狀態:重導向至後台 dashboard + $response = $this->actingAs($this->admin)->get('/c/test_redirect_comp'); + $response->assertRedirect(route('admin.dashboard')); + } + + public function test_tenant_preview_page_can_be_rendered() + { + $company = Company::factory()->create([ + 'code' => 'test_preview_comp', + 'status' => 1, + 'settings' => ['enable_custom_branding' => true] + ]); + + // 不論是否登入,preview 頁面都應該能正常渲染 + $response = $this->actingAs($this->admin)->get('/c/test_preview_comp/preview'); + $response->assertOk(); + $response->assertViewIs('auth.login'); + $response->assertViewHas('company'); + $response->assertViewHas('isPreview', true); + } + + public function test_tenant_preview_page_redirects_if_not_enabled() + { + $company = Company::factory()->create([ + 'code' => 'test_disabled_preview', + 'status' => 1, + 'settings' => ['enable_custom_branding' => false] + ]); + + // 未開啟客製化品牌功能,導回一般登入 + $response = $this->actingAs($this->admin)->get('/c/test_disabled_preview/preview'); + $response->assertRedirect(route('login')); + } + + public function test_branding_edit_page_can_be_rendered() + { + $company = Company::factory()->create([ + 'settings' => ['enable_custom_branding' => true] + ]); + + $response = $this->actingAs($this->admin)->get(route('admin.permission.companies.branding.edit', $company->id)); + $response->assertOk(); + $response->assertViewIs('admin.companies.branding'); + $response->assertViewHas('company'); + } + + public function test_branding_edit_page_redirects_if_not_enabled() + { + $company = Company::factory()->create([ + 'settings' => ['enable_custom_branding' => false] + ]); + + $response = $this->actingAs($this->admin)->get(route('admin.permission.companies.branding.edit', $company->id)); + $response->assertRedirect(route('admin.permission.companies.index')); + $response->assertSessionHas('error'); + } + + public function test_tenant_can_login_successfully() + { + $company = Company::factory()->create([ + 'code' => 'tenant_a', + 'status' => 1, + 'settings' => ['enable_custom_branding' => true] + ]); + + $user = User::factory()->create([ + 'company_id' => $company->id, + 'username' => 'tenant_user_a', + 'password' => bcrypt('password123'), + 'status' => 1 + ]); + + $response = $this->post('/c/tenant_a/login', [ + 'username' => 'tenant_user_a', + 'password' => 'password123', + ]); + + $response->assertRedirect(route('admin.dashboard')); + $this->assertAuthenticatedAs($user); + } + + public function test_tenant_cannot_login_if_user_belongs_to_another_company() + { + $companyA = Company::factory()->create([ + 'code' => 'tenant_a', + 'status' => 1, + 'settings' => ['enable_custom_branding' => true] + ]); + + $companyB = Company::factory()->create([ + 'code' => 'tenant_b', + 'status' => 1, + 'settings' => ['enable_custom_branding' => true] + ]); + + // User B belongs to Company B + $userB = User::factory()->create([ + 'company_id' => $companyB->id, + 'username' => 'user_b', + 'password' => bcrypt('password123'), + 'status' => 1 + ]); + + // Attempting to login User B at Company A's portal + $response = $this->from('/c/tenant_a/login')->post('/c/tenant_a/login', [ + 'username' => 'user_b', + 'password' => 'password123', + ]); + + $response->assertRedirect('/c/tenant_a/login'); + $response->assertSessionHasErrors(['username']); + $this->assertGuest(); + } + + public function test_tenant_cannot_login_if_account_disabled() + { + $company = Company::factory()->create([ + 'code' => 'tenant_a', + 'status' => 1, + 'settings' => ['enable_custom_branding' => true] + ]); + + // User is disabled (status = 0) + $user = User::factory()->create([ + 'company_id' => $company->id, + 'username' => 'disabled_user', + 'password' => bcrypt('password123'), + 'status' => 0 + ]); + + $response = $this->from('/c/tenant_a/login')->post('/c/tenant_a/login', [ + 'username' => 'disabled_user', + 'password' => 'password123', + ]); + + $response->assertRedirect('/c/tenant_a/login'); + $response->assertSessionHasErrors(['username']); + $this->assertGuest(); + } + + public function test_tenant_cannot_login_with_incorrect_password() + { + $company = Company::factory()->create([ + 'code' => 'tenant_a', + 'status' => 1, + 'settings' => ['enable_custom_branding' => true] + ]); + + $user = User::factory()->create([ + 'company_id' => $company->id, + 'username' => 'tenant_user', + 'password' => bcrypt('password123'), + 'status' => 1 + ]); + + $response = $this->from('/c/tenant_a/login')->post('/c/tenant_a/login', [ + 'username' => 'tenant_user', + 'password' => 'wrongpassword', + ]); + + $response->assertRedirect('/c/tenant_a/login'); + $response->assertSessionHasErrors(['password']); + $this->assertGuest(); + } } +