diff --git a/app/Http/Controllers/LeadController.php b/app/Http/Controllers/LeadController.php index 0c8a53c..bdded8a 100755 --- a/app/Http/Controllers/LeadController.php +++ b/app/Http/Controllers/LeadController.php @@ -3,22 +3,27 @@ namespace App\Http\Controllers; use App\Exports\ExcelExport; -use Request; -use App\User; -use Validator; -use App\Services\SysLog; -use App\Models\UserAccount; -use App\Models\UserHistory; -use Maatwebsite\Excel\Facades\Excel; -use App\Services\UserService; use App\Mail\MailAccountActive; use App\Mail\MailCustomMessage; use App\Mail\MailVerifyAccount; use App\Mail\MailVerifyContact; +use App\Models\UserAccount; +use App\Models\UserHistory; use App\Models\UserWhitelabelProduct; -use App\Repositories\UserRepository; -use Illuminate\Support\Facades\Mail; use App\Repositories\ContractPDFRepository; +use App\Repositories\UserRepository; +use App\Services\ProductImage; +use App\Services\SysLog; +use App\Services\UserService; +use App\User; +use Illuminate\Contracts\View\Factory; +use Illuminate\Http\RedirectResponse; +use Illuminate\Routing\Redirector; +use Illuminate\Support\Facades\Mail; +use Illuminate\View\View; +use Maatwebsite\Excel\Facades\Excel; +use Request; +use Validator; class LeadController extends Controller { @@ -32,7 +37,7 @@ class LeadController extends Controller } /** - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @return Factory|View */ public function index() { @@ -41,26 +46,26 @@ class LeadController extends Controller 'values' => User::where('admin', '=', 0)->where('confirmation_code_remider', '!=', 2)->get(), ]; + return view('admin.lead.index', $data); } /** - * @param $id - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @return Factory|View */ public function edit($id) { - if($id === "new"){ - $user = new User(); - $user->account = new UserAccount(); + if ($id === 'new') { + $user = new User; + $user->account = new UserAccount; $user->account->same_as_billing = 1; $user->account->country_id = 1; $user->account->shipping_country_id = 1; - $user->id = "new"; - }else{ + $user->id = 'new'; + } else { $user = User::findOrFail($id); - if(!$user->account){ - $user->account = new UserAccount(); + if (! $user->account) { + $user->account = new UserAccount; } } $data = [ @@ -70,39 +75,39 @@ class LeadController extends Controller 'm_data_load' => false, 'm_data_error' => false, ]; + return view('admin.lead.edit', $data); } /** - * @param $id - * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @return Factory|View */ public function editPost($id) { $m_data_load = false; $m_data_error = false; $data = Request::all(); - if(!isset($data['edit_m_data_key']) || $data['edit_m_data_key'] !== config('main.edit_data_pass')){ - $m_data_error = "Das Passwort ist falsch."; - }else{ + if (! isset($data['edit_m_data_key']) || $data['edit_m_data_key'] !== config('main.edit_data_pass')) { + $m_data_error = 'Das Passwort ist falsch.'; + } else { $m_data_load = true; } - if($id === "new"){ - $user = new User(); - $user->account = new UserAccount(); + if ($id === 'new') { + $user = new User; + $user->account = new UserAccount; $user->account->same_as_billing = 1; $user->account->country_id = 1; $user->account->shipping_country_id = 1; - $user->id = "new"; - }else{ + $user->id = 'new'; + } else { $user = User::findOrFail($id); - if(!$user->account){ - $user->account = new UserAccount(); + if (! $user->account) { + $user->account = new UserAccount; } } - $next_account_id = UserAccount::withTrashed()->max('m_account') +1; - if($user->account->m_account === null){ + $next_account_id = UserAccount::withTrashed()->max('m_account') + 1; + if ($user->account->m_account === null) { $user->account->m_account = $next_account_id; } @@ -112,14 +117,14 @@ class LeadController extends Controller 'm_data_load' => $m_data_load, 'm_data_error' => $m_data_error, 'can_change_mail' => true, - 'next_account_id' => $next_account_id + 'next_account_id' => $next_account_id, ]; + return view('admin.lead.edit', $data); } /** - * @param Request $request - * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View + * @return Factory|RedirectResponse|Redirector|View */ public function store(Request $request) { @@ -127,153 +132,157 @@ class LeadController extends Controller $data = Request::all(); $show = Request::get('show'); - if(isset($data['action']) && $data['action'] == "reverse_charge_validate" && isset($data['user_id'])){ + if (isset($data['action']) && $data['action'] == 'reverse_charge_validate' && isset($data['user_id'])) { $user = User::findOrFail($data['user_id']); + return $this->userRepo->reverse_charge_validate($data, $user, route('admin_lead_edit', [$user->id])); } - if(isset($data['action']) && $data['action'] == "reverse_charge_delete" && isset($data['user_id'])){ + if (isset($data['action']) && $data['action'] == 'reverse_charge_delete' && isset($data['user_id'])) { $user = User::findOrFail($data['user_id']); - return $this->userRepo->reverse_charge_delete($data, $user, route('admin_lead_edit', [$user->id])); - } - if ($data['user_id'] === "new" || $data['user_id'] == 0) { - $rules = array( + return $this->userRepo->reverse_charge_delete($data, $user, route('admin_lead_edit', [$user->id])); + } + + if ($data['user_id'] === 'new' || $data['user_id'] == 0) { + $rules = [ 'salutation' => 'required', - 'first_name'=>'required', - 'last_name'=>'required', + 'first_name' => 'required', + 'last_name' => 'required', 'email' => 'required|string|email|max:255|unique:users', 'email-confirm' => 'required|same:email', - ); - }else{ - $rules = array( + ]; + } else { + $rules = [ 'salutation' => 'required', - 'first_name'=>'required', - 'last_name'=>'required', - 'address'=>'required', - 'zipcode'=>'required', + 'first_name' => 'required', + 'last_name' => 'required', + 'address' => 'required', + 'zipcode' => 'required', 'city' => 'required', 'email' => 'required|string|email|max:255|exists:users,email', 'email-confirm' => 'required|same:email', - ); - if(!Request::get('same_as_billing')){ + ]; + if (! Request::get('same_as_billing')) { $rules = array_merge($rules, [ - 'shipping_firstname'=>'required', - 'shipping_lastname'=>'required', - 'shipping_address'=>'required', - 'shipping_zipcode'=>'required', + 'shipping_firstname' => 'required', + 'shipping_lastname' => 'required', + 'shipping_address' => 'required', + 'shipping_zipcode' => 'required', 'shipping_city' => 'required', - 'shipping_salutation' => 'required' + 'shipping_salutation' => 'required', ]); } } - if(isset($data['m_account']) && $data['m_account']){ + if (isset($data['m_account']) && $data['m_account']) { $user = User::findOrFail($data['user_id']); $rules['m_account'] = 'unique:user_accounts,m_account,'.$user->account->id.',id'; } $validator = Validator::make(Request::all(), $rules); if ($validator->fails()) { - if ($data['user_id'] === "new" || $data['user_id'] == 0) { - $user_id = "new"; - }else{ + if ($data['user_id'] === 'new' || $data['user_id'] == 0) { + $user_id = 'new'; + } else { $user = User::findOrFail($data['user_id']); $user_id = $user->id; } - return redirect(route('admin_lead_edit', [$user_id])."?show=".$show)->withErrors($validator)->withRequest(Request::all()); + + return redirect(route('admin_lead_edit', [$user_id]).'?show='.$show)->withErrors($validator)->withRequest(Request::all()); } - if ($data['user_id'] === "new" || $data['user_id'] == 0) { - $user = new User(); - $user->id = "new"; - $user->account = new UserAccount(); + if ($data['user_id'] === 'new' || $data['user_id'] == 0) { + $user = new User; + $user->id = 'new'; + $user->account = new UserAccount; - }else { - $user = User::findOrFail($data['user_id']); - if(!$user->account){ - $user->account = new UserAccount(); - } + } else { + $user = User::findOrFail($data['user_id']); + if (! $user->account) { + $user->account = new UserAccount; } + } - $this->userRepo->update($data); + $this->userRepo->update($data); - if(isset($data['m_data_edit']) && $data['m_data_edit'] === "TSOK"){ - $user = $this->userRepo->getModel(); - $user->m_level = isset($data['m_level']) ? $data['m_level'] : NULL; - $user->lead_type_id = isset($data['lead_type_id']) ? $data['lead_type_id'] : NULL; - $user->m_sponsor = isset($data['m_sponsor']) ? $data['m_sponsor'] : NULL; - $user->save(); - } + if (isset($data['m_data_edit']) && $data['m_data_edit'] === 'TSOK') { + $user = $this->userRepo->getModel(); + $user->m_level = isset($data['m_level']) ? $data['m_level'] : null; + $user->lead_type_id = isset($data['lead_type_id']) ? $data['lead_type_id'] : null; + $user->m_sponsor = isset($data['m_sponsor']) ? $data['m_sponsor'] : null; + $user->save(); + } - if(isset($data['contact_verify'])){ + if (isset($data['contact_verify'])) { - $user = $this->userRepo->getModel(); + $user = $this->userRepo->getModel(); - $confirmation_code = UserService::createConfirmationCode(); + $confirmation_code = UserService::createConfirmationCode(); - $user->lang = $user->getLandByCountry(); - $user->confirmation_code = $confirmation_code; - //10 == start wizard form create Lead - $user->wizard = 10; - $user->save(); - Mail::to($user->email)->bcc(config('app.info_mail'))->send(new MailVerifyContact($confirmation_code, $user)); - - \Session()->flash('alert-save', true); - return redirect(route('admin_leads')); - } + $user->lang = $user->getLandByCountry(); + $user->confirmation_code = $confirmation_code; + // 10 == start wizard form create Lead + $user->wizard = 10; + $user->save(); + Mail::to($user->email)->bcc(config('app.info_mail'))->send(new MailVerifyContact($confirmation_code, $user)); \Session()->flash('alert-save', true); - return redirect(route('admin_lead_edit', [$user->id])."?show=".$show); + return redirect(route('admin_leads')); + } + \Session()->flash('alert-save', true); + return redirect(route('admin_lead_edit', [$user->id]).'?show='.$show); } - public function update() { $data = Request::all(); $show = Request::get('show'); - if(!isset($data['user_id']) || $data['user_id'] === "new" || $data['user_id'] == 0){ + if (! isset($data['user_id']) || $data['user_id'] === 'new' || $data['user_id'] == 0) { abort(404, 'User not found'); } $user = User::findOrFail($data['user_id']); - if(isset($data['action'])){ + if (isset($data['action'])) { - //add whitelabel products - if($data['action'] === 'add_whitelabel_products'){ - if(isset($data['whitelabel_products']) && is_array($data['whitelabel_products'])){ - foreach ($data['whitelabel_products'] as $product_id){ + // add whitelabel products + if ($data['action'] === 'add_whitelabel_products') { + if (isset($data['whitelabel_products']) && is_array($data['whitelabel_products'])) { + foreach ($data['whitelabel_products'] as $product_id) { $user->whitelabel_products()->create(['product_id' => $product_id]); } } \Session()->flash('alert-save', true); } - //remove whitelabel products - if($data['action'] === 'remove_whitelabel_products'){ - if(isset($data['whitelabel_products']) && is_array($data['whitelabel_products'])){ + // remove whitelabel products + if ($data['action'] === 'remove_whitelabel_products') { + if (isset($data['whitelabel_products']) && is_array($data['whitelabel_products'])) { $user->whitelabel_products()->whereIn('product_id', $data['whitelabel_products'])->delete(); } - \Session()->flash('alert-success', "Produkt gelöscht"); + \Session()->flash('alert-success', 'Produkt gelöscht'); } - if($data['action'] === 'upload_white_label_image'){ - //id == user_whitelabel_product_id - $whitelabel_product = UserWhitelabelProduct::findOrFail($data['id']); - if($whitelabel_product && $user->id == $whitelabel_product->user_id){ - return \App\Services\ProductImage::imageUpload('user_wl_product', $whitelabel_product, Request::get('upload_type')); - } + if ($data['action'] === 'upload_white_label_image') { + // id == user_whitelabel_product_id + $whitelabel_product = UserWhitelabelProduct::findOrFail($data['id']); + if ($whitelabel_product && $user->id == $whitelabel_product->user_id) { + return ProductImage::imageUpload('user_wl_product', $whitelabel_product, Request::get('upload_type')); + } } - //update whitelabel products - if($data['action'] === 'update_whitelabel_products'){ - if(isset($data['image_wl_attributes']) && is_array($data['image_wl_attributes'])){ - foreach ( $user->whitelabel_products as $wl_product){ + // update whitelabel products + if ($data['action'] === 'update_whitelabel_products') { + $positions = isset($data['image_wl_position']) && is_array($data['image_wl_position']) ? $data['image_wl_position'] : []; + if (isset($data['image_wl_attributes']) && is_array($data['image_wl_attributes'])) { + foreach ($user->whitelabel_products as $wl_product) { foreach ($wl_product->whitelabel_images as $wl_image) { + $position = $positions[$wl_image->id] ?? null; $wl_image->update([ - 'attributes' => isset($data['image_wl_attributes'][$wl_image->id]) ? $data['image_wl_attributes'][$wl_image->id] : NULL, + 'attributes' => isset($data['image_wl_attributes'][$wl_image->id]) ? $data['image_wl_attributes'][$wl_image->id] : null, + 'position' => array_key_exists($position, \App\Models\ProductImage::POSITIONS) ? $position : ($wl_image->position ?: 'deckel'), ]); } } @@ -281,86 +290,95 @@ class LeadController extends Controller \Session()->flash('alert-save', true); } } - return redirect(route('admin_lead_edit', [$user->id])."?show=".$show); + + return redirect(route('admin_lead_edit', [$user->id]).'?show='.$show); } - public function remove($action, $user_id, $id, $mid=null){ + public function remove($action, $user_id, $id, $mid = null) + { $show = Request::get('show'); $user = User::findOrFail($user_id); - if($action === 'remove_whitelabel_label'){ + if ($action === 'remove_whitelabel_label') { $model = $user->whitelabel_products()->where('id', $id)->first(); - return \App\Services\ProductImage::imageDelete('user_wl_image', $model, $mid); + + return ProductImage::imageDelete('user_wl_image', $model, $mid); } - if($action === 'remove_whitelabel_product'){ + if ($action === 'remove_whitelabel_product') { $model = $user->whitelabel_products()->where('id', $id)->first(); - //remove images - foreach ($model->whitelabel_images as $image){ - \App\Services\ProductImage::imageDelete('user_wl_image', $model, $image->id); + // remove images + foreach ($model->whitelabel_images as $image) { + ProductImage::imageDelete('user_wl_image', $model, $image->id); } $model->delete(); - \Session()->flash('alert-success', "White Label Produkt entfernt"); + \Session()->flash('alert-success', 'White Label Produkt entfernt'); + return redirect(route('admin_lead_edit', [$user->id])."?show=$show"); } - - } - - public function download(){ - if(Request::get('action') === "export"){ + } + + public function download() + { + + if (Request::get('action') === 'export') { $query = $this->initSearch(); $columns = []; - $filename = "GS-VP-export-".date("d-m-Y"); - $headers = array( - 'ID', - 'Email', - 'Firma', - 'Anrede', - 'Vorname', - 'Nachname', - 'Mitglied', + $filename = 'GS-VP-export-'.date('d-m-Y'); + $headers = [ + 'ID', + 'Email', + 'Firma', + 'Anrede', + 'Vorname', + 'Nachname', + 'Mitglied', 'Bis', 'Art', - - ); + + ]; $objects = $query->get(); - if($objects){ - foreach ($objects as $obj){ - $columns[] = array( - 'ID' => $obj->id, - 'Email' => $obj->email, - 'Firma' => $obj->account ? $obj->account->company : '', - 'Anrede' => $obj->account ? ($obj->account->salutation == 'mr' ? 'Herr' : 'Frau') : '-', - 'Vorname' => $obj->account ? $obj->account->first_name : '', - 'Nachname' => $obj->account ? $obj->account->last_name : '', - 'Mitglied' => $obj->payment_account ? ($obj->isActiveAccount() ? 'JA' : 'Abgelaufen') : "Nein", - 'Bis' => $obj->payment_account ? $obj->getPaymentAccountDateFormat(false) : '-', - 'Art' => $obj->lead_type ? $obj->lead_type->name : '-', - ); + if ($objects) { + foreach ($objects as $obj) { + $columns[] = [ + 'ID' => $obj->id, + 'Email' => $obj->email, + 'Firma' => $obj->account ? $obj->account->company : '', + 'Anrede' => $obj->account ? ($obj->account->salutation == 'mr' ? 'Herr' : 'Frau') : '-', + 'Vorname' => $obj->account ? $obj->account->first_name : '', + 'Nachname' => $obj->account ? $obj->account->last_name : '', + 'Mitglied' => $obj->payment_account ? ($obj->isActiveAccount() ? 'JA' : 'Abgelaufen') : 'Nein', + 'Bis' => $obj->payment_account ? $obj->getPaymentAccountDateFormat(false) : '-', + 'Art' => $obj->lead_type ? $obj->lead_type->name : '-', + ]; } } + return Excel::download(new ExcelExport($columns, $headers), $filename.'.xls'); } } - private function setFilterVars(){ - if(!session('filter_lead_type_id')){ + private function setFilterVars() + { + if (! session('filter_lead_type_id')) { session(['filter_lead_type_id' => 'all']); } - if(Request::get('filter_lead_type_id')){ + if (Request::get('filter_lead_type_id')) { session(['filter_lead_type_id' => Request::get('filter_lead_type_id')]); } } - private function initSearch(){ + private function initSearch() + { $this->setFilterVars(); - $query = User::with('account')->select('users.*')->where('users.deleted_at', '=', null)->where('users.admin', "<", 4); - if(session('filter_lead_type_id') && session('filter_lead_type_id') !== null && session('filter_lead_type_id') !== "all"){ + $query = User::with('account')->select('users.*')->where('users.deleted_at', '=', null)->where('users.admin', '<', 4); + if (session('filter_lead_type_id') && session('filter_lead_type_id') !== null && session('filter_lead_type_id') !== 'all') { $query->where('lead_type_id', session('filter_lead_type_id')); } + return $query; } @@ -385,7 +403,7 @@ class LeadController extends Controller return $user->lead_type ? $user->lead_type->name : ''; }) ->addColumn('id', function (User $user) { - return ''; + return ''; }) ->addColumn('confirmed', function (User $user) { return $user->confirmed ? '' : ''; @@ -400,7 +418,7 @@ class LeadController extends Controller return $user->payment_account ? ' ' : ''; }) ->addColumn('payment_account_date', function (User $user) { - return $user->payment_account ? $user->getPaymentAccountDateFormat(false) : "-"; + return $user->payment_account ? $user->getPaymentAccountDateFormat(false) : '-'; }) /*->addColumn('payment_shop', function (User $user) { return $user->payment_shop ? ' ' : ''; @@ -409,10 +427,10 @@ class LeadController extends Controller return $user->payment_shop ? $user->getPaymentShopDateFormat(false) : "-"; })*/ ->addColumn('turnover', function (User $user) { - return "-"; + return '-'; }) ->addColumn('sales_total', function (User $user) { - return "-"; + return '-'; }) ->orderColumn('id', 'id $1') @@ -421,16 +439,17 @@ class LeadController extends Controller ->orderColumn('lead_type', 'lead_type_id $1') ->orderColumn('agreement', 'agreement $1') ->orderColumn('payment_account', 'payment_account $1') - //->orderColumn('payment_shop', 'payment_shop $1') + // ->orderColumn('payment_shop', 'payment_shop $1') ->rawColumns(['id', 'confirmed', 'active', 'agreement', 'payment_account']) ->make(true); } - private function activeAccountPayment($user){ + private function activeAccountPayment($user) + { - if($user->user_level){ - if($user->user_level->payment_year){ - //if true = payments + if ($user->user_level) { + if ($user->user_level->payment_year) { + // if true = payments $user->wizard = 20; $user->active = 1; $user->active_date = now(); @@ -438,8 +457,8 @@ class LeadController extends Controller $user->confirmation_code_to = null; $user->confirmation_code_remider = 0; $user->save(); - }else{ - //if false = no payments for 1 year + } else { + // if false = no payments for 1 year $user->wizard = 100; $user->payment_account = \Carbon::now()->modify('1 year'); $user->active = 1; @@ -449,37 +468,38 @@ class LeadController extends Controller $user->confirmation_code_remider = 0; $user->save(); } - } + } } - //user released when register is complete - public function released($action, $id){ + // user released when register is complete + public function released($action, $id) + { $user = User::findOrFail($id); - if($action === 'completed'){ + if ($action === 'completed') { $validator = Validator::make(Request::all(), []); /*if(!$user->m_sponsor){ $validator->errors()->add('m_sponsor', __('Vertriebspartner hat keinen Sponsor.')); }*/ - if(!$user->account->m_first_name){ + if (! $user->account->m_first_name) { $validator->errors()->add('m_first_name', __('Vertriebspartner hat keinen Vornamen.')); } - if(!$user->account->m_last_name){ + if (! $user->account->m_last_name) { $validator->errors()->add('m_last_name', __('Vertriebspartner hat keinen Nachnamen.')); } - if(!$user->m_level){ + if (! $user->m_level) { $validator->errors()->add('m_level', __('Vertriebspartner hat keine Rolle')); } if ($validator->errors()->count()) { return back()->withErrors($validator)->withRequest(Request::all()); } - if(!$user->account->m_account){ - $user->account->m_account = UserAccount::withTrashed()->max('m_account') +1; + if (! $user->account->m_account) { + $user->account->m_account = UserAccount::withTrashed()->max('m_account') + 1; $user->account->save(); } - //create PDF + // create PDF $pdf = new ContractPDFRepository($user); $pdf->_set('disk', 'user'); $pdf->_set('dir', '/'.$user->id.'/documents/'); @@ -487,14 +507,13 @@ class LeadController extends Controller $pdf->_set('identifier', 'contract'); $pdf->createContractPDF(); - //set wizard to payments / activate - $this->activeAccountPayment($user); + // set wizard to payments / activate + $this->activeAccountPayment($user); - //mail with code to user? + // mail with code to user? try { Mail::to($user->email)->bcc(config('app.info_mail'))->send(new MailAccountActive($user)); - } - catch(\Exception $e){ + } catch (\Exception $e) { SysLog::action('released', 'admin_lead', 5) ->setUserId($user->id) ->setModel($user->id, User::class) @@ -502,12 +521,12 @@ class LeadController extends Controller ->save(); } - UserHistory::create(['user_id' => $user->id, 'action'=>'released_completed', 'status'=>0]); - \Session()->flash('alert-success', "Vertriebspartner freigeschaltet!"); + UserHistory::create(['user_id' => $user->id, 'action' => 'released_completed', 'status' => 0]); + \Session()->flash('alert-success', 'Vertriebspartner freigeschaltet!'); } - if($action === 'incomplete'){ - //reset release + if ($action === 'incomplete') { + // reset release $confirmation_code = UserService::createConfirmationCode(); $user->confirmation_code = $confirmation_code; $user->confirmation_code_to = date('Y-m-d H:i:s', strtotime('+1 week')); @@ -524,55 +543,53 @@ class LeadController extends Controller ]; try { Mail::to($user->email)->bcc(config('app.info_mail'))->send(new MailCustomMessage($user, $data, \Auth::user(), true)); - } - catch(\Exception $e){ + } catch (\Exception $e) { SysLog::action('released_incomplete', 'admin_lead', 5) ->setUserId($user->id) ->setModel($user->id, User::class) ->setMessage('Error send released_incomplete E-Mail: '.$e->getMessage()) ->save(); } - UserHistory::create(['user_id' => $user->id, 'action'=>'released_incomplete', 'status'=>0]); - \Session()->flash('alert-success', "E-Mail an Vertriebspartner gesendet."); + UserHistory::create(['user_id' => $user->id, 'action' => 'released_incomplete', 'status' => 0]); + \Session()->flash('alert-success', 'E-Mail an Vertriebspartner gesendet.'); } - if($action === 'reset_switch'){ + if ($action === 'reset_switch') { $user->wizard = 3; $user->save(); - UserHistory::create(['user_id' => $user->id, 'action'=>'reset_switch', 'status'=>0]); - \Session()->flash('alert-success', "Vertriebspartner zurückgesetzt!"); + UserHistory::create(['user_id' => $user->id, 'action' => 'reset_switch', 'status' => 0]); + \Session()->flash('alert-success', 'Vertriebspartner zurückgesetzt!'); } - if($action === 'unlock'){ + if ($action === 'unlock') { $validator = Validator::make(Request::all(), []); /*if(!$user->m_sponsor){ $validator->errors()->add('m_sponsor', __('Vertriebspartner hat keinen Sponsor.')); }*/ - if(!$user->account->m_first_name){ + if (! $user->account->m_first_name) { $validator->errors()->add('m_first_name', __('Vertriebspartner hat keinen Vornamen.')); } - if(!$user->account->m_last_name){ + if (! $user->account->m_last_name) { $validator->errors()->add('m_last_name', __('Vertriebspartner hat keinen Nachnamen.')); } - if(!$user->m_level){ + if (! $user->m_level) { $validator->errors()->add('m_level', __('Vertriebspartner hat keine Rolle')); } - + if ($validator->errors()->count()) { return back()->withErrors($validator)->withRequest(Request::all()); } - if(!$user->account->m_account){ - $user->account->m_account = UserAccount::withTrashed()->max('m_account') +1; + if (! $user->account->m_account) { + $user->account->m_account = UserAccount::withTrashed()->max('m_account') + 1; $user->account->save(); } - //set wizard to payments / activate + // set wizard to payments / activate $this->activeAccountPayment($user); - - //mail with code to user? + + // mail with code to user? try { Mail::to($user->email)->bcc(config('app.info_mail'))->send(new MailAccountActive($user)); - } - catch(\Exception $e){ + } catch (\Exception $e) { SysLog::action('unlock', 'admin_lead', 5) ->setUserId($user->id) ->setModel($user->id, User::class) @@ -580,55 +597,56 @@ class LeadController extends Controller ->save(); } - UserHistory::create(['user_id' => $user->id, 'action'=>'unlock_completed', 'status'=>0]); - \Session()->flash('alert-success', "Vertriebspartner erneut freigeschaltet!"); + UserHistory::create(['user_id' => $user->id, 'action' => 'unlock_completed', 'status' => 0]); + \Session()->flash('alert-success', 'Vertriebspartner erneut freigeschaltet!'); } - return redirect(route('admin_lead_edit', [$user->id])); + return redirect(route('admin_lead_edit', [$user->id])); } + // send new verfified mail to user + /* public function newMailVerified($id){ - //send new verfified mail to user - /* public function newMailVerified($id){ + User Register sind in der DB UserRegister, erst bei bestätigung wird es in die User DB übertragen - User Register sind in der DB UserRegister, erst bei bestätigung wird es in die User DB übertragen + $user = User::findOrFail($id); - $user = User::findOrFail($id); + $confirmation_code = UserService::createConfirmationCode(); + $user->confirmation_code = $confirmation_code; + $user->confirmation_code_to = date('Y-m-d H:i:s', strtotime('+1 week')); + $user->confirmation_code_remider = 0; + $user->save(); - $confirmation_code = UserService::createConfirmationCode(); - $user->confirmation_code = $confirmation_code; - $user->confirmation_code_to = date('Y-m-d H:i:s', strtotime('+1 week')); - $user->confirmation_code_remider = 0; - $user->save(); + try { + Mail::to($user->email)->bcc(config('app.info_mail'))->send(new MailVerifyAccount($confirmation_code, $user)); + } + catch(\Exception $e){ + SysLog::action('new_mail_verified', 'admin_lead', 5) + ->setUserId($user->id) + ->setModel($user->id, User::class) + ->setMessage('Error send new_mail_verified E-Mail: '.$e->getMessage()) + ->save(); + } + UserHistory::create(['user_id' => $user->id, 'action'=>'new_mail_verified', 'status'=>0]); - try { - Mail::to($user->email)->bcc(config('app.info_mail'))->send(new MailVerifyAccount($confirmation_code, $user)); - } - catch(\Exception $e){ - SysLog::action('new_mail_verified', 'admin_lead', 5) - ->setUserId($user->id) - ->setModel($user->id, User::class) - ->setMessage('Error send new_mail_verified E-Mail: '.$e->getMessage()) - ->save(); - } - UserHistory::create(['user_id' => $user->id, 'action'=>'new_mail_verified', 'status'=>0]); + \Session()->flash('alert-success', "E-Mail erneut gesendet"); + return redirect(route('admin_lead_edit', [$user->id])); - \Session()->flash('alert-success', "E-Mail erneut gesendet"); - return redirect(route('admin_lead_edit', [$user->id])); + }*/ - }*/ + public function deleteFile($user_id, $file_id, $relation) + { - public function deleteFile($user_id, $file_id, $relation){ - - if($relation === 'upload'){ + if ($relation === 'upload') { $user = User::findOrFail($user_id); $file = $user->files()->findOrFail($file_id); - //remove file + // remove file \Storage::disk('user')->delete($file->dir.$file->filename); $file->delete(); - \Session()->flash('alert-success', "Datei gelöscht"); + \Session()->flash('alert-success', 'Datei gelöscht'); } + return back(); } -} \ No newline at end of file +} diff --git a/app/Models/Attribute.php b/app/Models/Attribute.php index 5613e44..2ae742f 100644 --- a/app/Models/Attribute.php +++ b/app/Models/Attribute.php @@ -3,7 +3,9 @@ namespace App\Models; use App\Services\Util; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Carbon; /** * App\Models\Attribute @@ -14,10 +16,11 @@ use Illuminate\Database\Eloquent\Model; * @property array|null $trans_name * @property int|null $pos * @property int $active - * @property \Illuminate\Support\Carbon|null $created_at - * @property \Illuminate\Support\Carbon|null $updated_at - * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Attribute[] $childrens - * @property-read \App\Models\Attribute|null $parent + * @property Carbon|null $created_at + * @property Carbon|null $updated_at + * @property-read Collection|Attribute[] $childrens + * @property-read Attribute|null $parent + * * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute whereActive($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute whereCreatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute whereId($value) @@ -26,17 +29,22 @@ use Illuminate\Database\Eloquent\Model; * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute wherePos($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute whereTransName($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute whereUpdatedAt($value) + * * @property string|null $slug + * * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute whereSlug($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute newQuery() * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Attribute query() + * * @property-read int|null $childrens_count * @property int $attribute_type_id * @property string|null $value - * @property-read \App\Models\AttributeType $attribute_type + * @property-read AttributeType $attribute_type + * * @method static \Illuminate\Database\Eloquent\Builder|Attribute whereAttributeTypeId($value) * @method static \Illuminate\Database\Eloquent\Builder|Attribute whereValue($value) + * * @mixin \Eloquent */ class Attribute extends Model @@ -45,28 +53,61 @@ class Attribute extends Model protected $casts = [ 'parent_id' => 'int', - 'pos' => 'int', - 'active' => 'bool', - 'trans_name' => 'array', + 'pos' => 'int', + 'active' => 'bool', + 'trans_name' => 'array', ]; - - protected $fillable = [ - 'parent_id', + protected $fillable = [ + 'parent_id', 'attribute_type_id', - 'name', + 'name', 'value', - 'trans_name', - 'pos', - 'active', - 'slug' - ]; + 'trans_name', + 'pos', + 'active', + 'slug', + ]; public function attribute_type() { return $this->belongsTo(AttributeType::class, 'attribute_type_id'); } + /** + * Variantenwert "Volumen#Größe" (z. B. "40#3,5") in Volumen (ml) und Etikettgröße (cm) zerlegen. + * Die Größe steht mit deutschem Komma im Bestand; ein reiner (float)-Cast würde "3,5" zu 3.0 kürzen. + * + * @return array{0: float, 1: float} + */ + public static function parseVariantValue(?string $value): array + { + $parts = explode('#', (string) $value); + + return [ + (float) reFormatNumber($parts[0] ?? ''), + (float) reFormatNumber($parts[1] ?? ''), + ]; + } + + public function variantVolume(): float + { + return self::parseVariantValue($this->value)[0]; + } + + public function variantSize(): float + { + return self::parseVariantValue($this->value)[1]; + } + + /** + * Farbname ohne Größenangabe: "Schwarz 40 ml" => "Schwarz". Namen ohne Volumen bleiben unverändert. + */ + public function colorName(): string + { + return trim((string) preg_replace('/\s+\d+([,.]\d+)?\s*ml\s*$/iu', '', (string) $this->name)); + } + public function parent() { return $this->belongsTo(Attribute::class, 'parent_id'); @@ -77,15 +118,16 @@ class Attribute extends Model return $this->hasMany(Attribute::class, 'parent_id', 'id'); } - public function setPosAttribute($value){ + public function setPosAttribute($value) + { $this->attributes['pos'] = is_numeric($value) ? $value : null; } public function getFormattedValue() { - // return isset($this->attributes['value']) ? Util::formatNumber($this->attributes['value']) : ""; + // return isset($this->attributes['value']) ? Util::formatNumber($this->attributes['value']) : ""; } - + public function getLang($key) { $lang = \App::getLocale(); @@ -93,27 +135,28 @@ class Attribute extends Model return $this->{$key}; } $trans = $this->getTrans($key, $lang); - if (!$trans || $trans == '') { + if (! $trans || $trans == '') { return $this->{$key}; } + return $trans; } public function getTrans($key, $lang) { - $key = 'trans_' . $key; - if (!empty($this->{$key}[$lang])) { + $key = 'trans_'.$key; + if (! empty($this->{$key}[$lang])) { return $this->{$key}[$lang]; } } public function getTranNames() { - $ret = ""; - foreach ((array) $this->trans_name as $value){ + $ret = ''; + foreach ((array) $this->trans_name as $value) { $ret .= $value.', '; } + return rtrim($ret, ', '); } - } diff --git a/app/Models/Product.php b/app/Models/Product.php index e88e92b..eafa8ea 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -204,6 +204,7 @@ class Product extends Model 'max_buy' => 'bool', 'max_buy_num' => 'int', 'whitelabel' => 'bool', + 'whitelabel_variant_id' => 'int', 'no_recipe_required' => 'bool', 'is_set' => 'bool', 'main_product_id' => 'int', @@ -224,6 +225,7 @@ class Product extends Model 'name', 'whitelabel', 'whitelabel_name', + 'whitelabel_variant_id', 'title', 'copy', 'short_copy', @@ -348,6 +350,12 @@ class Product extends Model return $this->hasMany(UserWhitelabelProduct::class, 'product_id', 'id'); } + /** Verbindliche Tiegelfarbe/Größe des White-Label-Produkts (WL-02). */ + public function whitelabel_variant() + { + return $this->belongsTo(Attribute::class, 'whitelabel_variant_id'); + } + public function country_prices() { return $this->hasMany(CountryPrice::class, 'product_id'); diff --git a/app/Models/ProductImage.php b/app/Models/ProductImage.php index 5c62b12..7a8adf2 100644 --- a/app/Models/ProductImage.php +++ b/app/Models/ProductImage.php @@ -48,6 +48,8 @@ use Illuminate\Support\Carbon; * @property int|null $user_wl_product_id * @property string|null $type * @property object|null $attributes + * @property string|null $position + * @property float|null $size_cm * @property-read UserWhitelabelProduct|null $user_wl_product * * @method static \Illuminate\Database\Eloquent\Builder|ProductImage whereAttributes($value) @@ -62,12 +64,16 @@ class ProductImage extends Model protected $table = 'product_images'; + public const POSITIONS = ['deckel' => 'Deckel', 'seite' => 'Seite', 'boden' => 'Boden']; + protected $casts = [ 'attributes' => 'object', + 'size_cm' => 'float', ]; protected $fillable = [ 'product_id', 'user_wl_product_id', 'type', 'filename', 'original_name', 'ext', 'mine', 'size', 'pos', 'active', 'attributes', + 'position', 'size_cm', ]; public function sluggable(): array diff --git a/app/Repositories/InvoiceRepository.php b/app/Repositories/InvoiceRepository.php index bba81d3..4decbc2 100644 --- a/app/Repositories/InvoiceRepository.php +++ b/app/Repositories/InvoiceRepository.php @@ -363,18 +363,10 @@ class InvoiceRepository extends BaseRepository } /** - * Attributwert "Volumen#Größe" (z. B. "40#3,5") in Volumen (ml) und Etikettgröße (cm) zerlegen. - * Die Größe steht mit deutschem Komma im Bestand; ein reiner (float)-Cast würde "3,5" zu 3.0 kürzen. - * * @return array{0: float, 1: float} */ public static function parseVariantValue(?string $value): array { - $parts = explode('#', (string) $value); - - return [ - (float) reFormatNumber($parts[0] ?? ''), - (float) reFormatNumber($parts[1] ?? ''), - ]; + return Attribute::parseVariantValue($value); } } diff --git a/app/Repositories/ProductRepository.php b/app/Repositories/ProductRepository.php index 84af33f..5e54e4d 100644 --- a/app/Repositories/ProductRepository.php +++ b/app/Repositories/ProductRepository.php @@ -35,6 +35,10 @@ class ProductRepository extends BaseRepository $data['amount_commission'] = isset($data['amount_commission']) ? 1 : 0; $data['exclude_stats_sales'] = isset($data['exclude_stats_sales']) ? 1 : 0; $data['whitelabel'] = isset($data['whitelabel']) ? 1 : 0; + // WL-02: verbindliche Tiegelfarbe/Größe (leer => null, Altlogik). + $data['whitelabel_variant_id'] = isset($data['whitelabel_variant_id']) && $data['whitelabel_variant_id'] !== '' + ? (int) $data['whitelabel_variant_id'] + : null; $data['shipping_addon'] = isset($data['shipping_addon']) ? 1 : 0; $data['max_buy'] = isset($data['max_buy']) ? 1 : 0; $data['no_recipe_required'] = isset($data['no_recipe_required']) ? 1 : 0; @@ -115,6 +119,7 @@ class ProductRepository extends BaseRepository $this->updateWLVariants(isset($data['whitelabel_variants']) ? $data['whitelabel_variants'] : []); $this->updateWLImageAttributs(isset($data['image_wl_attributes']) ? $data['image_wl_attributes'] : [], isset($data['whitelabel_variants']) ? $data['whitelabel_variants'] : []); + $this->updateWLImagePositions(isset($data['image_wl_position']) ? $data['image_wl_position'] : [], isset($data['image_wl_size']) ? $data['image_wl_size'] : []); if ($this->model->is_set) { // Sets haben keine eigene Rezeptur/Verpackung und werden nicht produziert. @@ -597,6 +602,26 @@ class ProductRepository extends BaseRepository return true; } + /** + * WL-02: Etikettposition und Größe (cm) je Standardetikett. + * + * @param array $positions Bild-ID => Position + * @param array $sizes Bild-ID => Größe in cm (deutsche Schreibweise erlaubt) + */ + public function updateWLImagePositions(array $positions = [], array $sizes = []): bool + { + foreach ($this->model->whitelabel_images as $image) { + $position = $positions[$image->id] ?? null; + $size = isset($sizes[$image->id]) && trim((string) $sizes[$image->id]) !== '' ? (float) reFormatNumber($sizes[$image->id]) : null; + $image->update([ + 'position' => array_key_exists($position, ProductImage::POSITIONS) ? $position : ($image->position ?: 'deckel'), + 'size_cm' => $size !== null && $size > 0 ? $size : null, + ]); + } + + return true; + } + public function updateCountryPrices($data) { if (! isset($data['country_prices']) || ! is_array($data['country_prices'])) { diff --git a/app/Services/WhiteLabel/ResolvedItem.php b/app/Services/WhiteLabel/ResolvedItem.php new file mode 100644 index 0000000..ce012cc --- /dev/null +++ b/app/Services/WhiteLabel/ResolvedItem.php @@ -0,0 +1,92 @@ + Position => Etikett */ + public array $labels = []; + + /** @var list */ + public array $errors = []; + + /** @var list */ + public array $warnings = []; + + public function __construct( + public readonly ShoppingOrderItem $item, + public readonly Product $product, + ) {} + + public function hasCustomerLogo(): bool + { + foreach ($this->labels as $label) { + if ($label->isCustomer()) { + return true; + } + } + + return false; + } + + public function isComplete(): bool + { + return $this->errors === [] && $this->color !== null && $this->labels !== []; + } + + /** Text für den Lieferschein, z. B. "Tiegel: Schwarz". */ + public function colorLine(): string + { + return 'Tiegel: '.($this->color ?? 'unbekannt'); + } + + /** Text für den Lieferschein, z. B. "Etikett: Kundenlogo". */ + public function labelSourceLine(): string + { + return 'Etikett: '.($this->hasCustomerLogo() ? 'Kundenlogo' : 'Standardetikett'); + } + + /** Größtes Etikett in cm; Sortierschlüssel für den Lieferschein (kleine zuerst). */ + public function maxLabelSize(): float + { + $max = 0.0; + foreach ($this->labels as $label) { + $max = max($max, $label->sizeCm); + } + + return $max; + } + + public function addError(string $message): void + { + $this->errors[] = $message; + } + + public function addWarning(string $message): void + { + $this->warnings[] = $message; + } +} diff --git a/app/Services/WhiteLabel/ResolvedLabel.php b/app/Services/WhiteLabel/ResolvedLabel.php new file mode 100644 index 0000000..9369cd4 --- /dev/null +++ b/app/Services/WhiteLabel/ResolvedLabel.php @@ -0,0 +1,40 @@ +source === self::SOURCE_CUSTOMER; + } + + public function sourceName(): string + { + return $this->isCustomer() ? 'Kundenlogo' : 'Standardetikett'; + } + + public function fileExists(): bool + { + return Storage::disk('public')->exists(ltrim($this->image->getImagePath(), '/')); + } +} diff --git a/app/Services/WhiteLabel/WhiteLabelResolver.php b/app/Services/WhiteLabel/WhiteLabelResolver.php new file mode 100644 index 0000000..1e23c33 --- /dev/null +++ b/app/Services/WhiteLabel/WhiteLabelResolver.php @@ -0,0 +1,254 @@ + Bestellpositions-ID => Ergebnis (nur White-Label-Positionen) + */ + public function resolveOrder(ShoppingOrder $order): Collection + { + $partner = $order->auth_user; + $results = collect(); + foreach ($order->shopping_order_items as $item) { + $resolved = $this->resolveItem($item, $partner); + if ($resolved) { + $results->put($item->id, $resolved); + } + } + + return $results; + } + + public function resolveItem(ShoppingOrderItem $item, ?User $partner): ?ResolvedItem + { + $product = $item->product; + if (! $product || ! $product->whitelabel) { + return null; + } + + $result = new ResolvedItem($item, $product); + $variant = $product->whitelabel_variant; + + if ($variant) { + $result->color = $variant->colorName(); + $result->variantId = $variant->id; + $result->volumeMl = $variant->variantVolume(); + $result->sizeCm = $variant->variantSize(); + $result->colorSource = ResolvedItem::COLOR_SOURCE_VARIANT; + } else { + $result->addWarning('Produkt ohne verbindliche Tiegelfarbe; Farbe wird aus dem Etikett abgeleitet (Altlogik).'); + } + + $standard = $this->groupByPosition($product->whitelabel_images, $result, 'Standardetikett'); + $assignment = $this->findAssignment($partner, $product->id); + $customer = $assignment + ? $this->groupByPosition($assignment->whitelabel_images, $result, 'Kundenlogo') + : []; + + if (! $assignment && $partner && $this->partnerHasAssignments($partner)) { + $result->addWarning('Für dieses Produkt keine Kundenzuordnung vorhanden; Partner hat Logos bei anderen Produkten.'); + } + + $positions = array_unique(array_merge(array_keys($standard), array_keys($customer))); + foreach ($positions as $position) { + $label = $variant + ? $this->resolveWithVariant($position, $standard[$position] ?? null, $customer[$position] ?? null, $result) + : $this->resolveLegacy($position, $standard[$position] ?? null, $customer[$position] ?? null, $product, $result); + if ($label) { + $result->labels[$position] = $label; + } + } + + if ($result->labels === [] && $result->errors === []) { + $result->addError('Kein Etikett vorhanden (weder Standardetikett noch Kundenlogo).'); + } + if ($variant === null && $result->color === null && $result->labels !== []) { + $result->color = 'Standard'; + $result->colorSource = ResolvedItem::COLOR_SOURCE_LEGACY; + } + if ($result->color === null) { + $result->addError('Tiegelfarbe nicht bestimmbar.'); + } + + ksort($result->labels); + + return $result; + } + + /** + * Variantenmodus: Position entscheidet, Farbe kommt vom Produkt. + */ + private function resolveWithVariant(string $position, ?ProductImage $standard, ?ProductImage $customer, ResolvedItem $result): ?ResolvedLabel + { + if ($customer) { + $imageAttributes = array_map('intval', (array) $customer->attributes); + if ($imageAttributes !== [] && ! in_array($result->variantId, $imageAttributes, true)) { + $colors = collect($imageAttributes) + ->map(fn ($id) => Attribute::find($id)?->colorName()) + ->filter()->unique()->join(', '); + $result->addError('Kundenlogo (Bild '.$customer->id.', Position '.$position.') ist für '.$colors.' hinterlegt, Produktfarbe ist '.$result->color.'. Zuordnung prüfen.'); + + return null; + } + $size = $this->sizeFor($customer, $result->sizeCm, $standard); + if ($size <= 0) { + $result->addError('Kundenlogo (Bild '.$customer->id.', Position '.$position.'): Etikettgröße nicht bestimmbar.'); + + return null; + } + $label = new ResolvedLabel($customer, ResolvedLabel::SOURCE_CUSTOMER, $position, $size, $result->volumeMl, $result->variantId); + if (! $label->fileExists()) { + $result->addError('Kundenlogo (Bild '.$customer->id.', Position '.$position.'): Datei fehlt. Kein Rückfall auf das Standardetikett.'); + + return null; + } + + return $label; + } + + if ($standard) { + $size = $this->sizeFor($standard, $result->sizeCm, null); + if ($size <= 0) { + $result->addError('Standardetikett (Bild '.$standard->id.', Position '.$position.'): Etikettgröße nicht bestimmbar.'); + + return null; + } + $label = new ResolvedLabel($standard, ResolvedLabel::SOURCE_STANDARD, $position, $size, $result->volumeMl, $result->variantId); + if (! $label->fileExists()) { + $result->addError('Standardetikett (Bild '.$standard->id.', Position '.$position.'): Datei fehlt.'); + + return null; + } + + return $label; + } + + return null; + } + + /** + * Altlogik für Produkte ohne verbindliche Variante: Kundenlogo ersetzt das Standardetikett nur bei + * Attributüberschneidung; Farbe und Größe kommen aus dem getroffenen Attribut. + */ + private function resolveLegacy(string $position, ?ProductImage $standard, ?ProductImage $customer, $product, ResolvedItem $result): ?ResolvedLabel + { + $standardAttributes = $standard ? array_map('intval', (array) $standard->attributes) : []; + $customerAttributes = $customer ? array_map('intval', (array) $customer->attributes) : []; + $overlap = array_values(array_intersect($customerAttributes, $standardAttributes)); + + if ($customer && $overlap !== []) { + $attribute = Attribute::find($overlap[0]); + $label = new ResolvedLabel($customer, ResolvedLabel::SOURCE_CUSTOMER, $position, $customer->size_cm ?: $attribute?->variantSize() ?: 0.0, $attribute?->variantVolume() ?? 0.0, $attribute?->id); + if (! $label->fileExists()) { + $result->addError('Kundenlogo (Bild '.$customer->id.', Position '.$position.'): Datei fehlt. Kein Rückfall auf das Standardetikett.'); + + return null; + } + if ($attribute) { + $result->color = $attribute->colorName(); + $result->variantId = $attribute->id; + $result->volumeMl = $attribute->variantVolume(); + $result->sizeCm = $attribute->variantSize(); + $result->colorSource = ResolvedItem::COLOR_SOURCE_LEGACY; + } + + return $label; + } + + if ($customer) { + $result->addWarning('Kundenlogo (Bild '.$customer->id.', Position '.$position.') greift nicht: keine Variantenüberschneidung mit dem Standardetikett.'); + } + if (! $standard) { + return null; + } + + // Standard: erstes Attribut des Etiketts, sonst Produktvarianten (wie bisher) + $attribute = $standardAttributes !== [] ? Attribute::find($standardAttributes[0]) : null; + if (! $attribute) { + $attribute = $product->attribute_variants->first()?->attribute; + } + $size = $standard->size_cm ?: ($attribute?->variantSize() ?? 0.0); + if ($size <= 0) { + $result->addError('Standardetikett (Bild '.$standard->id.', Position '.$position.'): Etikettgröße nicht bestimmbar.'); + + return null; + } + $label = new ResolvedLabel($standard, ResolvedLabel::SOURCE_STANDARD, $position, $size, $attribute?->variantVolume() ?? 0.0, $attribute?->id); + if (! $label->fileExists()) { + $result->addError('Standardetikett (Bild '.$standard->id.', Position '.$position.'): Datei fehlt.'); + + return null; + } + $result->volumeMl = $label->volumeMl; + $result->sizeCm = $size; + + return $label; + } + + private function sizeFor(ProductImage $image, float $variantSize, ?ProductImage $fallback): float + { + if ($image->size_cm) { + return (float) $image->size_cm; + } + if ($variantSize > 0) { + return $variantSize; + } + + return $fallback && $fallback->size_cm ? (float) $fallback->size_cm : 0.0; + } + + /** + * @param iterable $images + * @return array + */ + private function groupByPosition(iterable $images, ResolvedItem $result, string $kind): array + { + $byPosition = []; + foreach ($images as $image) { + $position = $image->position ?: self::DEFAULT_POSITION; + if (isset($byPosition[$position])) { + $result->addError($kind.': mehrere Bilder für Position "'.$position.'" (Bilder '.$byPosition[$position]->id.' und '.$image->id.'). Keine Auswahl nach Reihenfolge.'); + + continue; + } + $byPosition[$position] = $image; + } + + return $byPosition; + } + + private function findAssignment(?User $partner, int $productId): ?UserWhitelabelProduct + { + if (! $partner) { + return null; + } + + return $partner->whitelabel_products()->where('product_id', $productId)->where('active', true)->first(); + } + + private function partnerHasAssignments(User $partner): bool + { + return $partner->whitelabel_products()->where('active', true)->exists(); + } +} diff --git a/database/migrations/2026_09_17_110000_add_whitelabel_variant_and_label_position.php b/database/migrations/2026_09_17_110000_add_whitelabel_variant_and_label_position.php new file mode 100644 index 0000000..e7721ee --- /dev/null +++ b/database/migrations/2026_09_17_110000_add_whitelabel_variant_and_label_position.php @@ -0,0 +1,80 @@ +unsignedInteger('whitelabel_variant_id')->nullable()->after('whitelabel_name'); + $table->foreign('whitelabel_variant_id')->references('id')->on('attributes')->nullOnDelete(); + }); + + Schema::table('product_images', function (Blueprint $table) { + $table->string('position', 20)->nullable()->after('attributes'); + $table->decimal('size_cm', 5, 2)->nullable()->after('position'); + }); + + $this->backfill(); + } + + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + $table->dropForeign(['whitelabel_variant_id']); + $table->dropColumn('whitelabel_variant_id'); + }); + Schema::table('product_images', function (Blueprint $table) { + $table->dropColumn(['position', 'size_cm']); + }); + } + + private function backfill(): void + { + DB::table('product_images')->whereIn('type', ['wllogo', 'uwllogo'])->whereNull('position')->update(['position' => 'deckel']); + + $attributes = DB::table('attributes')->where('attribute_type_id', 1)->get()->keyBy('id'); + $normalize = fn (string $v): string => str_replace('ß', 'ss', mb_strtolower(trim($v))); + + foreach (DB::table('products')->where('whitelabel', true)->get() as $product) { + if (! preg_match('/\(Tiegel\s+([^)]+)\)/iu', (string) $product->name, $m)) { + continue; + } + $color = $normalize($m[1]); + $variantIds = DB::table('product_attributes')->where('product_id', $product->id)->where('type_id', 1)->pluck('attribute_id'); + $match = $variantIds->first(function ($id) use ($attributes, $normalize, $color) { + $attribute = $attributes->get($id); + + return $attribute && $normalize(strtok((string) $attribute->name, ' ') ?: '') === $color; + }); + if ($match) { + DB::table('products')->where('id', $product->id)->update(['whitelabel_variant_id' => $match]); + } + } + + foreach (DB::table('product_images')->where('type', 'wllogo')->whereNull('size_cm')->get() as $image) { + $ids = json_decode((string) $image->attributes, true); + $first = is_array($ids) && $ids !== [] ? $attributes->get((int) reset($ids)) : null; + if ($first && $first->value) { + [, $size] = Attribute::parseVariantValue($first->value); + if ($size > 0) { + DB::table('product_images')->where('id', $image->id)->update(['size_cm' => $size]); + } + } + } + } +}; diff --git a/dev/white-label/analyse-und-entwicklungsplan-2026-09-17.md b/dev/white-label/analyse-und-entwicklungsplan-2026-09-17.md index 2f5809d..a3b4e62 100644 --- a/dev/white-label/analyse-und-entwicklungsplan-2026-09-17.md +++ b/dev/white-label/analyse-und-entwicklungsplan-2026-09-17.md @@ -2,7 +2,7 @@ Stand: 17.09.2026 (ergänzt nach Gegenprüfung gegen Code und Datenbank am selben Tag). Grundlage: Prüfung des vorhandenen Codes, der angeschlossenen Datenbank und des gespeicherten Lieferscheins zur Bestellung 2050. -**Status:** Analyse und Entwicklungsplan. Das Sofortpaket WL-00 und die Bestandsprüfung WL-01 sind umgesetzt (siehe Abschnitt 5 und 8). Die Pakete WL-02 bis WL-05 sind noch nicht implementiert. +**Status:** Analyse und Entwicklungsplan. WL-00, WL-01 und WL-02 sind umgesetzt (siehe Abschnitt 5 und 8). WL-03 bis WL-05 sind noch nicht implementiert; WL-03 hängt an den Klärungen K6 und K7 (Abschnitt 9). ## 1. Ergebnis der Untersuchung @@ -172,7 +172,7 @@ Alte Bestellungen nicht pauschal mit heutigen Stammdaten überschreiben. Bestehe | --- | --- | --- | --- | | WL-00: Sofortpaket | Etikettgröße korrekt parsen (Schwachstelle 10). Produktkopie mit ausdrücklicher Option „Kundenzuordnungen übernehmen“. Befehl `whitelabel:transfer-assignments` zur wiederholbaren Übertragung von Kundenzuordnungen zwischen Produkten (Abschnitt 6). | Kopie und Übertragung erzeugen keine Dubletten; 40-ml-Etiketten sind 3,5 cm groß. | umgesetzt 17.09.2026 | | WL-01: Bestandsprüfung | Befehl `whitelabel:report`: Bestellungen ohne Kundenzuordnung bei Partnern mit Logos, Farbwidersprüche zwischen Kundenbild und Produkt, fehlende Dateien, Dubletten, Mehrfachetiketten, Bilder ohne Variantenzuordnung. Rein lesend. | Jeder Konflikt ist mit betroffenen IDs nachvollziehbar; keine automatische Bereinigung. | umgesetzt 17.09.2026 | -| WL-02: Datenmodell und Auflösung | Eindeutige Produktvariante, Etikettposition/Größe am Bild, Zuordnungsregeln und gemeinsamen Resolver ergänzen. Eindeutige Altdaten übernehmen, Konflikte zur Prüfung vorlegen. `attribute_variants` stilllegen. | Gleiche Eingaben ergeben dieselbe Farbe und dieselben Etiketten; keine Auswahl nach Reihenfolge. | offen | +| WL-02: Datenmodell und Auflösung | Eindeutige Produktvariante, Etikettposition/Größe am Bild, Zuordnungsregeln und gemeinsamen Resolver ergänzen. Eindeutige Altdaten übernehmen, Konflikte zur Prüfung vorlegen. `attribute_variants` stilllegen. | Gleiche Eingaben ergeben dieselbe Farbe und dieselben Etiketten; keine Auswahl nach Reihenfolge. | umgesetzt 17.09.2026 (Stilllegung `attribute_variants` wartet auf K6) | | WL-03: Bestellung und Lieferschein | Datenstand je Position speichern, Resolver anbinden, Farbe und Etikettquelle getrennt ausgeben, Kennzeichen im Bestellablauf ableiten, Hinweis „keine Kundenzuordnung“ in Bestelldetail und WaWi-Auftrag. | Vorschau und PDF stimmen überein; bestehende Dokumente ändern sich nicht unbemerkt. | offen | | WL-04: Verwaltungsbereich | Navigation „White Label“, beide Ansichten, Anlage/Bearbeitung, Vorschau und Übertragung in der Oberfläche. Bestehende Admin-Berechtigungen und `user`-Guard verwenden. | Kundenlogos und Farbprodukte sind eindeutig erkennbar und zentral pflegbar. Alte Bearbeitungswege verwenden dieselben Regeln oder verweisen auf den neuen Bereich. | offen | | WL-05: Migration und Abnahme | Bestandskonflikte fachlich auflösen, Regressionstests und manuelle PDF-Prüfung. | Standardfälle bleiben korrekt; alle Befunde aus `whitelabel:report` sind entweder behoben oder bewusst akzeptiert. | offen | @@ -251,6 +251,16 @@ Diese Korrektur löst die konkreten Fälle, ersetzt aber nicht die Bereinigung d **Auf Live noch auszuführen:** `php artisan migrate`, `whitelabel:migrate-colors --execute` nach Backup und Freigabe, danach Lieferschein 2050 über „Rechnung erstellen“ neu erzeugen (ohne Mailversand). +## 8a. Umsetzung WL-02 (17.09.2026) + +- Migration `2026_09_17_110000_add_whitelabel_variant_and_label_position`: `products.whitelabel_variant_id` (FK auf `attributes`, nullOnDelete), `product_images.position` (deckel/seite/boden) und `product_images.size_cm`. Backfill: Variante aus „(Tiegel X)“ im Produktnamen + gleichfarbige Produktvariante (alle 23 Tattoocreme-Produkte belegt, 7 Produkte ohne Farbe bleiben leer → K5); alle Etikettbilder auf Position `deckel`; Standardetiketten mit Größe aus dem Variantenwert. +- `Attribute::parseVariantValue()`, `variantVolume()`, `variantSize()`, `colorName()` („Schwarz 40 ml“ → „Schwarz“); `InvoiceRepository::parseVariantValue()` delegiert dorthin. +- `App\Services\WhiteLabel\WhiteLabelResolver` mit `ResolvedItem`/`ResolvedLabel`: liest nur, liefert je Bestellposition Farbe, Etikett je Position, Quelle (Kundenlogo/Standard), Fehler und Hinweise; Regeln aus Abschnitt 4.2. Produkte ohne `whitelabel_variant_id` laufen über die Altlogik (Attributüberschneidung), damit K5-Produkte weiter funktionieren. +- Ergebnis gegen Live-Daten: 2050 vollständig (Kundenlogo, Schwarz, 3,5 cm / 2 cm); 1880 Standard mit Hinweis „keine Kundenzuordnung“; 1729 (alte Bestellung auf 37/84) meldet den GOLD/Schwarz-Konflikt als Fehler → genau die Entscheidung K6. +- Produktformular: Feld „Tiegelfarbe / Größe (verbindlich)“, je Standardetikett Position und Größe (cm). Kundenformular: Position je Kundenlogo, Anzeige der Produktfarbe. `attribute_variants` bleibt bis K6 sichtbar. +- Noch **nicht** angebunden: Der Lieferschein nutzt weiterhin `prepairForDelivery()`. Die Umstellung ist WL-03 und braucht K6 (Umgang mit Konflikten auf den alten Artikeln: blockieren oder nur warnen) und K7 (Textformat). +- Tests: `tests/Feature/WhiteLabelResolverTest.php` (12 Tests: alle Fälle aus „Erforderliche Prüfungen“ außer PDF-Sichtprüfung). + ## 9. Offene Klärungen mit dem Kunden Stand 17.09.2026, abgeleitet aus `whitelabel:report` und `whitelabel:migrate-colors` (Dry-Run). Ohne diese Antworten werden keine Daten auf Live geändert. diff --git a/resources/views/admin/lead/m_white_label.blade.php b/resources/views/admin/lead/m_white_label.blade.php index 54b41dc..6859013 100644 --- a/resources/views/admin/lead/m_white_label.blade.php +++ b/resources/views/admin/lead/m_white_label.blade.php @@ -43,7 +43,10 @@ -

{{ $wlp->product->getWhiteLableName() }}

+

{{ $wlp->product->name }} {{ $wlp->product->wp_number }}

+ @if($wlp->product->whitelabel_variant) +
Tiegel: {{ $wlp->product->whitelabel_variant->colorName() }} Farbe kommt vom Produkt; das Logo wird an seiner Position eingesetzt.
+ @endif @@ -60,6 +63,16 @@ +
+
+ + +
+
Etikett löschen @endforeach diff --git a/resources/views/admin/product/form.blade.php b/resources/views/admin/product/form.blade.php index 3969888..c2e9be1 100755 --- a/resources/views/admin/product/form.blade.php +++ b/resources/views/admin/product/form.blade.php @@ -465,6 +465,15 @@ {{ Form::text('whitelabel_name', $product->whitelabel_name, ['placeholder' => __('Bezeichnung'), 'class' => 'form-control', 'id' => 'whitelabel_name']) }} +
+ + +
+ Bestimmt die Farbe auf dem Lieferschein („Tiegel: Schwarz“). Ohne Auswahl wird die Farbe wie bisher aus dem Etikett abgeleitet. +
+
@@ -497,6 +506,20 @@ multiple> {!! HTMLHelper::getProductAttributesOptions($product->attribute_variants, $image->attributes, false, 1) !!} +
+
+ + +
+
+ + {{ Form::text('image_wl_size['.$image->id.']', $image->size_cm !== null ? formatNumber($image->size_cm, 1) : '', ['class' => 'form-control', 'id' => 'image_wl_size_'.$image->id, 'placeholder' => 'aus Variante']) }} +
+
Etikett löschen diff --git a/tests/Feature/WhiteLabelResolverTest.php b/tests/Feature/WhiteLabelResolverTest.php new file mode 100644 index 0000000..baaee61 --- /dev/null +++ b/tests/Feature/WhiteLabelResolverTest.php @@ -0,0 +1,275 @@ +create(['email' => uniqid('wlr_', true).'@test.example', 'password' => bcrypt('password')]); + $user->forceFill(['admin' => $admin, 'confirmed' => true, 'active' => true, 'wizard' => 100, 'blocked' => false])->save(); + + return $user->fresh(); +} + +function wlrVariant(string $name, string $value): Attribute +{ + $type = AttributeType::query()->firstOrCreate(['id' => 1], ['name' => 'Variante', 'slug' => 'variante']); + + return Attribute::query()->create(['attribute_type_id' => $type->id, 'name' => $name, 'value' => $value, 'slug' => uniqid('v_'), 'active' => true, 'pos' => 0]); +} + +function wlrProduct(string $name, ?Attribute $variant, array $possibleVariants = []): Product +{ + $product = Product::query()->create(['name' => $name, 'title' => $name, 'active' => true, 'is_set' => false, 'whitelabel' => true, 'whitelabel_variant_id' => $variant?->id]); + foreach ($possibleVariants as $attribute) { + ProductAttribute::create(['product_id' => $product->id, 'type_id' => 1, 'attribute_id' => $attribute->id]); + } + + return $product; +} + +function wlrStandardLabel(Product $product, array $attributes, ?float $sizeCm = null, string $position = 'deckel', bool $withFile = true): ProductImage +{ + $name = uniqid('std_').'.png'; + if ($withFile) { + Storage::disk('public')->put('images/product/'.$product->id.'/'.$name, 'png'); + } + + return ProductImage::create(['product_id' => $product->id, 'type' => 'wllogo', 'filename' => $name, 'original_name' => $name, 'ext' => 'png', 'mine' => 'image/png', 'size' => 3, 'pos' => 0, 'active' => true, 'attributes' => $attributes, 'position' => $position, 'size_cm' => $sizeCm]); +} + +function wlrAssignment(User $user, Product $product, array $attributes, string $position = 'deckel', bool $withFile = true): UserWhitelabelProduct +{ + $assignment = UserWhitelabelProduct::firstOrCreate(['user_id' => $user->id, 'product_id' => $product->id], ['active' => true]); + $name = uniqid('logo_').'.png'; + if ($withFile) { + Storage::disk('public')->put('images/user_product/'.$assignment->id.'/'.$name, 'png'); + } + ProductImage::create(['user_wl_product_id' => $assignment->id, 'type' => 'uwllogo', 'filename' => $name, 'original_name' => $name, 'ext' => 'png', 'mine' => 'image/png', 'size' => 3, 'pos' => 0, 'active' => true, 'attributes' => $attributes, 'position' => $position]); + + return $assignment->fresh(); +} + +function wlrOrder(User $partner, array $products): ShoppingOrder +{ + $country = Country::query()->firstOrCreate(['code' => 'DE'], ['en' => 'Germany', 'de' => 'Deutschland', 'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Germanija']); + $shoppingUser = ShoppingUser::query()->forceCreate(['billing_country_id' => $country->id, 'shipping_country_id' => $country->id, 'billing_firstname' => 'Test', 'billing_lastname' => 'Partner', 'billing_address' => 'Weg 1', 'billing_zipcode' => '11111', 'billing_city' => 'Stadt', 'same_as_billing' => true]); + $shipping = Shipping::query()->firstOrCreate(['name' => 'Standard']); + $shippingCountry = ShippingCountry::query()->firstOrCreate(['shipping_id' => $shipping->id, 'country_id' => $country->id]); + $order = ShoppingOrder::query()->forceCreate(['shopping_user_id' => $shoppingUser->id, 'auth_user_id' => $partner->id, 'country_id' => $shippingCountry->id, 'shipped' => 0, 'txaction' => 'paid', 'total' => 10, 'user_white_label' => false]); + foreach ($products as $product) { + ShoppingOrderItem::query()->forceCreate(['shopping_order_id' => $order->id, 'product_id' => $product->id, 'qty' => 1, 'price' => 10]); + } + + return $order->fresh(); +} + +beforeEach(function () { + Storage::fake('public'); + $this->schwarz40 = wlrVariant('Schwarz 40 ml', '40#3,5'); + $this->gold40 = wlrVariant('Gold 40 ml', '40#3,5'); + $this->schwarz15 = wlrVariant('Schwarz 15 ml', '15#2'); +}); + +test('kundenlogo mit vorhandener datei ergibt kundenlogo und produktfarbe', function () { + $partner = wlrUser(); + $black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40); + wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5); + wlrAssignment($partner, $black, [(string) $this->schwarz40->id]); + + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first(); + + expect($result)->toBeInstanceOf(ResolvedItem::class) + ->and($result->isComplete())->toBeTrue() + ->and($result->color)->toBe('Schwarz') + ->and($result->colorSource)->toBe(ResolvedItem::COLOR_SOURCE_VARIANT) + ->and($result->hasCustomerLogo())->toBeTrue() + ->and($result->labels['deckel']->sizeCm)->toBe(3.5) + ->and($result->labels['deckel']->volumeMl)->toBe(40.0) + ->and($result->colorLine())->toBe('Tiegel: Schwarz') + ->and($result->labelSourceLine())->toBe('Etikett: Kundenlogo'); +}); + +test('standardetikett auf schwarzem produkt behaelt die produktfarbe und meldet fehlende zuordnung nur als hinweis', function () { + $partner = wlrUser(); + $black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40); + $other = wlrProduct('Creme 15 ml (Tiegel SCHWARZ)', $this->schwarz15); + wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5); + wlrAssignment($partner, $other, [(string) $this->schwarz15->id]); + + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first(); + + expect($result->isComplete())->toBeTrue() + ->and($result->color)->toBe('Schwarz') + ->and($result->hasCustomerLogo())->toBeFalse() + ->and($result->labelSourceLine())->toBe('Etikett: Standardetikett') + ->and($result->warnings)->toHaveCount(1) + ->and($result->warnings[0])->toContain('keine Kundenzuordnung'); +}); + +test('kundenlogo ohne standardetikett ist zulaessig wenn die groesse aus der variante kommt', function () { + $partner = wlrUser(); + $black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40); + wlrAssignment($partner, $black, []); + + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first(); + + expect($result->isComplete())->toBeTrue() + ->and($result->hasCustomerLogo())->toBeTrue() + ->and($result->labels['deckel']->sizeCm)->toBe(3.5); +}); + +test('fehlende logodatei ist ein fehler ohne rueckfall auf das standardetikett', function () { + $partner = wlrUser(); + $black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40); + wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5); + wlrAssignment($partner, $black, [(string) $this->schwarz40->id], 'deckel', false); + + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first(); + + expect($result->isComplete())->toBeFalse() + ->and($result->labels)->toBe([]) + ->and($result->errors[0])->toContain('Datei fehlt'); +}); + +test('farbwiderspruch zwischen kundenbild und produkt ist ein fehler', function () { + $partner = wlrUser(); + $gold = wlrProduct('Creme 40 ml (Tiegel GOLD)', $this->gold40); + wlrStandardLabel($gold, [(string) $this->gold40->id], 3.5); + wlrAssignment($partner, $gold, [(string) $this->schwarz40->id]); + + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$gold]))->first(); + + expect($result->isComplete())->toBeFalse() + ->and($result->errors)->toHaveCount(1) + ->and($result->errors[0])->toContain('für Schwarz hinterlegt, Produktfarbe ist Gold'); +}); + +test('mehrere kundenlogos auf derselben position sind ein konflikt ohne auswahl nach reihenfolge', function () { + $partner = wlrUser(); + $black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40); + wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5); + wlrAssignment($partner, $black, [(string) $this->schwarz40->id]); + wlrAssignment($partner, $black, [(string) $this->schwarz40->id]); + + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first(); + + expect($result->isComplete())->toBeFalse() + ->and($result->errors[0])->toContain('mehrere Bilder für Position "deckel"'); +}); + +test('positionen bleiben getrennt: kundenlogo am deckel ersetzt nicht das bodenetikett', function () { + $partner = wlrUser(); + $black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40); + wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5, 'deckel'); + $boden = wlrStandardLabel($black, [], 4.7, 'boden'); + wlrAssignment($partner, $black, [(string) $this->schwarz40->id], 'deckel'); + + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$black]))->first(); + + expect($result->isComplete())->toBeTrue() + ->and($result->labels)->toHaveCount(2) + ->and($result->labels['deckel']->isCustomer())->toBeTrue() + ->and($result->labels['boden']->isCustomer())->toBeFalse() + ->and($result->labels['boden']->image->id)->toBe($boden->id) + ->and($result->labels['boden']->sizeCm)->toBe(4.7) + ->and($result->maxLabelSize())->toBe(4.7); +}); + +test('altlogik ohne verbindliche variante: kundenlogo greift nur bei attributueberschneidung', function () { + $partner = wlrUser(); + $legacy = wlrProduct('Tattoobalm 30 ml', null, [$this->schwarz40, $this->gold40]); + wlrStandardLabel($legacy, [(string) $this->schwarz40->id, (string) $this->gold40->id]); + + $noOverlap = wlrAssignment($partner, $legacy, [(string) $this->schwarz15->id]); + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$legacy]))->first(); + expect($result->isComplete())->toBeTrue() + ->and($result->hasCustomerLogo())->toBeFalse() + ->and($result->color)->toBe('Standard') + ->and($result->colorSource)->toBe(ResolvedItem::COLOR_SOURCE_LEGACY) + ->and($result->labels['deckel']->sizeCm)->toBe(3.5) + ->and(collect($result->warnings)->first(fn ($w) => str_contains($w, 'greift nicht')))->not->toBeNull(); + + $noOverlap->whitelabel_images()->update(['attributes' => json_encode([(string) $this->schwarz40->id])]); + $result = (new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$legacy]))->first(); + expect($result->hasCustomerLogo())->toBeTrue() + ->and($result->color)->toBe('Schwarz') + ->and($result->volumeMl)->toBe(40.0); +}); + +test('resolver schreibt weder bestellung noch kennzeichen', function () { + $partner = wlrUser(); + $black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', $this->schwarz40); + wlrStandardLabel($black, [(string) $this->schwarz40->id], 3.5); + wlrAssignment($partner, $black, [(string) $this->schwarz40->id]); + $order = wlrOrder($partner, [$black]); + + (new WhiteLabelResolver)->resolveOrder($order); + + expect($order->fresh()->user_white_label)->toBeFalse(); +}); + +test('nicht-white-label-positionen werden nicht aufgeloest', function () { + $partner = wlrUser(); + $plain = Product::query()->create(['name' => 'Seife', 'title' => 'Seife', 'active' => true, 'is_set' => false, 'whitelabel' => false]); + + expect((new WhiteLabelResolver)->resolveOrder(wlrOrder($partner, [$plain])))->toHaveCount(0); +}); + +test('produktformular speichert verbindliche variante sowie position und groesse je etikett', function () { + $black = wlrProduct('Creme 40 ml (Tiegel SCHWARZ)', null); + $label = wlrStandardLabel($black, [(string) $this->schwarz40->id]); + + (new ProductRepository($black))->update([ + 'id' => (string) $black->id, + 'name' => $black->name, + 'title' => $black->title, + 'active' => '1', + 'whitelabel' => '1', + 'whitelabel_variant_id' => (string) $this->schwarz40->id, + 'whitelabel_variants' => [(string) $this->schwarz40->id], + 'image_wl_attributes' => [$label->id => [(string) $this->schwarz40->id]], + 'image_wl_position' => [$label->id => 'boden'], + 'image_wl_size' => [$label->id => '4,7'], + ]); + + $black->refresh(); + $label->refresh(); + expect($black->whitelabel_variant_id)->toBe($this->schwarz40->id) + ->and($black->whitelabel_variant->colorName())->toBe('Schwarz') + ->and($label->position)->toBe('boden') + ->and($label->size_cm)->toBe(4.7); + + $this->actingAs(wlrUser(9), 'user'); + $this->get(route('admin_product_edit', $black->id)) + ->assertOk() + ->assertSee('Tiegelfarbe / Größe (verbindlich)') + ->assertSee('Etikettposition'); +}); + +test('attribut-farbname trennt die groesse ab', function () { + expect($this->schwarz40->colorName())->toBe('Schwarz') + ->and(wlrVariant('Braunglasflasche 30 ml', '30#5')->colorName())->toBe('Braunglasflasche') + ->and(wlrVariant('Bodenetikett', '40#3,5')->colorName())->toBe('Bodenetikett') + ->and($this->schwarz40->variantSize())->toBe(3.5) + ->and($this->schwarz40->variantVolume())->toBe(40.0); +});