diff --git a/app/owner/properties/page.js b/app/owner/properties/page.js
index b6d705b..3776376 100644
--- a/app/owner/properties/page.js
+++ b/app/owner/properties/page.js
@@ -63,6 +63,7 @@ import {
editSaleProperty,
updateRentPropertyStatus,
updateSalePropertyStatus,
+ uploadPicture,
} from "../../utils/api";
import { PropertyService } from "../../enums/PropertyService";
import { PropertyTerm } from "../../enums/PropertyTerm";
@@ -621,6 +622,16 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
});
const [newCustomTerm, setNewCustomTerm] = useState('');
const [isSaving, setIsSaving] = useState(false);
+ const [existingImages, setExistingImages] = useState([]);
+ const [newImages, setNewImages] = useState([]);
+ const editApiBase =
+ process.env.NEXT_PUBLIC_API_URL || 'https://45.93.137.91.nip.io/api';
+
+ const editImgUrl = (path) => {
+ if (!path) return null;
+ if (path.startsWith('http://') || path.startsWith('https://')) return path;
+ return `${editApiBase}${path}`;
+ };
useEffect(() => {
if (!property || !isOpen) return;
@@ -718,6 +729,13 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
: { salePrice: property.salePrice || 0 }),
});
setNewCustomTerm('');
+ setNewImages([]);
+ const rawImages = Array.isArray(property.images)
+ ? property.images
+ : Array.isArray(info.images)
+ ? info.images
+ : [];
+ setExistingImages(rawImages.filter((p) => p && p.trim()));
}, [property, isOpen]);
const handleChange = (field, value) => {
@@ -762,6 +780,76 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
}));
};
+ const handleRemoveExistingImage = (index) => {
+ setExistingImages((prev) => prev.filter((_, i) => i !== index));
+ };
+
+ const handleMoveExistingImage = (index, direction) => {
+ setExistingImages((prev) => {
+ const next = [...prev];
+ const target = index + direction;
+ if (target < 0 || target >= next.length) return prev;
+ [next[index], next[target]] = [next[target], next[index]];
+ return next;
+ });
+ };
+
+ const handleAddImages = async (files) => {
+ const validFiles = Array.from(files).filter((f) => {
+ if (!f.type.startsWith('image/')) return false;
+ if (f.size > 5 * 1024 * 1024) {
+ toast.error(t('fileTooLarge') || 'الصورة كبيرة جداً');
+ return false;
+ }
+ return true;
+ });
+ if (validFiles.length === 0) return;
+ const maxTotal = 10;
+ const currentCount = existingImages.length + newImages.length;
+ const slots = maxTotal - currentCount;
+ if (slots <= 0) {
+ toast.error(t('maxImagesReached') || 'الحد الأقصى 10 صور');
+ return;
+ }
+ const toAdd = validFiles.slice(0, slots);
+ const entries = toAdd.map((file) => ({
+ file,
+ preview: URL.createObjectURL(file),
+ path: null,
+ uploading: true,
+ }));
+ setNewImages((prev) => [...prev, ...entries]);
+ for (let i = 0; i < entries.length; i++) {
+ const entry = entries[i];
+ try {
+ const result = await uploadPicture(entry.file);
+ const path = result?.value || result?.path || result?.url || result;
+ entry.path = path;
+ } catch {
+ toast.error(t('uploadFailed') || 'فشل رفع الصورة');
+ } finally {
+ entry.uploading = false;
+ setNewImages((prev) => [...prev]);
+ }
+ }
+ };
+
+ const handleRemoveNewImage = (index) => {
+ const entry = newImages[index];
+ if (entry?.preview) URL.revokeObjectURL(entry.preview);
+ setNewImages((prev) => prev.filter((_, i) => i !== index));
+ };
+
+ const handleMoveNewImage = (index, direction) => {
+ setNewImages((prev) => {
+ const next = [...prev];
+ const target = index + direction;
+ if (target < 0 || target >= next.length) return prev;
+ [next[index], next[target]] = [next[target], next[index]];
+ return next;
+ });
+ };
+
const handleSave = async () => {
const errors = [];
if (!formData.description?.trim()) errors.push(t('descriptionRequired') || 'الوصف مطلوب');
@@ -781,7 +869,11 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
}
setIsSaving(true);
try {
- await onSave(formData);
+ const uploadedPaths = newImages
+ .filter((e) => e.path && !e.uploading)
+ .map((e) => e.path);
+ const allImages = [...existingImages, ...uploadedPaths];
+ await onSave({ ...formData, _images: allImages });
} catch {
setIsSaving(false);
}
@@ -920,6 +1012,76 @@ const PropertyEditModal = ({ isOpen, onClose, property, onSave }) => {
+ {/* Images */}
+
+
{t('images')}
+
+ {existingImages.map((path, i) => (
+
+
})
+
+
+ {i > 0 && (
+
+ )}
+ {i < existingImages.length - 1 && (
+
+ )}
+
+
+ ))}
+ {newImages.map((entry, i) => (
+
+ {entry.uploading ? (
+
+
+
+ ) : (
+

+ )}
+
+
+ {i > 0 && (
+
+ )}
+ {i < newImages.length - 1 && (
+
+ )}
+
+
+ ))}
+ {existingImages.length + newImages.length < 10 && (
+
+ )}
+
+
{existingImages.length + newImages.length}/10
+
+
{/* Services */}
{t('services')}
@@ -1591,7 +1753,7 @@ export default function OwnerPropertiesPage() {
const propInfo = {
cordsX: rawInfo.cordsX || '',
cordsY: rawInfo.cordsY || '',
- images: rawInfo.images || [],
+ images: formData._images || rawInfo.images || [],
address: property.address || rawInfo.address || '',
description:
formData.description || rawInfo.description || '',
diff --git a/app/utils/api.js b/app/utils/api.js
index 1de1edf..7444a2a 100644
--- a/app/utils/api.js
+++ b/app/utils/api.js
@@ -369,7 +369,7 @@ export async function editRentProperty(id, data) {
return apiFetch(`/RentProperties/EditRentProperty/${id}`, {
method: "PUT",
- body: { rentPropertyDto: body },
+ body,
});
}
diff --git a/edit-property-flow.md b/edit-property-flow.md
new file mode 100644
index 0000000..1b1518c
--- /dev/null
+++ b/edit-property-flow.md
@@ -0,0 +1,199 @@
+# Edit Property Flow - Documentation
+
+## Overview
+
+The edit property flow allows property owners to update their rent property listing, including all fields and images. It shares the same screen as add property (`add_rent_property_screen.dart`) and switches behavior based on `widget.initialListing != null` (edit mode).
+
+## Screen: `add_rent_property_screen.dart`
+
+### State Variables for Images
+
+```dart
+// Existing images loaded from the listing (edit mode)
+List _existingImagePaths = [];
+
+// Newly selected images (both add & edit mode)
+List _formImages = [];
+List _imageSlotIds = [];
+List _imageFingerprints = [];
+List _imagePreviews = [];
+List _uploadedImagePaths = [];
+Set _uploadingImageKeys = {};
+Set _failedImageKeys = {};
+
+int get _totalImageCount => _existingImagePaths.length + _formImages.length;
+```
+
+### Initialization in Edit Mode
+
+When editing, `_prefillFromInitialListing()` populates `_existingImagePaths` from the API response:
+
+```dart
+_existingImagePaths = info.images
+ .map((path) => path.trim())
+ .where((path) => path.isNotEmpty)
+ .toList();
+```
+
+### Image Management UI (Edit Mode)
+
+| Feature | Implementation |
+|---------|---------------|
+| **View existing images** | `Image.network` with `ApiConstants.resolveApiFileUrl()` |
+| **Delete existing image** | X button → `_removeExistingImageAt(index)` removes from `_existingImagePaths` |
+| **Replace existing image** | Tap on image → `_replaceExistingImageAt(index)` → file picker → upload via `POST /Files/UploadPicture` → replaces URL at same index |
+| **Add new image** | Upload button → `_handleImageUpload()` → `_addSelectedImage()` → upload via `POST /Files/UploadPicture` |
+| **Reorder existing images** | Up/down arrow buttons → `_moveExistingImage(index, direction)` |
+| **Reorder new images** | Up/down arrow buttons → `_moveSelectedImage(index, direction)` (moves all 6 parallel lists together) |
+
+### Submit - Image Assembly
+
+On submit, the final image list is built from:
+
+```dart
+final allImagePaths = [
+ ..._existingImagePaths
+ .map((path) => path.trim())
+ .where((path) => path.isNotEmpty),
+ ...uploadedImagePaths,
+];
+```
+
+This combined list is passed to `AddRentPropertyDto` → `PropertyInformationDto` as `images`.
+
+## API Details
+
+### Image Upload Endpoint
+
+```
+POST /Files/UploadPicture
+Headers: Authorization: Bearer , Content-Type: multipart/form-data
+Body: image=
+Response: { "value": "/Pictures/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.jpg" }
+```
+
+The returned path is used in the edit request.
+
+### Edit Property Endpoint
+
+```
+PUT /RentProperties/EditRentProperty/{id}
+Content-Type: application/json
+Authorization: Bearer
+```
+
+#### Request Body (camelCase contract from swagger)
+
+```json
+{
+ "propertyInformation": {
+ "activityStatus": 0,
+ "images": [
+ "/Pictures/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.jpg"
+ ],
+ "cordsX": "33.51599774641418",
+ "cordsY": "36.281279544066294",
+ "address": "string",
+ "description": "string",
+ "numberOfBathRooms": 2,
+ "numberOfRooms": 3,
+ "numberOfBedRooms": 3,
+ "space": 120.0,
+ "detailsJSON": "{}",
+ "buildingType": 0,
+ "status": 0,
+ "propertyType": 0,
+ "city": 0
+ },
+ "deposit": 5.0,
+ "acceptedCertificate": 0,
+ "monthlyRent": 0.0,
+ "dailyRent": 56.0,
+ "rating": 1.0,
+ "currencyId": 1,
+ "rentType": 1,
+ "isSmokeAllow": false,
+ "specializedFor": false,
+ "isVisitorAllow": false,
+ "allowedPaymentPeriod": "10:02:00",
+ "type": 0
+}
+```
+
+### Payload Attempt Order (Flutter side)
+
+The app tries multiple payload shapes as fallback:
+
+| Order | Label | Structure | Example |
+|-------|-------|-----------|---------|
+| 1 | `plain_pascal` | PascalCase direct | `{PropertyInformation: {Images: [...]}}` |
+| 2 | `wrapped_pascal_outer` | PascalCase with DTO wrapper | `{RentPropertyDto: {PropertyInformation: {Images: [...]}}}` |
+| 3 | `wrapped_pascal_inner` | PascalCase inside camelCase wrapper | `{rentPropertyDto: {PropertyInformation: {Images: [...]}}}` |
+| 4 | `wrapped_camel` | camelCase wrapped | `{rentPropertyDto: {propertyInformation: {images: [...]}}}` |
+| 5 | `plain_camel` | camelCase direct | `{propertyInformation: {images: [...]}}` |
+| 6 | `multipart_form` | multipart form fields | `PropertyInformation.Images[0] = url` |
+
+---
+
+## 🚨 CONFIRMED BACKEND BUG
+
+### Symptoms
+- **All scalar fields** (deposit, dailyRent, address, etc.) update correctly via PUT
+- **`propertyInformation.images`** is ALWAYS ignored by the PUT endpoint
+- **`propertyInformation.detailsJSON`** is also ignored by the PUT endpoint
+- The **POST add endpoint** handles both fields correctly
+
+### Evidence from Direct API Tests
+
+**Test 1 - camelCase JSON via curl:**
+```
+PUT /RentProperties/EditRentProperty/1
+→ images sent: ["/Pictures/test1.jpg", "/Pictures/test2.jpg"]
+→ images response: ["/Pictures/f7062fc863a74306a6a0340982669386.png"] ❌ OLD
+→ address sent: "test address"
+→ address response: "test address" ✅ UPDATED
+```
+
+**Test 2 - PascalCase JSON via curl:**
+```
+PUT /RentProperties/EditRentProperty/1
+→ Images sent: ["/Pictures/new1.jpg", "/Pictures/new2.jpg"]
+→ images response: ["/Pictures/f7062fc863a74306a6a0340982669386.png"] ❌ OLD
+→ Address sent: "test pascal"
+→ Address response: "test pascal" ✅ UPDATED
+```
+
+**Test 3 - POST add endpoint (works correctly):**
+```
+POST /RentProperties/AddRentProperty
+→ images sent: ["/Pictures/test1.jpg", "/Pictures/test2.jpg"]
+→ images response: ["/Pictures/test1.jpg", "/Pictures/test2.jpg"] ✅ STORED CORRECTLY
+```
+
+### Root Cause (Backend Side)
+
+The `EditRentProperty` PUT handler in the backend likely:
+1. Creates a new `RentProperty` record (or updates it) - scalar fields work
+2. Updates or assigns the `PropertyInformation` record - but only scalar fields are copied
+3. The `Images` and `DetailsJSON` properties of `PropertyInformation` are **not mapped/copied** during update
+
+Comparison with `AddRentProperty`:
+- The POST handler correctly maps all `PropertyInformation` fields including `Images` and `DetailsJSON`
+- The PUT handler appears to skip these two fields specifically
+
+### Required Backend Fix
+
+The PUT endpoint handler needs to be updated to persist `propertyInformation.images` and `propertyInformation.detailsJSON` when updating existing property information records.
+
+---
+
+## Summary
+
+| Aspect | Status |
+|--------|--------|
+| Image upload (to server) | ✅ Working via `POST /Files/UploadPicture` |
+| Image management UI (edit mode) | ✅ Delete, Replace, Add, Reorder all implemented |
+| Image assembly for submit | ✅ Correctly combines kept + new uploaded paths |
+| Payload format (camelCase/PascalCase) | ✅ Both formats tried correctly |
+| **PUT edit endpoint** | **❌ Does NOT update images or detailsJSON** |
+| POST add endpoint | ✅ Fully functional |