Compare commits

..

2 Commits

Author SHA1 Message Date
3b2d156e84 ver. 0.323: fix import zdjęć, trwałe usuwanie produktów, fix API upload path
- IntegrationsRepository: refactor importu zdjęć — walidacja HTTP, curl timeouty, logi, czytelny komunikat
- ProductRepository: saveCustomFields tylko gdy klucz istnieje (partial API update), delete() czyści custom_fields
- ProductArchiveController: przycisk i metoda delete_permanent() do trwałego usunięcia z archiwum
- ProductsApiController: fix ścieżki upload (api.php działa z rootu projektu)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 21:05:23 +01:00
c44f59894e build: update package v0.322
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:13:19 +01:00
10 changed files with 171 additions and 20 deletions

View File

@@ -747,26 +747,55 @@ class IntegrationsRepository
// Import images
$images = $mdb2->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId ] );
$importLog = [];
$domainRaw = preg_replace( '#^https?://#', '', (string)($settings['domain'] ?? '') );
if ( is_array( $images ) ) {
foreach ( $images as $image ) {
$imageUrl = 'https://' . $settings['domain'] . $image['src'];
$srcPath = (string)($image['src'] ?? '');
$imageUrl = 'https://' . rtrim( $domainRaw, '/' ) . '/' . ltrim( $srcPath, '/' );
$imageName = basename( $srcPath );
if ( $imageName === '' ) {
$importLog[] = '[SKIP] Pusta nazwa pliku dla src: ' . $srcPath;
continue;
}
$ch = curl_init( $imageUrl );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, false );
$imageData = curl_exec( $ch );
curl_setopt( $ch, CURLOPT_TIMEOUT, 30 );
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 10 );
$imageData = curl_exec( $ch );
$httpCode = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$curlErrno = curl_errno( $ch );
$curlError = curl_error( $ch );
curl_close( $ch );
$imageName = basename( $imageUrl );
$imageDir = '../upload/product_images/product_' . $newProductId;
if ( $curlErrno !== 0 || $imageData === false ) {
$importLog[] = '[ERROR] cURL: ' . $imageUrl . ' — błąd ' . $curlErrno . ': ' . $curlError;
continue;
}
if ( $httpCode !== 200 ) {
$importLog[] = '[ERROR] HTTP ' . $httpCode . ': ' . $imageUrl;
continue;
}
$imageDir = dirname( __DIR__, 3 ) . '/upload/product_images/product_' . $newProductId;
$imagePath = $imageDir . '/' . $imageName;
if ( !file_exists( $imageDir ) )
mkdir( $imageDir, 0777, true );
if ( !file_exists( $imageDir ) && !mkdir( $imageDir, 0777, true ) && !file_exists( $imageDir ) ) {
$importLog[] = '[ERROR] Nie można utworzyć katalogu: ' . $imageDir;
continue;
}
file_put_contents( $imagePath, $imageData );
$written = file_put_contents( $imagePath, $imageData );
if ( $written === false ) {
$importLog[] = '[ERROR] Zapis pliku nieudany: ' . $imagePath;
continue;
}
$this->db->insert( 'pp_shop_products_images', [
'product_id' => $newProductId,
@@ -774,10 +803,50 @@ class IntegrationsRepository
'alt' => $image['alt'] ?? '',
'o' => $image['o'],
] );
$importLog[] = '[OK] ' . $imageUrl . ' → ' . $imagePath . ' (' . $written . ' B)';
}
}
return [ 'success' => true, 'message' => 'Produkt został zaimportowany.' ];
// Zapisz log importu zdjęć (ścieżka absolutna — niezależna od cwd)
$logDir = dirname( __DIR__, 3 ) . '/logs';
$logFile = $logDir . '/shoppro-import-debug.log';
$mkdirOk = file_exists( $logDir ) || mkdir( $logDir, 0755, true ) || file_exists( $logDir );
$logEntry = '[' . date( 'Y-m-d H:i:s' ) . '] Import produktu #' . $productId . ' → #' . $newProductId . "\n"
. ' Domain: ' . $domainRaw . "\n"
. ' Obrazy źródłowe: ' . count( $images ?: [] ) . "\n";
foreach ( $importLog as $line ) {
$logEntry .= ' ' . $line . "\n";
}
// Zawsze loguj do error_log (niezależnie od uprawnień do pliku)
error_log( '[shopPRO shoppro-import] ' . str_replace( "\n", ' | ', $logEntry ) );
if ( $mkdirOk && file_put_contents( $logFile, $logEntry, FILE_APPEND ) === false ) {
error_log( '[shopPRO shoppro-import] WARN: nie można zapisać logu do: ' . $logFile );
} elseif ( !$mkdirOk ) {
error_log( '[shopPRO shoppro-import] WARN: nie można utworzyć katalogu: ' . $logDir );
}
// Zbuduj czytelny komunikat z wynikiem importu zdjęć
$imgCount = count( $images ?: [] );
if ( $imgCount === 0 ) {
$imgSummary = 'Zdjęcia: brak w bazie źródłowej.';
} else {
$ok = 0;
$errors = [];
foreach ( $importLog as $line ) {
if ( strncmp( $line, '[OK]', 4 ) === 0 ) {
$ok++;
} else {
$errors[] = $line;
}
}
$imgSummary = 'Zdjęcia: ' . $ok . '/' . $imgCount . ' zaimportowanych.';
if ( !empty( $errors ) ) {
$imgSummary .= ' Błędy: ' . implode( '; ', $errors );
}
}
return [ 'success' => true, 'message' => 'Produkt został zaimportowany. ' . $imgSummary ];
}
private function missingShopproSetting( array $settings, array $requiredKeys ): ?string

