[FEAT] 優化後端帳號權限邏輯、開發商品管理功能及聯絡資訊 UI 改版
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m2s

This commit is contained in:
2026-03-27 13:43:08 +08:00
parent 8ec5473ec7
commit 740eaa30b7
22 changed files with 1783 additions and 615 deletions

View File

@@ -55,6 +55,7 @@ class CompanyController extends Controller
'valid_until' => 'nullable|date',
'status' => 'required|boolean',
'note' => 'nullable|string',
'settings' => 'nullable|array',
// 帳號相關欄位 (可選)
'admin_username' => 'nullable|string|max:255|unique:users,username',
'admin_password' => 'nullable|string|min:8',
@@ -62,6 +63,12 @@ class CompanyController extends Controller
'admin_role' => 'nullable|string|exists:roles,name',
]);
// 確保 settings 中的值為布林值
if (isset($validated['settings'])) {
$validated['settings']['enable_material_code'] = filter_var($validated['settings']['enable_material_code'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['enable_points'] = filter_var($validated['settings']['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN);
}
DB::transaction(function () use ($validated) {
$company = Company::create([
'name' => $validated['name'],
@@ -73,6 +80,7 @@ class CompanyController extends Controller
'valid_until' => $validated['valid_until'] ?? null,
'status' => $validated['status'],
'note' => $validated['note'] ?? null,
'settings' => $validated['settings'] ?? [],
]);
// 如果有填寫帳號資訊,則建立管理員帳號
@@ -130,8 +138,15 @@ class CompanyController extends Controller
'valid_until' => 'nullable|date',
'status' => 'required|boolean',
'note' => 'nullable|string',
'settings' => 'nullable|array',
]);
// 確保 settings 中的值為布林值,避免 JSON 存儲為字串導致前端判斷錯誤
if (isset($validated['settings'])) {
$validated['settings']['enable_material_code'] = filter_var($validated['settings']['enable_material_code'] ?? false, FILTER_VALIDATE_BOOLEAN);
$validated['settings']['enable_points'] = filter_var($validated['settings']['enable_points'] ?? false, FILTER_VALIDATE_BOOLEAN);
}
$company->update($validated);
// 分支邏輯:若停用客戶,連帶停用其所有帳號
@@ -168,6 +183,11 @@ class CompanyController extends Controller
return redirect()->back()->with('error', __('Cannot delete company with active accounts.'));
}
// 為了解決軟刪除導致的唯一索引佔用問題,刪除前先重新命名唯一欄位
$timestamp = now()->getTimestamp();
$company->code = $company->code . '.deleted.' . $timestamp;
$company->save();
$company->delete();
return redirect()->back()->with('success', __('Customer deleted successfully.'));

View File

@@ -377,8 +377,8 @@ class PermissionController extends Controller
{
$user = \App\Models\System\User::findOrFail($id);
if ($user->hasRole('super-admin')) {
return redirect()->back()->with('error', __('System super admin accounts cannot be modified via this interface.'));
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
return redirect()->back()->with('error', __('System super admin accounts can only be modified by other super admins.'));
}
$validated = $request->validate([
@@ -485,8 +485,8 @@ class PermissionController extends Controller
{
$user = \App\Models\System\User::findOrFail($id);
if ($user->hasRole('super-admin')) {
return redirect()->back()->with('error', __('System super admin accounts cannot be deleted.'));
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
return redirect()->back()->with('error', __('System super admin accounts can only be deleted by other super admins.'));
}
if ($user->id === auth()->id()) {
@@ -508,9 +508,9 @@ class PermissionController extends Controller
{
$user = \App\Models\System\User::findOrFail($id);
// 禁止切換 Super Admin 狀態
if ($user->hasRole('super-admin')) {
return back()->with('error', __('Cannot change Super Admin status.'));
// 非超級管理員禁止切換 Super Admin 狀態
if ($user->hasRole('super-admin') && !auth()->user()->hasRole('super-admin')) {
return back()->with('error', __('Only Super Admins can change other Super Admin status.'));
}
$user->status = $user->status ? 0 : 1;

View File

@@ -6,22 +6,21 @@ use App\Http\Controllers\Controller;
use App\Models\Product\Product;
use App\Models\Product\ProductCategory;
use App\Models\System\Company;
use App\Models\System\Translation;
use App\Traits\ImageHandler;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ProductController extends Controller
{
use \App\Traits\ImageHandler;
public function index(Request $request)
{
$user = auth()->user();
$query = Product::with(['category', 'translations', 'company']);
// 租戶隔離由 Global Scope (TenantScoped) 處理,但系統管理員可額外篩選
if ($user->isSystemAdmin() && $request->filled('company_id')) {
$query->where('company_id', $request->company_id);
}
// 搜尋
if ($request->filled('search')) {
$search = $request->search;
@@ -38,11 +37,23 @@ class ProductController extends Controller
}
$per_page = $request->input('per_page', 10);
$companyId = $user->company_id;
if ($user->isSystemAdmin()) {
if ($request->filled('company_id')) {
$companyId = $request->company_id;
$query->where('company_id', $companyId);
}
}
$products = $query->latest()->paginate($per_page)->withQueryString();
$categories = ProductCategory::all();
$companies = $user->isSystemAdmin() ? Company::all() : collect();
$companySettings = $user->company ? ($user->company->settings ?? []) : [];
// 系統管理員在過濾特定公司時,應顯示該公司的功能開關 (如物料代碼、點數規則)
$selectedCompany = $companyId ? Company::find($companyId) : $user->company;
$companySettings = $selectedCompany ? ($selectedCompany->settings ?? []) : [];
$routeName = 'admin.data-config.products.index';
return view('admin.products.index', [
@@ -54,6 +65,42 @@ class ProductController extends Controller
]);
}
public function create(Request $request)
{
$user = auth()->user();
$categories = ProductCategory::all();
$companies = $user->isSystemAdmin() ? Company::all() : collect();
// If system admin, check if company_id is provided in URL to get settings
$companyId = $request->query('company_id') ?? $user->company_id;
$selectedCompany = $companyId ? Company::find($companyId) : $user->company;
$companySettings = $selectedCompany ? ($selectedCompany->settings ?? []) : [];
return view('admin.products.create', [
'categories' => $categories,
'companies' => $companies,
'companySettings' => $companySettings,
]);
}
public function edit($id)
{
$user = auth()->user();
$product = Product::with(['translations', 'company'])->findOrFail($id);
$categories = ProductCategory::all();
$companies = $user->isSystemAdmin() ? Company::all() : collect();
// Use the product's company settings for editing
$companySettings = $product->company ? ($product->company->settings ?? []) : [];
return view('admin.products.edit', [
'product' => $product,
'categories' => $categories,
'companies' => $companies,
'companySettings' => $companySettings,
]);
}
public function store(Request $request)
{
$validated = $request->validate([
@@ -71,13 +118,18 @@ class ProductController extends Controller
'member_price' => 'required|numeric|min:0',
'metadata' => 'nullable|array',
'is_active' => 'nullable|boolean',
'company_id' => 'nullable|exists:companies,id',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
]);
try {
DB::beginTransaction();
$dictKey = (string) Str::uuid();
$company_id = auth()->user()->company_id;
$dictKey = \Illuminate\Support\Str::uuid()->toString();
// Determine company_id: prioritized from request (for sys admin) then from user
$company_id = (auth()->user()->isSystemAdmin() && $request->filled('company_id'))
? $request->company_id
: auth()->user()->company_id;
// Store translations
foreach ($request->names as $locale => $name) {
@@ -86,16 +138,25 @@ class ProductController extends Controller
'group' => 'product',
'key' => $dictKey,
'locale' => $locale,
'text' => $name,
'value' => $name,
'company_id' => $company_id,
]);
}
$imageUrl = null;
if ($request->hasFile('image')) {
$path = $this->storeAsWebp($request->file('image'), 'products');
$imageUrl = Storage::url($path);
}
$product = Product::create([
'company_id' => $company_id,
'category_id' => $request->category_id,
'name' => $request->names['zh_TW'], // Default name uses zh_TW
'name' => $request->names['zh_TW'] ?? (collect($request->names)->first() ?? 'Untitled'), // Fallback if zh_TW is missing
'name_dictionary_key' => $dictKey,
'image_url' => $imageUrl,
'barcode' => $request->barcode,
'spec' => $request->spec,
'manufacturer' => $request->manufacturer,
@@ -118,14 +179,14 @@ class ProductController extends Controller
]);
}
return redirect()->back()->with('success', __('Product created successfully'));
return redirect()->route('admin.data-config.products.index')->with('success', __('Product created successfully'));
} catch (\Exception $e) {
DB::rollBack();
if ($request->wantsJson()) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
return redirect()->back()->with('error', $e->getMessage());
return redirect()->back()->with('error', $e->getMessage())->withInput();
}
}
@@ -148,13 +209,15 @@ class ProductController extends Controller
'member_price' => 'required|numeric|min:0',
'metadata' => 'nullable|array',
'is_active' => 'nullable|boolean',
'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'remove_image' => 'nullable|boolean',
]);
try {
DB::beginTransaction();
$dictKey = $product->name_dictionary_key;
$company_id = auth()->user()->company_id;
$dictKey = $product->name_dictionary_key ?: \Illuminate\Support\Str::uuid()->toString();
$company_id = $product->company_id;
// Update or Create translations
foreach ($request->names as $locale => $name) {
@@ -174,15 +237,15 @@ class ProductController extends Controller
'locale' => $locale,
],
[
'text' => $name,
'value' => $name,
'company_id' => $company_id,
]
);
}
$product->update([
$data = [
'category_id' => $request->category_id,
'name' => $request->names['zh_TW'],
'name' => $request->names['zh_TW'] ?? ($product->name ?? 'Untitled'),
'barcode' => $request->barcode,
'spec' => $request->spec,
'manufacturer' => $request->manufacturer,
@@ -193,7 +256,25 @@ class ProductController extends Controller
'member_price' => $request->member_price,
'metadata' => $request->metadata ?? [],
'is_active' => $request->boolean('is_active', true),
]);
];
if ($request->hasFile('image')) {
// Delete old image
if ($product->image_url) {
$oldPath = str_replace('/storage/', '', $product->image_url);
Storage::disk('public')->delete($oldPath);
}
$path = $this->storeAsWebp($request->file('image'), 'products');
$data['image_url'] = Storage::url($path);
} elseif ($request->boolean('remove_image')) {
if ($product->image_url) {
$oldPath = str_replace('/storage/', '', $product->image_url);
Storage::disk('public')->delete($oldPath);
}
$data['image_url'] = null;
}
$product->update($data);
DB::commit();
@@ -205,14 +286,14 @@ class ProductController extends Controller
]);
}
return redirect()->back()->with('success', __('Product updated successfully'));
return redirect()->route('admin.data-config.products.index')->with('success', __('Product updated successfully'));
} catch (\Exception $e) {
DB::rollBack();
if ($request->wantsJson()) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
return redirect()->back()->with('error', $e->getMessage());
return redirect()->back()->with('error', $e->getMessage())->withInput();
}
}
@@ -226,6 +307,12 @@ class ProductController extends Controller
Translation::where('key', $product->name_dictionary_key)->delete();
}
// Delete image
if ($product->image_url) {
$oldPath = str_replace('/storage/', '', $product->image_url);
Storage::disk('public')->delete($oldPath);
}
$product->delete();
return redirect()->back()->with('success', __('Product deleted successfully'));