White Label WL-02: verbindliche Tiegelfarbe, Etikettposition und gemeinsamer Resolver
- products.whitelabel_variant_id, product_images.position/size_cm mit Backfill aus Produktnamen - WhiteLabelResolver: Farbe vom Produkt, Kundenlogo je Position, Fehler statt stillem Rückfall; Altlogik für Produkte ohne Variante - Attribute: Variantenwert-Parsing und Farbname ohne Größe - Produkt- und Kundenformular um Variante, Position und Größe ergänzt - Lieferschein noch nicht umgestellt (WL-03, wartet auf K6/K7) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0dc3dea85e
commit
cff749b133
14 changed files with 1159 additions and 280 deletions
|
|
@ -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 '<a href="' . route('admin_lead_edit', [$user->id]) . '" class="btn icon-btn btn-sm btn-primary"><span class="far fa-edit"></span></a>';
|
||||
return '<a href="'.route('admin_lead_edit', [$user->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="far fa-edit"></span></a>';
|
||||
})
|
||||
->addColumn('confirmed', function (User $user) {
|
||||
return $user->confirmed ? '<span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span>' : '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span>';
|
||||
|
|
@ -400,7 +418,7 @@ class LeadController extends Controller
|
|||
return $user->payment_account ? ' <span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span>' : '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span>';
|
||||
})
|
||||
->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 ? ' <span class="badge badge-pill badge-success"><i class="fa fa-check"></i></span>' : '<span class="badge badge-pill badge-danger"><i class="fa fa-times"></i></span>';
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ', ');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<int, string> $positions Bild-ID => Position
|
||||
* @param array<int, string> $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'])) {
|
||||
|
|
|
|||
92
app/Services/WhiteLabel/ResolvedItem.php
Normal file
92
app/Services/WhiteLabel/ResolvedItem.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\WhiteLabel;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
|
||||
/**
|
||||
* Auflösungsergebnis einer Bestellposition: Tiegelfarbe, Etiketten je Position, Fehler und Hinweise (WL-02).
|
||||
* Fehler verhindern die Freigabe des Lieferscheins, Hinweise nicht.
|
||||
*/
|
||||
final class ResolvedItem
|
||||
{
|
||||
public const COLOR_SOURCE_VARIANT = 'variant';
|
||||
|
||||
public const COLOR_SOURCE_LEGACY = 'legacy';
|
||||
|
||||
public const COLOR_SOURCE_NONE = 'none';
|
||||
|
||||
public ?string $color = null;
|
||||
|
||||
public ?int $variantId = null;
|
||||
|
||||
public float $volumeMl = 0.0;
|
||||
|
||||
public float $sizeCm = 0.0;
|
||||
|
||||
public string $colorSource = self::COLOR_SOURCE_NONE;
|
||||
|
||||
/** @var array<string, ResolvedLabel> Position => Etikett */
|
||||
public array $labels = [];
|
||||
|
||||
/** @var list<string> */
|
||||
public array $errors = [];
|
||||
|
||||
/** @var list<string> */
|
||||
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;
|
||||
}
|
||||
}
|
||||
40
app/Services/WhiteLabel/ResolvedLabel.php
Normal file
40
app/Services/WhiteLabel/ResolvedLabel.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\WhiteLabel;
|
||||
|
||||
use App\Models\ProductImage;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Ein aufgelöstes Etikett für eine Position (WL-02).
|
||||
*/
|
||||
final class ResolvedLabel
|
||||
{
|
||||
public const SOURCE_CUSTOMER = 'customer';
|
||||
|
||||
public const SOURCE_STANDARD = 'standard';
|
||||
|
||||
public function __construct(
|
||||
public readonly ProductImage $image,
|
||||
public readonly string $source,
|
||||
public readonly string $position,
|
||||
public readonly float $sizeCm,
|
||||
public readonly float $volumeMl,
|
||||
public readonly ?int $attributeId = null,
|
||||
) {}
|
||||
|
||||
public function isCustomer(): bool
|
||||
{
|
||||
return $this->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(), '/'));
|
||||
}
|
||||
}
|
||||
254
app/Services/WhiteLabel/WhiteLabelResolver.php
Normal file
254
app/Services/WhiteLabel/WhiteLabelResolver.php
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\WhiteLabel;
|
||||
|
||||
use App\Models\Attribute;
|
||||
use App\Models\ProductImage;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\UserWhitelabelProduct;
|
||||
use App\User;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Gemeinsame Auflösung von Tiegelfarbe und Etiketten je Bestellposition (WL-02, Abschnitt 4.2 des Plans).
|
||||
*
|
||||
* Grundregel: Das Produkt bestimmt die Farbe (whitelabel_variant), Kunde + Produkt bestimmen die
|
||||
* Kundenetiketten, die Position bestimmt, welches Standardetikett ersetzt wird.
|
||||
* Produkte ohne verbindliche Variante (Klärung K5) laufen über die Altlogik (Attributüberschneidung).
|
||||
*
|
||||
* Der Resolver liest nur; er speichert keine Bestellung und erzeugt keine PDF.
|
||||
*/
|
||||
class WhiteLabelResolver
|
||||
{
|
||||
public const DEFAULT_POSITION = 'deckel';
|
||||
|
||||
/**
|
||||
* @return Collection<int, ResolvedItem> 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<ProductImage> $images
|
||||
* @return array<string, ProductImage>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Attribute;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* WL-02: verbindliche Tiegelfarbe/Größe je White-Label-Produkt und Etikettposition/Größe je Etikettbild.
|
||||
*
|
||||
* Backfill:
|
||||
* - products.whitelabel_variant_id aus dem Farbnamen "(Tiegel X)" + Variante des Produkts mit gleicher Farbe.
|
||||
* Produkte ohne erkennbare Farbe bleiben leer (Klärung K5) und laufen weiter über die Altlogik.
|
||||
* - product_images.position = 'deckel' (im Bestand hat kein Produkt mehr als ein Etikett),
|
||||
* size_cm bei Standardetiketten aus dem Variantenwert "Volumen#Größe".
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@
|
|||
<div class="float-right">
|
||||
<a href="{{ route('admin_lead_remove', ['remove_whitelabel_product', $user->id, $wlp->id]) }}?show={{ $show }}" class="btn btn-sm btn-danger mt-2 mb-2" onclick="return confirm('Produkt wirklich entfernen?');">Produkt entfernen</a>
|
||||
</div>
|
||||
<h4>{{ $wlp->product->getWhiteLableName() }}</h4>
|
||||
<h4>{{ $wlp->product->name }} <small class="text-muted">{{ $wlp->product->wp_number }}</small></h4>
|
||||
@if($wlp->product->whitelabel_variant)
|
||||
<div class="mb-2"><span class="badge badge-dark">Tiegel: {{ $wlp->product->whitelabel_variant->colorName() }}</span> <small class="text-muted">Farbe kommt vom Produkt; das Logo wird an seiner Position eingesetzt.</small></div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -60,6 +63,16 @@
|
|||
<select class="selectpicker" name="image_wl_attributes[{{ $wlp_image->id }}][]" id="image_wl_attributes_{{ $wlp_image->id }}" data-style="btn-light" data-live-search="true" multiple>
|
||||
{!! HTMLHelper::getProductAttributesOptions($wlp->product->attribute_variants, $wlp_image->attributes, false, 1) !!}
|
||||
</select>
|
||||
<div class="form-row mt-2">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="form-label" for="image_wl_position_{{ $wlp_image->id }}">Etikettposition</label>
|
||||
<select class="form-control" name="image_wl_position[{{ $wlp_image->id }}]" id="image_wl_position_{{ $wlp_image->id }}">
|
||||
@foreach (\App\Models\ProductImage::POSITIONS as $key => $label)
|
||||
<option value="{{ $key }}" {{ ($wlp_image->position ?: 'deckel') === $key ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('admin_lead_remove', ['remove_whitelabel_label', $user->id, $wlp->id, $wlp_image->id]) }}?show={{ $show }}" class="btn btn-sm btn-danger mt-2 mb-2" onclick="return confirm('Etikett wirklich löschen?');">Etikett löschen</a>
|
||||
</div>
|
||||
@endforeach
|
||||
|
|
|
|||
|
|
@ -465,6 +465,15 @@
|
|||
<label class="form-label" for="whitelabel_name">{{ __('White-Label Bezeichnung') }}</label>
|
||||
{{ Form::text('whitelabel_name', $product->whitelabel_name, ['placeholder' => __('Bezeichnung'), 'class' => 'form-control', 'id' => 'whitelabel_name']) }}
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label" for="whitelabel_variant_id">Tiegelfarbe / Größe (verbindlich)</label>
|
||||
<select class="selectpicker" name="whitelabel_variant_id" id="whitelabel_variant_id" data-style="btn-light" data-live-search="true">
|
||||
{!! HTMLHelper::getAttributesOptions($product->whitelabel_variant_id ? [$product->whitelabel_variant_id] : [], true, 1) !!}
|
||||
</select>
|
||||
<div class="mt-2">
|
||||
<em>Bestimmt die Farbe auf dem Lieferschein („Tiegel: Schwarz“). Ohne Auswahl wird die Farbe wie bisher aus dem Etikett abgeleitet.</em>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label class="form-label" for="whitelabel_variants">Mögliche Varianten (<a
|
||||
href="{{ route('admin_product_attributes') }}">Anlage unter Attribute</a>)</label>
|
||||
|
|
@ -497,6 +506,20 @@
|
|||
multiple>
|
||||
{!! HTMLHelper::getProductAttributesOptions($product->attribute_variants, $image->attributes, false, 1) !!}
|
||||
</select>
|
||||
<div class="form-row mt-2">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="form-label" for="image_wl_position_{{ $image->id }}">Etikettposition</label>
|
||||
<select class="form-control" name="image_wl_position[{{ $image->id }}]" id="image_wl_position_{{ $image->id }}">
|
||||
@foreach (\App\Models\ProductImage::POSITIONS as $key => $label)
|
||||
<option value="{{ $key }}" {{ ($image->position ?: 'deckel') === $key ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="form-label" for="image_wl_size_{{ $image->id }}">Etikettgröße (cm)</label>
|
||||
{{ 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']) }}
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('admin_product_image_delete', [$image->id, $product->id]) }}"
|
||||
class="btn btn-sm btn-danger mt-2 mb-2"
|
||||
onclick="return confirm('Etikett wirklich löschen?');">Etikett löschen</a>
|
||||
|
|
|
|||
275
tests/Feature/WhiteLabelResolverTest.php
Normal file
275
tests/Feature/WhiteLabelResolverTest.php
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Attribute;
|
||||
use App\Models\AttributeType;
|
||||
use App\Models\Country;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductAttribute;
|
||||
use App\Models\ProductImage;
|
||||
use App\Models\Shipping;
|
||||
use App\Models\ShippingCountry;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingOrderItem;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserWhitelabelProduct;
|
||||
use App\Repositories\ProductRepository;
|
||||
use App\Services\WhiteLabel\ResolvedItem;
|
||||
use App\Services\WhiteLabel\WhiteLabelResolver;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function wlrUser(int $admin = 0): User
|
||||
{
|
||||
$user = User::query()->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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue