Compare commits

..

4 Commits

Author SHA1 Message Date
4f66dbe42c ver. 0.319: usunięcie shopPRO eksportu produktów + rozszerzenie API o custom_fields i security_information
- Usunięto shopproExportProduct() z IntegrationsRepository
- Usunięto shoppro_product_export() z IntegrationsController
- Usunięto przycisk "Eksportuj do shopPRO" z ShopProductController
- ProductRepository: dodano custom_fields i security_information do odpowiedzi API
- Zaktualizowano docs/API.md i testy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 12:29:13 +01:00
4e720c5689 build: update package v0.318
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 11:59:04 +01:00
702e3a94be ver. 0.318: shopPRO export produktów + nowe API endpoints
- NEW: IntegrationsRepository::shopproExportProduct() — eksport produktu do
  zdalnej instancji shopPRO (pola główne, tłumaczenia, custom fields, zdjęcia)
- NEW: sendImageToShopproApi() — wysyłka zdjęć przez API shopPRO (base64 POST)
- REFACTOR: shopproImportProduct() — wydzielono shopproDb() i
  missingShopproSetting(); dodano security_information, producer_id,
  custom fields, alt zdjęcia
- NEW: AttributeRepository::ensureAttributeForApi() i
  ensureAttributeValueForApi() — idempotent find-or-create dla słowników
- NEW: API POST dictionaries/ensure_attribute — utwórz lub znajdź atrybut
- NEW: API POST dictionaries/ensure_attribute_value — utwórz lub znajdź wartość
- NEW: API POST products/upload_image — przyjmuje base64, zapisuje plik i DB
- NEW: IntegrationsController::shoppro_product_export() — akcja admina
- NEW: przycisk "Eksportuj do shopPRO" w liście produktów
- NEW: pole API key w ustawieniach integracji shopPRO

Tests: 765 tests, 2153 assertions — all green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 11:43:17 +01:00
9a351c16ee build: update package v0.317
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 23:31:39 +01:00
20 changed files with 525 additions and 19 deletions

View File

@@ -36,7 +36,7 @@ composer test
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
Current suite: **758 tests, 2135 assertions**.
Current suite: **765 tests, 2153 assertions**.
### Creating Updates
See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs.

View File

@@ -91,6 +91,20 @@
</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 class="col-lg-6">
@@ -123,4 +137,4 @@
});
})
});
</script>
</script>

View File

