A1: Jeder Monatstag ist als Liefertag erlaubt (UserAbo::allowedDeliveryDays), Tage 29-31 liefern in kuerzeren Monaten am letzten Tag. Eine Stelle AboHelper::dateForDayInMonth klammert; setNextDate, Erstausfuehrung, Admin-Termin und Pause rechnen darueber, der Folgemonat immer aus dem Liefertag (31.01 -> 28.02 -> 31.03). Liefertag-Aenderung gilt fuer den laufenden Zyklus und nur nach vorn: ist der Tag im Referenzmonat vorbei, wird abgelehnt, der Monat verschiebt sich nie (Rueckfrage R1). Admin-Filter "naechste Ausfuehrung" als Datumsbereich. Prognose-Kacheln bekommen die Kachel Monatsende, Team-Abos rechnen bis Monatsende. A2: Sperrfristen Default 0, Setting 0 ist ausdruecklich 0 (fiel bisher auf den Default zurueck). Bedingung Kalendertage <= Sperre, zentral in AboHelper::isWithinLock: am Ausfuehrungstag ab 0:00 Uhr fixiert, Vortag bis 23:59 Uhr frei, fuer Liefertag, Produkte, Pause und Kuendigung (R2). Texte mit _zero-Varianten ueber AboHelper::lockMessage. Sprachdateien de/en/es/fr, neuer AboDeliveryDayTest, bestehende Tests angepasst, Plan-Status mit Deploy-Schritt (Settings auf 0). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
298 lines
11 KiB
PHP
298 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\UserAbo;
|
|
use App\Repositories\AboRepository;
|
|
use App\Services\AboExternalCompletionService;
|
|
use App\Services\AboItemHistoryService;
|
|
use App\Services\AboOrderCart;
|
|
use App\Services\AboRetryPaymentService;
|
|
use App\Services\HTMLHelper;
|
|
use App\Services\Shop;
|
|
use Carbon\Carbon;
|
|
use Request;
|
|
|
|
class AboController extends Controller
|
|
{
|
|
protected $aboRepository;
|
|
|
|
public function __construct(AboRepository $aboRepository)
|
|
{
|
|
$this->middleware('admin');
|
|
$this->aboRepository = $aboRepository;
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
if (Request::get('reset') === 'filter') {
|
|
foreach (['filter_member_id', 'filter_status', 'filter_state', 'filter_active', 'filter_is_for', 'filter_payment', 'filter_start_month', 'filter_start_year', 'filter_next_from', 'filter_next_to'] as $attr) {
|
|
set_user_attr($attr, null);
|
|
}
|
|
|
|
return redirect(route('admin_abos'));
|
|
}
|
|
|
|
$filter_members = UserAbo::join('users', 'user_id', '=', 'users.id')->groupBy('user_id')->join('user_accounts', 'account_id', '=', 'user_accounts.id')->select('users.id', 'users.email', 'user_accounts.first_name', 'user_accounts.last_name')->get();
|
|
|
|
$data = [
|
|
'filter_members' => $filter_members,
|
|
'filter_statuses' => $this->getStatusFilterOptions(),
|
|
'filter_months' => $this->getMonthFilterOptions(),
|
|
'filter_years' => $this->getStartYearFilterOptions(),
|
|
];
|
|
|
|
return view('admin.abo.index', $data);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function getStatusFilterOptions(): array
|
|
{
|
|
return UserAbo::query()
|
|
->select('status')
|
|
->distinct()
|
|
->orderBy('status')
|
|
->pluck('status')
|
|
->mapWithKeys(fn ($status) => [$status => __('abo.'.(UserAbo::$statusTypes[$status] ?? $status))])
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function getMonthFilterOptions(): array
|
|
{
|
|
$months = [];
|
|
for ($month = 1; $month <= 12; $month++) {
|
|
$months[$month] = Carbon::create(null, $month, 1)->locale(app()->getLocale())->translatedFormat('F');
|
|
}
|
|
|
|
return $months;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, int>
|
|
*/
|
|
private function getStartYearFilterOptions(): array
|
|
{
|
|
$minStart = UserAbo::whereNotNull('start_date')->min('start_date');
|
|
$fromYear = $minStart ? (int) substr($minStart, 0, 4) : (int) date('Y');
|
|
|
|
return range((int) date('Y'), $fromYear);
|
|
}
|
|
|
|
public function detail($id)
|
|
{
|
|
$data = Request::all();
|
|
$user_abo = UserAbo::findOrFail($id);
|
|
|
|
// init Yard
|
|
AboOrderCart::initYard($user_abo);
|
|
$customer_detail = AboOrderCart::getCustomerDetail();
|
|
AboOrderCart::makeOrderYard($user_abo);
|
|
|
|
$comp_products = [];
|
|
if ($user_abo->is_for === 'me') {
|
|
$comp_products = Shop::getCompProducts('abo-me');
|
|
}
|
|
|
|
$data = [
|
|
'user_abo' => $user_abo,
|
|
'isAdmin' => true,
|
|
'customer_detail' => $customer_detail,
|
|
'view' => $user_abo->is_for,
|
|
'comp_products' => $comp_products,
|
|
];
|
|
|
|
return view('admin.abo.detail', $data);
|
|
}
|
|
|
|
public function update($id)
|
|
{
|
|
$data = Request::all();
|
|
if (isset($data['action'])) {
|
|
if ($data['action'] === 'abo_update_settings') {
|
|
$user_abo = UserAbo::findOrFail($data['id']);
|
|
$data['view'] = 'admin';
|
|
$this->aboRepository->setModel($user_abo);
|
|
$this->aboRepository->update($data);
|
|
|
|
return redirect(route('admin_abos_detail', [$id]));
|
|
}
|
|
}
|
|
}
|
|
|
|
public function rollback($id)
|
|
{
|
|
$user_abo = UserAbo::findOrFail($id);
|
|
|
|
AboOrderCart::initYard($user_abo);
|
|
|
|
$success = AboItemHistoryService::rollbackToInitial($user_abo);
|
|
|
|
if ($success) {
|
|
$user_abo->refresh();
|
|
AboOrderCart::makeOrderYard($user_abo);
|
|
AboOrderCart::checkNumOfCompProducts($user_abo);
|
|
|
|
\Session()->flash('alert-success', __('abo_history.rollback_success'));
|
|
} else {
|
|
\Session()->flash('alert-error', __('abo_history.rollback_no_data'));
|
|
}
|
|
|
|
return redirect(route('admin_abos_detail', [$id]));
|
|
}
|
|
|
|
public function retryPayment($id, AboRetryPaymentService $retryPaymentService)
|
|
{
|
|
$user_abo = UserAbo::findOrFail($id);
|
|
$result = $retryPaymentService->retry($user_abo);
|
|
|
|
\Session()->flash($result['success'] ? 'alert-success' : 'alert-error', $result['message']);
|
|
|
|
return redirect(route('admin_abos_detail', [$id]));
|
|
}
|
|
|
|
public function externDone($id, AboExternalCompletionService $externalCompletion)
|
|
{
|
|
$user_abo = UserAbo::findOrFail($id);
|
|
$result = $externalCompletion->completeForAbo($user_abo);
|
|
|
|
\Session()->flash($result['success'] ? 'alert-success' : 'alert-error', $result['message']);
|
|
|
|
return redirect(route('admin_abos_detail', [$id]));
|
|
}
|
|
|
|
public function destroy($id)
|
|
{
|
|
$user_abo = UserAbo::findOrFail($id);
|
|
$user_abo->delete();
|
|
|
|
\Session()->flash('alert-success', __('abo.admin_delete_success'));
|
|
|
|
return redirect(route('admin_abos'));
|
|
}
|
|
|
|
public function datatable()
|
|
{
|
|
|
|
$query = UserAbo::with('user_abo_orders')->with('shopping_user')->select('user_abos.*');
|
|
|
|
set_user_attr('filter_member_id', Request::get('filter_member_id'));
|
|
if (Request::get('filter_member_id') != '') {
|
|
$query->where('user_id', '=', Request::get('filter_member_id'));
|
|
}
|
|
|
|
set_user_attr('filter_status', Request::get('filter_status'));
|
|
if (Request::get('filter_status') != '') {
|
|
$query->where('status', '=', Request::get('filter_status'));
|
|
}
|
|
|
|
set_user_attr('filter_active', Request::get('filter_active'));
|
|
if (Request::get('filter_active') != '') {
|
|
$query->where('active', '=', (int) Request::get('filter_active'));
|
|
}
|
|
|
|
// Ticket A3: Zustand (pausiert / Zahlung fehlgeschlagen) — quer zum Status,
|
|
// weil eine Pause keinen eigenen Status hat.
|
|
set_user_attr('filter_state', Request::get('filter_state'));
|
|
if (Request::get('filter_state') === 'paused') {
|
|
$query->paused();
|
|
} elseif (Request::get('filter_state') === 'payment_failed') {
|
|
$query->paymentFailed();
|
|
}
|
|
|
|
set_user_attr('filter_is_for', Request::get('filter_is_for'));
|
|
if (Request::get('filter_is_for') != '') {
|
|
$query->where('is_for', '=', Request::get('filter_is_for'));
|
|
}
|
|
|
|
set_user_attr('filter_payment', Request::get('filter_payment'));
|
|
if (Request::get('filter_payment') != '') {
|
|
$query->where('clearingtype', '=', Request::get('filter_payment'));
|
|
}
|
|
|
|
set_user_attr('filter_start_month', Request::get('filter_start_month'));
|
|
if (Request::get('filter_start_month') != '') {
|
|
$query->whereMonth('start_date', (int) Request::get('filter_start_month'));
|
|
}
|
|
|
|
set_user_attr('filter_start_year', Request::get('filter_start_year'));
|
|
if (Request::get('filter_start_year') != '') {
|
|
$query->whereYear('start_date', (int) Request::get('filter_start_year'));
|
|
}
|
|
|
|
// Ticket A1: Liefertage sind frei (1-31), daher Datumsbereich statt Tages-Dropdown.
|
|
set_user_attr('filter_next_from', Request::get('filter_next_from'));
|
|
if (Request::get('filter_next_from') != '') {
|
|
$query->whereDate('next_date', '>=', Request::get('filter_next_from'));
|
|
}
|
|
|
|
set_user_attr('filter_next_to', Request::get('filter_next_to'));
|
|
if (Request::get('filter_next_to') != '') {
|
|
$query->whereDate('next_date', '<=', Request::get('filter_next_to'));
|
|
}
|
|
|
|
return \DataTables::eloquent($query)
|
|
->addColumn('id', function (UserAbo $user_abo) {
|
|
return '<a href="'.route('admin_abos_detail', [$user_abo->id]).'" class="btn icon-btn btn-sm btn-primary"><span class="fa fa-edit"></span></a>';
|
|
})
|
|
->addColumn('start_date', function (UserAbo $user_abo) {
|
|
return $user_abo->start_date;
|
|
})
|
|
->addColumn('next_date', function (UserAbo $user_abo) {
|
|
return $user_abo->next_date;
|
|
})
|
|
->addColumn('abo_interval', function (UserAbo $user_abo) {
|
|
return HTMLHelper::getAboStrLang($user_abo->abo_interval);
|
|
})
|
|
->addColumn('status', function (UserAbo $user_abo) {
|
|
return view('admin.abo._status_explain', ['abo' => $user_abo, 'compact' => true])->render();
|
|
})
|
|
->addColumn('active', function (UserAbo $user_abo) {
|
|
return get_active_badge($user_abo->active);
|
|
})
|
|
|
|
->addColumn('is_for', function (UserAbo $user_abo) {
|
|
return $user_abo->getIsForFormated();
|
|
})
|
|
->addColumn('count', function (UserAbo $user_abo) {
|
|
return $user_abo->getCountOrders();
|
|
})
|
|
->addColumn('amount', function (UserAbo $user_abo) {
|
|
return $user_abo->getFormattedAmount().' €';
|
|
})
|
|
->addColumn('payment', function (UserAbo $user_abo) {
|
|
return $user_abo->getPaymentType();
|
|
})
|
|
->addColumn('member', function (UserAbo $user_abo) {
|
|
if (isset($user_abo->shopping_user) && $user_abo->shopping_user->member_id > 0) {
|
|
return '<a href="'.route('admin_lead_edit', [$user_abo->shopping_user->member_id]).'">'.$user_abo->shopping_user->member->getFullName().'</a>';
|
|
}
|
|
})
|
|
->addColumn('payone_userid', function (UserAbo $user_abo) {
|
|
return $user_abo->payone_userid;
|
|
})
|
|
->filterColumn('payone_userid', function ($query, $keyword) {
|
|
if ($keyword != '') {
|
|
$query->whereRaw('user_abos.payone_userid LIKE ?', ['%'.$keyword.'%']);
|
|
}
|
|
})
|
|
|
|
->orderColumn('id', 'id $1')
|
|
->orderColumn('start_date', 'start_date $1')
|
|
->orderColumn('next_date', 'next_date $1')
|
|
->orderColumn('abo_interval', 'abo_interval $1')
|
|
->orderColumn('status', 'status $1')
|
|
->orderColumn('active', 'active $1')
|
|
->orderColumn('is_for', 'is_for $1')
|
|
->orderColumn('count', '(select count(*) from user_abo_orders where user_abo_orders.user_abo_id = user_abos.id and user_abo_orders.status >= 2) $1')
|
|
->orderColumn('amount', 'amount $1')
|
|
->orderColumn('payone_userid', 'payone_userid $1')
|
|
->rawColumns(['id', 'status', 'active', 'is_for', 'member'])
|
|
->make(true);
|
|
}
|
|
}
|