This commit is contained in:
2026-03-10 21:37:24 +01:00
parent 5bce68c56b
commit 9d70ea0547
37 changed files with 1534 additions and 282 deletions

2
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/cache
/project.local.yml

View File

@@ -0,0 +1,44 @@
# Project Overview: pomysloweprezenty.pl
## Purpose
Polish e-commerce website (gift shop) - a custom-built online store at pomysloweprezenty.pl.
## Tech Stack
- **Language**: PHP (no framework, custom MVC-like architecture)
- **Database**: MySQL via RedBeanPHP ORM (`\R::`) and Medoo query builder
- **Email**: PHPMailer
- **Caching**: Redis (optional)
- **Frontend**: PHP templates, SCSS for admin styles, vanilla JavaScript
- **Libraries**: Included directly in `/libraries/` (no Composer)
- **Server**: Apache (.htaccess files present)
## Architecture
- **`autoload/`** - Core PHP classes (auto-loaded via custom spl_autoload_register)
- `Domain/` - Business logic repositories (Article, Product, Category, Order, Client, etc.)
- `admin/Controllers/` - Admin panel controllers
- `admin/ViewModels/` - View models for admin forms and tables
- `admin/Support/` - Form handling, table list support
- `api/` - REST API router and controllers
- `front/` - Frontend app, controllers, views, layout engine
- `Shared/` - Helpers, Cache, Email, HTML, Image, Template engine
- **`admin/`** - Admin panel (templates, JS, AJAX handlers, styles)
- **`templates/`** - Frontend templates (shop, articles, basket, client, etc.)
- **`templates_user/`** - User-customizable templates
- **`libraries/`** - Third-party libraries (medoo, RedBeanPHP, PHPMailer)
- **`cron/`** - Cron job scripts
- **`plugins/`** - Plugin system
## Entry Points
- `index.php` - Main frontend entry point
- `admin/index.php` - Admin panel
- `api.php` - API entry point
- `ajax.php` - AJAX handler (frontend)
- `admin/ajax.php` - AJAX handler (admin)
- `cron.php` - Cron jobs entry point
## Key Patterns
- Repositories pattern for data access (e.g., `ProductRepository`, `OrderRepository`)
- PHP templates with `Tpl` engine for rendering
- Admin uses form-edit and table-list component patterns
- Session-based authentication with IP validation
- Timezone: Europe/Warsaw

View File

@@ -0,0 +1,28 @@
# Code Style and Conventions
## PHP Style
- Spaces around brackets in array access: `$_SESSION[ 'key' ]`, `$array[ 'field' ]`
- Spaces inside parentheses: `function( $param )`, `if ( condition )`
- Opening braces on new line for functions/classes
- Opening braces on same line for control structures (mixed - some on new line)
- No type hints on most functions (legacy codebase)
- No docblocks on most methods
- camelCase for methods, PascalCase for classes
- File naming: `class.ClassName.php` for autoloaded classes, regular names for others
## Naming Conventions
- Controllers: `{Entity}Controller.php` (e.g., `ShopProductController.php`)
- Repositories: `{Entity}Repository.php` (e.g., `ProductRepository.php`)
- Views: `{Entity}.php` in `Views/` directory
- Templates: kebab-case directories (e.g., `shop-product/`, `shop-basket/`)
- Admin templates: organized by feature in `admin/templates/`
## Database
- RedBeanPHP ORM with `\R::` facade
- Medoo for complex queries via `$mdb`
- Tables use snake_case naming
## Frontend
- SCSS compiled to CSS in admin (`style.scss` -> `style.css`)
- Vanilla JavaScript (no build system, no npm/node)
- jQuery likely used in admin panel

View File

@@ -0,0 +1,26 @@
# Suggested Commands
## System (Windows with Git Bash)
- `git` - version control
- `ls`, `cd`, `grep`, `find` - standard Unix utils via Git Bash
## Development
- No build system or package manager (no Composer, no npm)
- PHP files are edited directly and served by Apache
- SCSS needs manual compilation for admin styles (`admin/layout/style-scss/style.scss`)
## Testing
- No automated test framework detected
- Manual testing via browser
## Deployment
- FTP-based deployment (`.vscode/ftp-kr.sync.cache.json` in gitignore suggests VS Code FTP extension)
## Cron
- `cron.php` - main cron entry point
- `cron-turstmate.php` - Trustmate integration cron
## Notes
- No linter or formatter configured
- No CI/CD pipeline detected
- Changes are tested manually on the live/staging server

View File

@@ -0,0 +1,11 @@
# Task Completion Checklist
When completing a task in this project:
1. **Code style**: Follow existing spacing conventions (spaces inside brackets/parentheses)
2. **No build step**: PHP changes take effect immediately (no compilation needed, except SCSS)
3. **SCSS**: If admin styles were changed, SCSS needs to be recompiled
4. **Test manually**: No automated tests - verify changes work by describing what to test in browser
5. **Database**: If DB schema changes are needed, document them (no migration system)
6. **Git**: Commit changes with descriptive message
7. **Security**: Be careful with SQL queries, XSS, and session handling

135
.serena/project.yml Normal file
View File

@@ -0,0 +1,135 @@
# the name by which the project can be referenced within Serena
project_name: "pomysloweprezenty.pl"
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp
# csharp_omnisharp dart elixir elm erlang
# fortran fsharp go groovy haskell
# java julia kotlin lua markdown
# matlab nix pascal perl php
# php_phpactor powershell python python_jedi r
# rego ruby ruby_solargraph rust scala
# swift terraform toml typescript typescript_vts
# vue yaml zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- typescript
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# line ending convention to use when writing source files.
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
line_ending:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# whether to use project's .gitignore files to ignore files
ignore_all_files_in_gitignore: true
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
included_optional_tools: []
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# list of mode names to that are always to be included in the set of active modes
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this setting overrides the global configuration.
# Set this to [] to disable base modes for this project.
# Set this to a list of mode names to always include the respective modes for this project.
base_modes:
# list of mode names that are to be activated by default.
# The full set of modes to be activated is base_modes + default_modes.
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
# This setting can, in turn, be overridden by CLI parameters (--mode).
default_modes:
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
# time budget (seconds) per tool call for the retrieval of additional symbol information
# such as docstrings or parameter information.
# This overrides the corresponding setting in the global configuration; see the documentation there.
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# list of regex patterns which, when matched, mark a memory entry as readonly.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []

View File

@@ -0,0 +1,229 @@
/* ==========================================================================
Order Details — Mobile Responsive
Scoped to .od-actions and .order-details
All rules inside @media (max-width: 767px) — desktop layout untouched
========================================================================== */
@media (max-width: 767px) {
/* --- 1. Action buttons bar --- */
.od-actions {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 6px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 8px;
}
.od-actions .btn {
flex-shrink: 0;
font-size: 12px;
padding: 6px 10px;
margin-right: 0 !important;
margin-left: 0 !important;
}
/* Hide button labels on small screens — show only icons */
.od-actions .od-actions-label {
display: none;
}
/* Integrations dropdown — push to end, cancel float */
.od-actions .pull-right,
.od-actions .dropdown.pull-right {
float: none !important;
margin-left: auto;
}
/* Dropdown menu must not clip inside scroll container */
.od-actions .dropdown {
position: static;
}
.od-actions + .order-details .dropdown-menu,
#integrationsDropdownMenu.show {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
width: 100%;
max-width: 100%;
border-radius: 12px 12px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
z-index: 1060;
padding: 10px 0;
}
#integrationsDropdownMenu.show .dropdown-item {
padding: 12px 20px;
font-size: 14px;
}
/* --- 2. Full-width columns stacking --- */
.order-details .col-sm-6,
.order-details .col-lg-8,
.order-details .col-lg-4,
.order-details .col-xl-5,
.order-details .col-xl-7 {
width: 100%;
flex: 0 0 100%;
max-width: 100%;
}
/* Spacing between stacked sections */
.order-details .col-sm-6 {
margin-bottom: 15px;
}
/* "Resend confirmation" button — allow text to wrap */
.order-details .resend_order_confirmation_email .btn {
font-size: 12px;
white-space: normal;
text-align: left;
}
/* --- 3. Status section --- */
.order-details .status_select #order-status {
max-width: 100%;
width: 100%;
}
.order-details .buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.order-details .buttons .btn {
width: 100%;
white-space: normal;
font-size: 13px;
}
.order-details .order-status {
font-size: 16px;
}
.order-details .order-history {
margin-top: 10px;
}
/* --- 4. Products table — compact list --- */
.order-details table thead {
display: none;
}
.order-details table {
border: none;
}
.order-details table tbody {
display: block;
}
.order-details table tr.order-product-details {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
padding: 12px 0;
border-bottom: 1px solid #eee;
gap: 0;
}
.order-details table tr.order-product-details:last-child {
border-bottom: none;
}
/* Image cell */
.order-details table td.product-image {
width: 60px;
height: 60px;
flex-shrink: 0;
border: none;
padding: 0;
margin-right: 10px;
float: none;
}
.order-details table td.product-image img {
width: 60px;
height: 60px;
object-fit: contain;
}
/* Name cell — takes remaining space next to image */
.order-details table tr.order-product-details td:nth-child(2) {
flex: 1 1 0;
min-width: 0;
border: none;
padding: 0;
white-space: normal;
}
.order-details table tr.order-product-details td:nth-child(2) a {
font-weight: 600;
font-size: 13px;
display: block;
word-break: break-word;
}
/* Hide quantity, price, discounted-price, total columns on mobile */
.order-details table tr.order-product-details td.tab-center,
.order-details table tr.order-product-details td.tab-right {
display: none;
}
/* Show mobile price summary line */
.od-mobile-price-line {
display: block !important;
margin-top: 4px;
font-size: 13px;
font-weight: 600;
color: #2a3042;
}
/* --- 5. Notes --- */
.order-details #notes {
font-size: 14px;
min-height: 120px;
}
/* --- 6. Order summary panel --- */
.order-details .panel .panel-body {
font-size: 13px;
}
/* --- 7. Paid status --- */
.order-details .paid-status .panel-body a {
display: flex;
align-items: center;
gap: 10px;
}
/* --- 8. Message section — hide empty spacer columns --- */
.order-details .d-none.d-lg-block {
display: none !important;
}
}
/* --- Desktop: action bar spacing (replaces mr5 class) --- */
.od-actions .btn + .btn,
.od-actions .btn + .dropdown {
margin-left: 5px;
}
/* Mobile price line — hidden on desktop */
.od-mobile-price-line {
display: none;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -57,6 +57,36 @@
width: 100% !important; width: 100% !important;
} }
/* --- Filter toggle --- */
.table-filters-wrapper {
display: none;
overflow: hidden;
}
.table-filters-wrapper.open {
display: block;
}
.js-filter-toggle-btn.active {
background-color: #e8e8e8;
border-color: #adadad;
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
.table-filter-badge {
display: inline-block;
min-width: 16px;
padding: 2px 5px;
font-size: 10px;
font-weight: 700;
line-height: 1;
color: #fff;
background-color: #337ab7;
border-radius: 10px;
margin-left: 3px;
}
/* --- Column visibility toggle --- */ /* --- Column visibility toggle --- */
.table-list-header-actions { .table-list-header-actions {

View File

@@ -455,23 +455,14 @@ body {
} }
.site-content { .site-content {
margin-left: 0;
&.with-menu { background-color: #fff;
width: 100%;
@include respond-above(xs) { @include respond-above(xs) {
width: calc(100% - 243px); width: calc(100% - 243px);
margin-left: 243px; margin-left: 243px;
} }
}
@include respond-below(md) {
margin-left: 0;
}
background-color: #fff;
margin-left: 244px;
.top-user { .top-user {
text-align: right; text-align: right;
@@ -1351,39 +1342,6 @@ li.sort-collapsed.sort-hover div {
} }
} }
input[type="checkbox"] {
position: relative;
width: 40px;
height: 20px;
-webkit-appearance: none;
background: $cGrayLight;
outline: none;
border-radius: 10px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, .2);
}
input:checked[type="checkbox"] {
background: $cMenuText;
}
input[type="checkbox"]:before {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 10px;
top: 0;
left: 0;
background: #fff;
transform: scale(1.1);
box-shadow: 0 2px 5px rgba(0, 0, 0, .2);
transition: .5s;
}
input:checked[type="checkbox"]:before {
left: 20px;
}
#images-uploader, #images-uploader,
#files-uploader { #files-uploader {
clear: both; clear: both;
@@ -1783,33 +1741,16 @@ input:checked[type="checkbox"]:before {
} }
} }
#table-products {
.product-categories { .product-categories {
display: block; display: block;
width: 100%; width: 100%;
text-wrap: wrap; text-wrap: wrap;
}
.product-name { &--cats {
display: flex; white-space: nowrap;
justify-content: space-between; overflow: hidden;
text-overflow: ellipsis;
.duplicate-product { max-width: 600px;
margin-left: 15px;
}
}
.duplicate-product {
float: right;
font-size: 13px;
}
.btn-success {
color: #FFF !important;
&.btn-create-product {
margin-top: 5px;
}
} }
} }
@@ -2137,6 +2078,10 @@ textarea.form-control {
} }
.order-details { .order-details {
.fa-copy {
cursor: pointer !important;
}
.paid-status { .paid-status {
margin-top: 10px; margin-top: 10px;

View File

@@ -61,6 +61,10 @@ $_SESSION['can_use_rfm'] = true;
<a href="<?= htmlspecialchars($action->url) ?>" class="btn btn-dark btn-sm" id="g-edit-cancel"> <a href="<?= htmlspecialchars($action->url) ?>" class="btn btn-dark btn-sm" id="g-edit-cancel">
<i class="fa fa-reply mr5"></i>Wstecz <i class="fa fa-reply mr5"></i>Wstecz
</a> </a>
<?php elseif ($action->name === 'preview'): ?>
<a href="<?= htmlspecialchars($action->url) ?>" class="btn btn-info btn-sm" target="_blank">
<i class="fa fa-eye mr5"></i><?= htmlspecialchars($action->label) ?>
</a>
<?php else: ?> <?php else: ?>
<a href="<?= htmlspecialchars($action->url) ?>" class="btn <?= htmlspecialchars($action->cssClass) ?> btn-sm"> <a href="<?= htmlspecialchars($action->url) ?>" class="btn <?= htmlspecialchars($action->cssClass) ?> btn-sm">
<?= htmlspecialchars($action->label) ?> <?= htmlspecialchars($action->label) ?>

View File

@@ -9,7 +9,7 @@ $buildUrl = function(array $params = []) use ($list): string {
} }
} }
$qs = http_build_query($query); $qs = http_build_query($query);
return $list->basePath . ($qs ? ('?' . $qs) : ''); return $list->basePath . $qs;
}; };
$currentSort = $list->sort['column'] ?? ''; $currentSort = $list->sort['column'] ?? '';
@@ -19,6 +19,14 @@ $totalPages = max(1, (int)($list->pagination['total_pages'] ?? 1));
$total = (int)($list->pagination['total'] ?? 0); $total = (int)($list->pagination['total'] ?? 0);
$perPage = (int)($list->pagination['per_page'] ?? 15); $perPage = (int)($list->pagination['per_page'] ?? 15);
$hasActiveFilters = false;
foreach ($list->filters as $filter) {
if (isset($filter['value']) && (string)$filter['value'] !== '') {
$hasActiveFilters = true;
break;
}
}
$isCompactColumn = function(array $column): bool { $isCompactColumn = function(array $column): bool {
$key = strtolower(trim((string)($column['key'] ?? ''))); $key = strtolower(trim((string)($column['key'] ?? '')));
$label = strtolower(trim((string)($column['label'] ?? ''))); $label = strtolower(trim((string)($column['label'] ?? '')));
@@ -48,6 +56,14 @@ $isCompactColumn = function(array $column): bool {
<div class="col-sm-4 text-right"> <div class="col-sm-4 text-right">
<div class="table-list-header-actions"> <div class="table-list-header-actions">
<span class="text-muted">Wyników: <?= $total; ?></span> <span class="text-muted">Wyników: <?= $total; ?></span>
<?php if (!empty($list->filters)): ?>
<button type="button" class="btn btn-default btn-sm js-filter-toggle-btn<?= $hasActiveFilters ? ' active' : ''; ?>" title="Filtry">
<i class="fa fa-filter"></i>
<?php if ($hasActiveFilters): ?>
<span class="badge badge-primary table-filter-badge"><?= count(array_filter($list->filters, function($f) { return isset($f['value']) && (string)$f['value'] !== ''; })); ?></span>
<?php endif; ?>
</button>
<?php endif; ?>
<div class="table-col-toggle-wrapper"> <div class="table-col-toggle-wrapper">
<button type="button" class="btn btn-default btn-sm js-col-toggle-btn" title="Widoczność kolumn"> <button type="button" class="btn btn-default btn-sm js-col-toggle-btn" title="Widoczność kolumn">
<i class="fa fa-columns"></i> <i class="fa fa-columns"></i>
@@ -75,7 +91,8 @@ $isCompactColumn = function(array $column): bool {
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="row mb15 js-table-filters-form"> <div class="js-table-filters-wrapper table-filters-wrapper<?= $hasActiveFilters ? ' open' : ''; ?>">
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" data-path-submit="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="row mb15 js-table-filters-form">
<?php foreach ($list->filters as $filter): ?> <?php foreach ($list->filters as $filter): ?>
<?php <?php
$filterKey = (string)($filter['key'] ?? ''); $filterKey = (string)($filter['key'] ?? '');
@@ -145,9 +162,10 @@ $isCompactColumn = function(array $column): bool {
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-primary btn-sm">Szukaj</button> <button type="submit" class="btn btn-primary btn-sm">Szukaj</button>
<a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm">Wyczyść</a> <a href="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="btn btn-default btn-sm js-table-filters-clear">Wyczyść</a>
</div> </div>
</form> </form>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-striped table-bordered mbn table-list-table"> <table class="table table-hover table-striped table-bordered mbn table-list-table">
@@ -274,7 +292,7 @@ $isCompactColumn = function(array $column): bool {
</ul> </ul>
</div> </div>
<div class="col-sm-6 text-right"> <div class="col-sm-6 text-right">
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="form-inline table-list-per-page-form"> <form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" data-path-submit="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="form-inline table-list-per-page-form">
<?php foreach ($list->query as $key => $value): ?> <?php foreach ($list->query as $key => $value): ?>
<?php if ($key !== 'per_page' && $key !== 'page'): ?> <?php if ($key !== 'per_page' && $key !== 'page'): ?>
<input type="hidden" name="<?= htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8'); ?>" value="<?= htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); ?>" /> <input type="hidden" name="<?= htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8'); ?>" value="<?= htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); ?>" />
@@ -282,7 +300,7 @@ $isCompactColumn = function(array $column): bool {
<?php endforeach; ?> <?php endforeach; ?>
<input type="hidden" name="page" value="1" /> <input type="hidden" name="page" value="1" />
Wyświetlaj Wyświetlaj
<select name="per_page" class="form-control input-sm" onchange="this.form.submit()"> <select name="per_page" class="form-control input-sm js-per-page-select">
<?php foreach ($list->perPageOptions as $opt): ?> <?php foreach ($list->perPageOptions as $opt): ?>
<option value="<?= (int)$opt; ?>"<?= ((int)$opt === $perPage) ? ' selected="selected"' : ''; ?>><?= (int)$opt; ?></option> <option value="<?= (int)$opt; ?>"<?= ((int)$opt === $perPage) ? ' selected="selected"' : ''; ?>><?= (int)$opt; ?></option>
<?php endforeach; ?> <?php endforeach; ?>
@@ -294,6 +312,40 @@ $isCompactColumn = function(array $column): bool {
</div> </div>
</div> </div>
<script type="text/javascript">
// Table state persistence — redirect ASAP to saved view
(function() {
var basePath = <?= json_encode($list->basePath); ?>;
var stateKey = 'tableListQuery_' + basePath;
var clearKey = 'tableListCleared_' + basePath;
var pathname = window.location.pathname.replace(/\/+$/, '/');
var bp = basePath.replace(/\/+$/, '/');
var queryPart = '';
if (pathname.length > bp.length && pathname.indexOf(bp) === 0) {
queryPart = pathname.substring(bp.length);
}
if (!queryPart && window.location.search) {
queryPart = window.location.search.substring(1);
}
try {
var justCleared = sessionStorage.getItem(clearKey) === '1';
sessionStorage.removeItem(clearKey);
if (queryPart) {
localStorage.setItem(stateKey, queryPart);
} else if (!justCleared) {
var saved = localStorage.getItem(stateKey);
if (saved) {
window.location.replace(basePath + saved);
}
}
} catch (e) {}
})();
</script>
<script type="text/javascript"> <script type="text/javascript">
(function($) { (function($) {
if (!$) { if (!$) {
@@ -469,5 +521,80 @@ $isCompactColumn = function(array $column): bool {
saveHiddenCols([]); saveHiddenCols([]);
applyColumnVisibility([]); applyColumnVisibility([]);
}); });
// --- Filter toggle ---
var filterStorageKey = 'tableListFilters_' + <?= json_encode($list->basePath); ?>;
function isFilterVisible() {
try {
return localStorage.getItem(filterStorageKey) === '1';
} catch (e) {}
return false;
}
function saveFilterState(visible) {
try {
localStorage.setItem(filterStorageKey, visible ? '1' : '0');
} catch (e) {}
}
var $filterWrapper = $('.js-table-filters-wrapper');
var $filterBtn = $('.js-filter-toggle-btn');
var hasActiveFilters = $filterWrapper.hasClass('open');
if (!hasActiveFilters && isFilterVisible()) {
$filterWrapper.addClass('open');
$filterBtn.addClass('active');
}
$(document).off('click.filterToggle', '.js-filter-toggle-btn');
$(document).on('click.filterToggle', '.js-filter-toggle-btn', function() {
var $wrapper = $('.js-table-filters-wrapper');
var $btn = $(this);
var isOpen = $wrapper.hasClass('open');
if (isOpen) {
$wrapper.removeClass('open');
$btn.removeClass('active');
saveFilterState(false);
} else {
$wrapper.addClass('open');
$btn.addClass('active');
saveFilterState(true);
}
});
// --- Path-based form submission (admin URL routing) ---
$(document).off('submit.tablePathSubmit', 'form[data-path-submit]');
$(document).on('submit.tablePathSubmit', 'form[data-path-submit]', function(e) {
e.preventDefault();
var basePath = $(this).attr('data-path-submit');
var data = $(this).serializeArray();
var parts = [];
for (var i = 0; i < data.length; i++) {
if (String(data[i].value) !== '') {
parts.push(encodeURIComponent(data[i].name) + '=' + encodeURIComponent(data[i].value));
}
}
window.location.href = basePath + (parts.length ? parts.join('&') : '');
});
// Per-page select auto-submit
$(document).off('change.tablePerPage', '.js-per-page-select');
$(document).on('change.tablePerPage', '.js-per-page-select', function() {
$(this).closest('form').trigger('submit');
});
// --- Table state clear on "Wyczyść" ---
var stateStorageKey = 'tableListQuery_' + <?= json_encode($list->basePath); ?>;
var stateClearKey = 'tableListCleared_' + <?= json_encode($list->basePath); ?>;
$(document).off('click.tableClearState', '.js-table-filters-clear');
$(document).on('click.tableClearState', '.js-table-filters-clear', function() {
try {
localStorage.removeItem(stateStorageKey);
sessionStorage.setItem(stateClearKey, '1');
} catch (e) {}
});
})(window.jQuery); })(window.jQuery);
</script> </script>

View File

@@ -0,0 +1,19 @@
<?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
<div class="mt15">
<a href="/admin/integrations/logs_clear/" class="btn btn-danger btn-sm"
onclick="return confirm('Na pewno chcesz usunac wszystkie logi?');">
<i class="fa fa-trash"></i> Wyczysc wszystkie logi
</a>
</div>
<script type="text/javascript">
$(function() {
$('body').on('click', '.log-context-btn', function(e) {
e.preventDefault();
var id = $(this).data('id');
$('#log-context-' + id).toggle();
$(this).text($('#log-context-' + id).is(':visible') ? 'Ukryj' : 'Pokaz');
});
});
</script>

View File

@@ -91,6 +91,20 @@
</div> </div>
</div> </div>
</div> </div>
<!-- API key -->
<div class="form-group">
<label class="col-lg-3 control-label" for="inputDefault">API key</label>
<div class="col-lg-9">
<div class="bs-component">
<div class="input-group">
<input class="form-control" type="text" id="api_key" name="api_key" placeholder="" value="<?= $this -> settings['api_key'];?>">
<span class="input-group-addon cursor" field-id="api_key">
<i class="fa fa-save"></i>
</span>
</div>
</div>
</div>
</div>
</div> </div>
<div class="col-lg-6"> <div class="col-lg-6">

View File

@@ -1,4 +1,26 @@
<style type="text/css"> <style type="text/css">
.bulk-action-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
margin-bottom: 10px;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
}
.bulk-action-bar__info {
font-weight: 600;
color: #856404;
}
.table-col-bulk-check {
width: 36px;
padding-left: 10px !important;
padding-right: 10px !important;
}
.product-archive-thumb-wrap { .product-archive-thumb-wrap {
display: inline-block; display: inline-block;
} }
@@ -96,5 +118,119 @@
$popup.removeClass('is-visible'); $popup.removeClass('is-visible');
$popupImage.attr('src', ''); $popupImage.attr('src', '');
}); });
// --- Bulk select ---
var $table = $('.table-list-table');
var $bar = $('#js-bulk-action-bar');
var $label = $bar.find('.js-bulk-count-label');
// Inject select-all checkbox into _checkbox column header
$table.find('thead th.table-col-bulk-check').html(
'<input type="checkbox" id="js-bulk-select-all" title="Zaznacz wszystkie">'
);
function updateBar() {
var count = $table.find('.js-bulk-check:checked').length;
if (count > 0) {
$label.text('Zaznaczono: ' + count);
$bar.show();
} else {
$bar.hide();
}
}
$(document).on('change.bulkSelect', '#js-bulk-select-all', function() {
var checked = $(this).is(':checked');
$table.find('.js-bulk-check').prop('checked', checked);
updateBar();
});
$(document).on('change.bulkSelect', '.js-bulk-check', function() {
var total = $table.find('.js-bulk-check').length;
var checked = $table.find('.js-bulk-check:checked').length;
$('#js-bulk-select-all').prop('indeterminate', checked > 0 && checked < total);
$('#js-bulk-select-all').prop('checked', checked === total && total > 0);
updateBar();
});
$(document).on('click.bulkDelete', '.js-bulk-delete-btn', function() {
var ids = [];
$table.find('.js-bulk-check:checked').each(function() {
ids.push($(this).val());
});
if (ids.length === 0) {
return;
}
var confirmMsg = 'UWAGA! Operacja nieodwracalna!\n\n'
+ 'Wybrane produkty (' + ids.length + ' szt.) zostaną trwale usunięte razem ze wszystkimi zdjęciami i załącznikami z serwera.\n\n'
+ 'Czy na pewno chcesz usunąć zaznaczone produkty?';
var doDelete = function() {
var $btn = $('.js-bulk-delete-btn');
$btn.prop('disabled', true).text('Usuwanie…');
var formData = [];
for (var i = 0; i < ids.length; i++) {
formData.push('ids%5B%5D=' + encodeURIComponent(ids[i]));
}
$.ajax({
url: '/admin/product_archive/bulk_delete_permanent/',
type: 'POST',
data: formData.join('&'),
contentType: 'application/x-www-form-urlencoded',
dataType: 'json',
success: function(resp) {
if (resp && resp.deleted > 0) {
window.location.reload();
} else {
alert('Nie udało się usunąć produktów. Spróbuj ponownie.');
$btn.prop('disabled', false).html('<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale');
}
},
error: function() {
alert('Błąd podczas usuwania produktów. Spróbuj ponownie.');
$btn.prop('disabled', false).html('<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale');
}
});
};
if (typeof $.confirm === 'function') {
$.confirm({
title: 'Potwierdzenie',
content: confirmMsg,
type: 'red',
boxWidth: '560px',
useBootstrap: false,
animation: 'scale',
closeAnimation: 'scale',
backgroundDismissAnimation: 'shake',
container: 'body',
theme: 'modern',
columnClass: '',
typeAnimated: true,
lazyOpen: false,
draggable: false,
closeIcon: true,
containerFluid: true,
escapeKey: true,
backgroundDismiss: true,
buttons: {
cancel: {
text: 'Anuluj',
btnClass: 'btn-default'
},
confirm: {
text: 'Tak, usuń trwale',
btnClass: 'btn-danger',
action: doDelete
}
}
});
} else if (window.confirm(confirmMsg)) {
doDelete();
}
});
})(window.jQuery); })(window.jQuery);
</script> </script>

View File

@@ -1,3 +1,10 @@
<div id="js-bulk-action-bar" class="bulk-action-bar" style="display:none;">
<span class="bulk-action-bar__info js-bulk-count-label">Zaznaczono: 0</span>
<button type="button" class="btn btn-danger btn-sm js-bulk-delete-btn">
<i class="fa fa-trash-o"></i> Usuń zaznaczone trwale
</button>
</div>
<?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?> <?= \Shared\Tpl\Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
<?php if (!empty($this->viewModel->customScriptView)): ?> <?php if (!empty($this->viewModel->customScriptView)): ?>

View File

@@ -1,3 +1,30 @@
<style type="text/css">
.attr-copy-btn {
display: inline-block;
padding: 1px 5px;
font-size: 11px;
line-height: 1.5;
background: transparent;
border: 1px solid #d0d0d0;
border-radius: 3px;
color: #999;
cursor: pointer;
vertical-align: middle;
margin-left: 4px;
transition: background .12s, color .12s, border-color .12s;
}
.attr-copy-btn:hover {
background: #f4f4f4;
border-color: #aaa;
color: #555;
}
.attr-copy-btn--copied {
background: #d4edda !important;
border-color: #28a745 !important;
color: #28a745 !important;
}
</style>
<script type="text/javascript"> <script type="text/javascript">
(function() { (function() {
var orderId = <?= (int)($this->order_id ?? 0);?>; var orderId = <?= (int)($this->order_id ?? 0);?>;
@@ -378,6 +405,68 @@
}); });
} }
$(function() {
function fallbackCopy(text) {
var $tmp = $('<textarea>').css({position: 'fixed', top: 0, left: 0, opacity: 0}).val(text);
$('body').append($tmp);
$tmp[0].select();
try { document.execCommand('copy'); } catch (e) {}
$tmp.remove();
}
$('.atributes').each(function() {
var $div = $(this);
var html = $.trim($div.html());
if (!html) { return; }
var parts = html.split(/<br\s*\/?>/i);
var newParts = [];
for (var i = 0; i < parts.length; i++) {
var part = $.trim(parts[i]);
if (!part) { continue; }
var match = part.match(/^(<b>[^<]*<\/b>\s*:\s*)(.+)$/);
if (match) {
var labelHtml = match[1];
var value = $.trim(match[2]);
var escapedValue = $('<div>').text(value).html();
part = labelHtml + escapedValue
+ ' <button type="button" class="js-attr-copy-btn attr-copy-btn" data-value="'
+ escapedValue + '" title="Kopiuj: ' + escapedValue + '">'
+ '<i class="fa fa-copy"></i></button>';
}
newParts.push(part);
}
$div.html(newParts.join('<br>'));
});
$(document).on('click', '.js-attr-copy-btn', function() {
var $btn = $(this);
var value = String($btn.data('value'));
function showCopied() {
$btn.addClass('attr-copy-btn--copied');
$btn.find('i').removeClass('fa-copy').addClass('fa-check');
setTimeout(function() {
$btn.removeClass('attr-copy-btn--copied');
$btn.find('i').removeClass('fa-check').addClass('fa-copy');
}, 1500);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(value).then(showCopied, function() {
fallbackCopy(value);
showCopied();
});
} else {
fallbackCopy(value);
showCopied();
}
});
});
$('body').on('click', '.btn-toggle-trustmate', function(e) { $('body').on('click', '.btn-toggle-trustmate', function(e) {
e.preventDefault(); e.preventDefault();

View File

@@ -3,24 +3,25 @@ $orderId = (int)($this -> order['id'] ?? 0);
?> ?>
<div class="site-title">Szczegóły zamówienia: <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?></div> <div class="site-title">Szczegóły zamówienia: <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?></div>
<script>document.title = 'Zamówienie <?= htmlspecialchars((string)($this -> order['number'] ?? ''), ENT_QUOTES, 'UTF-8');?> - shopPro';</script>
<div class="mb15"> <div class="od-actions mb15">
<a href="/admin/shop_order/list/" class="btn btn-dark btn-sm mr5"> <a href="/admin/shop_order/list/" class="btn btn-dark btn-sm">
<i class="fa fa-reply"></i> Wstecz <i class="fa fa-reply"></i> <span class="od-actions-label">Wstecz</span>
</a> </a>
<a href="/admin/shop_order/order_edit/order_id=<?= $orderId;?>" class="btn btn-danger btn-sm mr5"> <a href="/admin/shop_order/order_edit/order_id=<?= $orderId;?>" class="btn btn-danger btn-sm">
<i class="fa fa-pencil"></i> Edytuj zamówienie <i class="fa fa-pencil"></i> <span class="od-actions-label">Edytuj</span>
</a> </a>
<? if ( $this -> prev_order_id ):?> <? if ( $this -> prev_order_id ):?>
<a href="/admin/shop_order/order_details/order_id=<?= (int)$this -> prev_order_id;?>" class="btn btn-success btn-sm mr5"> <a href="/admin/shop_order/order_details/order_id=<?= (int)$this -> prev_order_id;?>" class="btn btn-success btn-sm">
<i class="fa fa-arrow-left"></i> Poprzednie zamówienie <i class="fa fa-arrow-left"></i> <span class="od-actions-label">Poprzednie</span>
</a> </a>
<? endif;?> <? endif;?>
<? if ( $this -> next_order_id ):?> <? if ( $this -> next_order_id ):?>
<a href="/admin/shop_order/order_details/order_id=<?= (int)$this -> next_order_id;?>" class="btn btn-success btn-sm mr5"> <a href="/admin/shop_order/order_details/order_id=<?= (int)$this -> next_order_id;?>" class="btn btn-success btn-sm">
<i class="fa fa-arrow-right"></i> Następne zamówienie <i class="fa fa-arrow-right"></i> <span class="od-actions-label">Następne</span>
</a> </a>
<? endif;?> <? endif;?>
@@ -89,6 +90,19 @@ $orderId = (int)($this -> order['id'] ?? 0);
<div> <div>
<b><?= $this -> order[ 'payment_method' ];?> </b> <b><?= $this -> order[ 'payment_method' ];?> </b>
</div> </div>
<? if ( !empty($this -> order['apilo_order_id']) ):?>
<br/>
<div>
<i class="fa fa-cloud"></i> Apilo: <b style="color: #27ae60;">tak</b>
&mdash; ID: <b id="order-apilo-id"><?= htmlspecialchars((string)$this -> order['apilo_order_id'], ENT_QUOTES, 'UTF-8');?></b>
<i class="fa fa-copy" onclick="copyToClipboard( 'order-apilo-id' ); return false;"></i>
</div>
<? else:?>
<br/>
<div>
<i class="fa fa-cloud"></i> Apilo: <b style="color: #c0392b;">nie</b>
</div>
<? endif;?>
</div> </div>
</div> </div>
<div class="paid-status panel"> <div class="paid-status panel">
@@ -183,11 +197,15 @@ $orderId = (int)($this -> order['id'] ?? 0);
<div class="product-message"> <div class="product-message">
<?= $product[ 'message' ] != '' ? '<strong>Wiadomość:</strong> ' . $product['message'] : '';?> <?= $product[ 'message' ] != '' ? '<strong>Wiadomość:</strong> ' . $product['message'] : '';?>
</div> </div>
<div class="od-mobile-price-line">
<? $effective = ((float)$product['price_brutto_promo'] > 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?>
<?= (int)$product['quantity'];?> &times; <?= \Shared\Helpers\Helpers::decimal( $effective );?> = <?= \Shared\Helpers\Helpers::decimal( $effective * $product['quantity'] );?> zł
</div>
</td> </td>
<td class="tab-center"><?= $product[ 'quantity' ];?></td> <td class="tab-center"><?= $product[ 'quantity' ];?></td>
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto' ] );?> zł</td> <td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto' ] );?> zł</td>
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto_promo' ] );?> zł</td> <td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $effective );?> zł</td>
<td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $product[ 'price_brutto_promo' ] * $product[ 'quantity' ] );?> zł</td> <td class="tab-right"><?= \Shared\Helpers\Helpers::decimal( $effective * $product[ 'quantity' ] );?> zł</td>
</tr> </tr>
<? endforeach; endif;?> <? endforeach; endif;?>
</tbody> </tbody>

View File

@@ -81,9 +81,6 @@
</div> </div>
</div> </div>
</div> </div>
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/minimal.css">
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/blue.css">
<script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.min.js"></script>
<script type="text/javascript"> <script type="text/javascript">
$( function() $( function()
{ {

View File

@@ -8,44 +8,52 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="www.project-pro.pl - internetowe rozwi&#261;zania dla biznesu"> <meta name="author" content="www.project-pro.pl - internetowe rozwi&#261;zania dla biznesu">
<link rel='stylesheet' type="text/css" href='https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700'> <link rel='stylesheet' type="text/css" href='https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700'>
<link rel="stylesheet" type="text/css" href="/libraries/framework/skin/default_skin/css/theme.css"> <link rel="stylesheet" type="text/css" href="/libraries/framework/skin/default_skin/css/theme.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/skin/default_skin/css/theme.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/magnific/magnific-popup.css"> <link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/magnific/magnific-popup.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/magnific/magnific-popup.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css"> <link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css"> <link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css"> <link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css"> <link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/daterange/daterangepicker.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/jquery-confirm/jquery-confirm.min.css"> <link rel="stylesheet" type="text/css" href="/libraries/jquery-confirm/jquery-confirm.min.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/jquery-confirm/jquery-confirm.min.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/easy-tabs/css/easy-responsive-tabs.css"> <link rel="stylesheet" type="text/css" href="/libraries/easy-tabs/css/easy-responsive-tabs.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/easy-tabs/css/easy-responsive-tabs.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/bootstrap-4.5.2-dist/css/bootstrap.css"> <link rel="stylesheet" type="text/css" href="/libraries/bootstrap-4.5.2-dist/css/bootstrap.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/bootstrap-4.5.2-dist/css/bootstrap.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/font-awesome-4.7.0/css/font-awesome.css"> <link rel="stylesheet" type="text/css" href="/libraries/font-awesome-4.7.0/css/font-awesome.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/font-awesome-4.7.0/css/font-awesome.css'); ?>">
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/square/blue.css"> <link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/minimal.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/grid/plugins/icheck/skins/minimal/minimal.css'); ?>">
<script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery-1.11.1.min.js"></script> <link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/blue.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/grid/plugins/icheck/skins/minimal/blue.css'); ?>">
<script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.min.js"></script> <script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery-1.11.1.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/jquery/jquery-1.11.1.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/js/utility/utility.js"></script> <script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/magnific/jquery.magnific-popup.js"></script> <script type="text/javascript" src="/libraries/framework/js/utility/utility.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/js/utility/utility.js'); ?>"></script>
<script type="text/javascript" src="/libraries/easy-tabs/js/easyResponsiveTabs.js"></script> <script type="text/javascript" src="/libraries/framework/vendor/plugins/magnific/jquery.magnific-popup.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/magnific/jquery.magnific-popup.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/moment.js"></script> <script type="text/javascript" src="/libraries/easy-tabs/js/easyResponsiveTabs.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/easy-tabs/js/easyResponsiveTabs.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/pl.js"></script> <script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/moment.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/moment/moment.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js"></script> <script type="text/javascript" src="/libraries/framework/vendor/plugins/moment/pl.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/moment/pl.js'); ?>"></script>
<script type="text/javascript" src="/libraries/framework/vendor/plugins/daterange/daterangepicker.js"></script> <script type="text/javascript" src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js'); ?>"></script>
<script type="text/javascript" src="/libraries/jquery-confirm/jquery-confirm.min.js"></script> <script type="text/javascript" src="/libraries/framework/vendor/plugins/daterange/daterangepicker.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/framework/vendor/plugins/daterange/daterangepicker.js'); ?>"></script>
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.min.js"></script> <script type="text/javascript" src="/libraries/jquery-confirm/jquery-confirm.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/jquery-confirm/jquery-confirm.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.bundle.min.js"></script> <script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/bootstrap-4.5.2-dist/js/bootstrap.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.js"></script> <script type="text/javascript" src="/libraries/bootstrap-4.5.2-dist/js/bootstrap.bundle.min.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/bootstrap-4.5.2-dist/js/bootstrap.bundle.min.js'); ?>"></script>
<script type="text/javascript" src="/libraries/functions.js"></script> <script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/grid/plugins/icheck/icheck.js'); ?>"></script>
<script type="text/javascript" src="/admin/js/functions.js"></script> <script type="text/javascript" src="/libraries/functions.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/libraries/functions.js'); ?>"></script>
<link rel="stylesheet" href="/admin/layout/style-css/style.css" /> <script type="text/javascript" src="/admin/js/functions.js?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/admin/js/functions.js'); ?>"></script>
<link rel="stylesheet" href="/admin/layout/style-css/table-list.css" /> <link rel="stylesheet" href="/admin/layout/style-css/style.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/admin/layout/style-css/style.css'); ?>" />
<link rel="stylesheet" href="/admin/layout/style-css/table-list.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/admin/layout/style-css/table-list.css'); ?>" />
<link rel="stylesheet" href="/admin/layout/style-css/order-details-mobile.css?ver=<?= filemtime($_SERVER['DOCUMENT_ROOT'] . '/admin/layout/style-css/order-details-mobile.css'); ?>" />
</head> </head>
<body> <body>
<div class="admin-page"> <div class="admin-page">
<div class="menu"> <div class="menu">
<div class="logo sticky-top"> <div class="logo sticky-top">
shop<b>Pro</b> shop<b>Pro</b>
<span>ver. <?= \Shared\Helpers\Helpers::get_version();?></span><br> <span>ver. <?= \Shared\Helpers\Helpers::get_version();?>
<? if ( $settings[ 'update' ] ):?>
<i class="fa fa-refresh check-update-btn" id="check-update-btn" title="Sprawdź aktualizacje" style="cursor:pointer;margin-left:4px;font-size:11px;opacity:0.7;"></i>
<? endif;?>
</span><br>
<span id="update-badge-wrap">
<? if ( $settings[ 'update' ] and \Shared\Helpers\Helpers::get_new_version() > \Shared\Helpers\Helpers::get_version() ):?> <? if ( $settings[ 'update' ] and \Shared\Helpers\Helpers::get_new_version() > \Shared\Helpers\Helpers::get_version() ):?>
<a href="/admin/update/main_view/" class="label label-danger">aktualizacja</a> <a href="/admin/update/main_view/" class="label label-danger">aktualizacja</a>
<? endif;?> <? endif;?>
</span>
</div> </div>
<div class="menu-content"> <div class="menu-content">
<ul> <ul>
@@ -145,6 +153,11 @@
<i class="fa fa-cogs" aria-hidden="true"></i>shopPRO <i class="fa fa-cogs" aria-hidden="true"></i>shopPRO
</a> </a>
</li> </li>
<li>
<a href="/admin/integrations/logs/">
<i class="fa fa-list-alt" aria-hidden="true"></i>Logi
</a>
</li>
</ul> </ul>
</div> </div>
<div class="preview"> <div class="preview">
@@ -309,7 +322,7 @@
$.ajax({ $.ajax({
url: '/admin/settings/globalSearchAjax/', url: '/admin/settings/globalSearchAjax/',
type: 'GET', type: 'POST',
dataType: 'json', dataType: 'json',
data: { q: phrase }, data: { q: phrase },
success: function(response) { success: function(response) {
@@ -320,8 +333,12 @@
renderResults(response.items || []); renderResults(response.items || []);
}, },
error: function() { error: function(xhr) {
$results.html('<div class="admin-global-search-empty">Błąd połączenia</div>').addClass('open'); var msg = 'Błąd połączenia';
if (xhr.status === 200) {
msg = 'Błąd parsowania odpowiedzi';
}
$results.html('<div class="admin-global-search-empty">' + msg + '</div>').addClass('open');
} }
}); });
} }
@@ -354,6 +371,32 @@
}); });
})(); })();
(function() {
$(document).off('click.checkUpdate', '#check-update-btn').on('click.checkUpdate', '#check-update-btn', function(e) {
e.preventDefault();
var $btn = $(this);
if ($btn.hasClass('fa-spin')) return;
$btn.addClass('fa-spin').css('opacity', 1);
$.ajax({
url: '/admin/update/checkUpdate/',
type: 'GET',
dataType: 'json',
success: function(data) {
$btn.removeClass('fa-spin').css('opacity', 0.7);
var $wrap = $('#update-badge-wrap');
if (data.has_update) {
$wrap.html('<a href="/admin/update/main_view/" class="label label-danger">aktualizacja</a>');
} else {
$wrap.html('');
}
},
error: function() {
$btn.removeClass('fa-spin').css('opacity', 0.7);
}
});
});
})();
$(document).ready(function () { $(document).ready(function () {
var user_agent = navigator.userAgent.toLowerCase(); var user_agent = navigator.userAgent.toLowerCase();
var click_event = user_agent.match(/(iphone|ipod|ipad)/) ? "touchend" : "click"; var click_event = user_agent.match(/(iphone|ipod|ipad)/) ? "touchend" : "click";

View File

@@ -41,12 +41,23 @@
</div> </div>
</div> </div>
<? if ( !empty( $this->log ) ): ?>
<div class="panel">
<div class="panel-heading">
<span class="panel-title">Log ostatniej aktualizacji</span>
</div>
<div class="panel-body">
<pre style="max-height: 400px; overflow-y: auto; font-size: 12px;"><?= htmlspecialchars( $this->log ); ?></pre>
</div>
</div>
<? endif; ?>
<div class="panel"> <div class="panel">
<div class="panel-heading"> <div class="panel-heading">
<span class="panel-title">Changelog</span> <span class="panel-title">Changelog</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<?= @file_get_contents( 'https://shoppro.project-dc.pl/updates/changelog.php' ); ?> <?= @file_get_contents( 'https://shoppro.project-dc.pl/updates/changelog.php?ver=' . $this->ver ); ?>
</div> </div>
</div> </div>

View File

@@ -360,6 +360,9 @@ class ArticleRepository
public function archive(int $articleId): bool public function archive(int $articleId): bool
{ {
$result = $this->db->update('pp_articles', ['status' => -1], ['id' => $articleId]); $result = $this->db->update('pp_articles', ['status' => -1], ['id' => $articleId]);
if ($result) {
$this->db->delete('pp_routes', ['article_id' => $articleId]);
}
return (bool)$result; return (bool)$result;
} }
@@ -381,6 +384,7 @@ class ArticleRepository
$this->db->delete('pp_articles_langs', ['article_id' => $articleId]); $this->db->delete('pp_articles_langs', ['article_id' => $articleId]);
$this->db->delete('pp_articles_images', ['article_id' => $articleId]); $this->db->delete('pp_articles_images', ['article_id' => $articleId]);
$this->db->delete('pp_articles_files', ['article_id' => $articleId]); $this->db->delete('pp_articles_files', ['article_id' => $articleId]);
$this->db->delete('pp_routes', ['article_id' => $articleId]);
$this->db->delete('pp_articles', ['id' => $articleId]); $this->db->delete('pp_articles', ['id' => $articleId]);
\Shared\Helpers\Helpers::delete_dir('../upload/article_images/article_' . $articleId . '/'); \Shared\Helpers\Helpers::delete_dir('../upload/article_images/article_' . $articleId . '/');

View File

@@ -174,6 +174,7 @@ class CategoryRepository
$deleted = (bool)$this->db->delete('pp_shop_categories', ['id' => $id]); $deleted = (bool)$this->db->delete('pp_shop_categories', ['id' => $id]);
if ($deleted) { if ($deleted) {
$this->db->delete('pp_routes', ['category_id' => $id]);
$this->refreshCategoryArtifacts(); $this->refreshCategoryArtifacts();
} }

View File

@@ -296,7 +296,7 @@ class LayoutsRepository
if (is_array($layoutRows) && isset($layoutRows[0])) { if (is_array($layoutRows) && isset($layoutRows[0])) {
$layout = $layoutRows[0]; $layout = $layoutRows[0];
} else { } else {
$layout = $this->db->get('pp_layouts', '*', ['categories_default' => 1]); $layout = $this->db->get('pp_layouts', '*', ['status' => 1]);
} }
} }

View File

@@ -134,7 +134,11 @@ class PagesRepository
return false; return false;
} }
return (bool)$this->db->delete('pp_pages', ['id' => $pageId]); $deleted = (bool)$this->db->delete('pp_pages', ['id' => $pageId]);
if ($deleted) {
$this->db->delete('pp_routes', ['page_id' => $pageId]);
}
return $deleted;
} }
/** /**

View File

@@ -654,6 +654,10 @@ class ProductRepository
'custom_label_2' => $product['custom_label_2'], 'custom_label_2' => $product['custom_label_2'],
'custom_label_3' => $product['custom_label_3'], 'custom_label_3' => $product['custom_label_3'],
'custom_label_4' => $product['custom_label_4'], 'custom_label_4' => $product['custom_label_4'],
'new_to_date' => $product['new_to_date'],
'additional_message' => (int)($product['additional_message'] ?? 0),
'additional_message_required' => (int)($product['additional_message_required'] ?? 0),
'additional_message_text' => $product['additional_message_text'],
'set_id' => $product['set_id'] !== null ? (int)$product['set_id'] : null, 'set_id' => $product['set_id'] !== null ? (int)$product['set_id'] : null,
'product_unit_id' => $product['product_unit_id'] !== null ? (int)$product['product_unit_id'] : null, 'product_unit_id' => $product['product_unit_id'] !== null ? (int)$product['product_unit_id'] : null,
'producer_id' => $product['producer_id'] !== null ? (int)$product['producer_id'] : null, 'producer_id' => $product['producer_id'] !== null ? (int)$product['producer_id'] : null,

View File

@@ -172,13 +172,19 @@ class UpdateRepository
foreach ( $manifest['sql'] as $query ) { foreach ( $manifest['sql'] as $query ) {
$query = trim( $query ); $query = trim( $query );
if ( $query !== '' ) { if ( $query === '' || strpos( $query, '--' ) === 0 ) {
continue;
}
try {
if ( $this->db->query( $query ) ) { if ( $this->db->query( $query ) ) {
$success++; $success++;
} else { } else {
$errors++; $errors++;
$log[] = '[WARNING] Błąd SQL: ' . $query; $log[] = '[WARNING] Błąd SQL: ' . $query;
} }
} catch ( \Exception $e ) {
$errors++;
$log[] = '[WARNING] Wyjątek SQL: ' . $e->getMessage() . ' | Query: ' . substr( $query, 0, 200 );
} }
} }

View File

@@ -425,36 +425,200 @@ class Helpers
$site_map .= '<priority>1</priority>' . PHP_EOL; $site_map .= '<priority>1</priority>' . PHP_EOL;
$site_map .= '</url>' . PHP_EOL; $site_map .= '</url>' . PHP_EOL;
$htaccess_data = file_get_contents( $dir . 'libraries/htaccess.conf' ); //
$htaccess_data = str_replace( '{PAGE}', $url, $htaccess_data ); // SYSTEM ROUTES — delete all and reinsert
//
$mdb->delete( 'pp_routes', [ 'type' => 'system' ] );
// Static system routes (hardcoded, never change)
$systemRoutes = [
// Wyszukiwarka
[ 'pattern' => '^wyszukiwarka/([^/]+)/([0-9]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=$2' ],
[ 'pattern' => '^wyszukiwarka/([^/]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=1' ],
// Zamowienia
[ 'pattern' => '^zamowienie/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=order_details&order_hash=$1' ],
[ 'pattern' => '^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=payment_confirmation&order_hash=$1' ],
// Platnosci
[ 'pattern' => '^tpay-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_tpay' ],
[ 'pattern' => '^platnosc-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_hotpay' ],
[ 'pattern' => '^przelewy24-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_przelewy24pl' ],
// Koszyk
[ 'pattern' => '^koszyk$', 'destination' => 'index.php?module=shop_basket&action=main_view' ],
[ 'pattern' => '^koszyk-podsumowanie$', 'destination' => 'index.php?module=shop_basket&action=summary_view' ],
[ 'pattern' => '^zloz-zamowienie$', 'destination' => 'index.php?module=shop_basket&action=basket_save' ],
// Klient
[ 'pattern' => '^rejestracja$', 'destination' => 'index.php?module=shop_client&action=register_form' ],
[ 'pattern' => '^logowanie$', 'destination' => 'index.php?module=shop_client&action=login_form' ],
[ 'pattern' => '^wylogowanie$', 'destination' => 'index.php?module=shop_client&action=logout' ],
[ 'pattern' => '^odzyskiwanie-hasla$', 'destination' => 'index.php?module=shop_client&action=recover_password' ],
[ 'pattern' => '^panel-klienta/zamowienia$', 'destination' => 'index.php?module=shop_client&action=client_orders' ],
[ 'pattern' => '^panel-klienta/adresy$', 'destination' => 'index.php?module=shop_client&action=client_addresses' ],
[ 'pattern' => '^panel-klienta/nowy-adres$', 'destination' => 'index.php?module=shop_client&action=address_edit' ],
[ 'pattern' => '^panel-klienta/edytuj-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_edit&id=$1' ],
[ 'pattern' => '^panel-klienta/usun-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_delete&id=$1' ],
// Newsletter
[ 'pattern' => '^newsletter/signin$', 'destination' => 'index.php?module=newsletter&action=signin' ],
[ 'pattern' => '^newsletter/confirm/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=confirm&hash=$1' ],
[ 'pattern' => '^newsletter/unsubscribe/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=unsubscribe&hash=$1' ],
// Moduły AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search)
[ 'pattern' => '^shopBasket/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopBasket&action=$1&$2' ],
[ 'pattern' => '^shopBasket/([^/]+)$', 'destination' => 'index.php?module=shopBasket&action=$1' ],
[ 'pattern' => '^shopClient/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopClient&action=$1&$2' ],
[ 'pattern' => '^shopClient/([^/]+)$', 'destination' => 'index.php?module=shopClient&action=$1' ],
[ 'pattern' => '^shopProduct/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopProduct&action=$1&$2' ],
[ 'pattern' => '^shopProduct/([^/]+)$', 'destination' => 'index.php?module=shopProduct&action=$1' ],
[ 'pattern' => '^shopCoupon/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopCoupon&action=$1&$2' ],
[ 'pattern' => '^shopCoupon/([^/]+)$', 'destination' => 'index.php?module=shopCoupon&action=$1' ],
[ 'pattern' => '^search/([^/]+)/(.+)$', 'destination' => 'index.php?module=search&action=$1&$2' ],
[ 'pattern' => '^search/([^/]+)$', 'destination' => 'index.php?module=search&action=$1' ],
];
foreach ( $systemRoutes as $route )
{
$mdb->insert( 'pp_routes', [
'type' => 'system',
'lang_id' => 0,
'pattern' => $route['pattern'],
'destination' => $route['destination'],
] );
}
// Dynamic system routes — languages
$results = $mdb->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); $results = $mdb->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) ) foreach ( $results as $row ) if ( is_array( $results ) ) foreach ( $results as $row )
{ {
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $row['id'] . '/$ index.php?a=change_language&id=' . $row['id'] . ' [L]'; $mdb->insert( 'pp_routes', [
'type' => 'system',
'lang_id' => 0,
'pattern' => '^' . $row['id'] . '$',
'destination' => 'index.php?a=change_language&id=' . $row['id'],
] );
} }
// // Dynamic system routes — producenci
// INNE
//
$htaccess_data .= PHP_EOL;
$htaccess_data .= 'RewriteRule ^newsletter/signin/$ index.php?module=newsletter&action=signin [L]' . PHP_EOL;
$htaccess_data .= 'RewriteRule ^newsletter/confirm/hash=(.*)$ index.php?module=newsletter&action=confirm&hash=$1 [L]' . PHP_EOL;
$htaccess_data .= 'RewriteRule ^newsletter/unsubscribe/hash=(.*)$ index.php?module=newsletter&action=unsubscribe&hash=$1 [L]' . PHP_EOL;
//
// PRODUCENCI
//
$categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId(); $categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId();
$htaccess_data .= 'RewriteRule ^producenci$ index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId . '&%{QUERY_STRING} [L]' . PHP_EOL;
$mdb->insert( 'pp_routes', [
'type' => 'system',
'lang_id' => 0,
'pattern' => '^producenci$',
'destination' => 'index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId,
] );
$rows = $mdb->select( 'pp_shop_producer', '*', [ 'status' => 1 ] ); $rows = $mdb->select( 'pp_shop_producer', '*', [ 'status' => 1 ] );
if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row ) if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row )
{ {
$htaccess_data .= 'RewriteRule ^producent/' . self::seo( $row['name'] ) . '$ index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&%{QUERY_STRING} [L]' . PHP_EOL; $mdb->insert( 'pp_routes', [
$htaccess_data .= 'RewriteRule ^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$ index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1&%{QUERY_STRING} [L]' . PHP_EOL; 'type' => 'system',
'lang_id' => 0,
'pattern' => '^producent/' . self::seo( $row['name'] ) . '$',
'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId,
] );
$mdb->insert( 'pp_routes', [
'type' => 'system',
'lang_id' => 0,
'pattern' => '^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$',
'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1',
] );
} }
//
// HTACCESS — generuj z PHP (bez szablonu htaccess.conf)
//
$htaccess_data = 'RewriteEngine On' . PHP_EOL;
$htaccess_data .= 'RewriteBase /' . PHP_EOL;
$htaccess_data .= 'Options +FollowSymlinks' . PHP_EOL;
$htaccess_data .= 'Options -Indexes' . PHP_EOL;
$htaccess_data .= PHP_EOL;
$htaccess_data .= '# Przekierowanie z www na bez www i z http na https w jednym kroku' . PHP_EOL;
$htaccess_data .= 'RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]' . PHP_EOL;
$htaccess_data .= 'RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]' . PHP_EOL;
$htaccess_data .= PHP_EOL;
$htaccess_data .= '# Przekierowanie z http na https, jesli nie zawiera www' . PHP_EOL;
$htaccess_data .= 'RewriteCond %{HTTPS} off' . PHP_EOL;
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]' . PHP_EOL;
$htaccess_data .= 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]' . PHP_EOL;
$htaccess_data .= PHP_EOL;
$htaccess_data .= '# Usuwanie koncowego slasha dla niekatalogów' . PHP_EOL;
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL;
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC]' . PHP_EOL;
$htaccess_data .= 'RewriteCond %{REQUEST_URI} (.+)/$' . PHP_EOL;
$htaccess_data .= 'RewriteRule ^ %1 [R=301,L]' . PHP_EOL;
$htaccess_data .= PHP_EOL;
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]' . PHP_EOL;
$htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]' . PHP_EOL;
$htaccess_data .= 'RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]' . PHP_EOL;
$htaccess_data .= PHP_EOL;
$htaccess_data .= 'RewriteRule ^admin/$ admin/index.php [L]' . PHP_EOL;
$htaccess_data .= PHP_EOL;
$htaccess_data .= 'RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]' . PHP_EOL;
$htaccess_data .= PHP_EOL;
$htaccess_data .= 'RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php' . PHP_EOL;
$htaccess_data .= 'RewriteRule ^ /%1 [R=301,L]' . PHP_EOL;
/* cache block */
if ( $settings['htaccess_cache'] )
{
$htaccess_data .= '<IfModule mod_deflate.c>' . PHP_EOL
. 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL
. '</IfModule>' . PHP_EOL
. '<IfModule mod_headers.c>' . PHP_EOL
. 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL
. '</IfModule>' . PHP_EOL
. '<IfModule mod_expires.c>' . PHP_EOL
. 'ExpiresActive on' . PHP_EOL
. 'ExpiresDefault "access plus 1 month"' . PHP_EOL
. 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL
. 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL
. 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL
. 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL
. 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL
. 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL
. 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL
. 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL
. 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL
. 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL
. 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL
. 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL
. 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL
. 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL
. 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL
. 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL
. 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL
. 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL
. '</IfModule>' . PHP_EOL;
}
else
{
$htaccess_data .= '<IfModule mod_headers.c>' . PHP_EOL
. 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL
. 'Header set Pragma "no-cache"' . PHP_EOL
. 'Header set Expires 0' . PHP_EOL
. '</IfModule>' . PHP_EOL;
}
$htaccess_data .= '<Files *.conf>' . PHP_EOL;
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
$htaccess_data .= ' Deny from all' . PHP_EOL;
$htaccess_data .= '</Files>' . PHP_EOL;
$htaccess_data .= '<Files *.log>' . PHP_EOL;
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
$htaccess_data .= ' Deny from all' . PHP_EOL;
$htaccess_data .= '</Files>' . PHP_EOL;
$htaccess_data .= '<Files *.ini>' . PHP_EOL;
$htaccess_data .= ' Order Deny,Allow' . PHP_EOL;
$htaccess_data .= ' Deny from all' . PHP_EOL;
$htaccess_data .= '</Files>' . PHP_EOL;
//
// KATEGORIE — sitemap + pp_routes (bez zmian)
//
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) ) foreach ( $results as $row ) if ( is_array( $results ) ) foreach ( $results as $row )
{ {
@@ -475,22 +639,29 @@ class Helpers
$site_map .= '<priority>1</priority>' . PHP_EOL; $site_map .= '<priority>1</priority>' . PHP_EOL;
$site_map .= '</url>' . PHP_EOL; $site_map .= '</url>' . PHP_EOL;
if ( $row2['seo_link'] ) $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] );
{
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; $mdb->delete( 'pp_routes', [ 'AND' => [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'] ] ] );
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9]+)$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]';
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/1$ ' . $language_link . self::seo( $row2['seo_link'] ) . ' [R=301,L]'; $mdb->insert( 'pp_routes', [
} 'category_id' => $row2['category_id'],
else 'lang_id' => $row['id'],
{ 'pattern' => '^' . $language_link . $seoSlug . '$',
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'],
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '/([0-9]+)$ index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]'; ] );
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '/1$ ' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . ' [R=301,L]'; $mdb->insert( 'pp_routes', [
} 'category_id' => $row2['category_id'],
'lang_id' => $row['id'],
'pattern' => '^' . $language_link . $seoSlug . '/([0-9]+)$',
'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1',
] );
} }
} }
} }
//
// PRODUKTY — sitemap + pp_routes (bez zmian)
//
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) ) if ( is_array( $results ) )
{ {
@@ -519,27 +690,13 @@ class Helpers
if ( $row2['seo_link'] ) if ( $row2['seo_link'] )
{ {
$pattern = '^' . $language_link . self::seo( $row2['seo_link'] ) . '$'; $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
$destination = 'index.php?product=' . $row2['product_id']; $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
$mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] );
$pattern = '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$';
$destination = 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1';
$mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] );
} }
else else
{ {
$pattern = '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$'; $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] );
$destination = 'index.php?product=' . $row2['product_id']; $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] );
$mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] );
$pattern = '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$';
$destination = 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1';
$mdb -> insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => $pattern, 'destination' => $destination ] );
} }
} }
} }
@@ -547,6 +704,9 @@ class Helpers
} }
} }
//
// STRONY + ARTYKULY — sitemap + pp_routes (bez zmian)
//
$results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] );
if ( is_array( $results ) ) if ( is_array( $results ) )
foreach ( $results as $row ) foreach ( $results as $row )
@@ -590,26 +750,29 @@ class Helpers
{ {
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$'; $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$';
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
$htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$'; $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$';
$htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]';
} }
$htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]'; $htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]';
} }
if ( $row2['seo_link'] ) $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] );
{ $langPrefix = $row2['start'] ? '' : $language_link;
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]';
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9]+)$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]'; $mdb->delete( 'pp_routes', [ 'AND' => [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'] ] ] );
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '/1$ ' . $language_link . self::seo( $row2['seo_link'] ) . ' [R=301,L]';
} $mdb->insert( 'pp_routes', [
else 'page_id' => $row2['page_id'],
{ 'lang_id' => $row['id'],
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; 'pattern' => '^' . $langPrefix . $seoSlug . '$',
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '/([0-9]+)$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1&%{QUERY_STRING} [L]'; 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'],
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '/1$ ' . $language_link . 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . ' [R=301,L]'; ] );
} $mdb->insert( 'pp_routes', [
'page_id' => $row2['page_id'],
'lang_id' => $row['id'],
'pattern' => '^' . $langPrefix . $seoSlug . '/([0-9]+)$',
'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1',
] );
} }
} }
@@ -619,15 +782,7 @@ class Helpers
{ {
if ( $row2['copy_from'] != null ) if ( $row2['copy_from'] != null )
{ {
$results_tmp = $mdb -> get( 'pp_articles_langs', [ $results_tmp = $mdb->get( 'pp_articles_langs', [ 'seo_link', 'title' ], [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row2['copy_from'] ] ] );
'seo_link',
'title'
], [
'AND' => [
'article_id' => $row2['article_id'],
'lang_id' => $row2['copy_from']
]
] );
$row2['seo_link'] = $results_tmp['seo_link']; $row2['seo_link'] = $results_tmp['seo_link'];
$row2['title'] = $results_tmp['title']; $row2['title'] = $results_tmp['title'];
} }
@@ -650,11 +805,34 @@ class Helpers
$robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL; $robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL;
} }
$mdb->delete( 'pp_routes', [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'] ] ] );
if ( $row2['seo_link'] ) if ( $row2['seo_link'] )
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . self::seo( $row2['seo_link'] ) . '$ index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; {
else if ( $row2['title'] != null ) $mdb->insert( 'pp_routes', [
$htaccess_data .= PHP_EOL . 'RewriteRule ^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$ index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'] . '&%{QUERY_STRING} [L]'; 'article_id' => $row2['article_id'],
'lang_id' => $row['id'],
'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$',
'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'],
] );
} }
else if ( $row2['title'] != null )
{
$mdb->insert( 'pp_routes', [
'article_id' => $row2['article_id'],
'lang_id' => $row['id'],
'pattern' => '^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$',
'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'],
] );
}
}
}
// Invalidacja cache tras
try {
( new \Shared\Cache\CacheHandler() )->delete( 'pp_routes:all' );
} catch ( \Exception $e ) {
// Redis niedostepny — ignorujemy
} }
$results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] ); $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] );
@@ -667,64 +845,12 @@ class Helpers
$site_map .= '</urlset>'; $site_map .= '</urlset>';
/* cache */
if ( $settings['htaccess_cache'] )
{
$htaccess_data = str_replace( '{HTACCESS_CACHE}',
'<IfModule mod_deflate.c>' . PHP_EOL
. 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL
. '</IfModule>' . PHP_EOL
. '<IfModule mod_headers.c>' . PHP_EOL
. 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL
. '</IfModule>' . PHP_EOL
. '<IfModule mod_expires.c>' . PHP_EOL
. 'ExpiresActive on' . PHP_EOL
. 'ExpiresDefault "access plus 1 month"' . PHP_EOL
. 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL
. 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL
. 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL
. 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL
. 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL
. 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL
. 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL
. 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL
. 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL
. 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL
. 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL
. 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL
. 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL
. 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL
. 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL
. 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL
. 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL
. 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL
. 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL
. '</IfModule>'
, $htaccess_data );
}
else
{
$htaccess_data = str_replace( '{HTACCESS_CACHE}',
'<IfModule mod_headers.c>' . PHP_EOL
. 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL
. 'Header set Pragma "no-cache"' . PHP_EOL
. 'Header set Expires 0' . PHP_EOL
. '</IfModule>',
$htaccess_data );
}
$htaccess_data .= PHP_EOL; $htaccess_data .= PHP_EOL;
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL;
$htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL;
$htaccess_data .= 'RewriteRule ^ index.php [L]'; $htaccess_data .= 'RewriteRule ^ index.php [L]';
// Niektore hostingi blokuja zmiane wersji PHP przez .htaccess. // Niektore hostingi blokuja zmiane wersji PHP przez .htaccess.
// Automatycznie komentujemy niedozwolone dyrektywy, aby generowany plik byl kompatybilny.
$htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data ); $htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data );
$fp = fopen( $dir . '.htaccess', 'w' ); $fp = fopen( $dir . '.htaccess', 'w' );

View File

@@ -92,6 +92,7 @@ class ProductArchiveController
. $skuEanHtml; . $skuEanHtml;
$rows[] = [ $rows[] = [
'_checkbox' => '<input type="checkbox" class="js-bulk-check" value="' . $id . '" aria-label="Zaznacz produkt">',
'lp' => $lp++ . '.', 'lp' => $lp++ . '.',
'product' => $productCell, 'product' => $productCell,
'price_brutto' => $priceBrutto !== '' ? $priceBrutto : '-', 'price_brutto' => $priceBrutto !== '' ? $priceBrutto : '-',
@@ -123,6 +124,7 @@ class ProductArchiveController
$viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel( $viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel(
[ [
['key' => '_checkbox', 'label' => '', 'class' => 'text-center table-col-bulk-check', 'sortable' => false, 'raw' => true],
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
['key' => 'product', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], ['key' => 'product', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true],
['key' => 'price_brutto', 'sort_key' => 'price_brutto', 'label' => 'Cena', 'class' => 'text-center', 'sortable' => true], ['key' => 'price_brutto', 'sort_key' => 'price_brutto', 'label' => 'Cena', 'class' => 'text-center', 'sortable' => true],
@@ -190,4 +192,40 @@ class ProductArchiveController
header( 'Location: /admin/product_archive/list/' ); header( 'Location: /admin/product_archive/list/' );
exit; exit;
} }
public function bulk_delete_permanent(): void
{
header( 'Content-Type: application/json; charset=utf-8' );
$rawIds = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? $_POST['ids'] : [];
$ids = [];
foreach ( $rawIds as $raw ) {
$id = (int) $raw;
if ( $id > 0 ) {
$ids[] = $id;
}
}
if ( empty( $ids ) ) {
echo json_encode( ['success' => false, 'message' => 'Nie wybrano żadnych produktów.'] );
exit;
}
$deleted = 0;
$errors = [];
foreach ( $ids as $id ) {
if ( $this->productRepository->delete( $id ) ) {
$deleted++;
} else {
$errors[] = $id;
}
}
echo json_encode( [
'success' => empty( $errors ),
'deleted' => $deleted,
'errors' => $errors,
] );
exit;
}
} }

View File

@@ -104,6 +104,9 @@ class ApiRouter
$producerRepo = new \Domain\Producer\ProducerRepository($db); $producerRepo = new \Domain\Producer\ProducerRepository($db);
return new Controllers\DictionariesApiController($statusRepo, $transportRepo, $paymentRepo, $attrRepo, $producerRepo); return new Controllers\DictionariesApiController($statusRepo, $transportRepo, $paymentRepo, $attrRepo, $producerRepo);
}, },
'categories' => function () use ($db) {
return new Controllers\CategoriesApiController();
},
]; ];
} }

View File

@@ -0,0 +1,104 @@
<?php
namespace api\Controllers;
use api\ApiRouter;
class CategoriesApiController
{
public function list(): void
{
if (!ApiRouter::requireMethod('GET')) {
return;
}
$db = $GLOBALS['mdb'] ?? null;
if (!$db) {
ApiRouter::sendError('INTERNAL_ERROR', 'Database not available', 500);
return;
}
// Default shop language
$defaultLang = $db->get('pp_langs', 'id', ['start' => 1]);
if (!$defaultLang) {
$defaultLang = 'pl';
}
$defaultLang = (string)$defaultLang;
// All active categories, ordered by display order
$rows = $db->select(
'pp_shop_categories',
['id', 'parent_id'],
[
'status' => 1,
'ORDER' => ['o' => 'ASC'],
]
);
if (!is_array($rows) || empty($rows)) {
ApiRouter::sendSuccess(['categories' => []]);
return;
}
$categoryIds = array_values(array_filter(
array_map(fn($row) => (int)($row['id'] ?? 0), $rows),
fn($id) => $id > 0
));
// Bulk fetch titles for default language
$titlesByCategory = [];
$titleRows = $db->select('pp_shop_categories_langs', ['category_id', 'title'], [
'AND' => [
'category_id' => $categoryIds,
'lang_id' => $defaultLang,
'title[!]' => '',
],
]);
if (is_array($titleRows)) {
foreach ($titleRows as $tr) {
$tid = (int)($tr['category_id'] ?? 0);
if ($tid > 0 && !isset($titlesByCategory[$tid])) {
$titlesByCategory[$tid] = (string)($tr['title'] ?? '');
}
}
}
// Bulk fetch fallback titles for categories without a title in default language
$missingIds = array_values(array_filter($categoryIds, fn($id) => !isset($titlesByCategory[$id])));
if (!empty($missingIds)) {
$fallbackRows = $db->select('pp_shop_categories_langs', ['category_id', 'title'], [
'AND' => [
'category_id' => $missingIds,
'title[!]' => '',
],
]);
if (is_array($fallbackRows)) {
foreach ($fallbackRows as $fr) {
$fid = (int)($fr['category_id'] ?? 0);
if ($fid > 0 && !isset($titlesByCategory[$fid])) {
$titlesByCategory[$fid] = (string)($fr['title'] ?? '');
}
}
}
}
// Build flat category list
$categories = [];
foreach ($rows as $row) {
$categoryId = (int)($row['id'] ?? 0);
if ($categoryId <= 0) {
continue;
}
$parentId = $row['parent_id'] !== null ? (int)$row['parent_id'] : null;
$title = $titlesByCategory[$categoryId] ?? ('Kategoria #' . $categoryId);
$categories[] = [
'id' => $categoryId,
'parent_id' => $parentId,
'title' => $title,
];
}
ApiRouter::sendSuccess(['categories' => $categories]);
}
}

View File

@@ -437,7 +437,7 @@ class ProductsApiController
// String fields — direct mapping // String fields — direct mapping
$stringFields = [ $stringFields = [
'sku', 'ean', 'custom_label_0', 'custom_label_1', 'custom_label_2', 'sku', 'ean', 'custom_label_0', 'custom_label_1', 'custom_label_2',
'custom_label_3', 'custom_label_4', 'wp', 'custom_label_3', 'custom_label_4', 'wp', 'new_to_date', 'additional_message_text',
]; ];
foreach ($stringFields as $field) { foreach ($stringFields as $field) {
if (isset($body[$field])) { if (isset($body[$field])) {
@@ -447,6 +447,18 @@ class ProductsApiController
} }
} }
if (isset($body['additional_message'])) {
$d['additional_message'] = !empty($body['additional_message']) ? 'on' : '';
} elseif ($existing !== null) {
$d['additional_message'] = !empty($existing['additional_message']) ? 'on' : '';
}
if (isset($body['additional_message_required'])) {
$d['additional_message_required'] = !empty($body['additional_message_required']) ? 'on' : '';
} elseif ($existing !== null) {
$d['additional_message_required'] = !empty($existing['additional_message_required']) ? 'on' : '';
}
// Foreign keys // Foreign keys
if (isset($body['set_id'])) { if (isset($body['set_id'])) {
$d['set'] = $body['set_id']; $d['set'] = $body['set_id'];

View File

@@ -6,6 +6,7 @@ if ( is_array( $this -> pages ) ) {
foreach ( $this -> pages as $page ) { foreach ( $this -> pages as $page ) {
$url = ""; $url = "";
$hasChildren = is_array( $page['pages'] ) && !empty( $page['pages'] );
if ( $page['page_type'] == 3 ) { if ( $page['page_type'] == 3 ) {
$page['language']['link'] ? $url = $page['language']['link'] : $url = '#'; $page['language']['link'] ? $url = $page['language']['link'] : $url = '#';
@@ -26,10 +27,10 @@ if ( is_array( $this -> pages ) ) {
if ( $page['id'] == $this -> current_page ) if ( $page['id'] == $this -> current_page )
echo ' active'; echo ' active';
if ( is_array( $page['pages'] ) and in_array( $this -> current_page, $children ) ) if ( $hasChildren and is_array( $children ) and in_array( $this -> current_page, $children ) )
echo ' open'; echo ' open';
if ( is_array( $page['pages'] ) ) if ( $hasChildren )
echo ' parent'; echo ' parent';
echo '">'; echo '">';
@@ -40,13 +41,14 @@ if ( is_array( $this -> pages ) ) {
if ( $page['language']['noindex'] ) if ( $page['language']['noindex'] )
echo 'rel="nofollow"'; echo 'rel="nofollow"';
echo ' title="' . $page['language']['title'] . '"'; echo ' title="' . $page['language']['title'] . '"';
if ( is_array( $page['pages'] ) ) if ( $hasChildren )
echo "class='menu-trigger'"; echo "class='menu-trigger'";
echo '>'; echo '>';
echo $page['language']['title']; echo $page['language']['title'];
echo '</a>'; echo '</a>';
if ( is_array( $page['pages'] ) ) if ( $hasChildren )
echo '<i class="fa fa-chevron-down menu-toggle" menu-id="link-' . $page['id'] . '"></i>'; echo '<i class="fa fa-chevron-down menu-toggle" menu-id="link-' . $page['id'] . '"></i>';
if ( $hasChildren )
echo \front\Views\Menu::pages( $page['pages'], $this -> level + 1, $this -> current_page ); echo \front\Views\Menu::pages( $page['pages'], $this -> level + 1, $this -> current_page );
echo '</li>'; echo '</li>';
} }

View File

@@ -13,7 +13,14 @@
$basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ) + $transport_cost; $basket_summary = \Domain\Basket\BasketCalculator::summaryPrice( $basket, $coupon ) + $transport_cost;
?> ?>
<? if ( is_array( $this -> payment_methods ) ): foreach ( $this -> payment_methods as $payment_method ):?> <? if ( is_array( $this -> payment_methods ) ): foreach ( $this -> payment_methods as $payment_method ):?>
<? if ( $payment_method['id'] != 6 or $payment_method['id'] == 6 and $basket_summary >= 40 and $basket_summary <= 1000 ):?> <?
$min = isset($payment_method['min_order_amount']) ? (float)$payment_method['min_order_amount'] : null;
$max = isset($payment_method['max_order_amount']) ? (float)$payment_method['max_order_amount'] : null;
$show = true;
if ($min !== null && $min > 0 && $basket_summary < $min) $show = false;
if ($max !== null && $max > 0 && $basket_summary > $max) $show = false;
?>
<? if ( $show ):?>
<div class="options"> <div class="options">
<div class="check"> <div class="check">
<input type="radio" class="icheck" name="payment_method" value="<?= $payment_method['id'];?>" <input type="radio" class="icheck" name="payment_method" value="<?= $payment_method['id'];?>"

View File

@@ -52,6 +52,32 @@ if ( is_array( $this -> transports_methods ) )
</div> </div>
<? endif;?> <? endif;?>
<? endforeach; endif;?> <? endforeach; endif;?>
<?php if ( $this->free_delivery > 0 ): ?>
<?php
$percentage = min(100, ($this->basket_summary / $this->free_delivery) * 100);
$remaining = $this->free_delivery - $this->basket_summary;
?>
<div class="free-delivery-bar <?= $percentage >= 100 ? 'success' : '' ?>">
<div class="free-delivery-bar__icon">&#x1F69A;</div>
<div class="free-delivery-bar__content">
<?php if ($percentage >= 100): ?>
<div class="free-delivery-bar__text">Gratulacje! Masz darmow&#261; dostaw&#281;!</div>
<?php else: ?>
<div class="free-delivery-bar__text">
Zr&#243;b zakupy za <?= \Shared\Helpers\Helpers::decimal( $this->free_delivery, 2 );?> z&#322; i otrzymaj darmow&#261; dostaw&#281;
</div>
<?php endif; ?>
<div class="free-delivery-bar__progress">
<div class="free-delivery-bar__progress-fill" style="width: <?= $percentage ?>%"></div>
</div>
<?php if ($percentage < 100): ?>
<div class="free-delivery-bar__remaining">
Brakuje <strong><?= \Shared\Helpers\Helpers::decimal( $remaining, 2 );?> z&#322;</strong> do darmowej dostawy.
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<script class="footer" type="text/javascript"> <script class="footer" type="text/javascript">
$( function() { $( function() {

View File

@@ -179,7 +179,7 @@
'id': <?= (int)$product['product_id'];?>, 'id': <?= (int)$product['product_id'];?>,
'name': '<?= $product['name'];?>', 'name': '<?= $product['name'];?>',
'quantity': <?= $product['quantity'];?>, 'quantity': <?= $product['quantity'];?>,
'price': <?= $product['price_brutto_promo'];?> 'price': <?= ((float)$product['price_brutto_promo'] > 0 && (float)$product['price_brutto_promo'] < (float)$product['price_brutto']) ? (float)$product['price_brutto_promo'] : (float)$product['price_brutto'];?>
}<? if ( $product != end( $this -> order['products'] ) ) echo ',';?> }<? if ( $product != end( $this -> order['products'] ) ) echo ',';?>
<? endforeach;?> <? endforeach;?>
] ]