From 30693c64957d879f1a84eace400f7fa53ff30ba7 Mon Sep 17 00:00:00 2001 From: mouazkh Date: Sun, 26 Jul 2026 00:09:45 +0300 Subject: [PATCH] fixed some changes in the ccode --- app/owner/properties/page.js | 166 ++++++++++++++++++++++++++++- app/utils/api.js | 2 +- edit-property-flow.md | 199 +++++++++++++++++++++++++++++++++++ 3 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 edit-property-flow.md 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 |