@@ -655,6 +655,95 @@ class AttributeRepository
return $result;
}
/**
* Find existing attribute by name/type or create a new one for API integration.
*
* @return array{id:int,created:bool}|null
*/
public function ensureAttributeForApi(string $name, int $type = 0, string $langId = 'pl'): ?array
{
$normalizedName = trim($name);
$normalizedLangId = trim($langId) !== '' ? trim($langId) : 'pl';
$normalizedType = $this->toTypeValue($type);
if ($normalizedName === '') {
return null;
}
$existingId = $this->findAttributeIdByNameAndType($normalizedName, $normalizedType);
if ($existingId > 0) {
return ['id' => $existingId, 'created' => false];
}
$this->db->insert('pp_shop_attributes', [
'status' => 1,
'type' => $normalizedType,
'o' => $this->nextOrder(),
]);
$attributeId = (int) $this->db->id();
if ($attributeId <= 0) {
return null;
}
$this->db->insert('pp_shop_attributes_langs', [
'attribute_id' => $attributeId,
'lang_id' => $normalizedLangId,
'name' => $normalizedName,
]);
$this->clearTempAndCache();
$this->clearFrontCache($attributeId, 'frontAttributeDetails');
return ['id' => $attributeId, 'created' => true];
}
/**
* Find existing value by name within attribute or create a new one for API integration.
*
* @return array{id:int,created:bool}|null
*/
public function ensureAttributeValueForApi(int $attributeId, string $name, string $langId = 'pl'): ?array
{
$normalizedName = trim($name);
$normalizedLangId = trim($langId) !== '' ? trim($langId) : 'pl';
$attributeId = max(0, $attributeId);
if ($attributeId <= 0 || $normalizedName === '') {
return null;
}
$attributeExists = (int) $this->db->count('pp_shop_attributes', ['id' => $attributeId]) > 0;
if (!$attributeExists) {
return null;
}
$existingId = $this->findAttributeValueIdByName($attributeId, $normalizedName);
if ($existingId > 0) {
return ['id' => $existingId, 'created' => false];
}
$this->db->insert('pp_shop_attributes_values', [
'attribute_id' => $attributeId,
'impact_on_the_price' => null,
'is_default' => 0,
]);
$valueId = (int) $this->db->id();
if ($valueId <= 0) {
return null;
}
$this->db->insert('pp_shop_attributes_values_langs', [
'value_id' => $valueId,
'lang_id' => $normalizedLangId,
'name' => $normalizedName,
'value' => null,
]);
$this->clearTempAndCache();
$this->clearFrontCache($valueId, 'frontValueDetails');
return ['id' => $valueId, 'created' => true];
}
/**
* @return array{sql: string, params: array<string, mixed>}
*/
@@ -972,6 +1061,52 @@ class AttributeRepository
return $this->defaultLangId;
}
private function findAttributeIdByNameAndType(string $name, int $type): int
{
$statement = $this->db->query(
'SELECT sa.id
FROM pp_shop_attributes sa
INNER JOIN pp_shop_attributes_langs sal ON sal.attribute_id = sa.id
WHERE sa.type = :type
AND LOWER(TRIM(sal.name)) = LOWER(TRIM(:name))
ORDER BY sa.id ASC
LIMIT 1',
[
':type' => $type,
':name' => $name,
]
);
if (!$statement) {
return 0;
}
$id = $statement->fetchColumn();
return $id === false ? 0 : (int) $id;
}
private function findAttributeValueIdByName(int $attributeId, string $name): int
{
$statement = $this->db->query(
'SELECT sav.id
FROM pp_shop_attributes_values sav
INNER JOIN pp_shop_attributes_values_langs savl ON savl.value_id = sav.id
WHERE sav.attribute_id = :attribute_id
AND LOWER(TRIM(savl.name)) = LOWER(TRIM(:name))
ORDER BY sav.id ASC
LIMIT 1',
[
':attribute_id' => $attributeId,
':name' => $name,
]
);
if (!$statement) {
return 0;
}
$id = $statement->fetchColumn();
return $id === false ? 0 : (int) $id;
}
// ── Frontend methods ──────────────────────────────────────────
public function frontAttributeDetails(int $attributeId, string $langId): array

View File

@@ -668,15 +668,12 @@ class IntegrationsRepository
public function shopproImportProduct( int $productId ): array
{
$settings = $this->getSettings( 'shoppro' );
$missingSetting = $this->missingShopproSetting( $settings, [ 'domain', 'db_name', 'db_host', 'db_user' ] );
if ( $missingSetting !== null ) {
return [ 'success' => false, 'message' => 'Brakuje konfiguracji shopPRO: ' . $missingSetting . '.' ];
}
$mdb2 = new \medoo( [
'database_type' => 'mysql',
'database_name' => $settings['db_name'],
'server' => $settings['db_host'],
'username' => $settings['db_user'],
'password' => $settings['db_password'],
'charset' => 'utf8'
] );
$mdb2 = $this->shopproDb( $settings );
$product = $mdb2->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
if ( !$product )
@@ -700,6 +697,7 @@ class IntegrationsRepository
'additional_message_text' => $product['additional_message_text'],
'additional_message_required'=> $product['additional_message_required'],
'weight' => $product['weight'],
'producer_id' => $product['producer_id'] ?? null,
] );
$newProductId = $this->db->id();
@@ -729,6 +727,20 @@ class IntegrationsRepository
'warehouse_message_nonzero'=> $lang['warehouse_message_nonzero'],
'canonical' => $lang['canonical'],
'xml_name' => $lang['xml_name'],
'security_information' => $lang['security_information'] ?? null,
] );
}
}
// Import custom fields
$customFields = $mdb2->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );
if ( is_array( $customFields ) ) {
foreach ( $customFields as $field ) {
$this->db->insert( 'pp_shop_products_custom_fields', [
'id_product' => $newProductId,
'name' => (string)($field['name'] ?? ''),
'type' => (string)($field['type'] ?? 'text'),
'is_required' => !empty( $field['is_required'] ) ? 1 : 0,
] );
}
}
@@ -759,6 +771,7 @@ class IntegrationsRepository
$this->db->insert( 'pp_shop_products_images', [
'product_id' => $newProductId,
'src' => '/upload/product_images/product_' . $newProductId . '/' . $imageName,
'alt' => $image['alt'] ?? '',
'o' => $image['o'],
] );
}
@@ -766,4 +779,28 @@ class IntegrationsRepository
return [ 'success' => true, 'message' => 'Produkt został zaimportowany.' ];
}
private function missingShopproSetting( array $settings, array $requiredKeys ): ?string
{
foreach ( $requiredKeys as $requiredKey ) {
if ( trim( (string)($settings[$requiredKey] ?? '') ) === '' ) {
return $requiredKey;
}
}
return null;
}
private function shopproDb( array $settings ): \medoo
{
return new \medoo( [
'database_type' => 'mysql',
'database_name' => $settings['db_name'],
'server' => $settings['db_host'],
'username' => $settings['db_user'],
'password' => $settings['db_password'] ?? '',
'charset' => 'utf8'
] );
}
}

