Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b2d156e84 | |||
| c44f59894e | |||
| fe2a77e995 | |||
| f0b1152ab1 |
@@ -747,26 +747,55 @@ class IntegrationsRepository
|
|||||||
|
|
||||||
// Import images
|
// Import images
|
||||||
$images = $mdb2->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId ] );
|
$images = $mdb2->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId ] );
|
||||||
|
$importLog = [];
|
||||||
|
$domainRaw = preg_replace( '#^https?://#', '', (string)($settings['domain'] ?? '') );
|
||||||
if ( is_array( $images ) ) {
|
if ( is_array( $images ) ) {
|
||||||
foreach ( $images as $image ) {
|
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 );
|
$ch = curl_init( $imageUrl );
|
||||||
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
|
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
|
||||||
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
|
curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, true );
|
||||||
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
|
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
|
||||||
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 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 );
|
curl_close( $ch );
|
||||||
|
|
||||||
$imageName = basename( $imageUrl );
|
if ( $curlErrno !== 0 || $imageData === false ) {
|
||||||
$imageDir = '../upload/product_images/product_' . $newProductId;
|
$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;
|
$imagePath = $imageDir . '/' . $imageName;
|
||||||
|
|
||||||
if ( !file_exists( $imageDir ) )
|
if ( !file_exists( $imageDir ) && !mkdir( $imageDir, 0777, true ) && !file_exists( $imageDir ) ) {
|
||||||
mkdir( $imageDir, 0777, true );
|
$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', [
|
$this->db->insert( 'pp_shop_products_images', [
|
||||||
'product_id' => $newProductId,
|
'product_id' => $newProductId,
|
||||||
@@ -774,10 +803,50 @@ class IntegrationsRepository
|
|||||||
'alt' => $image['alt'] ?? '',
|
'alt' => $image['alt'] ?? '',
|
||||||
'o' => $image['o'],
|
'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
|
private function missingShopproSetting( array $settings, array $requiredKeys ): ?string
|
||||||
|
|||||||
@@ -737,7 +737,16 @@ class ProductRepository
|
|||||||
|
|
||||||
// Custom fields (Dodatkowe pola)
|
// Custom fields (Dodatkowe pola)
|
||||||
$customFields = $this->db->select('pp_shop_products_custom_fields', ['name', 'type', 'is_required'], ['id_product' => $id]);
|
$customFields = $this->db->select('pp_shop_products_custom_fields', ['name', 'type', 'is_required'], ['id_product' => $id]);
|
||||||
$result['custom_fields'] = is_array($customFields) ? $customFields : [];
|
$result['custom_fields'] = [];
|
||||||
|
if (is_array($customFields)) {
|
||||||
|
foreach ($customFields as $cf) {
|
||||||
|
$result['custom_fields'][] = [
|
||||||
|
'name' => $cf['name'],
|
||||||
|
'type' => !empty($cf['type']) ? $cf['type'] : 'text',
|
||||||
|
'is_required' => $cf['is_required'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Variants (only for parent products)
|
// Variants (only for parent products)
|
||||||
if (empty($product['parent_id'])) {
|
if (empty($product['parent_id'])) {
|
||||||
@@ -1322,7 +1331,10 @@ class ProductRepository
|
|||||||
$this->saveImagesOrder( $productId, $d['gallery_order'] );
|
$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 ) {
|
if ( !$isNew ) {
|
||||||
$this->cleanupDeletedFiles( $productId );
|
$this->cleanupDeletedFiles( $productId );
|
||||||
@@ -1636,6 +1648,7 @@ class ProductRepository
|
|||||||
$this->db->delete( 'pp_shop_products_langs', [ 'product_id' => $productId ] );
|
$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_images', [ 'product_id' => $productId ] );
|
||||||
$this->db->delete( 'pp_shop_products_files', [ '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_attributes', [ 'product_id' => $productId ] );
|
||||||
$this->db->delete( 'pp_shop_products', [ 'id' => $productId ] );
|
$this->db->delete( 'pp_shop_products', [ 'id' => $productId ] );
|
||||||
$this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] );
|
$this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] );
|
||||||
|
|||||||
@@ -106,6 +106,14 @@ class ProductArchiveController
|
|||||||
'confirm_ok' => 'Przywroc',
|
'confirm_ok' => 'Przywroc',
|
||||||
'confirm_cancel' => 'Anuluj',
|
'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/' );
|
header( 'Location: /admin/product_archive/list/' );
|
||||||
exit;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,7 +338,8 @@ class ProductsApiController
|
|||||||
$safeName = 'image_' . md5((string)microtime(true)) . '.jpg';
|
$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)) {
|
if (!is_dir($baseDir) && !mkdir($baseDir, 0775, true) && !is_dir($baseDir)) {
|
||||||
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to create target directory', 500);
|
ApiRouter::sendError('INTERNAL_ERROR', 'Failed to create target directory', 500);
|
||||||
return;
|
return;
|
||||||
@@ -507,7 +508,7 @@ class ProductsApiController
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$d['custom_field_name'][] = (string)$cf['name'];
|
$d['custom_field_name'][] = (string)$cf['name'];
|
||||||
$d['custom_field_type'][] = isset($cf['type']) ? (string)$cf['type'] : 'text';
|
$d['custom_field_type'][] = !empty($cf['type']) ? (string)$cf['type'] : 'text';
|
||||||
$d['custom_field_required'][] = !empty($cf['is_required']) ? 1 : 0;
|
$d['custom_field_required'][] = !empty($cf['is_required']) ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
## 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::shopproExportProduct()` — eksport produktu do zdalnej instancji shopPRO: pola główne, tłumaczenia, custom fields, zdjęcia przez API (base64)
|
||||||
|
|||||||
@@ -46,6 +46,17 @@ Zdjęcia produktów.
|
|||||||
| src | Ścieżka do pliku |
|
| src | Ścieżka do pliku |
|
||||||
| alt | Tekst alternatywny |
|
| 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
|
## pp_shop_products_categories
|
||||||
Przypisanie produktów do kategorii.
|
Przypisanie produktów do kategorii.
|
||||||
|
|
||||||
|
|||||||
BIN
updates/0.30/ver_0.321.zip
Normal file
BIN
updates/0.30/ver_0.321.zip
Normal file
Binary file not shown.
23
updates/0.30/ver_0.321_manifest.json
Normal file
23
updates/0.30/ver_0.321_manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"changelog": "NEW - API: obsługa custom_fields w create/update produktu",
|
||||||
|
"version": "0.321",
|
||||||
|
"files": {
|
||||||
|
"added": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"deleted": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"modified": [
|
||||||
|
"autoload/api/Controllers/ProductsApiController.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checksum_zip": "sha256:a04ac9975618bc3b21d80a8e449f98e5bc825ce49a37142e1e621a2ef34a19f1",
|
||||||
|
"sql": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"date": "2026-02-24",
|
||||||
|
"directories_deleted": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
updates/0.30/ver_0.322.zip
Normal file
BIN
updates/0.30/ver_0.322.zip
Normal file
Binary file not shown.
24
updates/0.30/ver_0.322_manifest.json
Normal file
24
updates/0.30/ver_0.322_manifest.json
Normal 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
@@ -1,5 +1,5 @@
|
|||||||
<?
|
<?
|
||||||
$current_ver = 321;
|
$current_ver = 323;
|
||||||
|
|
||||||
for ($i = 1; $i <= $current_ver; $i++)
|
for ($i = 1; $i <= $current_ver; $i++)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user