[PROMOTE] 晉升 dev 變更至 demo 分支
1. 實作客戶自訂品牌樣式 (Custom Branding) 設定(支援 Logo、登入頁大背景、卡片背景上傳)。
2. 提供專屬登入頁面與預覽功能 (/c/{company_code}/preview),補全相關安全驗證邏輯與測試。
3. 優化 .gitea/workflows/deploy-prod.yaml 的 Docker 容器建置與健康檢查部署流程。
This commit is contained in:
commit
750108fa83
@ -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.'));
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
lang/en.json
35
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"
|
||||
}
|
||||
|
||||
35
lang/ja.json
35
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": "警告"
|
||||
}
|
||||
|
||||
@ -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": "警告"
|
||||
}
|
||||
|
||||
342
resources/views/admin/companies/branding.blade.php
Normal file
342
resources/views/admin/companies/branding.blade.php
Normal file
@ -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
|
||||
|
||||
<div class="space-y-6 pb-20"
|
||||
x-data="{
|
||||
logoPreview: @js($logoUrl),
|
||||
clearLogo: false,
|
||||
bgPreview: @js($bgUrl),
|
||||
clearBg: false,
|
||||
cardBgPreview: @js($cardBgUrl),
|
||||
clearCardBg: false
|
||||
}">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('admin.permission.companies.index') }}?tab=branding"
|
||||
class="p-2.5 rounded-xl bg-white dark:bg-slate-900 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200 transition-colors border border-slate-200/50 dark:border-slate-700/50 shadow-sm">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-slate-800 dark:text-white font-display tracking-tight">{{ __('Brand Styling') }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit" form="branding-form" class="btn-luxury-primary px-8 py-3 h-12">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
<span>{{ __('Save Changes') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Errors -->
|
||||
@if ($errors->any())
|
||||
<div class="luxury-card p-4 rounded-xl border-rose-500/20 bg-rose-500/5 sm:max-w-xl animate-luxury-in">
|
||||
<div class="flex gap-3">
|
||||
<svg class="w-5 h-5 text-rose-500 shrink-0 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
|
||||
</svg>
|
||||
<div class="space-y-1">
|
||||
<p class="text-sm font-black text-rose-500 uppercase tracking-widest">{{ __('Validation Error') }}</p>
|
||||
<ul class="text-xs font-bold text-rose-500/80 list-disc list-inside space-y-0.5">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form id="branding-form" action="{{ route('admin.permission.companies.branding.update', $company->id) }}" method="POST" enctype="multipart/form-data" class="space-y-8">
|
||||
@csrf
|
||||
|
||||
<!-- Card 1: Company Profile -->
|
||||
<div class="luxury-card p-8 rounded-[2.5rem] animate-luxury-in relative overflow-hidden" style="animation-delay: 50ms">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-2xl bg-emerald-500/10 text-emerald-500 flex items-center justify-center border border-emerald-500/20 shadow-sm shrink-0">
|
||||
<svg class="w-6 h-6 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-extrabold text-slate-800 dark:text-white tracking-tight">{{ $company->name }}</h3>
|
||||
<p class="text-xs font-mono font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Code') }}: {{ $company->code }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Text Title Configuration -->
|
||||
<div class="luxury-card p-8 rounded-[2.5rem] animate-luxury-in relative overflow-hidden" style="animation-delay: 100ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-indigo-500/10 text-indigo-500 flex items-center justify-center border border-indigo-500/20 shadow-sm shrink-0">
|
||||
<svg class="w-6 h-6 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-extrabold text-slate-800 dark:text-white tracking-tight">{{ __('Text Titles Configuration') }}</h3>
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Customize left-side main title, subtitle and right-side welcome description.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<!-- Left Side Main Title -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Custom Left Main Title') }}</label>
|
||||
<input type="text" name="login_main_title" value="{{ old('login_main_title', $company->settings['login_main_title'] ?? '') }}"
|
||||
class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g. AIoT Intelligent Management Platform') }}">
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider pl-1 mt-1">
|
||||
{{ __('Displayed below Logo on the left (defaults to customer name).') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Left Side Subtitle -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Custom Left Subtitle') }}</label>
|
||||
<input type="text" name="login_sub_title" value="{{ old('login_sub_title', $company->settings['login_sub_title'] ?? '') }}"
|
||||
class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g. SMART DEVICE MANAGEMENT HUB') }}">
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider pl-1 mt-1">
|
||||
{{ __('Displayed below main title on the left.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side Form Welcome Subtitle -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Custom Welcome Subtitle') }}</label>
|
||||
<input type="text" name="login_title" value="{{ old('login_title', $company->settings['login_title'] ?? '') }}"
|
||||
class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g. Please enter your account and password to enter the admin dashboard.') }}">
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider pl-1 mt-1">
|
||||
{{ __('Displayed below "Welcome Back" on the login portal.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Branding Images & Backgrounds -->
|
||||
<div class="luxury-card p-8 rounded-[2.5rem] animate-luxury-in relative overflow-hidden" style="animation-delay: 150ms">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<div class="w-12 h-12 rounded-2xl bg-cyan-500/10 text-cyan-500 flex items-center justify-center border border-cyan-500/20 shadow-sm shrink-0">
|
||||
<svg class="w-6 h-6 stroke-[2]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-extrabold text-slate-800 dark:text-white tracking-tight">{{ __('Branding Images & Backgrounds') }}</h3>
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-widest mt-0.5">{{ __('Upload corporate logo, login portal overall background and card left side image.') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Company Logo -->
|
||||
<div class="space-y-3">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Upload Company Logo') }}</label>
|
||||
<div class="relative group w-40 h-40">
|
||||
<input type="hidden" name="clear_logo" :value="clearLogo ? '1' : '0'">
|
||||
|
||||
<!-- Empty Logo / Upload Trigger -->
|
||||
<template x-if="!logoPreview">
|
||||
<div @click="$refs.logoInput.click()"
|
||||
class="w-full h-full rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 flex flex-col items-center justify-center gap-2.5 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-cyan-500/50 transition-all duration-300 group">
|
||||
<div class="p-2.5 rounded-2xl bg-white dark:bg-slate-800 shadow-sm border border-slate-100 dark:border-slate-700 group-hover:scale-110 group-hover:text-cyan-500 transition-all duration-300">
|
||||
<svg class="w-6 h-6 text-slate-400 dark:text-slate-500 group-hover:text-cyan-500 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center px-2">
|
||||
<p class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Click to upload') }}</p>
|
||||
<p class="text-[8px] text-slate-400 dark:text-slate-500 mt-0.5 uppercase">Max 10MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Active Image Preview & Hover Actions -->
|
||||
<template x-if="logoPreview">
|
||||
<div class="relative w-full h-full rounded-3xl overflow-hidden border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-xl group/image flex items-center justify-center p-3">
|
||||
<img :src="logoPreview" class="max-w-full max-h-full object-contain">
|
||||
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
|
||||
<button type="button" @click="$refs.logoInput.click()"
|
||||
class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-cyan-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click="logoPreview = null; clearLogo = true; $refs.logoInput.value = '';"
|
||||
class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-rose-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<input type="file" name="logo_file" x-ref="logoInput" class="hidden" accept="image/*" @change="
|
||||
const file = $event.target.files[0];
|
||||
if (file) {
|
||||
clearLogo = false;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => { logoPreview = e.target.result; };
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Images Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 pt-4">
|
||||
<!-- Overall Background -->
|
||||
<div class="space-y-3">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Overall Background Image') }}</label>
|
||||
<div class="relative group aspect-video rounded-[2rem] overflow-hidden border border-slate-200 dark:border-slate-800 shadow-sm bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<input type="hidden" name="clear_login_bg" :value="clearBg ? '1' : '0'">
|
||||
|
||||
<!-- Empty overall BG -->
|
||||
<template x-if="!bgPreview">
|
||||
<div @click="$refs.bgInput.click()"
|
||||
class="w-full h-full flex flex-col items-center justify-center gap-3 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-cyan-500/50 border-2 border-dashed border-slate-200 dark:border-slate-800 rounded-[2rem] transition-all duration-300 group">
|
||||
<div class="p-3 rounded-2xl bg-white dark:bg-slate-800 shadow-sm border border-slate-100 dark:border-slate-700 group-hover:scale-110 group-hover:text-cyan-500 transition-all duration-300">
|
||||
<svg class="w-7 h-7 text-slate-400 dark:text-slate-500 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center px-4">
|
||||
<p class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Click to upload') }}</p>
|
||||
<p class="text-[10px] text-slate-400 dark:text-slate-500 mt-1">{{ __('1920x1080 (Max 10MB)') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- BG Image & Hover mask -->
|
||||
<template x-if="bgPreview">
|
||||
<div class="relative w-full h-full group/image bg-white dark:bg-slate-900">
|
||||
<img :src="bgPreview" class="w-full h-full object-cover animate-luxury-in">
|
||||
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
|
||||
<button type="button" @click="$refs.bgInput.click()"
|
||||
class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-cyan-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click="bgPreview = null; clearBg = true; $refs.bgInput.value = '';"
|
||||
class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-rose-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<input type="file" name="login_bg_file" x-ref="bgInput" class="hidden" accept="image/*" @change="
|
||||
const file = $event.target.files[0];
|
||||
if (file) {
|
||||
clearBg = false;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => { bgPreview = e.target.result; };
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Left Side Background -->
|
||||
<div class="space-y-3">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Card Side Background') }}</label>
|
||||
<div class="relative group aspect-video rounded-[2rem] overflow-hidden border border-slate-200 dark:border-slate-800 shadow-sm bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<input type="hidden" name="clear_login_card_bg" :value="clearCardBg ? '1' : '0'">
|
||||
|
||||
<!-- Empty Card BG -->
|
||||
<template x-if="!cardBgPreview">
|
||||
<div @click="$refs.cardBgInput.click()"
|
||||
class="w-full h-full flex flex-col items-center justify-center gap-3 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800/80 hover:border-cyan-500/50 border-2 border-dashed border-slate-200 dark:border-slate-800 rounded-[2rem] transition-all duration-300 group">
|
||||
<div class="p-3 rounded-2xl bg-white dark:bg-slate-800 shadow-sm border border-slate-100 dark:border-slate-700 group-hover:scale-110 group-hover:text-cyan-500 transition-all duration-300">
|
||||
<svg class="w-7 h-7 text-slate-400 dark:text-slate-500 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-center px-4">
|
||||
<p class="text-xs font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest">{{ __('Click to upload') }}</p>
|
||||
<p class="text-[10px] text-slate-400 dark:text-slate-500 mt-1">{{ __('Aspect Ratio 3:4 (Max 10MB)') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Card BG Image & Hover mask -->
|
||||
<template x-if="cardBgPreview">
|
||||
<div class="relative w-full h-full group/image bg-white dark:bg-slate-900">
|
||||
<img :src="cardBgPreview" class="w-full h-full object-cover animate-luxury-in">
|
||||
<div class="absolute inset-0 bg-slate-950/40 opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 flex items-center justify-center gap-3">
|
||||
<button type="button" @click="$refs.cardBgInput.click()"
|
||||
class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-cyan-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click="cardBgPreview = null; clearCardBg = true; $refs.cardBgInput.value = '';"
|
||||
class="p-3 rounded-2xl bg-white text-slate-800 hover:bg-rose-500 hover:text-white transition-all duration-300 shadow-lg">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<input type="file" name="login_card_bg_file" x-ref="cardBgInput" class="hidden" accept="image/*" @change="
|
||||
const file = $event.target.files[0];
|
||||
if (file) {
|
||||
clearCardBg = false;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => { cardBgPreview = e.target.result; };
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Bar (Footer) -->
|
||||
<div class="pt-8 flex items-center justify-between border-t border-slate-100 dark:border-slate-800 animate-luxury-in" style="animation-delay: 200ms">
|
||||
<div>
|
||||
@if(!empty($company->code))
|
||||
<a href="{{ route('tenant.login.preview', $company->code) }}" target="_blank"
|
||||
class="btn-luxury-secondary flex items-center gap-1.5 px-6 py-3 h-12 text-xs font-black tracking-widest uppercase shadow-lg shadow-slate-100 dark:shadow-none hover:shadow-cyan-500/10 hover:border-cyan-500/30 hover:text-cyan-600 dark:hover:text-cyan-400 transition-all">
|
||||
<svg class="w-4 h-4 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
<span>{{ __('Preview Login Page') }}</span>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ route('admin.permission.companies.index') }}?tab=branding"
|
||||
class="btn-luxury-ghost px-8 h-12 flex items-center justify-center transition-all">
|
||||
{{ __('Cancel') }}
|
||||
</a>
|
||||
<button type="submit" class="btn-luxury-primary px-12 h-14 shadow-xl shadow-cyan-500/20 transition-all">
|
||||
<svg class="w-5 h-5 stroke-[2.5]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
<span>{{ __('Save Changes') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@endsection
|
||||
@ -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)
|
||||
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Points') }}</span>
|
||||
@endif
|
||||
@if($settings['enable_custom_branding'] ?? false)
|
||||
<span class="px-2.5 py-1 rounded-lg bg-cyan-500/10 text-cyan-500 dark:text-cyan-400 text-[13px] font-bold uppercase tracking-wider">{{ __('Custom Branding') }}</span>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
@ -737,7 +734,7 @@
|
||||
<thead>
|
||||
<tr class="bg-slate-50/50 dark:bg-slate-900/30">
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Customer Info') }}</th>
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Authorized Status') }}</th>
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-center">{{ __('Enable Status') }}</th>
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800">{{ __('Current Brand Visual') }}</th>
|
||||
<th class="px-6 py-4 text-[12px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest border-b border-slate-100 dark:border-slate-800 text-right">{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
@ -762,21 +759,30 @@
|
||||
<td class="px-6 py-6 text-center">
|
||||
@if(filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN))
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black bg-emerald-500/10 text-emerald-500 border border-emerald-500/20 tracking-widest uppercase">
|
||||
{{ __('Authorized') }}
|
||||
{{ __('Enabled') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-black bg-slate-100 dark:bg-slate-800 text-slate-400 tracking-widest uppercase">
|
||||
{{ __('Unauthorized') }}
|
||||
{{ __('Disabled') }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-slate-50 dark:bg-slate-800/80 border border-slate-200/50 dark:border-slate-700/50 flex items-center justify-center overflow-hidden shrink-0">
|
||||
@if(isset($settings['logo_path']))
|
||||
<img src="{{ Storage::disk('public')->url($settings['logo_path']) }}" class="max-w-full max-h-full object-contain">
|
||||
@if(!empty($settings['logo_path']))
|
||||
<img src="{{ Storage::disk('public')->url($settings['logo_path']) }}" class="max-w-full max-h-full object-contain" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||||
<div class="hidden w-full h-full items-center justify-center bg-slate-50 dark:bg-slate-800/50">
|
||||
<svg class="w-5 h-5 text-slate-400 dark:text-slate-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase">{{ __('No Logo') }}</span>
|
||||
<div class="flex w-full h-full items-center justify-center bg-slate-50/50 dark:bg-slate-900/30">
|
||||
<svg class="w-5 h-5 text-slate-400 dark:text-slate-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
@ -789,16 +795,16 @@
|
||||
</td>
|
||||
<td class="px-6 py-6 text-right">
|
||||
@if(filter_var($settings['enable_custom_branding'] ?? false, FILTER_VALIDATE_BOOLEAN))
|
||||
<button type="button" @click='openBrandingModal({{ json_encode($company) }})'
|
||||
class="px-4 py-2 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 text-xs font-black tracking-widest uppercase shadow-lg shadow-cyan-500/10 active:scale-95 transition-all"
|
||||
<a href="{{ route('admin.permission.companies.branding.edit', $company->id) }}"
|
||||
class="px-4 py-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 text-xs font-black tracking-widest uppercase shadow-lg shadow-cyan-500/10 active:scale-95 transition-all inline-block"
|
||||
title="{{ __('Customize Brand') }}">
|
||||
{{ __('Setup Brand') }}
|
||||
</button>
|
||||
</a>
|
||||
@else
|
||||
<button type="button" @click='openSettingsModal({{ json_encode($company) }})'
|
||||
class="px-4 py-2 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 hover:text-slate-600 text-xs font-black tracking-widest uppercase transition-all"
|
||||
title="{{ __('Authorize first') }}">
|
||||
{{ __('Go Authorize') }}
|
||||
title="{{ __('Enable first') }}">
|
||||
{{ __('Go Enable') }}
|
||||
</button>
|
||||
@endif
|
||||
</td>
|
||||
@ -1149,7 +1155,7 @@
|
||||
</div>
|
||||
|
||||
<div class="p-8 overflow-y-auto custom-scrollbar flex-1">
|
||||
<form :action="`{{ route('admin.permission.companies.index') }}/${currentCompany.id}/settings`" method="POST">
|
||||
<form :action="settingsAction" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
@ -1198,142 +1204,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brand Styling Edit Modal -->
|
||||
<div x-show="showBrandingModal" class="fixed inset-0 z-[100] flex items-center justify-center" x-cloak>
|
||||
<div x-show="showBrandingModal"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 bg-slate-900/40 backdrop-blur-sm" @click="showBrandingModal = false"></div>
|
||||
|
||||
<div x-show="showBrandingModal"
|
||||
x-transition:enter="ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
class="relative bg-white dark:bg-slate-900 rounded-3xl shadow-2xl border border-slate-100 dark:border-slate-800 w-full max-w-2xl mx-4 overflow-hidden z-[101] flex flex-col max-h-[90vh]">
|
||||
|
||||
<div class="px-8 py-6 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50 dark:bg-slate-900/50">
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Customize Brand & Style') }}</h3>
|
||||
<p class="text-xs font-bold text-slate-400 mt-1" x-text="currentCompany.name"></p>
|
||||
</div>
|
||||
<button type="button" @click="showBrandingModal = false" class="text-slate-400 hover:text-slate-500 bg-white dark:bg-slate-800 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-xl p-2 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-8 overflow-y-auto custom-scrollbar flex-1">
|
||||
<form :action="`{{ route('admin.permission.companies.index') }}/${currentCompany.id}/branding`" method="POST" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Logo Upload Section -->
|
||||
<div class="space-y-2" x-data="{ logoPreview: null, clearLogo: false }">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Upload Company Logo') }}</label>
|
||||
|
||||
<!-- Visual Logo Preview / Upload Area -->
|
||||
<div class="flex flex-col sm:flex-row items-center gap-6 p-6 rounded-2xl bg-slate-50 dark:bg-slate-800/40 border border-slate-100 dark:border-slate-800/80">
|
||||
<!-- Preview Window -->
|
||||
<div class="w-24 h-24 rounded-2xl bg-white dark:bg-slate-900 border border-slate-200/50 dark:border-slate-700/50 flex items-center justify-center overflow-hidden shrink-0 shadow-sm relative group">
|
||||
<!-- DB Existing Logo -->
|
||||
<template x-if="currentCompany.settings.logo_path && !logoPreview && !clearLogo">
|
||||
<img :src="`{{ Storage::disk('public')->url('') }}${currentCompany.settings.logo_path}`" class="max-w-full max-h-full object-contain">
|
||||
</template>
|
||||
|
||||
<!-- Upload New Preview -->
|
||||
<template x-if="logoPreview">
|
||||
<img :src="logoPreview" class="max-w-full max-h-full object-contain">
|
||||
</template>
|
||||
|
||||
<!-- Empty / Cleared Fallback -->
|
||||
<template x-if="(!currentCompany.settings.logo_path || clearLogo) && !logoPreview">
|
||||
<span class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase">{{ __('No Logo') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 w-full space-y-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<label class="px-4 py-2.5 rounded-xl bg-cyan-500 text-white hover:bg-cyan-600 text-xs font-black tracking-widest uppercase cursor-pointer shadow-lg shadow-cyan-500/10 active:scale-95 transition-all text-center">
|
||||
{{ __('Choose File') }}
|
||||
<input type="file" name="logo_file" class="hidden" accept="image/*" @change="
|
||||
const file = $event.target.files[0];
|
||||
if (file) {
|
||||
clearLogo = false;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => { logoPreview = e.target.result; };
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
">
|
||||
</label>
|
||||
|
||||
<!-- Clear Logo Button -->
|
||||
<template x-if="currentCompany.settings.logo_path || logoPreview">
|
||||
<button type="button" @click="
|
||||
logoPreview = null;
|
||||
clearLogo = true;
|
||||
$el.form.querySelector('input[name=logo_file]').value = '';
|
||||
" class="px-4 py-2.5 rounded-xl bg-slate-100 dark:bg-slate-800 text-slate-400 dark:text-slate-500 hover:text-rose-500 hover:bg-rose-500/5 text-xs font-black tracking-widest uppercase transition-all">
|
||||
{{ __('Clear Logo') }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<input type="hidden" name="clear_logo" :value="clearLogo ? '1' : '0'">
|
||||
</div>
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
|
||||
{{ __('Supports JPEG, PNG, SVG (Max 2MB). Ideal proportion: Horizontal or Square.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Welcome Title Section -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-black text-slate-500 uppercase tracking-widest pl-1">{{ __('Custom Welcome Subtitle') }}</label>
|
||||
<input type="text" name="login_title" x-model="currentCompany.settings.login_title" class="luxury-input w-full"
|
||||
placeholder="{{ __('e.g. Please enter your account and password to enter the admin dashboard.') }}">
|
||||
<p class="text-[10px] font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider pl-1 mt-1">
|
||||
{{ __('Displayed below "Welcome Back" on the login portal.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Future Expansion Area (Pre-designed, styled with luxury disable) -->
|
||||
<div class="p-6 rounded-2xl border border-dashed border-slate-200 dark:border-slate-800/80 bg-slate-50/30 dark:bg-slate-900/10 space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-cyan-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<h4 class="text-xs font-black text-slate-700 dark:text-slate-300 uppercase tracking-widest">{{ __('Enterprise Premium Features') }}</h4>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 opacity-50 select-none">
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Custom Background Image') }}</label>
|
||||
<div class="py-2.5 px-4 rounded-xl bg-slate-100 dark:bg-slate-800 border border-slate-200/50 dark:border-slate-700/50 text-xs font-bold text-slate-400">{{ __('Locked') }}</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-1">{{ __('Primary Theme Color') }}</label>
|
||||
<div class="py-2.5 px-4 rounded-xl bg-slate-100 dark:bg-slate-800 border border-slate-200/50 dark:border-slate-700/50 text-xs font-bold text-slate-400">{{ __('Locked') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-x-4 pt-8 mt-6 border-t border-slate-100 dark:border-slate-800">
|
||||
<button type="button" @click="showBrandingModal = false" class="btn-luxury-ghost px-8">{{ __('Cancel') }}</button>
|
||||
<button type="submit" class="btn-luxury-primary px-12">
|
||||
<span>{{ __('Save Changes') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-delete-confirm-modal :message="__('Are you sure to delete this customer?')" />
|
||||
<x-status-confirm-modal :title="__('Disable Customer Confirmation')" :message="__('Disabling this customer will also disable all accounts under this customer. Are you sure you want to continue?')" />
|
||||
@ -1471,7 +1341,6 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1502,10 +1371,24 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
|
||||
:class="detailCompany.settings?.enable_points ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
|
||||
<span x-text="detailCompany.settings?.enable_points ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
|
||||
<span x-text="detailCompany.settings?.enable_points ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Custom Branding -->
|
||||
<div class="bg-white dark:bg-slate-800/20 p-4 rounded-2xl border border-slate-100 dark:border-slate-800/50 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-cyan-50 dark:bg-cyan-500/10 text-cyan-500">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-1.242 2.25 2.25 0 00-.22-2.131zm0 0a3 3 0 015.78-1.128 2.25 2.25 0 012.4-2.245 4.5 4.5 0 00-8.4 1.242 2.25 2.25 0 00.22 2.13z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-bold text-slate-700 dark:text-slate-300">{{ __('Custom Branding') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-black uppercase tracking-widest"
|
||||
:class="detailCompany.settings?.enable_custom_branding ? 'bg-emerald-500/10 text-emerald-500' : 'bg-slate-100 dark:bg-slate-800 text-slate-400'">
|
||||
<span x-text="detailCompany.settings?.enable_custom_branding ? '{{ __('Enabled') }}' : '{{ __('Disabled') }}'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Electronic Invoice, Card System, etc. moved to Machine settings -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@ -29,11 +29,23 @@
|
||||
</head>
|
||||
<body class="font-sans antialiased bg-[#070B14]">
|
||||
|
||||
<!-- 預覽模式高階提示橫條 -->
|
||||
@if(isset($isPreview) && $isPreview)
|
||||
<div class="fixed top-0 inset-x-0 bg-cyan-600/90 backdrop-blur-md text-white text-center py-2.5 px-4 text-xs font-black tracking-widest uppercase z-50 shadow-md flex items-center justify-center gap-2 select-none">
|
||||
<svg class="w-4 h-4 animate-pulse text-cyan-200" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||
<span>{{ __('Preview Mode - Interactive Vending Portal Mockup') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 全局科技星雲底圖 Canvas -->
|
||||
<div class="min-h-screen w-full flex items-center justify-center p-4 sm:p-6 md:p-10 relative overflow-hidden bg-[#070B14]">
|
||||
<div class="min-h-screen w-full flex items-center justify-center p-4 sm:p-6 md:p-10 relative overflow-hidden bg-[#070B14] {{ (isset($isPreview) && $isPreview) ? 'pt-16 sm:pt-20' : '' }}">
|
||||
|
||||
<!-- 精美淡淡的科技不規則線條背景圖 (低透明度,優雅不花俏) -->
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-[0.18] pointer-events-none mix-blend-lighten" style="background-image: url('{{ asset('tech_lines_bg.png') }}');"></div>
|
||||
<!-- 精美淡淡的科技不規則線條背景圖 (低透明度,優雅不花俏) / 自訂品牌大背景圖 -->
|
||||
@if(isset($company) && !empty($company->settings['login_bg_path']))
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-85 pointer-events-none" style="background-image: url('{{ Storage::disk('public')->url($company->settings['login_bg_path']) }}');"></div>
|
||||
@else
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-[0.18] pointer-events-none mix-blend-lighten" style="background-image: url('{{ asset('tech_lines_bg.png') }}');"></div>
|
||||
@endif
|
||||
|
||||
<!-- 霓虹光暈 (Radial Glow) -->
|
||||
<div class="absolute top-[-10%] right-[-10%] w-[500px] h-[500px] rounded-full bg-indigo-600/10 blur-[120px] pointer-events-none"></div>
|
||||
@ -41,7 +53,9 @@
|
||||
<div class="absolute top-[30%] left-[20%] w-[350px] h-[350px] rounded-full bg-cyan-600/5 blur-[100px] pointer-events-none"></div>
|
||||
|
||||
<!-- 科技感精密網格線 (Grid Overlay) -->
|
||||
<div class="absolute inset-0 bg-[linear-gradient(to_right,#1e293b_1px,transparent_1px),linear-gradient(to_bottom,#1e293b_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] opacity-[0.08] pointer-events-none"></div>
|
||||
@if(!isset($company) || empty($company->settings['login_bg_path']))
|
||||
<div class="absolute inset-0 bg-[linear-gradient(to_right,#1e293b_1px,transparent_1px),linear-gradient(to_bottom,#1e293b_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)] opacity-[0.08] pointer-events-none"></div>
|
||||
@endif
|
||||
|
||||
<!-- 懸浮珍珠白高奢卡片 (Unified Card - Option B: Solid Pristine White) -->
|
||||
<div class="w-full max-w-4xl bg-white rounded-[2.5rem] shadow-[0_30px_75px_-15px_rgba(0,0,0,0.65)] flex flex-col md:flex-row overflow-hidden relative z-10 animate-luxury-in">
|
||||
@ -49,8 +63,12 @@
|
||||
<!-- 左側品牌區 (Branding Area - Solid Premium Slate) -->
|
||||
<div class="w-full md:w-1/2 bg-[#F8FAFC] border-b md:border-b-0 md:border-r border-slate-100 flex flex-col justify-center items-center p-8 lg:p-12 relative overflow-hidden shrink-0 select-none min-h-[320px] md:min-h-0">
|
||||
|
||||
<!-- 精美淡淡的智慧販賣機背景圖 (浮水印效果 - 提升透明度) -->
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-[0.20] pointer-events-none mix-blend-multiply" style="background-image: url('{{ asset('tech_vending_bg.png') }}');"></div>
|
||||
<!-- 精美淡淡的智慧販賣機背景圖 / 自訂卡片左側背景圖 -->
|
||||
@if(isset($company) && !empty($company->settings['login_card_bg_path']))
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-90 pointer-events-none" style="background-image: url('{{ Storage::disk('public')->url($company->settings['login_card_bg_path']) }}');"></div>
|
||||
@else
|
||||
<div class="absolute inset-0 bg-cover bg-center opacity-[0.20] pointer-events-none mix-blend-multiply" style="background-image: url('{{ asset('tech_vending_bg.png') }}');"></div>
|
||||
@endif
|
||||
|
||||
|
||||
|
||||
@ -61,12 +79,12 @@
|
||||
class="w-64 sm:w-72 md:w-80 h-auto max-h-36 object-contain drop-shadow-sm">
|
||||
|
||||
<!-- 平台名稱 -->
|
||||
<div class="text-center">
|
||||
<h2 class="text-xl sm:text-2xl font-black text-slate-800 tracking-tight font-display">
|
||||
{{ isset($company) ? $company->name : 'AIoT 自動化設備智慧營運平台' }}
|
||||
<div class="text-center px-4">
|
||||
<h2 class="text-xl sm:text-2xl font-black text-slate-800 tracking-tight font-display break-words">
|
||||
{{ (isset($company) && !empty($company->settings['login_main_title'])) ? $company->settings['login_main_title'] : (isset($company) ? $company->name : 'AIoT 自動化設備智慧營運平台') }}
|
||||
</h2>
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-2 font-mono">
|
||||
{{ isset($company) ? 'Smart Vending Portal' : 'Smart Device Management Hub' }}
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] mt-2 font-mono break-all">
|
||||
{{ (isset($company) && !empty($company->settings['login_sub_title'])) ? $company->settings['login_sub_title'] : (isset($company) ? 'Smart Vending Portal' : 'Smart Device Management Hub') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -96,7 +114,7 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ isset($company) ? route('tenant.login.post', $company->code) : route('login') }}" class="space-y-6">
|
||||
<form method="POST" action="{{ (isset($isPreview) && $isPreview) ? '#' : (isset($company) ? route('tenant.login.post', $company->code) : route('login')) }}" onsubmit="if ({{ (isset($isPreview) && $isPreview) ? 'true' : 'false' }}) { alert('{{ __('Preview Mode: Login functionality is disabled.') }}'); return false; }" class="space-y-6">
|
||||
@csrf
|
||||
|
||||
<!-- 帳號輸入框 -->
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user