Added city and document type
All checks were successful
Build frontend / build (push) Successful in 54s
All checks were successful
Build frontend / build (push) Successful in 54s
This commit is contained in:
19
app/enums/DocumentType.js
Normal file
19
app/enums/DocumentType.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Document type enum for owner property submission.
|
||||||
|
* Values are intentionally English keys so the UI can store normalized identifiers.
|
||||||
|
*/
|
||||||
|
const DocumentType = Object.freeze({
|
||||||
|
PASSPORT: 'Passport',
|
||||||
|
IDCARD: 'IDCard',
|
||||||
|
BOTH: 'Both',
|
||||||
|
});
|
||||||
|
|
||||||
|
const DocumentTypeLabels = Object.freeze({
|
||||||
|
[DocumentType.PASSPORT]: 'جواز سفر',
|
||||||
|
[DocumentType.IDCARD]: 'بطاقة شخصية',
|
||||||
|
[DocumentType.BOTH]: 'كلاهما',
|
||||||
|
});
|
||||||
|
|
||||||
|
const DocumentTypeList = Object.freeze(Object.values(DocumentType));
|
||||||
|
|
||||||
|
export { DocumentType, DocumentTypeLabels, DocumentTypeList };
|
||||||
39
app/enums/Governorate.js
Normal file
39
app/enums/Governorate.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Syrian governorates enum using English normalized values.
|
||||||
|
* Labels are provided separately for Arabic display.
|
||||||
|
*/
|
||||||
|
const Governorate = Object.freeze({
|
||||||
|
DAMASCUS: 'Damascus',
|
||||||
|
ALEPPO: 'Aleppo',
|
||||||
|
LATAKIA: 'Latakia',
|
||||||
|
HAMA: 'Hama',
|
||||||
|
TARTOUS: 'Tartous',
|
||||||
|
DEIR_EZZOR: 'DeirEzzor',
|
||||||
|
RAQQA: 'Raqqa',
|
||||||
|
IDLIB: 'Idlib',
|
||||||
|
DRAA: 'Daraa',
|
||||||
|
SUWEIDA: 'Suweida',
|
||||||
|
HASAKAH: 'Hasakah',
|
||||||
|
QUNEITRA: 'Quneitra',
|
||||||
|
RURAL_DAMASCUS: 'RuralDamascus',
|
||||||
|
});
|
||||||
|
|
||||||
|
const GovernorateLabels = Object.freeze({
|
||||||
|
[Governorate.DAMASCUS]: 'دمشق',
|
||||||
|
[Governorate.ALEPPO]: 'حلب',
|
||||||
|
[Governorate.LATAKIA]: 'اللاذقية',
|
||||||
|
[Governorate.HAMA]: 'حماة',
|
||||||
|
[Governorate.TARTOUS]: 'طرطوس',
|
||||||
|
[Governorate.DEIR_EZZOR]: 'دير الزور',
|
||||||
|
[Governorate.RAQQA]: 'الرقة',
|
||||||
|
[Governorate.IDLIB]: 'إدلب',
|
||||||
|
[Governorate.DRAA]: 'درعا',
|
||||||
|
[Governorate.SUWEIDA]: 'السويداء',
|
||||||
|
[Governorate.HASAKAH]: 'الحسكة',
|
||||||
|
[Governorate.QUNEITRA]: 'القنيطرة',
|
||||||
|
[Governorate.RURAL_DAMASCUS]: 'ريف دمشق',
|
||||||
|
});
|
||||||
|
|
||||||
|
const GovernorateList = Object.freeze(Object.values(Governorate));
|
||||||
|
|
||||||
|
export { Governorate, GovernorateLabels, GovernorateList };
|
||||||
@ -18,6 +18,8 @@ export { RentPropertyType, RentPropertyTypeLabels } from './RentPropertyType';
|
|||||||
export { RentType, RentTypeLabels } from './RentType';
|
export { RentType, RentTypeLabels } from './RentType';
|
||||||
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
|
export { PropertyService, PropertyServiceLabels, PropertyServicesList } from './PropertyService';
|
||||||
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
|
export { PropertyTerm, PropertyTermLabels, PropertyTermsList } from './PropertyTerm';
|
||||||
|
export { DocumentType, DocumentTypeLabels, DocumentTypeList } from './DocumentType';
|
||||||
|
export { Governorate, GovernorateLabels, GovernorateList } from './Governorate';
|
||||||
export { Currency, CurrencyLabels, CurrencySymbols } from './Currency';
|
export { Currency, CurrencyLabels, CurrencySymbols } from './Currency';
|
||||||
export { DeviceType } from './DeviceType';
|
export { DeviceType } from './DeviceType';
|
||||||
export { SalePropertiesStatus, SalePropertiesStatusLabels } from './SalePropertiesStatus';
|
export { SalePropertiesStatus, SalePropertiesStatusLabels } from './SalePropertiesStatus';
|
||||||
|
|||||||
@ -1149,6 +1149,11 @@ const resources = {
|
|||||||
"addProperty.furnishedStatus": "Property Condition",
|
"addProperty.furnishedStatus": "Property Condition",
|
||||||
"addProperty.furnished": "Furnished",
|
"addProperty.furnished": "Furnished",
|
||||||
"addProperty.unfurnished": "Unfurnished",
|
"addProperty.unfurnished": "Unfurnished",
|
||||||
|
"addProperty.documentTypeLabel": "Document Type",
|
||||||
|
"addProperty.cityLabel": "Governorate",
|
||||||
|
"documentType.passport": "Passport",
|
||||||
|
"documentType.idCard": "ID Card",
|
||||||
|
"documentType.both": "Both",
|
||||||
"addProperty.additionalDescription": "Additional Description (optional)",
|
"addProperty.additionalDescription": "Additional Description (optional)",
|
||||||
"addProperty.servicesLabel": "Available Services",
|
"addProperty.servicesLabel": "Available Services",
|
||||||
"addProperty.serviceDetailPlaceholder": "Service details (e.g., in all rooms)...",
|
"addProperty.serviceDetailPlaceholder": "Service details (e.g., in all rooms)...",
|
||||||
@ -2786,6 +2791,11 @@ const resources = {
|
|||||||
"addProperty.furnishedStatus": "حالة العقار",
|
"addProperty.furnishedStatus": "حالة العقار",
|
||||||
"addProperty.furnished": "مفروش",
|
"addProperty.furnished": "مفروش",
|
||||||
"addProperty.unfurnished": "غير مفروش",
|
"addProperty.unfurnished": "غير مفروش",
|
||||||
|
"addProperty.documentTypeLabel": "نوع الوثيقة",
|
||||||
|
"addProperty.cityLabel": "المحافظة",
|
||||||
|
"documentType.passport": "جواز سفر",
|
||||||
|
"documentType.idCard": "بطاقة شخصية",
|
||||||
|
"documentType.both": "كلاهما",
|
||||||
"addProperty.additionalDescription": "وصف إضافي (اختياري)",
|
"addProperty.additionalDescription": "وصف إضافي (اختياري)",
|
||||||
"addProperty.servicesLabel": "الخدمات المتوفرة",
|
"addProperty.servicesLabel": "الخدمات المتوفرة",
|
||||||
"addProperty.serviceDetailPlaceholder": "تفاصيل الخدمة (مثال: في جميع الغرف)...",
|
"addProperty.serviceDetailPlaceholder": "تفاصيل الخدمة (مثال: في جميع الغرف)...",
|
||||||
|
|||||||
@ -64,6 +64,11 @@ import {
|
|||||||
PropertyTerm,
|
PropertyTerm,
|
||||||
PropertyTermLabels,
|
PropertyTermLabels,
|
||||||
PropertyTermsList,
|
PropertyTermsList,
|
||||||
|
DocumentType,
|
||||||
|
DocumentTypeList,
|
||||||
|
Governorate,
|
||||||
|
GovernorateLabels,
|
||||||
|
GovernorateList,
|
||||||
Currency,
|
Currency,
|
||||||
CurrencyLabels
|
CurrencyLabels
|
||||||
} from '../../../enums';
|
} from '../../../enums';
|
||||||
@ -84,6 +89,7 @@ export default function AddPropertyPage() {
|
|||||||
propertyType: 'apartment',
|
propertyType: 'apartment',
|
||||||
|
|
||||||
furnished: false,
|
furnished: false,
|
||||||
|
documentType: DocumentType.PASSPORT,
|
||||||
|
|
||||||
bedrooms: 1,
|
bedrooms: 1,
|
||||||
bathrooms: 1,
|
bathrooms: 1,
|
||||||
@ -118,7 +124,7 @@ export default function AddPropertyPage() {
|
|||||||
deposit: '',
|
deposit: '',
|
||||||
allowedPaymentPeriod: '',
|
allowedPaymentPeriod: '',
|
||||||
|
|
||||||
city: '',
|
city: Governorate.DAMASCUS,
|
||||||
district: '',
|
district: '',
|
||||||
address: '',
|
address: '',
|
||||||
lat: null,
|
lat: null,
|
||||||
@ -206,6 +212,19 @@ export default function AddPropertyPage() {
|
|||||||
{ id: 'both', label: t('addProperty.dailyAndMonthly'), icon: Calendar },
|
{ id: 'both', label: t('addProperty.dailyAndMonthly'), icon: Calendar },
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
|
const getDocumentTypeLabel = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case DocumentType.PASSPORT:
|
||||||
|
return t('documentType.passport');
|
||||||
|
case DocumentType.IDCARD:
|
||||||
|
return t('documentType.idCard');
|
||||||
|
case DocumentType.BOTH:
|
||||||
|
return t('documentType.both');
|
||||||
|
default:
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch available currencies
|
// Fetch available currencies
|
||||||
getCurrencies().then((data) => {
|
getCurrencies().then((data) => {
|
||||||
@ -662,6 +681,8 @@ const handleMarkerDragEnd = async (lat, lng) => {
|
|||||||
buildingType: buildingTypeMap[formData.propertyType] ?? BuildingType.APARTMENT,
|
buildingType: buildingTypeMap[formData.propertyType] ?? BuildingType.APARTMENT,
|
||||||
status: 0,
|
status: 0,
|
||||||
propertyType: formData.furnished ? RentPropertyCondition.WITH_FURNITURE : RentPropertyCondition.WITHOUT_FURNITURE,
|
propertyType: formData.furnished ? RentPropertyCondition.WITH_FURNITURE : RentPropertyCondition.WITHOUT_FURNITURE,
|
||||||
|
documentType: formData.documentType,
|
||||||
|
governorate: formData.city,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -822,6 +843,23 @@ const handleMarkerDragEnd = async (lat, lng) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t('addProperty.documentTypeLabel')} <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.documentType}
|
||||||
|
onChange={(e) => setFormData({...formData, documentType: e.target.value})}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500"
|
||||||
|
>
|
||||||
|
{DocumentTypeList.map((type) => (
|
||||||
|
<option key={type} value={type}>
|
||||||
|
{getDocumentTypeLabel(type)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
{t('addProperty.additionalDescription')}
|
{t('addProperty.additionalDescription')}
|
||||||
@ -1519,38 +1557,72 @@ const handleMarkerDragEnd = async (lat, lng) => {
|
|||||||
<p className="text-gray-600">{t('addProperty.locationAndImagesSubtitle')}</p>
|
<p className="text-gray-600">{t('addProperty.locationAndImagesSubtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="rounded-3xl border border-amber-100 bg-gradient-to-br from-amber-50 via-white to-orange-50 p-4 sm:p-5 md:p-6 shadow-sm">
|
||||||
<h3 className="text-lg font-bold text-gray-900 mb-4">{t('addProperty.selectLocationTitle')}</h3>
|
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between mb-5">
|
||||||
|
<div>
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex items-center gap-2 text-amber-700 font-semibold mb-1">
|
||||||
<div className="flex-1 relative">
|
<Navigation className="w-4 h-4" />
|
||||||
<input
|
{t('addProperty.selectLocationTitle')}
|
||||||
type="text"
|
</div>
|
||||||
value={searchQuery}
|
<p className="text-sm text-gray-600">{t('addProperty.clickMapHint')}</p>
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
</div>
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
<div className="inline-flex items-center gap-2 rounded-full border border-amber-200 bg-white px-3 py-1.5 text-sm font-medium text-amber-700 shadow-sm">
|
||||||
placeholder={t('addProperty.searchAddress')}
|
<MapPin className="w-4 h-4" />
|
||||||
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 pr-12"
|
{t('addProperty.cityLabel')}
|
||||||
/>
|
|
||||||
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={handleSearch}
|
|
||||||
className="px-6 py-3 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors"
|
|
||||||
>
|
|
||||||
{t('addProperty.searchButton')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 mb-4 text-sm text-amber-800 leading-relaxed">
|
<div className="grid grid-cols-1 xl:grid-cols-[minmax(220px,260px)_1fr] gap-4 mb-5">
|
||||||
<Search className="w-4 h-4 inline ml-1" />
|
<div className="rounded-2xl border border-white/80 bg-white/90 p-4 shadow-sm">
|
||||||
{t('addProperty.searchHint')}
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
<br />
|
{t('addProperty.cityLabel')} <span className="text-red-500">*</span>
|
||||||
<MapPin className="w-4 h-4 inline ml-1" />
|
</label>
|
||||||
{t('addProperty.clickMapHint')}
|
<select
|
||||||
|
value={formData.city}
|
||||||
|
onChange={(e) => setFormData({...formData, city: e.target.value})}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 bg-white"
|
||||||
|
>
|
||||||
|
{GovernorateList.map((city) => (
|
||||||
|
<option key={city} value={city}>
|
||||||
|
{GovernorateLabels[city] || city}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-gray-500 mt-2">{t('addProperty.searchHint')}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-2xl border border-white/80 bg-white/90 p-4 shadow-sm">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t('addProperty.searchAddress')}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
|
placeholder={t('addProperty.searchAddress')}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 pr-10"
|
||||||
|
/>
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSearch}
|
||||||
|
className="px-5 py-3 bg-amber-500 text-white rounded-xl hover:bg-amber-600 transition-colors font-medium flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
{t('addProperty.searchButton')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex items-start gap-2 text-sm text-amber-700">
|
||||||
|
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
|
<span>{t('addProperty.clickMapHint')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative w-full h-96 rounded-xl overflow-hidden border-2 border-gray-200 mb-4">
|
<div className="relative w-full h-96 rounded-2xl overflow-hidden border border-gray-200 shadow-inner">
|
||||||
<AddPropertyMap
|
<AddPropertyMap
|
||||||
mapCenter={mapCenter}
|
mapCenter={mapCenter}
|
||||||
mapZoom={mapZoom}
|
mapZoom={mapZoom}
|
||||||
@ -1560,28 +1632,35 @@ const handleMarkerDragEnd = async (lat, lng) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedLocation && !formData.lat && (
|
<div className="mt-4 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||||
<button
|
{selectedLocation && !formData.lat ? (
|
||||||
onClick={confirmLocation}
|
<button
|
||||||
className="w-full mb-4 bg-green-500 text-white py-3 rounded-xl font-medium hover:bg-green-600 transition-colors flex items-center justify-center gap-2"
|
onClick={confirmLocation}
|
||||||
>
|
className="w-full md:w-auto bg-green-600 text-white py-3 px-5 rounded-xl font-medium hover:bg-green-700 transition-colors flex items-center justify-center gap-2 shadow-sm"
|
||||||
<CheckCircle className="w-5 h-5" />
|
>
|
||||||
{t('addProperty.confirmLocation')}
|
<CheckCircle className="w-5 h-5" />
|
||||||
</button>
|
{t('addProperty.confirmLocation')}
|
||||||
)}
|
</button>
|
||||||
|
) : (
|
||||||
{formData.lat && (
|
<div className="rounded-2xl border border-amber-100 bg-amber-50 px-4 py-3 text-sm text-amber-800 flex items-start gap-2">
|
||||||
<div className="bg-green-50 border border-green-200 rounded-xl p-4 mb-4">
|
<Info className="w-4 h-4 mt-0.5 shrink-0" />
|
||||||
<div className="flex items-center gap-2">
|
<span>{t('addProperty.clickMapHint')}</span>
|
||||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
|
||||||
<span className="text-green-800 font-medium">{t('addProperty.locationConfirmed')}:</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-green-700 text-sm mt-2 line-clamp-2">{formData.address}</p>
|
)}
|
||||||
</div>
|
|
||||||
)}
|
{formData.lat && (
|
||||||
|
<div className="flex-1 rounded-2xl border border-green-200 bg-green-50 p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||||
|
<span className="text-green-800 font-medium">{t('addProperty.locationConfirmed')}:</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-green-700 text-sm mt-2 line-clamp-2">{formData.address}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{errors.location && (
|
{errors.location && (
|
||||||
<p className="text-red-500 text-sm text-center mb-4">{errors.location}</p>
|
<p className="text-red-500 text-sm text-center mt-4">{errors.location}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user