diff --git a/api/app.py b/api/app.py index 5839aad..bdf7926 100644 --- a/api/app.py +++ b/api/app.py @@ -3,6 +3,15 @@ from database import * from werkzeug.utils import secure_filename app = Flask(__name__) +app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'photos') +app.config['URL_PREFIX'] = '/uploads' # public URL prefix + +# make sure the folder exists +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + +# helper to build a public URL +def public_url(filename: str) -> str: + return f"{app.config['URL_PREFIX']}/{filename}" # Initialise database on startup initialize_db() @@ -179,29 +188,60 @@ def setting_detail(name: str): # ----------------------------- Photos ----------------------------- -@app.route('/api/providers//photos', methods=['POST']) +# photo upload endpoint +@app.route('/api/providers//photos', methods=['POST']) def upload_photos(provider_id): if 'photos' not in request.files: return jsonify({'error': 'No files uploaded'}), 400 - - # Secure filename + save to persistent storage - upload_folder = f'{os.path.dirname(__file__)}/photos' - os.makedirs(upload_folder, exist_ok=True) - - photo_paths = [] - for file in request.files.getlist('photos'): - if file.filename == '': + + files = request.files.getlist('photos') + if not files: + return jsonify({'error': 'No files in list'}), 400 + + allowed_ext = {'.jpg', '.jpeg', '.png', '.gif'} + saved_filenames = [] + + for f in files: + if f.filename == '': continue - - filename = secure_filename(file.filename) - file.save(os.path.join(upload_folder, filename)) - photo_paths.append(f"/uploads/{filename}") - - # Store paths in DB (e.g., PostgreSQL JSON field) - db.execute("UPDATE providers SET photos = %s WHERE id = %s", - (json.dumps(photo_paths), provider_id)) - - return jsonify({'success': True, 'paths': photo_paths}), 200 + filename = secure_filename(f.filename) + ext = os.path.splitext(filename)[1].lower() + if ext not in allowed_ext: + return jsonify({'error': f'Unsupported file type: {ext}'}), 400 + # rename to avoid collisions + new_name = f"{uuid.uuid4().hex}{ext}" + f.save(os.path.join(app.config['UPLOAD_FOLDER'], new_name)) + saved_filenames.append(public_url(new_name)) + + # Merge with any pre‑existing photos + existing = db.fetch(f"SELECT photos FROM providers WHERE id=?", (provider_id,))[0][0] + try: + photos = json.loads(existing) + except Exception: + photos = [] + photos.extend(saved_filenames) + + db.execute("UPDATE providers SET photos = %s WHERE id = %s", + (json.dumps(photos), provider_id)) + + return jsonify({'success': True, 'photos': photos}), 200 + +# Endpoint to change the main photo +@app.route('/api/providers//main_photo', methods=['PUT']) +def set_main_photo(provider_id): + data = request.get_json() + url = data.get('url') + if not url: + return jsonify({'error': 'Missing url'}), 400 + + # Validate that the URL belongs to this provider + photos = db.fetch(f"SELECT photos FROM providers WHERE id=?", (provider_id,))[0][0] + if json.loads(photos).count(url) == 0: + return jsonify({'error': 'URL not part of this provider'}), 400 + + db.execute("UPDATE providers SET main_photo = %s WHERE id = %s", + (url, provider_id)) + return jsonify({'success': True, 'main_photo': url}) # Run the app if this file is executed directly if __name__ == "__main__": diff --git a/api/database.py b/api/database.py index a504924..6a1d73c 100644 --- a/api/database.py +++ b/api/database.py @@ -52,7 +52,9 @@ def initialize_db() -> None: age INTEGER, photo TEXT, rates TEXT, - private INTEGER NOT NULL DEFAULT 0 + private INTEGER NOT NULL DEFAULT 0, + photos TEXT DEFAULT '[]', + main_photo TEXT ); """ ) diff --git a/web/photos.php b/web/photos.php index 5caa74d..9829887 100644 --- a/web/photos.php +++ b/web/photos.php @@ -1,37 +1,314 @@ - - - + +// Validate provider ID +if (!isset($_GET['id']) || empty($_GET['id'])) { + die('Provider ID missing. Use: photos.php?id=123'); +} +$provider_id = $_GET['id']; -
-
- - +// Fetch existing photos from API +$api_url = BASE_API_URL . '/providers/' . urlencode($provider_id) . "/photos"; +$existing_photos = []; +try { + $response = file_get_contents($api_url); + $existing_photos = json_decode($response, true); + if (isset($existing_photos['error'])) { + trigger_error($existing_photos['error'], E_USER_WARNING); + $existing_photos = []; + } +} catch (Exception $e) { + trigger_error('API error: ' . $e->getMessage(), E_USER_WARNING); +} + +// Check upload permissions +$has_upload_perm = true; // Add authorization logic here + +// Generate unique token for upload (security) +$upload_token = bin2hex(random_bytes(16)); +?> + + + + + + + Photos for Service Provider #<?php echo htmlspecialchars($provider_id); ?> + + + +

