[FEAT] 優化後端帳號權限邏輯、開發商品管理功能及聯絡資訊 UI 改版
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m2s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 1m2s
This commit is contained in:
@@ -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.'));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -7,9 +7,10 @@ use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Translation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasFactory, \App\Traits\TenantScoped;
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'group',
|
||||
'key',
|
||||
'locale',
|
||||
|
||||
Reference in New Issue
Block a user