View File

@@ -682,6 +682,7 @@ class ProductRepository
'tab_name_2' => $lang['tab_name_2'],
'tab_description_2' => $lang['tab_description_2'],
'canonical' => $lang['canonical'],
'security_information' => $lang['security_information'] ?? null,
];
}
}
@@ -733,6 +734,10 @@ class ProductRepository
}
}
// Custom fields (Dodatkowe pola)
$customFields = $this->db->select('pp_shop_products_custom_fields', ['name', 'type', 'is_required'], ['id_product' => $id]);
$result['custom_fields'] = is_array($customFields) ? $customFields : [];
// Variants (only for parent products)
if (empty($product['parent_id'])) {
$result['variants'] = $this->findVariantsForApi($id);
@@ -1239,7 +1244,7 @@ class ProductRepository
$productData = [
'date_modify' => date( 'Y-m-d H:i:s' ),
'modify_by' => $userId,
'modify_by' => $userId !== null ? (int) $userId : 0,
'status' => ( $d['status'] ?? '' ) === 'on' ? 1 : 0,
'price_netto' => $this->nullIfEmpty( $d['price_netto'] ?? null ),
'price_brutto' => $this->nullIfEmpty( $d['price_brutto'] ?? null ),

View File

@@ -140,6 +140,7 @@ class ShopProductController
}
}
$rows[] = $row;
}

View File

@@ -94,4 +94,81 @@ class DictionariesApiController
ApiRouter::sendSuccess($attributes);
}
public function ensure_attribute(): void
{
if (!ApiRouter::requireMethod('POST')) {
return;
}
$body = ApiRouter::getJsonBody();
if (!is_array($body)) {
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
return;
}
$name = trim((string) ($body['name'] ?? ''));
if ($name === '') {
ApiRouter::sendError('BAD_REQUEST', 'Missing name', 400);
return;
}
$type = (int) ($body['type'] ?? 0);
$lang = trim((string) ($body['lang'] ?? 'pl'));
if ($lang === '') {
$lang = 'pl';
}
$result = $this->attrRepo->ensureAttributeForApi($name, $type, $lang);
if (!is_array($result) || (int) ($result['id'] ?? 0) <= 0) {
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to ensure attribute', 500);
return;
}
ApiRouter::sendSuccess([
'id' => (int) ($result['id'] ?? 0),
'created' => !empty($result['created']),
]);
}
public function ensure_attribute_value(): void
{
if (!ApiRouter::requireMethod('POST')) {
return;
}
$body = ApiRouter::getJsonBody();
if (!is_array($body)) {
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
return;
}
$attributeId = (int) ($body['attribute_id'] ?? 0);
if ($attributeId <= 0) {
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid attribute_id', 400);
return;
}
$name = trim((string) ($body['name'] ?? ''));
if ($name === '') {
ApiRouter::sendError('BAD_REQUEST', 'Missing name', 400);
return;
}
$lang = trim((string) ($body['lang'] ?? 'pl'));
if ($lang === '') {
$lang = 'pl';
}
$result = $this->attrRepo->ensureAttributeValueForApi($attributeId, $name, $lang);
if (!is_array($result) || (int) ($result['id'] ?? 0) <= 0) {
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to ensure attribute value', 500);
return;
}
ApiRouter::sendSuccess([
'id' => (int) ($result['id'] ?? 0),
'created' => !empty($result['created']),
]);
}
}

View File

@@ -296,6 +296,95 @@ class ProductsApiController
ApiRouter::sendSuccess(['id' => $variantId, 'deleted' => true]);
}
public function upload_image(): void
{
if (!ApiRouter::requireMethod('POST')) {
return;
}
$body = ApiRouter::getJsonBody();
if ($body === null) {
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400);
return;
}
$productId = (int)($body['id'] ?? 0);
if ($productId <= 0) {
ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid product id', 400);
return;
}
$product = $this->productRepo->find($productId);
if ($product === null) {
ApiRouter::sendError('NOT_FOUND', 'Product not found', 404);
return;
}
$fileName = trim((string)($body['file_name'] ?? ''));
$base64 = (string)($body['content_base64'] ?? '');
if ($fileName === '' || $base64 === '') {
ApiRouter::sendError('BAD_REQUEST', 'Missing file_name or content_base64', 400);
return;
}
$binary = base64_decode($base64, true);
if ($binary === false) {
ApiRouter::sendError('BAD_REQUEST', 'Invalid content_base64 payload', 400);
return;
}
$safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', basename($fileName));
if ($safeName === '' || $safeName === null) {
$safeName = 'image_' . md5((string)microtime(true)) . '.jpg';
}
$baseDir = '../upload/product_images/product_' . $productId;
if (!is_dir($baseDir) && !mkdir($baseDir, 0775, true) && !is_dir($baseDir)) {
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to create target directory', 500);
return;
}
$targetPath = $baseDir . '/' . $safeName;
if (is_file($targetPath)) {
$name = pathinfo($safeName, PATHINFO_FILENAME);
$ext = pathinfo($safeName, PATHINFO_EXTENSION);
$targetPath = $baseDir . '/' . $name . '_' . substr(md5($safeName . microtime(true)), 0, 8) . ($ext !== '' ? '.' . $ext : '');
}
if (file_put_contents($targetPath, $binary) === false) {
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to save image file', 500);
return;
}
$src = '/upload/product_images/product_' . $productId . '/' . basename($targetPath);
$alt = (string)($body['alt'] ?? '');
$position = isset($body['o']) ? (int)$body['o'] : null;
$db = $GLOBALS['mdb'] ?? null;
if (!$db) {
ApiRouter::sendError('INTERNAL_ERROR', 'Database not available', 500);
return;
}
if ($position === null) {
$max = $db->max('pp_shop_products_images', 'o', ['product_id' => $productId]);
$position = (int)$max + 1;
}
$db->insert('pp_shop_products_images', [
'product_id' => $productId,
'src' => $src,
'alt' => $alt,
'o' => $position,
]);
ApiRouter::sendSuccess([
'src' => $src,
'alt' => $alt,
'o' => $position,
]);
}
/**
* Mapuje dane z JSON API na format oczekiwany przez saveProduct().
*
@@ -339,6 +428,11 @@ class ProductsApiController
}
}
// saveProduct() traktuje float 0.00 jako "puste", ale cena 0 musi pozostać jawnie ustawiona.
if (isset($d['price_brutto']) && is_numeric($d['price_brutto']) && (float)$d['price_brutto'] === 0.0) {
$d['price_brutto'] = '0';
}
// String fields — direct mapping
$stringFields = [
'sku', 'ean', 'custom_label_0', 'custom_label_1', 'custom_label_2',

View File

@@ -237,7 +237,8 @@ Odpowiedz:
"tab_description_1": null,
"tab_name_2": null,
"tab_description_2": null,
"canonical": null
"canonical": null,
"security_information": null
}
},
"images": [
@@ -253,6 +254,9 @@ Odpowiedz:
"value_names": {"pl": "Czerwony", "en": "Red"}
}
],
"custom_fields": [
{"name": "Napis na koszulce", "type": "text", "is_required": 1}
],
"variants": [
{
"id": 101,

View File

@@ -4,6 +4,21 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
## ver. 0.318 (2026-02-24) - ShopPRO export produktów + API endpoints
- **NEW**: `IntegrationsRepository::shopproExportProduct()` — eksport produktu do zdalnej instancji shopPRO: pola główne, tłumaczenia, custom fields, zdjęcia przez API (base64)
- **NEW**: `IntegrationsRepository::sendImageToShopproApi()` — wysyłka zdjęć do remote API shopPRO (endpoint `upload_image`) z base64
- **REFACTOR**: `shopproImportProduct()` — wydzielono `shopproDb()` i `missingShopproSetting()` jako prywatne helpery; dodano import `security_information`, `producer_id`, custom fields i `alt` zdjęcia
- **NEW**: `AttributeRepository::ensureAttributeForApi()` i `ensureAttributeValueForApi()` — idempotent find-or-create dla atrybutów i ich wartości (integracje API)
- **NEW**: API endpoint `POST /api.php?endpoint=dictionaries&action=ensure_attribute` — utwórz lub znajdź atrybut po nazwie i typie
- **NEW**: API endpoint `POST /api.php?endpoint=dictionaries&action=ensure_attribute_value` — utwórz lub znajdź wartość atrybutu po nazwie
- **NEW**: API endpoint `POST /api.php?endpoint=products&action=upload_image` — przyjmuje zdjęcie produktu jako base64 JSON, zapisuje plik i rekord w `pp_shop_products_images`
- **NEW**: `IntegrationsController::shoppro_product_export()` — akcja admina eksportująca produkt do shopPRO
- **NEW**: Przycisk "Eksportuj do shopPRO" w liście produktów (widoczny gdy shopPRO enabled)
- **NEW**: Pole "API key" w ustawieniach integracji shopPRO (`shoppro-settings.php`)
---
## ver. 0.317 (2026-02-23) - Klucz API: przycisk generowania + fix zapisu
- **FIX**: `SettingsRepository::saveSettings()` — pole `api_key` brakowało w whiteliście zapisywanych pól, przez co wartość była tracona przy każdym zapisie (TRUNCATE + insert)

View File

@@ -23,10 +23,10 @@ composer test # standard
## Aktualny stan
```text
OK (758 tests, 2135 assertions)
OK (765 tests, 2153 assertions)
```
Zweryfikowano: 2026-02-22 (ver. 0.304)
Zweryfikowano: 2026-02-24 (ver. 0.318)
## Konfiguracja

View File

@@ -0,0 +1 @@
1. Dodać przycisk kopiowania przy atrybutach produktu w zamówieniu

View File

@@ -186,4 +186,52 @@ class DictionariesApiControllerTest extends TestCase
$this->assertSame(405, http_response_code());
}
public function testEnsureAttributeRejectsGetMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
ob_start();
$this->controller->ensure_attribute();
ob_get_clean();
$this->assertSame(405, http_response_code());
}
public function testEnsureAttributeReturns400WhenNoBody(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start();
$this->controller->ensure_attribute();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
$json = json_decode($output, true);
$this->assertSame('BAD_REQUEST', $json['code']);
}
public function testEnsureAttributeValueRejectsGetMethod(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
ob_start();
$this->controller->ensure_attribute_value();
ob_get_clean();
$this->assertSame(405, http_response_code());
}
public function testEnsureAttributeValueReturns400WhenNoBody(): void
{
$_SERVER['REQUEST_METHOD'] = 'POST';
ob_start();
$this->controller->ensure_attribute_value();
$output = ob_get_clean();
$this->assertSame(400, http_response_code());
$json = json_decode($output, true);
$this->assertSame('BAD_REQUEST', $json['code']);
}
}

View File

@@ -351,6 +351,19 @@ class ProductsApiControllerTest extends TestCase
$this->assertSame('5901234123457', $result['ean']);
}
public function testMapApiToFormDataPreservesZeroBasePriceForSaveProduct(): void
{
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');
$method->setAccessible(true);
$result = $method->invoke($this->controller, [
'price_brutto' => 0.0,
'languages' => ['pl' => ['name' => 'Zero']],
]);
$this->assertSame('0', $result['price_brutto']);
}
public function testMapApiToFormDataMapsCategories(): void
{
$method = new \ReflectionMethod(ProductsApiController::class, 'mapApiToFormData');

BIN
updates/0.30/ver_0.317.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,26 @@
{
"changelog": "FIX - klucz API: fix zapisu (brakowalo w whiteliście), przycisk Generuj losowy klucz, ulepszony routing API",
"version": "0.317",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"api.php",
"autoload/Domain/Settings/SettingsRepository.php",
"autoload/admin/Controllers/SettingsController.php",
"autoload/api/ApiRouter.php"
]
},
"checksum_zip": "sha256:130e5f4fd15b635bb277dc639d528e017de866fbaada5db2d08d9426251824b3",
"sql": [
],
"date": "2026-02-23",
"directories_deleted": [
]
}

BIN
updates/0.30/ver_0.318.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,30 @@
{
"changelog": "NEW - shopPRO export produktów + API endpoints (ensure_attribute, ensure_attribute_value, upload_image)",
"version": "0.318",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"admin/templates/integrations/shoppro-settings.php",
"autoload/Domain/Attribute/AttributeRepository.php",
"autoload/Domain/Integrations/IntegrationsRepository.php",
"autoload/Domain/Product/ProductRepository.php",
"autoload/admin/Controllers/IntegrationsController.php",
"autoload/admin/Controllers/ShopProductController.php",
"autoload/api/Controllers/DictionariesApiController.php",
"autoload/api/Controllers/ProductsApiController.php"
]
},
"checksum_zip": "sha256:6a7eba1b390db94ccda210a5f2cbcd33f17f43d9f34031c4d0793d224df5d541",
"sql": [
],
"date": "2026-02-24",
"directories_deleted": [
]
}

View File

@@ -1,3 +1,9 @@
<b>ver. 0.318 - 24.02.2026</b><br />
NEW - shopPRO export produktów + API endpoints (ensure_attribute, ensure_attribute_value, upload_image)
<hr>
<b>ver. 0.317 - 23.02.2026</b><br />
FIX - klucz API: fix zapisu (brakowalo w whiteliĹcie), przycisk Generuj losowy klucz, ulepszony routing API
<hr>
<b>ver. 0.316 - 23.02.2026</b><br />
FIX - migracja brakujacej kolumny type w pp_shop_products_custom_fields
<hr>
@@ -5,16 +11,16 @@ FIX - migracja brakujacej kolumny type w pp_shop_products_custom_fields
FIX - PDOException w listowaniu atrybutow admin (SQLSTATE HY093)
<hr>
<b>ver. 0.314 - 23.02.2026</b><br />
FIX - naprawa globalnej wyszukiwarki admin (Content-Type, Cache-Control, POST, try/catch), NEW - title strony z numerem zamÄĹ‚wienia
FIX - naprawa globalnej wyszukiwarki admin (Content-Type, Cache-Control, POST, try/catch), NEW - title strony z numerem zamÄ„‚Ĺ‚wienia
<hr>
<b>ver. 0.313 - 23.02.2026</b><br />
FIX - sync pÄąââ¬ĹˇatnoÄąââ¬Ĺźci Apilo (int cast na apilo_order_id PPxxxxxx dawaĹ‚ 0) + logowanie decyzji sync do pp_log
FIX - sync pÄ‚ââ¬ĹľÄ„ââ¬Â¦Ă„‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąĂ‡atnoÄ‚ââ¬ĹľÄ„ââ¬Â¦Ă„‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąĹşci Apilo (int cast na apilo_order_id PPxxxxxx dawaĹ‚ 0) + logowanie decyzji sync do pp_log
<hr>
<b>ver. 0.312 - 23.02.2026</b><br />
FIX - krytyczne bugi integracji Apilo: curl_getinfo po curl_close, nieskoĄąĢ€žczona pÄ‚ââ¬ĹľÄ˘ââ¬ĹľĂ˜tla wysyĄąĢ€ški, ceny 0.00 PLN, walidacja cen
FIX - krytyczne bugi integracji Apilo: curl_getinfo po curl_close, nieskoÄ„ââ¬ĹˇÄ˘â‚¬ĹľÄ‚ââ¬ĹľÄ˘â‚¬Ă¦Ä„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘â‚¬ĹˇÄ‚Ă¬Ä„Ä…Ä„Äľczona pÄ‚ââ¬ĹľÄ˘â‚¬ĹˇÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąÄľÄ„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąÄľÄ‚ââ¬Ä…Ä‚Ă˜tla wysyÄ„ââ¬ĹˇÄ˘â‚¬ĹľÄ‚ââ¬ĹľÄ˘â‚¬Ă¦Ä„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘â‚¬ĹˇÄ‚Ă¬Ä„Ä…Ä‹ââ¬Ë‡ki, ceny 0.00 PLN, walidacja cen
<hr>
<b>ver. 0.311 - 23.02.2026</b><br />
FIX - race condition callback pÄ‚ââ¬ĹľÄ„ââ¬Â¦Ă„‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąĂ‡atnoÄ‚ââ¬ĹľÄ„ââ¬Â¦Ă„‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąĹşci Apilo, persistence filtrÄ‚ââ¬ĹľÄ˘â‚¬ĹˇÄ„ąĢ€šw tabel admin, poprawki cen zamÄ‚ââ¬ĹľÄ˘â‚¬ĹˇÄ„ąĢ€šwieÄ‚ââ¬ĹľÄ„ââ¬Â¦Ă„‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąÄľ
FIX - race condition callback pÄ‚ââ¬ĹľÄ˘â‚¬ĹˇÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąÄľÄ„ââ¬ĹˇÄ˘â‚¬ĹľÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬Ä‚æĂââ¬ĹľÄ˘â‚¬ĹˇÄ‚ââ¬Ä…Ä‚Ă˜Ä„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąĂ‡Ä‚ââ¬ĹˇÄ‚ìĂââ¬ĹľÄ„ââ¬Â¦Ă„‚ââ¬Ä…Ģ€ÇatnoÄ‚ââ¬ĹľÄ˘â‚¬ĹˇÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąÄľÄ„ââ¬ĹˇÄ˘â‚¬ĹľÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬Ä‚æĂââ¬ĹľÄ˘â‚¬ĹˇÄ‚ââ¬Ä…Ä‚Ă˜Ä„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąĂ‡Ä‚ââ¬ĹˇÄ‚ìĂââ¬ĹľÄ„ââ¬Â¦Ä„ąĹźci Apilo, persistence filtrÄ‚ââ¬ĹľÄ˘â‚¬ĹˇÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąÄľÄ„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘â‚¬ĹˇÄ‚Ă¬Ä„Ä…Ä‹ââ¬Ë‡Ä„ââ¬ĹˇÄ˘â‚¬ĹľÄ‚ââ¬ĹľÄ˘â‚¬Ă¦Ä„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘â‚¬ĹˇÄ‚Ă¬Ä„Ä…Ä‹ââ¬Ë‡w tabel admin, poprawki cen zamÄ‚ââ¬ĹľÄ˘â‚¬ĹˇÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąÄľÄ„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘â‚¬ĹˇÄ‚Ă¬Ä„Ä…Ä‹ââ¬Ë‡Ä„ââ¬ĹˇÄ˘â‚¬ĹľÄ‚ââ¬ĹľÄ˘â‚¬Ă¦Ä„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘â‚¬ĹˇÄ‚Ă¬Ä„Ä…Ä‹ââ¬Ë‡wieÄ‚ââ¬ĹľÄ˘â‚¬ĹˇÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąÄľÄ„ââ¬ĹˇÄ˘â‚¬ĹľÄ‚Ă˜Ä˘ââ¬ĹˇĂ¬Ä‚æĂââ¬ĹľÄ˘â‚¬ĹˇÄ‚ââ¬Ä…Ä‚Ă˜Ä„ââ¬ĹˇÄ‹Ă˜Ä‚Ă˜Ä˘ââ¬ĹˇĂ¬ÄąĂ‡Ä‚ââ¬ĹˇÄ‚ìĂââ¬ĹľÄ„ââ¬Â¦Ă„‚ââ¬ĹľÄ„Äľ
<hr>
<b>ver. 0.310 - 23.02.2026</b><br />
NEW - Zakladka Logi w sekcji Integracje (podglad pp_log z paginacja, sortowaniem, filtrami)

View File

@@ -1,5 +1,5 @@
<?
$current_ver = 317;
$current_ver = 319;
for ($i = 1; $i <= $current_ver; $i++)
{