productRepo = $productRepo; $this->attrRepo = $attrRepo; } public function list(): void { if (!ApiRouter::requireMethod('GET')) { return; } $filters = [ 'search' => isset($_GET['search']) ? $_GET['search'] : '', 'status' => isset($_GET['status']) ? $_GET['status'] : '', 'promoted' => isset($_GET['promoted']) ? $_GET['promoted'] : '', ]; // Attribute filters: attribute_{id}={value_id} $attrFilters = []; foreach ($_GET as $key => $value) { if (strpos($key, 'attribute_') === 0) { $attrId = (int)substr($key, 10); if ($attrId > 0 && (int)$value > 0) { $attrFilters[$attrId] = (int)$value; } } } if (!empty($attrFilters)) { $filters['attributes'] = $attrFilters; } $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; $sortDir = isset($_GET['sort_dir']) ? $_GET['sort_dir'] : 'DESC'; $page = max(1, (int)(isset($_GET['page']) ? $_GET['page'] : 1)); $perPage = max(1, min(100, (int)(isset($_GET['per_page']) ? $_GET['per_page'] : 50))); $result = $this->productRepo->listForApi($filters, $sort, $sortDir, $page, $perPage); ApiRouter::sendSuccess($result); } public function get(): void { if (!ApiRouter::requireMethod('GET')) { return; } $id = (int)(isset($_GET['id']) ? $_GET['id'] : 0); if ($id <= 0) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400); return; } $product = $this->productRepo->findForApi($id); if ($product === null) { ApiRouter::sendError('NOT_FOUND', 'Product not found', 404); return; } ApiRouter::sendSuccess($product); } public function create(): void { if (!ApiRouter::requireMethod('POST')) { return; } $body = ApiRouter::getJsonBody(); if ($body === null) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400); return; } if (empty($body['languages']) || !is_array($body['languages'])) { ApiRouter::sendError('BAD_REQUEST', 'Missing languages (at least one language with name is required)', 400); return; } $hasName = false; foreach ($body['languages'] as $lang) { if (is_array($lang) && !empty($lang['name'])) { $hasName = true; break; } } if (!$hasName) { ApiRouter::sendError('BAD_REQUEST', 'At least one language must have a name', 400); return; } if (!isset($body['price_brutto'])) { ApiRouter::sendError('BAD_REQUEST', 'Missing price_brutto', 400); return; } if (!is_numeric($body['price_brutto']) || (float)$body['price_brutto'] < 0) { ApiRouter::sendError('BAD_REQUEST', 'price_brutto must be a non-negative number', 400); return; } $formData = $this->mapApiToFormData($body); $productId = $this->productRepo->saveProduct($formData); if ($productId === null) { ApiRouter::sendError('INTERNAL_ERROR', 'Failed to create product', 500); return; } http_response_code(201); echo json_encode([ 'status' => 'ok', 'data' => ['id' => $productId], ], JSON_UNESCAPED_UNICODE); } public function update(): void { if (!ApiRouter::requireMethod('PUT')) { return; } $id = (int)(isset($_GET['id']) ? $_GET['id'] : 0); if ($id <= 0) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400); return; } $existing = $this->productRepo->find($id); if ($existing === null) { ApiRouter::sendError('NOT_FOUND', 'Product not found', 404); return; } $body = ApiRouter::getJsonBody(); if ($body === null) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400); return; } $formData = $this->mapApiToFormData($body, $existing); $formData['id'] = $id; $this->productRepo->saveProduct($formData); $updated = $this->productRepo->findForApi($id); ApiRouter::sendSuccess($updated); } public function variants(): void { if (!ApiRouter::requireMethod('GET')) { return; } $id = (int)(isset($_GET['id']) ? $_GET['id'] : 0); if ($id <= 0) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400); return; } $product = $this->productRepo->find($id); if ($product === null) { ApiRouter::sendError('NOT_FOUND', 'Product not found', 404); return; } if (!empty($product['parent_id'])) { ApiRouter::sendError('BAD_REQUEST', 'Cannot get variants of a variant product', 400); return; } $variants = $this->productRepo->findVariantsForApi($id); // Available attributes for this product $allAttributes = $this->attrRepo->listForApi(); $usedAttrIds = []; foreach ($variants as $variant) { foreach ($variant['attributes'] as $a) { $usedAttrIds[(int)$a['attribute_id']] = true; } } $availableAttributes = []; foreach ($allAttributes as $attr) { if (isset($usedAttrIds[$attr['id']])) { $availableAttributes[] = $attr; } } ApiRouter::sendSuccess([ 'product_id' => $id, 'available_attributes' => $availableAttributes, 'variants' => $variants, ]); } public function create_variant(): void { if (!ApiRouter::requireMethod('POST')) { return; } $parentId = (int)(isset($_GET['id']) ? $_GET['id'] : 0); if ($parentId <= 0) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400); return; } $body = ApiRouter::getJsonBody(); if ($body === null) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400); return; } if (empty($body['attributes']) || !is_array($body['attributes'])) { ApiRouter::sendError('BAD_REQUEST', 'Missing or empty attributes', 400); return; } $result = $this->productRepo->createVariantForApi($parentId, $body); if ($result === null) { ApiRouter::sendError('BAD_REQUEST', 'Cannot create variant: parent not found, is archived, is itself a variant, or combination already exists', 400); return; } $variant = $this->productRepo->findVariantForApi($result['id']); http_response_code(201); echo json_encode([ 'status' => 'ok', 'data' => $variant !== null ? $variant : $result, ], JSON_UNESCAPED_UNICODE); } public function update_variant(): void { if (!ApiRouter::requireMethod('PUT')) { return; } $variantId = (int)(isset($_GET['id']) ? $_GET['id'] : 0); if ($variantId <= 0) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400); return; } $body = ApiRouter::getJsonBody(); if ($body === null) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid JSON body', 400); return; } $success = $this->productRepo->updateVariantForApi($variantId, $body); if (!$success) { ApiRouter::sendError('NOT_FOUND', 'Variant not found', 404); return; } $variant = $this->productRepo->findVariantForApi($variantId); ApiRouter::sendSuccess($variant); } public function delete_variant(): void { if (!ApiRouter::requireMethod('DELETE')) { return; } $variantId = (int)(isset($_GET['id']) ? $_GET['id'] : 0); if ($variantId <= 0) { ApiRouter::sendError('BAD_REQUEST', 'Missing or invalid id parameter', 400); return; } $success = $this->productRepo->deleteVariantForApi($variantId); if (!$success) { ApiRouter::sendError('NOT_FOUND', 'Variant not found', 404); return; } 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'; } // 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; } $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(). * * @param array $body Dane z JSON body * @param array|null $existing Istniejące dane produktu (partial update) * @return array Dane w formacie formularza */ private function mapApiToFormData(array $body, ?array $existing = null): array { $d = []; // Status/promoted — saveProduct expects 'on' for checkboxes if (isset($body['status'])) { $d['status'] = $body['status'] ? 'on' : ''; } elseif ($existing !== null) { $d['status'] = !empty($existing['status']) ? 'on' : ''; } if (isset($body['promoted'])) { $d['promoted'] = $body['promoted'] ? 'on' : ''; } elseif ($existing !== null) { $d['promoted'] = !empty($existing['promoted']) ? 'on' : ''; } if (isset($body['stock_0_buy'])) { $d['stock_0_buy'] = $body['stock_0_buy'] ? 'on' : ''; } elseif ($existing !== null) { $d['stock_0_buy'] = !empty($existing['stock_0_buy']) ? 'on' : ''; } // Numeric fields — direct mapping $numericFields = [ 'price_brutto', 'price_netto', 'price_brutto_promo', 'price_netto_promo', 'vat', 'quantity', 'weight', ]; foreach ($numericFields as $field) { if (isset($body[$field])) { $d[$field] = $body[$field]; } elseif ($existing !== null && isset($existing[$field])) { $d[$field] = $existing[$field]; } } // 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', 'custom_label_3', 'custom_label_4', 'wp', ]; foreach ($stringFields as $field) { if (isset($body[$field])) { $d[$field] = $body[$field]; } elseif ($existing !== null && isset($existing[$field])) { $d[$field] = $existing[$field]; } } // Foreign keys if (isset($body['set_id'])) { $d['set'] = $body['set_id']; } elseif ($existing !== null && isset($existing['set_id'])) { $d['set'] = $existing['set_id']; } if (isset($body['producer_id'])) { $d['producer_id'] = $body['producer_id']; } elseif ($existing !== null && isset($existing['producer_id'])) { $d['producer_id'] = $existing['producer_id']; } if (isset($body['product_unit_id'])) { $d['product_unit'] = $body['product_unit_id']; } elseif ($existing !== null && isset($existing['product_unit_id'])) { $d['product_unit'] = $existing['product_unit_id']; } // Languages: body.languages.pl.name → d['name']['pl'] if (isset($body['languages']) && is_array($body['languages'])) { $langFields = [ 'name', 'short_description', 'description', 'meta_description', 'meta_keywords', 'meta_title', 'seo_link', 'copy_from', 'warehouse_message_zero', 'warehouse_message_nonzero', 'tab_name_1', 'tab_description_1', 'tab_name_2', 'tab_description_2', 'canonical', 'security_information', ]; foreach ($body['languages'] as $langId => $langData) { if (!is_array($langData)) { continue; } foreach ($langFields as $field) { if (isset($langData[$field])) { $d[$field][$langId] = $langData[$field]; } } } } // Categories if (isset($body['categories']) && is_array($body['categories'])) { $d['categories'] = $body['categories']; } // Related products if (isset($body['products_related']) && is_array($body['products_related'])) { $d['products_related'] = $body['products_related']; } // Custom fields (Dodatkowe pola) if (isset($body['custom_fields']) && is_array($body['custom_fields'])) { $d['custom_field_name'] = []; $d['custom_field_type'] = []; $d['custom_field_required'] = []; foreach ($body['custom_fields'] as $cf) { if (!is_array($cf) || empty($cf['name'])) { continue; } $d['custom_field_name'][] = (string)$cf['name']; $d['custom_field_type'][] = !empty($cf['type']) ? (string)$cf['type'] : 'text'; $d['custom_field_required'][] = !empty($cf['is_required']) ? 1 : 0; } } return $d; } }