Files
2025-08-02 16:30:27 +02:00

810 lines
26 KiB
PHP

<?php
namespace KupShop\AdminBundle\AdminListMassEdit;
use KupShop\AdminBundle\Admin\Actions\AbstractAction;
use KupShop\AdminBundle\Admin\Actions\ActionsLocator;
use KupShop\AdminBundle\Admin\Actions\IAction;
use KupShop\AdminBundle\AdminList\BaseList;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\HtmlBuilder\HTML;
use Query\Operator;
use Symfony\Contracts\Service\Attribute\Required;
class BaseListMassEdit implements IListMassEdit
{
/**
* @var BaseList
*/
protected $listClass;
protected $template = 'listEdit.tpl';
protected $figureDivide = [40, 100, 200, 500, 1000, 2000, 5000, 10000];
protected $fetchedItems;
protected $prefetchValues = [];
protected $actions;
protected $actionsFailed = [];
protected $actionCount = 0;
/** @required */
public ActionsLocator $actionsLocator;
protected array $currencies = [];
#[Required]
public CurrencyContext $currencyContext;
public function setListClass(BaseList $listClass): void
{
$this->listClass = $listClass;
if (!empty($this->getActions())) {
$tableDef = &$this->listClass->getTableDef();
$tableDef['fields']['Akce'] = ['visible' => 'Y', 'fieldType' => BaseList::TYPE_LIST_ACTIONS];
}
}
protected function getActions()
{
if (empty($this->actions)) {
$actions = $this->actionsLocator->getActions(getVal('type'));
/** @var AbstractAction $action */
foreach ($actions as $action) {
if ($action->showInMassEdit()) {
$this->actions[$action->getActionType()] = $action;
}
}
}
return $this->actions;
}
public function get_vars()
{
$divide = getVal('divide', null, $this->figureDivide[0]);
$this->listClass->setPageDivide($divide);
$vars = $this->listClass->get_vars();
$vars['object'] = $vars['view'] = $this;
$vars['parent'] = $this->listClass->getTemplate();
$vars['divide']['items'] = array_combine($this->figureDivide, $this->figureDivide);
$vars['divide']['item'] = $divide;
$vars['listQueryString'] = $this->getListQueryString();
$this->listClass->setTemplate($this->template);
return $vars;
}
protected function stringCamelize($input, $separator = '_')
{
return str_replace($separator, '', ucwords($input, $separator));
}
public function getItemClass($class, $values, $fieldType)
{
if (is_callable($class)) {
$class = $class($values);
} elseif ($class && method_exists($this, $class)) {
$class = $this->$class($values);
}
if ($fieldType) {
$class .= ' edit_list_item';
}
return $class;
}
public function renderEditField($column, $values, $firstRow = false)
{
$editMethod = $this->getRenderMethodName($column);
$renders = $this->$editMethod($column, $values, $firstRow);
$renders = is_array($renders) ? $renders : [$renders];
$fieldType = $column['fieldType'];
if (!$firstRow && $fieldType !== BaseList::TYPE_POSITION) {
foreach ($renders as $renderItem) {
$renderItem->attr('disabled', true);
}
}
if ($fieldType == 'actions') {
$field_name = $fieldType;
} else {
$field_name = $this->listClass->getFieldArrayName($column['field']);
}
$columnEnabler = HTML::create('div')
->class('checkbox')
->tag('input')
->attr('type', 'checkbox')
->attr('style', 'margin: 0 10px 0 0; float:left;')
->class('columnEnabler'.($firstRow ? ' editFirstRow' : ''))
->attr('name', "checked[{$field_name}][{$values['id']}]")
->attr('id', "checked[{$field_name}][{$values['id']}]")
->end()
->tag('label')
->attr('for', "checked[{$field_name}][{$values['id']}]")
->end();
if ($fieldType === BaseList::TYPE_POSITION) {
// checked="checked" will be added from jQuery in listSortable.tpl
// after DnD stop
$columnEnabler = HTML::create('input')
->attr('type', 'checkbox')
->attr('style', 'display: none')
->class('columnEnabler'.($firstRow ? ' editFirstRow' : ''))
->attr('name', "checked[{$field_name}][{$values['id']}]")
->attr('id', "checked[{$field_name}][{$values['id']}]")
->end();
}
return [
$columnEnabler,
...$renders,
];
}
public function getBoolChecked($values): string
{
return $values == 'Y' ? 'checked' : '';
}
protected function massRenderString($column, $values)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
$value = $this->listClass->getListRowValue($values, $column['field']);
return HTML::create('input')
->attr('type', 'text')
->attr('name', "data[{$field_name}][{$values['id']}]")
->class('input')
->attr('style', 'width: 100%;')
->attr('value', $value);
}
protected function massRenderInt($column, $values, $firstRow)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
return HTML::create('input')
->attr('type', $firstRow ? 'text' : 'number')
->attr('name', "data[{$field_name}][{$values['id']}]")
->class('input')
->attr('style', 'width: 100%;')
->attr('value', $values[$field_name]);
}
protected function massRenderFloat($column, $values, $firstRow)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
return HTML::create('input')
->attr('type', $firstRow ? 'text' : 'number')
->attr('name', "data[{$field_name}][{$values['id']}]")
->class('input')
->attr('step', '0.0001')
->attr('value', $values[$field_name]);
}
protected function massRenderPrice($column, $values, $firstRow)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
$vat = (new \KupShop\AdminBundle\AdminList\BaseList())->getListRowValue($values, 'vat', getAdminVat()['id']);
$defaultWithVat = \Settings::getDefault()->prod_prefer_price_vat;
$vat = getVat($vat);
$select = HTML::create('select')
->class('input mass_price_vat listEdit-select')->attr('style', 'width: 35%;')
->attr('name', "data[{$field_name}_iswithvat][{$values['id']}]");
$option0 = $select->tag('option')
->text('Bez DPH')
->attr('value', 0);
$option1 = $select->tag('option')
->text('S DPH')
->attr('title', $vat.'%')
->attr('value', 1);
$price = $values[$field_name];
if ($defaultWithVat) {
$price = toDecimal($price)->addVat($vat);
$option1->attr('selected', 1);
} else {
$option0->attr('selected', 1);
}
return [
HTML::create('input')
->attr('type', $firstRow ? 'text' : 'number')
->attr('name', "data[{$field_name}][{$values['id']}]")
->class('input vat_target')
->attr('style', 'width: 65%;')
->attr('step', '0.0001')
->attr('value', $price),
HTML::create('input')
->class('input vat_value')
->attr('name', "data[{$field_name}_vat][{$values['id']}]")
->attr('type', 'hidden')
->attr('value', $vat),
$select,
];
}
protected function massRenderPriceWithCurrency($column, $values, $firstRow)
{
return $this->addCurrencySymbol($values['currency'] ?? null, $this->massRenderPrice($column, $values, $firstRow));
}
protected function massRenderFloatWithCurrency($column, $values, $firstRow)
{
return $this->addCurrencySymbol($values['currency'] ?? null, [$this->massRenderFloat($column, $values, $firstRow)]);
}
protected function massRenderBool($column, $values)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
$checked = $this->getBoolChecked($values[$field_name]);
return HTML::create('input')
->attr('type', 'checkbox')
->class('input')
->attr('style', 'margin: 0 10px 0 0; float:left;')
->attr('name', "data[{$field_name}][{$values['id']}]")
->attr('value', '1')
->attr($checked, $checked);
}
protected function massRenderList($column, $values)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
$select = HTML::create('select')
->class('input selecter listEdit-select')
->attr('style', 'width: 100%;')
->attr('name', "data[{$field_name}][{$values['id']}]");
$options = $column['fieldOptions'];
if ($values['id'] == 0 || empty($values[$field_name])) {
$options = ['' => '-'] + $options;
}
foreach ($options as $id => $value) {
$option = $select->tag('option')
->text($value)
->attr('value', $id);
if ($id == $values[$field_name]) {
$option->attr('selected', true);
}
}
return $select;
}
public function massRenderListMultiselect($column, $values)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
$select = HTML::create('select')
->class('input')
->attr('name', "data[{$field_name}][{$values['id']}][]")
->class('input selecter listEdit-multiselect')
->attr('data-filter-type', 'multiselect')
->attr('multiple');
$options = $column['fieldOptions'];
$selected = explode(',', $values[$field_name] ?? '');
foreach ($options as $id => $value) {
$option = $select->tag('option')
->text($value)
->attr('value', $id);
if (in_array($id, $selected)) {
$option->attr('selected', true);
}
}
return $select;
}
public function massRenderAutocomplete($column, $values)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
$value = $this->listClass->getListRowValue($values, $column['field']);
$select = HTML::create('select')
->class('input listEdit-autocomplete ajax-selecter')
->attr('style', 'width: 100%;')
->attr('data-type', $column['fieldOptions']['autocomplete'] ?? '')
->attr('name', "data[{$field_name}][{$values['id']}]");
if ($value !== null) {
if ($column['fieldOptions']['prefetchField'] ?? false) {
$name = $values[$column['fieldOptions']['prefetchField']] ?? $value;
} else {
$name = $this->getPrefetchFields($column['fieldOptions'], $field_name)[$value]['name'] ?? $value;
}
$select->tag('option')
->text($name)
->attr('value', $value)
->attr('selected', true);
}
return $select;
}
public function massRenderMultiselectAutocomplete($column, $values)
{
$field_name = $this->listClass->getFieldArrayName($column['field']);
$value = $this->listClass->getListRowValue($values, $column['field']);
if (($column['fieldOptions']['fieldType'] ?? '') == 'json') {
try {
$value = json_decode($value ?? '', true);
} catch (\Throwable $e) {
$value = [];
}
}
$select = HTML::create('select')
->class('input selecter listEdit-multiselectAutocomplete ajax-selecter')
->attr('style', 'width: 100%;')
->attr('data-type', $column['fieldOptions']['autocomplete'] ?? '')
->attr('name', "data[{$field_name}][{$values['id']}][]")
->attr('data-filter-type', 'multiselect')
->attr('multiple');
if ($value) {
if ($column['fieldOptions']['prefetchField'] ?? false) {
$prefetchValues = array_map(function ($val) use ($column) {
return ['id' => $val['id'], 'name' => $val[$column['fieldOptions']['prefetchField']] ?? $val['name']];
}, $value);
} else {
$prefetchValues = $this->getPrefetchFields($column['fieldOptions'], $field_name, array_keys($value));
}
foreach ($prefetchValues as $id => $value) {
$select->tag('option')
->text($value['name'])
->attr('value', $id)
->attr('selected', true);
}
}
return $select;
}
protected function massRenderActions($column, $values)
{
$html = HTML::create('div')
->tag('input')
->attr('type', 'checkbox')
->class('input')
->attr('style', 'margin: 0 10px 0 0; float:left;')
->attr('name', "data[actions][{$values['id']}]")
->attr('value', '0')
->attr($values['id'] == 0 ? '' : 'disabled');
if ($values['id'] == 0) {
$html->tag('input')
->attr('type', 'hidden')
->attr('name', 'actionsConfig')
->attr('id', 'actionsConfig');
$select = $html->tag('select')
->class('input')
->attr('style', 'width: 100%;')
->attr('name', 'selectedAction');
$select->tag('option')->text('--------')->attr('value', -1000);
/**
* @var $action AbstractAction
*/
foreach ($this->getActions() as $i => $action) {
$option = $select->tag('option')
->text($action->getName())
->attr('value', $i);
if ($i == $values['actions']) {
$option->attr('selected', true);
}
}
}
return $html;
}
public function massRenderPosition($column, $values)
{
if ($values['id'] == 0) {
return [];
}
$fieldName = $this->listClass->getFieldArrayName($column['field']);
return HTML::create('span')
->attr('class', 'drag-drop-mover')
->tag('span')
->attr('class', 'bi bi-arrows-move handle')
->end()
->tag('input')
->attr('type', 'hidden')
->attr('name', "data[{$fieldName}][{$values['id']}]")
->attr('data-sort', $values['id'])
->attr('value', $values[$fieldName])
->end()
->end();
}
protected function getPrefetchFields($fieldOptions, $fieldName, $ids = null)
{
if (empty($this->prefetchValues[$fieldName])) {
$prefetchMethod = 'massPrefetch'.$this->stringCamelize($fieldName);
if (method_exists($this, $prefetchMethod)) {
$this->prefetchValues[$fieldName] = $this->$prefetchMethod($fieldOptions, $fieldName, $ids);
} else {
$this->prefetchValues[$fieldName] = $this->massPrefetchBase($fieldOptions, $fieldName, $ids);
}
}
return $this->prefetchValues[$fieldName];
}
protected function massPrefetchBase($fieldOptions, $fieldName, $ids = null)
{
$ids = $ids ?? array_unique(array_map(function ($item) use ($fieldName) {
return $this->listClass->getListRowValue($item, $fieldName);
}, $this->fetchedItems));
return sqlFetchAll(sqlQueryBuilder()->select('id',
($fieldOptions['field'] ?? 'name').' as name')->from($fieldOptions['table'])->where(Operator::inStringArray($ids, 'id'))
->execute(),
'id');
}
protected function massSaveString($field, $activeIds, $values)
{
foreach ($activeIds as $id) {
if ($id == 0) {
continue;
}
$value = $values[$id] ?? '';
sqlQueryBuilder()->update($this->listClass->getTableName())
->directValues([$field => $value])
->where(Operator::equals(['id' => $id]))
->execute();
}
}
protected function massSaveInt($field, $activeIds, $values)
{
foreach ($values as $id => $value) {
sqlQueryBuilder()->update($this->listClass->getTableName())
->directValues([$field => $value])
->where(Operator::equals(['id' => $id]))
->execute();
}
}
protected function massSavePosition()
{
$this->massSaveInt(...func_get_args());
}
protected function massSaveFloat($field, $activeIds, $values)
{
$this->massSaveInt($field, $activeIds, $values);
}
protected function massSaveFloatWithCurrency($field, $activeIds, $values)
{
$this->massSaveFloat($field, $activeIds, $values);
}
protected function massSavePrice($field, $activeIds, $values)
{
$vatOptions = getVal('data')["{$field}_iswithvat"] ?? [];
$vatValues = getVal('data')["{$field}_vat"] ?? [];
foreach ($activeIds as $id) {
if (($vatOptions[$id] ?? false) && ($vatValues[$id] ?? false)) {
$values[$id] = toDecimal($values[$id])->removeVat($vatValues[$id])->asFloat();
}
}
$this->massSaveFloat($field, $activeIds, $values);
}
protected function massSavePriceWithCurrency($field, $activeIds, $values)
{
$this->massSavePrice($field, $activeIds, $values);
}
protected function massSaveBool($field, $activeIds, $values)
{
foreach ($activeIds as $id) {
sqlQueryBuilder()->update($this->listClass->getTableName())
->directValues([$field => ($values[$id] ?? false) ? 'Y' : 'N'])
->where(Operator::equals(['id' => $id]))
->execute();
}
}
protected function massSaveActions($fieldName, $activeIds, $values)
{
$actionIndex = getVal('selectedAction');
if ($actionIndex == -1000) {
return;
}
$values = $values ?: [];
$this->actionCount = count($values);
foreach ($values as $id => $tmp) {
if ($id == 0) {
$this->actionCount--;
continue;
}
/* @var IAction $action */
$action = $this->getActions()[$actionIndex];
$action->setId($id);
// parse actions config
parse_str(urldecode(getVal('actionsConfig')), $actionsConfig);
$data = $actionsConfig['data'] ?? [];
$result = $action->execute($data, $actionsConfig['config'] ?? [], getVal('type'));
if (!$result->isSuccessful()) {
$this->actionsFailed[$result->getMsg()][] = $action->getId();
}
}
}
protected function massSaveList($field, $activeIds, $values)
{
$this->massSaveString($field, $activeIds, $values);
}
protected function massSaveListMultiSelect($field, $activeIds, $values)
{
$values = array_map(function ($val) {
return implode(',', $val);
}, $values ?? []);
$this->massSaveString($field, $activeIds, $values);
}
protected function massSaveAutocomplete($field, $activeIds, $values)
{
$this->massSaveList($field, $activeIds, $values);
}
protected function massSaveMultiselectAutocomplete($field, $activeIds, $values)
{
$this->massSaveListMultiSelect($field, $activeIds, $values);
}
protected function massSaveAllFields(): void
{
$data = getVal('data');
$checked = getVal('checked');
foreach ($checked as $fieldName => $activeChecks) {
$activeIds = array_keys($activeChecks);
$fieldType = $this->findFieldTypeForName($fieldName);
if ($fieldType) {
$method = 'massSave'.$this->stringCamelize($fieldType);
$this->$method($fieldName, $activeIds, $data[$fieldName]);
}
}
}
protected function handleSave()
{
$checked = getVal('checked') ?? [];
$editedIds = array_unique(
array_reduce(
$checked,
function ($v, $activeChecks) {
return array_merge($v, array_keys($activeChecks));
},
[]
)
);
$this->massSaveAllFields();
$actionName = '';
if (getVal('selectedAction') && getVal('selectedAction') != -1000) {
$actionName = $this->actionsLocator->getServiceByActionClassName(getVal('selectedAction'))->getName();
}
$list_type = getVal('type');
$typeName = translate('navigation', $list_type, true);
$typeName = $typeName ?: $list_type;
addActivityLog(
ActivityLog::SEVERITY_NOTICE,
ActivityLog::TYPE_CHANGE,
"{$typeName}: Provedena hromadná úprava {$actionName}",
[
'item_ids' => $editedIds,
'edited_fields' => array_keys($checked ?: []),
'action' => $actionName,
'list_type' => $list_type,
'actions_failed' => $this->actionsFailed,
]
);
if (empty($this->actionsFailed)) {
$this->returnOk($GLOBALS['txt_str']['status']['saved']);
} else {
$this->addHtmlError($this->prepareError());
}
}
protected function findFieldTypeForName($name)
{
$tableDef = $this->getTableDef();
if ($name == 'actions') {
return 'actions';
}
foreach ($tableDef['fields'] as $field) {
$tableDefName = $this->listClass->getFieldArrayName($field['field']);
if ($name === $tableDefName && isset($field['fieldType'])) {
return $field['fieldType'];
}
}
return null;
}
public function printListFirstRowItem($column)
{
if (empty($column['fieldType']) || !method_exists($this, $this->getRenderMethodName($column)) || $column['editable'] == 'N') {
return '-';
}
$result = $this->renderEditField($column, ['id' => 0], true);
$this->massRenderResult($result);
}
protected function massRenderResult($result)
{
$output = function ($snippet) {
echo $snippet instanceof HTML ? (string) $snippet : htmlspecialchars($snippet);
};
if (is_array($result)) {
foreach ($result as $part) {
$output($part);
}
} else {
$output($result);
}
}
protected function getRenderMethodName($column)
{
return 'massRender'.$this->stringCamelize($column['fieldType']);
}
public function printListRowItem($column, &$values)
{
if (empty($column['fieldType']) || !method_exists($this, $this->getRenderMethodName($column)) || $column['editable'] == 'N') {
return $this->listClass->printListRowItem($column, $values);
}
$result = $this->renderEditField($column, $values);
$this->massRenderResult($result);
}
public function __call($name, $arguments)
{
if (method_exists($this->listClass, $name)) {
return call_user_func_array([$this->listClass, $name], $arguments);
}
throw new \Exception("Undefined method '{$name}'");
}
public function run()
{
// Process POSTEd values
$this->handle();
// Collect template variables
$vars = $this->collectVariables();
// Init templating system
$this->listClass->init_smarty();
// Assign template variables to template
$this->listClass->assign_($vars);
// Render template
$this->listClass->display();
}
protected function collectVariables()
{
$vars = $this->get_vars();
$this->fetchedItems = $vars['SQL'] = $vars['SQL']->fetchAll();
return $vars;
}
public function handle()
{
$acn = getVal('acn');
if ($acn) {
$action = 'handle'.ucfirst($acn);
if (method_exists($this, $action)) {
call_user_func([$this, $action]);
}
}
}
protected function prepareError()
{
$error = '<ul>';
$errorCount = 0;
foreach ($this->actionsFailed as $key => $value) {
$items = implode('', array_map(function ($x) {return "<li>{$this->getDisplayId($x)}</li>"; }, $value));
$error .= "<li>{$key}<ul>{$items}</ul></li>";
$errorCount += count($value);
}
$error .= '</ul>';
$successCount = $this->actionCount - $errorCount;
return "<strong style='text-align: center'>Úspěšně provedeno {$successCount}/{$this->actionCount} akcí.</strong>".$error;
}
protected function getDisplayId($id)
{
return $id;
}
protected function getListQueryString(): string
{
$filter = ['s', 'type', 'order', 'page'];
$params = array_filter($_GET, fn ($k) => !in_array($k, $filter), ARRAY_FILTER_USE_KEY);
return http_build_query($params);
}
protected function addCurrencySymbol($currencyId, $return)
{
if ($currencyId) {
$return[] = HTML::create('span')
->attr('style', 'margin-left: 5px;')
->text($this->getCurrency($currencyId));
}
return $return;
}
protected function getCurrency(string $currency): string
{
if (empty($this->currencies)) {
$this->currencies = $this->currencyContext->getSupported();
}
return $this->currencies[$currency]->getSymbol() ?? $currency;
}
}