View File

@@ -1331,7 +1331,10 @@ class ProductRepository
$this->saveImagesOrder( $productId, $d['gallery_order'] );
}
$this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] );
// Zapisz custom fields tylko gdy jawnie podane (partial update przez API może nie zawierać tego klucza)
if ( array_key_exists( 'custom_field_name', $d ) ) {
$this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] );
}
if ( !$isNew ) {
$this->cleanupDeletedFiles( $productId );
@@ -1645,6 +1648,7 @@ class ProductRepository
$this->db->delete( 'pp_shop_products_langs', [ 'product_id' => $productId ] );
$this->db->delete( 'pp_shop_products_images', [ 'product_id' => $productId ] );
$this->db->delete( 'pp_shop_products_files', [ 'product_id' => $productId ] );
$this->db->delete( 'pp_shop_products_custom_fields', [ 'id_product' => $productId ] );
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $productId ] );
$this->db->delete( 'pp_shop_products', [ 'id' => $productId ] );
$this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] );

View File

@@ -106,6 +106,14 @@ class ProductArchiveController
'confirm_ok' => 'Przywroc',
'confirm_cancel' => 'Anuluj',
],
[
'label' => 'Usun trwale',
'url' => '/admin/product_archive/delete_permanent/product_id=' . $id,
'class' => 'btn btn-xs btn-danger',
'confirm' => 'UWAGA! Operacja nieodwracalna!' . "\n\n" . 'Produkt "' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '" zostanie trwale usuniety razem ze wszystkimi zdjeciami i zalacznikami z serwera.' . "\n\n" . 'Czy na pewno chcesz usunac ten produkt?',
'confirm_ok' => 'Tak, usun trwale',
'confirm_cancel' => 'Anuluj',
],
],
];
}
@@ -162,4 +170,24 @@ class ProductArchiveController
header( 'Location: /admin/product_archive/list/' );
exit;
}
public function delete_permanent(): void
{
$productId = (int) \Shared\Helpers\Helpers::get( 'product_id' );
if ( $productId <= 0 ) {
\Shared\Helpers\Helpers::alert( 'Nieprawidłowe ID produktu.' );
header( 'Location: /admin/product_archive/list/' );
exit;
}
if ( $this->productRepository->delete( $productId ) ) {
\Shared\Helpers\Helpers::set_message( 'Produkt został trwale usunięty wraz ze zdjęciami i załącznikami.' );
} else {
\Shared\Helpers\Helpers::alert( 'Podczas usuwania produktu wystąpił błąd. Proszę spróbować ponownie.' );
}
header( 'Location: /admin/product_archive/list/' );
exit;
}
}

View File

@@ -338,7 +338,8 @@ class ProductsApiController
$safeName = 'image_' . md5((string)microtime(true)) . '.jpg';
}
$baseDir = '../upload/product_images/product_' . $productId;
// api.php działa z rootu projektu (nie z admin/), więc ścieżka bez ../
$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;

View File

@@ -4,6 +4,17 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
---
## ver. 0.323 (2026-02-24) - Import zdjęć, trwałe usuwanie, fix API upload
- **FIX**: `IntegrationsRepository::shopproImportProduct()` — kompletny refactor importu zdjęć: walidacja HTTP response, curl timeouty, bezpieczna budowa URL, szczegółowy log do `logs/shoppro-import-debug.log` i `error_log`, czytelny komunikat z wynikiem
- **FIX**: `ProductRepository::saveProduct()``saveCustomFields()` wywoływane tylko gdy klucz `custom_field_name` istnieje w danych (partial update przez API nie czyści custom fields)
- **FIX**: `ProductRepository::delete()` — usuwanie rekordów z `pp_shop_products_custom_fields` przy kasowaniu produktu
- **FIX**: `ProductsApiController::upload_image()` — poprawka ścieżki uploadu (`upload/` zamiast `../upload/` — api.php działa z rootu projektu)
- **NEW**: `ProductArchiveController::delete_permanent()` — trwałe usunięcie produktu z archiwum (wraz ze zdjęciami i załącznikami)
- **NEW**: Przycisk "Usuń trwale" w liście produktów archiwalnych z potwierdzeniem
---
## 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)

View File

@@ -46,6 +46,17 @@ Zdjęcia produktów.
| src | Ścieżka do pliku |
| alt | Tekst alternatywny |
## pp_shop_products_custom_fields
Dodatkowe pola produktów (custom fields).
| Kolumna | Opis |
|---------|------|
| id_additional_field | PK |
| id_product | FK do pp_shop_products |
| name | Nazwa pola |
| type | Typ pola (VARCHAR 30) |
| is_required | Czy wymagane (0/1) |
## pp_shop_products_categories
Przypisanie produktów do kategorii.

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

Binary file not shown.

View File

@@ -0,0 +1,24 @@
{
"changelog": "FIX - custom_fields: jawne mapowanie kluczy w ProductRepository, spójne !empty w ProductsApiController",
"version": "0.322",
"files": {
"added": [
],
"deleted": [
],
"modified": [
"autoload/Domain/Product/ProductRepository.php",
"autoload/api/Controllers/ProductsApiController.php"
]
},
"checksum_zip": "sha256:7c6e04cc393fdcd94e6fc9d2ea7a85f9c078e4beb9075d490be90675e4f6eae7",
"sql": [
],
"date": "2026-02-24",
"directories_deleted": [
]
}

File diff suppressed because one or more lines are too long

View File

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