Photos for Service Provider #

+ + +
+
-
- - -
-
- - -
+ + +
+ + +
+
+ +

Upload permissions disabled. Contact administrator.

+ - - - + + // Handle main photo selection + gallery.addEventListener('change', async (e) => { + if (e.target.className === 'main-radio') { + const mainPhoto = e.target.value; + const providerId = ''; + + try { + const response = await fetch( + BASE_API_URL . '/providers/${providerId}/main_photo', + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + photo_path: mainPhoto + }) + } + ); + + if (!response.ok) { + throw new Error(`API error: ${response.status}`); + } + + // Update visuals + gallery.querySelectorAll('.main-indicator').forEach(ind => { + ind.style.display = 'none'; + }); + e.target.parentElement.querySelector('.main-indicator').style.display = 'block'; + } catch (error) { + console.error('Setting main photo failed:', error); + } + } + }); + + // Hide loading message + loading.style.display = 'none'; + }); + + + \ No newline at end of file diff --git a/web/add_provider.php b/web/scripts/add_provider.php similarity index 100% rename from web/add_provider.php rename to web/scripts/add_provider.php diff --git a/web/config.php b/web/scripts/config.php similarity index 100% rename from web/config.php rename to web/scripts/config.php diff --git a/web/edit_provider.php b/web/scripts/edit_provider.php similarity index 59% rename from web/edit_provider.php rename to web/scripts/edit_provider.php index fe437bd..a3ad9d0 100644 --- a/web/edit_provider.php +++ b/web/scripts/edit_provider.php @@ -33,7 +33,39 @@ function callApi(string $method, string $path, $body = null) } // ------------------------------------------------------------------ -// 1️⃣ Get provider details +// ------------------------------------------------------------------ +// Helper: upload photos (multipart) +// Returns the JSON response from the API (or null on error) +function uploadPhotosApi(int $provider_id, array $files): ?array +{ + $url = BASE_API_URL . "/api/providers/{$provider_id}/photos"; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Build multipart array + $multipart = []; + foreach ($files['tmp_name'] as $i => $tmp) { + if ($files['name'][$i] === '') continue; + $multipart[] = [ + 'name' => 'photos', + 'contents' => fopen($tmp, 'r'), + 'filename' => $files['name'][$i] + ]; + } + curl_setopt($ch, CURLOPT_POSTFIELDS, $multipart); + // Let PHP set the correct Content‑Type boundary + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: multipart/form-data']); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + return ($httpCode < 400) ? json_decode($response, true) : null; +} + +// Get provider details $provider = callApi('GET', '/providers?id=' . $provider_id); if (!$provider) { echo '

Error

Could not load provider details.

'; @@ -43,14 +75,14 @@ if (is_array($provider) && count($provider) > 0 && isset($provider[0])) { $provider = $provider[0]; } -// 2️⃣ Get rate‑option list (comma‑separated string → array) +// Get rate‑option list (comma‑separated string → array) $rateSetting = callApi('GET', '/settings/rate_options'); $rateOptions = []; if ($rateSetting && isset($rateSetting['value'])) { $rateOptions = array_map('trim', explode(',', $rateSetting['value'])); } -// 3️⃣ Decode the API’s rate string (JSON → array of arrays) +// Decode the API’s rate string (JSON → array of arrays) $ratesData = $provider['rates'] ?? '[]'; $decoded = []; if (is_string($ratesData)) { @@ -62,7 +94,7 @@ if (is_string($ratesData)) { $decoded = $ratesData; // already an array – legacy path } -// 4️⃣ Build two helper arrays +// Build two helper arrays $selectedRateKeys = []; // lowercase keys of selected rates $ratesAssoc = []; // key → value @@ -75,7 +107,7 @@ foreach ($decoded as $entry) { } } -// 5️⃣ Find orphan rates (present in $decoded but not in $rateOptions) +// Find orphan rates (present in $decoded but not in $rateOptions) $orphanRates = []; $orphanKeys = []; foreach ($decoded as $entry) { @@ -90,8 +122,8 @@ foreach ($decoded as $entry) { } // ------------------------------------------------------------------ -// 6️⃣ Handle form POST -// --- Handle form POST ------------------------------------------------- +// Handle form POST +/* if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Base payload $payload = [ @@ -107,14 +139,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Rebuild the rates array from the form. $finalRates = []; - // 1️⃣ Add the selected valid rate options (value may be changed by the user) + // Add the selected valid rate options (value may be changed by the user) $selectedRateKeysRaw = $_POST['rate_selected'] ?? []; foreach ($selectedRateKeysRaw as $opt) { // $opt is the original rate name $newVal = $_POST['rate_value'][$opt] ?? '0'; // the updated value $finalRates[] = [$opt => $newVal]; } - // 2️⃣ Add orphan rates that the user chose **not** to delete + // Add orphan rates that the user chose **not** to delete $orphanDeleteSet = array_flip(array_map('strtolower', $_POST['orphan_delete'] ?? [])); foreach ($orphanRates as $key => $value) { // $key is already lower‑cased if (!isset($orphanDeleteSet[strtolower($key)])) { @@ -122,7 +154,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } - // 3️⃣ Encode the new array as JSON + // Encode the new array as JSON //$payload['rates'] = !empty($finalRates) ? json_encode($finalRates) : null; $payload['rates'] = !empty($finalRates) ? $finalRates : null; @@ -135,6 +167,68 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit; } echo '

Error

Failed to update provider.

'; +}*/ + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + + /* -------- 1. Build the base payload -------- */ + $payload = [ + 'name' => $_POST['name'] ?? '', + 'age' => (int)($_POST['age'] ?? 0), + 'photo' => $_POST['photo'] ?? '', + 'phone' => $_POST['phone'] ?? '', + 'private' => isset($_POST['private']), + ]; + + /* -------- 2. Re‑build the rates array -------- */ + $finalRates = []; + $selectedRateKeysRaw = $_POST['rate_selected'] ?? []; + foreach ($selectedRateKeysRaw as $opt) { + $newVal = $_POST['rate_value'][$opt] ?? '0'; + $finalRates[] = [$opt => $newVal]; + } + // orphan rates that the user chose *not* to delete + $orphanDeleteSet = array_flip(array_map('strtolower', $_POST['orphan_delete'] ?? [])); + foreach ($orphanRates as $key => $value) { + if (!isset($orphanDeleteSet[$key])) { + $finalRates[] = [$key => $value]; + } + } + $payload['rates'] = !empty($finalRates) ? $finalRates : null; + + /* -------- 3. Call the API to update provider -------- */ + $result = callApi('PUT', "/providers/{$provider_id}", $payload); + if (!$result) { + echo '

Error

Failed to update provider.

'; + exit; + } + + /* -------- 4. Upload photos if any were chosen -------- */ + if (!empty($_FILES['photos']['name'][0])) { + $uploadRes = uploadPhotosApi($provider_id, $_FILES['photos']); + if ($uploadRes && isset($uploadRes['photos'])) { + // Update main photo if the user selected one + $selectedMain = $_POST['main_photo'] ?? null; + if ($selectedMain) { + callApi('PUT', "/api/providers/{$provider_id}/main_photo", + ['url' => $selectedMain]); + } + } else { + // Photo upload failed – you may want to show a message + echo '

Photo upload failed.

'; + } + } + + /* -------- 5. Set the chosen main photo from the new uploads -------- */ + // If a main photo was chosen from the new uploads, send it + if (!empty($_POST['main_photo'])) { + callApi('PUT', "/api/providers/{$provider_id}/main_photo", + ['url' => $_POST['main_photo']]); + } + + /* -------- 6. Redirect back to the list -------- */ + header('Location: list_providers.php'); + exit; } ?> @@ -158,6 +252,30 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {

Edit Provider

+ + +
+ Existing Photos +
+ $url): ?> +
+ +
+ > + +
+ +
+
+ +
+ Upload New Photos + +

You can select several images at once.

+
diff --git a/web/list_providers.php b/web/scripts/list_providers.php similarity index 100% rename from web/list_providers.php rename to web/scripts/list_providers.php diff --git a/web/settings.php b/web/scripts/settings.php similarity index 100% rename from web/settings.php rename to web/scripts/settings.php diff --git a/web/scripts/upload_photos.php b/web/scripts/upload_photos.php new file mode 100644 index 0000000..efecec7 --- /dev/null +++ b/web/scripts/upload_photos.php @@ -0,0 +1,28 @@ + $tmp) { + $multipart[] = [ + 'name' => 'photos', + 'contents' => fopen($tmp, 'r'), + 'filename' => $files['name'][$i], + ]; + } + curl_setopt($c, CURLOPT_POSTFIELDS, $multipart); + curl_setopt($c, CURLOPT_RETURNTRANSFER, true); + + $res = curl_exec($c); + $http = curl_getinfo($c, CURLINFO_HTTP_CODE); + curl_close($c); + + return $http < 400 ? json_decode($res, true) : null; +} +?> \ No newline at end of file