diff --git a/frontend/mobile-ui/.eslintrc.cjs b/frontend/mobile-ui/.eslintrc.cjs
new file mode 100644
index 0000000..83a5444
--- /dev/null
+++ b/frontend/mobile-ui/.eslintrc.cjs
@@ -0,0 +1,18 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true, node: true },
+ parser: '@typescript-eslint/parser',
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
+ settings: { react: { version: 'detect' } },
+ plugins: ['react-refresh', 'react-hooks', '@typescript-eslint'],
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ 'plugin:react-refresh/recommended',
+ 'prettier'
+ ],
+ rules: {
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
+ }
+};
diff --git a/frontend/mobile-ui/.gitignore b/frontend/mobile-ui/.gitignore
new file mode 100644
index 0000000..a4ab49a
--- /dev/null
+++ b/frontend/mobile-ui/.gitignore
@@ -0,0 +1,22 @@
+# Dependencies
+node_modules
+
+.env.*
+.env
+*.zip
+# Logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor
+.idea
+.vscode/*.log
+.DS_Store
+
+# Build
+/dist
+*.tsbuildinfo
+
+
\ No newline at end of file
diff --git a/frontend/mobile-ui/.prettierrc b/frontend/mobile-ui/.prettierrc
new file mode 100644
index 0000000..9365378
--- /dev/null
+++ b/frontend/mobile-ui/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "singleQuote": true,
+ "semi": true,
+ "trailingComma": "es5",
+ "printWidth": 90
+}
diff --git a/frontend/mobile-ui/DATA_MANAGEMENT.md b/frontend/mobile-ui/DATA_MANAGEMENT.md
new file mode 100644
index 0000000..1460790
--- /dev/null
+++ b/frontend/mobile-ui/DATA_MANAGEMENT.md
@@ -0,0 +1,155 @@
+# Data Management & Filtering System
+
+## Overview
+The application now uses a centralized `db.json` file for all data instead of hardcoded values in components. This provides a clean separation of data and logic, making it easier to maintain and update.
+
+## Database Structure
+
+### `db.json` Structure
+```json
+{
+ "properties": [...], // Array of property items with filtering flags
+ "location": {...}, // Location data with coordinates
+ "searchResults": [...], // Search results data
+ "messages": [...], // Inbox messages data
+ "insights": {...}, // Insights page data
+ "drafts": [...] // Draft properties data
+}
+```
+
+### Property Item Schema
+Each property item has the following structure:
+- `id`: Unique property identifier
+- `type`: Property type (Survey, Residential, Commercial)
+- `description`: Property description
+- `address`: Full address
+- `dueDate`: Due date in YYYY-MM-DD format
+- `status`: Priority level (High, Medium, Low)
+- `phoneNumber`: Contact phone number
+- `area`: Property area with units
+- `propertyType`: Residential or Commercial
+- `isVerified`: Boolean - true if property is verified/reviewed
+- `isDraft`: Boolean - true if property is in draft status
+- `isNew`: Boolean - true if property is newly created
+- `createdDate`: Creation date in YYYY-MM-DD format
+
+## Filtering System
+
+The home page now supports four filter types:
+
+### 1. All Button
+- Shows all properties regardless of status
+- Default view showing complete property list
+
+### 2. New Button
+- Shows only properties where `isNew: true`
+- Sorted by `createdDate` with latest first
+- Helps identify recently added properties
+
+### 3. Reviewed Button
+- Shows only properties where `isVerified: true`
+- Displays completed/verified properties
+
+### 4. Draft Button
+- Shows only properties where `isDraft: true`
+- Special draft layout with continue functionality
+- Shows map when "View Map" is clicked
+
+## Data Service (`dataService.ts`)
+
+### Key Functions
+- `fetchData()`: Loads data from db.json
+- `filterProperties()`: Filters properties by type
+- `searchProperties()`: Search functionality
+- `getPropertiesByStatus()`: Filter by priority status
+
+### Usage Example
+```typescript
+import { fetchData, filterProperties } from './services/dataService';
+
+// Load all data
+const data = await fetchData();
+
+// Filter for new properties
+const newProperties = filterProperties(data.properties, 'new');
+```
+
+## Implementation Details
+
+### App.tsx Changes
+- Removed hardcoded `mockProperties` and `mockLocation`
+- Added `useEffect` to fetch data on component mount
+- Added loading state management
+- Updated all data references to use fetched data
+
+### HomePage.tsx Changes
+- Added filtering logic with `useEffect`
+- Updated calendar tab functionality
+- Dynamic property rendering based on filter
+- Added status badges (Verified, New)
+- Improved draft mode with map toggle
+
+### SearchPropertyPage.tsx Changes
+- Removed hardcoded `mockSearchResults`
+- Now uses `searchResults` prop from App component
+
+## Benefits
+
+1. **Centralized Data**: All data in one JSON file
+2. **Easy Updates**: Modify db.json without touching code
+3. **Flexible Filtering**: Multiple filter criteria
+4. **Better UX**: Clear status indicators and sorting
+5. **Maintainable**: Clean separation of concerns
+
+## Complete Functionality Testing
+
+### HomePage Filter Testing
+1. **All**: Should show 7 properties total from db.json
+2. **New**: Should show 2 properties (PRP-2025-003, PRP-2025-005) sorted by latest date first
+3. **Reviewed**: Should show 3 verified properties (PRP-2024-001, PRP-2024-234, PRP-2024-150)
+4. **Draft**: Should show 2 draft properties (PRP-2024-002, PRP-2024-324) with special draft layout
+
+### Other Pages Testing
+1. **SearchPropertyPage**: Uses searchResults from db.json (3 properties)
+2. **LocationPage**: Uses location data from db.json
+3. **InboxPage**: Uses hardcoded messages (can be connected to db.json messages later)
+4. **InsightsPage**: Uses hardcoded insights (can be connected to db.json insights later)
+5. **DraftPage**: Uses hardcoded drafts (can be connected to db.json drafts later)
+
+### Navigation Testing
+- All bottom navigation tabs should work
+- Back buttons should navigate properly
+- Header icons should be functional
+
+### Map Component Testing
+- Map should render on HomePage for non-Draft filters
+- Map should be toggleable in Draft view with "View Map" button
+- Map tabs (Map/Land Use) should switch properly
+
+### Data Integrity
+- All data comes from centralized db.json
+- No hardcoded property data in components (except pages not yet connected)
+- Filtering logic works with database flags (isNew, isVerified, isDraft)
+
+## Adding New Data
+
+To add new properties, simply edit `db.json` and add objects to the `properties` array with the required schema. The application will automatically pick up the changes on next load.
+
+### Sample Property Object
+```json
+{
+ "id": "PRP-2025-NEW",
+ "type": "Survey",
+ "description": "New property description",
+ "address": "New Address, Guntur, Andhra Pradesh",
+ "dueDate": "2025-10-30",
+ "status": "High",
+ "phoneNumber": "+91 12345 67890",
+ "area": "1,00,000 sqft",
+ "propertyType": "Residential",
+ "isVerified": false,
+ "isDraft": false,
+ "isNew": true,
+ "createdDate": "2025-10-13"
+}
+```
\ No newline at end of file
diff --git a/frontend/mobile-ui/LICENSE b/frontend/mobile-ui/LICENSE
new file mode 100644
index 0000000..b77bf2a
--- /dev/null
+++ b/frontend/mobile-ui/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/frontend/mobile-ui/README.md b/frontend/mobile-ui/README.md
new file mode 100644
index 0000000..3505d98
--- /dev/null
+++ b/frontend/mobile-ui/README.md
@@ -0,0 +1,462 @@
+
+
+# ๐ Property Tax Frontend
+
+
+A modern, mobile-first React + TypeScript application for property tax management. This frontend provides agents and citizens with workflows to submit, verify, and manage property tax forms, upload documents, and track application status. The UI is optimized for usability, accessibility, and seamless integration with backend APIs.
+
+
+
+## ๐ ๏ธ Tech Stack
+
+
+- **โ๏ธ Framework:** React 19 (with TypeScript)
+- **๐๏ธ State Management:** Redux Toolkit (RTK Query)
+- **๐จ UI Library:** Material-UI (MUI)
+- **๐งญ Routing:** React Router v7
+- **๐
Styling:** CSS Modules, custom CSS, MUI theming
+- **๐ Localization:** Custom context-based localization
+- **โก Build Tool:** Vite
+- **๐งช Testing:** Vitest, React Testing Library
+- **๐๏ธ API Mocking:** json-server
+- **๐ฆ Package Manager:** npm
+
+---
+
+
+## ๐ Prerequisites
+
+
+- **๐ข Node.js:** v18.x or higher recommended
+- **๐ฆ npm:** v9.x or higher (comes with Node.js)
+- No global dependencies required
+
+---
+
+
+## ๐ฅ Installation
+
+1. **๐ Clone the repository:**
+ ```sh
+ git clone https://github.com/your-org/property-tax-frontend.git
+ cd property-tax-frontend
+ ```
+
+2. **๐ฆ Install dependencies:**
+ ```sh
+ npm install
+ ```
+
+
+
+---
+
+
+## ๐ Running the Project
+
+
+### ๐ ๏ธ Development mode
+
+```sh
+npm run dev
+```
+- Starts the Vite development server at http://localhost:5174.
+
+
+### ๐๏ธ Build for production
+
+```sh
+npm run build
+```
+- Builds the app for production (output in `dist/`).
+
+
+### ๐ Preview production build
+
+```sh
+npm run preview
+```
+- Serves the production build locally for testing.
+
+---
+
+
+## ๐๏ธ Project Structure
+
+```
+src/
+โโโ App.tsx # Main app component and route definitions
+โโโ app/ # Feature modules, pages, and services
+โโโ assets/ # Images, icons, and static assets
+โโโ components/ # Shared and agent-specific components
+โโโ config/ # App configuration (env, theme, httpClient)
+โโโ context/ # React context providers
+โโโ hooks/ # Custom React hooks
+โโโ localization/ # Localization utilities
+โโโ models/ # Data models and types
+โโโ pages/ # Page-level components
+โโโ redux/ # Redux slices, store, and API logic
+โโโ services/ # API and business logic services
+โโโ storage/ # Local storage helpers
+โโโ styles/ # CSS styles
+โโโ types/ # TypeScript types
+โโโ validations/ # Validation logic
+public/ # Static assets and manifest
+json-server/ # Mock backend data for development/testing
+docs/ # Documentation
+```
+
+---
+
+
+## ๐ Scripts
+
+
+- `npm run dev` โ โก Start dev server
+- `npm run build` โ ๐๏ธ Build production bundle
+- `npm run preview` โ ๐ Preview production build
+- `npm run lint` โ ๐งน Run linter
+
+---
+
+
+## ๐จ Styling & UI
+
+- **CSS Methodology:** Uses CSS Modules for component-scoped styles, along with custom CSS files for global and utility styles.
+- **UI Library & Theming:** Material-UI (MUI) is used for consistent design and theming, with theme configuration in `src/config/theme.ts`.
+- **Organization:**
+ - Component-specific styles are placed alongside their components.
+ - Global styles and resets are in `src/styles/` and `src/index.css`.
+ - Asset-specific styles (icons, images) are organized under `src/assets/`.
+ - Supports responsive layouts and dark/light theme customization.
+
+---
+
+
+## ๐ API Integration
+
+- **API Layer:** All backend interactions are organized by feature in `src/app/features/`:
+ - `src/app/features/Agent/api/` โ Agent-specific API calls
+ - `src/app/features/Citizen/api/` โ Citizen-specific API calls
+ - `src/app/features/PropertyForm/api/` โ Property form API calls
+ Each feature's `api` folder contains logic for interacting with backend services, typically using RTK Query.
+- **Mock API:** Use `json-server` for local development and testing.
+- **Example endpoint:**
+ - `GET /propertyApplications` (mocked via `json-server/propertyApplications.json`)
+
+
+
+
+## ๐จโ๐ง Agent Module in DIGIT 3.0
+
+The Agent module in DIGIT 3.0 is designed to assist field agents in managing property tax-related tasks efficiently. Agents can add new properties, verify property details, review applications, and communicate with other stakeholders. The user interface is optimized for mobile and field use, providing map-based property management and streamlined workflows.
+
+
+
+## โญ Features of Each Page
+
+### ๐ค Profile Page
+- **๐ Access:** Click the Profile icon at the top right of the Home page.
+- **๐ Personal Details:**
+ - Agent name and verification status (e.g., โ
Verified Agent)
+ - ๐ง Email address and ๐ฑ phone number
+ - ๐บ๏ธ Assigned jurisdictions/wards
+- **๐ Notification Settings:**
+ - Toggle application updates (receive updates on new or updated applications)
+ - Toggle appointment scheduling notifications (for rescheduling and scheduling appointments)
+ - Select delivery methods for notifications:
+ - ๐ฉ SMS Notifications
+ - ๐ข WhatsApp Notifications
+ - ๐ง Email Notifications
+- **๐ช Log Out:** Securely log out of the application from the profile page
+- **โฌ
๏ธ Navigation:** Use the Previous button to return to the previous screen
+
+### ๐ Language Selection
+- **๐ Access:** Click the Language icon beside the Profile icon on the Home page.
+- **๐ Functionality:** Allows the agent to select the preferred language from the list of languages assigned to them.
+- **โจ Dynamic Language Support:** The interface updates to reflect the selected language for a personalized experience.
+
+### ๐ Home Page
+- **๐บ๏ธ Jurisdiction Display:** Shows the agent's assigned wards.
+- **๐ & ๐ค Language & Profile:** Quick access to language selection and profile management.
+- **๐ & ๐ Agent ID & Help:** Access agent credentials and help resources.
+- **๐บ๏ธ/๐พ Map/Land Use Toggle:** Switch between property map and land use zoning view.
+- **โ Add New Property:** Start a new property application.
+- **๐ Generate No Dues:** Generate a no-dues certificate for a property.
+- **๐ Tabs (All/New/Drafts/Others):** Filter properties by status.
+- **๐ท๏ธ Property List:** View properties with status, category, address, and date. Click to view or edit.
+- **โถ๏ธ Pagination:** Navigate through multiple pages of property records.
+- **๐ฝ Bottom Navigation:** Quick access to Home, Inbox, Notifications, and Search.
+
+### ๐ Property Form Verification Page
+- **๐บ๏ธ & ๐ Map & Address:** Visualize property location and details.
+- **โ ๏ธ Summary of Required/Review Fields:** Highlights missing or unverified fields.
+- **โก๏ธ Continue to Form:** Proceed to complete or verify property details.
+- **โ๏ธ Send Email:** Internal correspondence with Service Manager.
+- **๐ Application Log:** View property application history and comments.
+- **โ Add Comment/Request:** Submit comments or upload documents.
+
+### ๐ Reviewed Properties Page
+- **๐ Search & Filter:** Search reviewed properties by location and filter by date.
+- **โ
Verified Status:** See which properties are verified.
+- **๐๏ธ Property List:** View details and navigate to Form Summary.
+- **โถ๏ธ Pagination:** Navigate through reviewed properties.
+
+### ๐๏ธ Form Summary Page
+- **๐ Property Details:** View all submitted property information.
+- **๐บ๏ธ IGSR Details:** Survey, GIS, cadastral, and registration info.
+- **๐ค Owner & Assessment Details:** Owner contact, usage, construction, tax zone, etc.
+- **๐ Documents Uploaded:** List and download supporting documents.
+- **๐บ๏ธ Map Integration:** View property on Google Maps, Apple Maps, or Open Street Maps.
+
+### ๐ Search Property Page
+- **๐ Field Selection:** Search by owner name, location, or phone number.
+- **๐๏ธ Voice Input:** Option to use voice for search fields.
+- **๐ Search Results:** List of properties with status and location.
+- **๐ View Location:** Directly view property location on map.
+
+### ๐ Application Log
+- **โณ Timeline View:** Chronological history of application events and comments.
+- **๐ Status Updates:** See inspection, verification, and admin actions.
+- **โฌ๏ธ Document Download:** Download attached documents.
+- **โ๏ธ Add Request:** Add new requests or comments.
+
+### ๐ฌ Add Comment or Request
+- **๐ญ Comment/Request Input:** Enter comments or requests for the application log.
+- **๐ File Upload:** Attach supporting documents (PDF, images, etc.).
+- **โ
/โ Submit/Cancel:** Submit or cancel the comment/request.
+
+### ๐ง Send Email
+- **โ๏ธ Compose Email:** Internal communication with Service Manager.
+- **๐ Attachment Support:** Attach documents to emails.
+- **๐พ/๐๏ธ Draft/Discard:** Save as draft or discard the email.
+
+## ๐งญ Agent Workflow and Navigation
+
+### 1๏ธโฃ Home Page
+- **๐บ๏ธ Map Section:**
+ - Two views: **Map** (shows properties on a map) and **Land Use** (shows land use zoning).
+- **๐ Action Buttons:**
+ - **โ Add New Property:** Start a new property application.
+ - **๐ Generate No Dues:** Generate a no-dues certificate for a property.
+ - **๐ Tabs:**
+ - **All:** View all properties assigned to the agent.
+ - **New:** View newly assigned or finished properties.
+ - **Drafts:** View properties with incomplete applications.
+ - **Others:** Other property categories.
+- **๐ท๏ธ Property List:**
+ - Shows properties with status (๐ด High/๐ก Medium), category ID, address, and date.
+ - Clicking a property navigates to the **Property Form Verification** page.
+
+### 2๏ธโฃ Property Form Verification Page
+- **๐ Summary of Property:**
+ - Map location, address, and property details.
+ - Shows missing required fields and fields needing verification.
+- **๐ Actions:**
+ - **โก๏ธ Continue to Form:** Proceed to complete or verify property details.
+ - **โ๏ธ Send Email:** Internal correspondence with the Service Manager.
+ - **๐ Application Log:** View the history of comments and requests for the property.
+ - **โ Add Comment/Request:** Submit comments or upload documents for the application log.
+
+### 3๏ธโฃ Reviewed Properties Page
+- Accessed by clicking the โ
tick button on the Home page.
+- Shows a list of properties that have been reviewed and verified.
+- Clicking a property navigates to the **Form Summary** page.
+
+### 4๏ธโฃ Form Summary Page
+- Displays detailed information about the submitted property, including:
+ - ๐ Property details (type, zone, door number, area)
+ - ๐บ๏ธ IGSR details (survey number, GIS reference, cadastral map)
+ - ๐ค Owner information
+ - ๐ Assessment details (usage, construction year, built-up area, tax zone)
+ - ๐ Uploaded documents
+- Provides options to view the property location on Google Maps, Apple Maps, or Open Street Maps.
+
+### 5๏ธโฃ Search Property Page
+- Accessed from the bottom navigation bar (๐ Search icon).
+- Allows searching for properties by owner name, location, or phone number.
+- Displays search results with property status (โ
Verified/๐ Pending) and location.
+
+### 6๏ธโฃ Application Log & Comments
+- **๐ Application Log:**
+ - Timeline of actions, comments, and document uploads related to a property application.
+ - โฌ๏ธ Downloadable documents and status updates.
+- **๐ฌ Add Comment/Request:**
+ - Submit comments or requests and upload supporting documents.
+
+### 7๏ธโฃ Send Email
+- Internal communication tool for agents to correspond with the Service Manager.
+- ๐ Attach documents and send messages regarding property applications.
+
+---
+
+## ๐บ๏ธ Navigation Flow (Agent)
+1. **๐ Home Page** โ Select property โ **๐ Property Form Verification**
+2. **๐ Property Form Verification** โ
+ - โก๏ธ Continue to Form
+ - โ๏ธ Send Email
+ - ๐ Application Log
+ - โ Add Comment/Request
+3. **โ
Reviewed Properties** (via tick button on Home) โ Select property โ **๐๏ธ Form Summary**
+4. **๐ Search** (bottom nav) โ **๐ท๏ธ Search Property**
+
+---
+
+## ๐ Add New Property Flow (Agent)
+
+When the agent clicks the **โ Add New Property** button from the Home page, the following step-by-step workflow is initiated. Each step corresponds to a form page where the agent must enter or select relevant property details. Navigation between steps is performed using the **Next** button, and drafts can be saved at any stage.
+
+### 1๏ธโฃ Property Information
+- **Fields:**
+ - ๐ข Category of Ownership (e.g., Private, Government, Vacant Land)
+ - ๐๏ธ Building Use (Mixed, Non-Residential, Residential)
+ - ๐ข Apartment/Complex Name
+- **๐ Location Tag/Polygon:**
+ - Click **Add Location Tag** or **Add Polygon** to open the map interface.
+ - Select the property location or draw a polygon on the map.
+ - โ๏ธ Confirm the location; the selected address and coordinates are displayed on the form.
+
+### 2๏ธโฃ Owner Details
+- **Fields:**
+ - ๐ Aadhaar Number
+ - ๐ค Owner Name
+ - ๐ฑ Mobile Number
+ - โง Gender
+ - ๐ง Email Address
+ - ๐งโ๐ฆณ Guardian and Guardian Relationship (optional)
+- **โ Add Owner:** Add multiple owners as needed. Each ownerโs details are displayed in a list.
+
+### 3๏ธโฃ Owner Details - Identity Proof
+- **Fields:**
+ - ๐ค Select the primary owner
+ - ๐ชช Upload proof of identity (Aadhaar, Voter ID, PAN, etc.)
+
+### 4๏ธโฃ Property Address
+- **Fields:**
+ - ๐๏ธ Locality, ๐ท๏ธ Zone No, ๐๏ธ Ward No, ๐งฑ Block No, ๐ฃ๏ธ Street, ๐ณ๏ธ Election Ward, ๐ข Secretariat Ward, ๐ข Pincode
+ - Option to specify a correspondence address if different from the property address
+
+### 5๏ธโฃ Assessment Details
+- **Fields:**
+ - ๐ Reason for Creation
+ - ๐ Occupancy Certificate Number & Date
+ - ๐ Extent of Site (Sq.Yds)
+ - ๐ Land Underneath the Building (Sq.Yds)
+ - Option for Unspecified/Undivided Share
+
+### 6๏ธโฃ IGRS and Building Setback Details
+- **Fields:**
+ - ๐๏ธ Habitation, ๐บ๏ธ IGRS Ward, ๐ Locality, ๐งฑ Block, ๐ข Door No (From/To), ๐๏ธ Classification
+ - ๐ Built Up Area (percentage)
+ - โ๏ธ Front, Rear, Side Setbacks (meters)
+ - โป๏ธ Total Plinth Area (meters square)
+
+### 7๏ธโฃ IGRS Details (Amenities)
+- **Fields:**
+ - โก Select amenities (Lift, Toilets, Water Tap, Cable Connection, Electricity, Attached Bathroom, Water Harvesting)
+
+### 8๏ธโฃ Construction Details
+- **Fields:**
+ - ๐๏ธ Floor Type, ๐ Roof Type, ๐งฑ Wall Type, ๐ชต Wood Type
+
+### 9๏ธโฃ Floor Details
+- **Fields:**
+ - ๐ข Floor Number, ๐๏ธ Classification of Building, ๐ญ Nature of Usage, ๐ข Firm Name, ๐ถ Occupancy, ๐ค Occupant Name
+ - ๐๏ธ Construction Date, ๐๏ธ Effective From Date
+ - ๐พ Unstructured Land, ๐ Length, Breadth, Plinth Area, ๐ Building Permission No
+ - ๐ **Clone Floor:** Option to copy details to other floors
+ - โฌ๏ธ **Select Fields to be Copied:** Choose which fields to clone for additional floors
+
+### ๐ Document Upload
+- **Fields:**
+ - ๐ Document Type, ๐ข Serial No, ๐งพ Revenue Document No
+ - ๐ Upload supporting documents (PDF, images, etc.)
+
+### 1๏ธโฃ1๏ธโฃ Summary & Submission
+- **๐ Summary Page:**
+ - ๐ง Review all entered property, owner, assessment, and document details
+ - โ๏ธ Edit any section if needed
+ - ๐๏ธ Add important notes and upload photos if required
+- **๐ค Submit:** Final submission of the new property application
+
+---
+
+**โน๏ธ Note:** At each step, agents can save the form as a draft and return later to complete the process. The form is designed to ensure all mandatory fields are filled and all required documents are uploaded before submission.
+
+---
+
+This is the complete flow of the Agent role in the DIGIT 3.0 Property Tax frontend.
+
+By following this structured workflow, agents can efficiently manage property tax applications, ensure data accuracy, and streamline communication with other stakeholders. The intuitive interface and stepwise process help reduce errors, improve compliance, and enhance the overall service delivery experience for both agents and citizens.
+
+๐ This documentation serves as a comprehensive guide for understanding and utilizing all features available to agents in the DIGIT 3.0 Property Tax system.
+
+# ๐ก Citizen Module Documentation
+
+## ๐ Overview
+
+The Citizen module in DIGIT 3.0 enables property owners and residents to manage their property-related information, view notifications, and interact with municipal services through a user-friendly interface. The module is designed to provide transparency, convenience, and self-service capabilities for citizens.
+
+---
+
+## โจ Features of Each Page
+
+### ๐ Home Page
+- **๐ Active Licenses & Number of Properties:** Quick summary cards showing the count of active licenses and registered properties.
+- **๐จ Urgent Attention:** Alerts for important actions (e.g., license renewal, bill payments, tax registration).
+- **๐บ๏ธ/๐ Map/List Toggle:** Switch between map view and list view of properties.
+- **๐ Property Markers:** Visual representation of properties on the map with enumeration progress.
+- **โ Add New Property:** Start a new property application (uses the same form flow as Agent).
+- **๐ Navigation Bar:** Access Home, Utilities, Properties, and My City sections.
+- **๐, ๐ & ๐ค Language, Notification, and Profile:** Quick access icons at the top for language selection, notifications, and profile management.
+
+### ๐๏ธ My Properties Page
+- **๐ Property List:** View all registered properties and draft applications.
+- **๐งฎ Tax Calculator:** Access property tax calculation tool.
+- **๐ท๏ธ Property Cards:** Each card shows address, status (Enumerated/Under Enumeration), and map location.
+- **๐บ๏ธ View Location:** Directly view property location on the map.
+- **๐ Draft Applications:** Access and manage draft property applications.
+
+### ๐ข Property Information Page
+- **๐๏ธ Overview Tab:**
+ - Property type, construction year, built-up area, plot area, property value, survey number, ward.
+ - Land use and zoning details (current use, approved use, zoning, FSI).
+- **๐ Plot Info Tab:**
+ - Survey number, subdivision, village, district, hobli, taluk, and building setbacks (front, rear, side, coverage).
+- **๐ History Tab:**
+ - Ownership and registration history, including owner names, property type, registration dates, and status.
+- **๐ Documents Tab:**
+ - Upload and view property-related documents (sale deed, affidavit, permission documents, property card, etc.).
+ - Take image or upload files (PDF, JPG, PNG).
+ - View, download, or delete uploaded documents.
+- **๐ Enumeration Progress:**
+ - Progress bar showing the completion status of property enumeration.
+- **๐บ๏ธ/๐พ Map/Land Use Toggle:**
+ - Switch between map and land use zoning view for the property.
+
+---
+
+## ๐งญ Citizen Workflow and Navigation
+
+1. **๐ Login โ ๐ Home Page**
+ - Citizen logs in and lands on the Home page, viewing summary cards, urgent notifications, and a map/list of properties.
+2. **๐บ๏ธ/๐ Map/List Toggle**
+ - Switch between map and list view to see properties visually or in a list format.
+3. **โ Add New Property**
+ - Click the Add New Property button to start a new property application (follows the same multi-step form as Agent).
+4. **๐๏ธ Properties Tab (Bottom Navigation)**
+ - Navigate to the My Properties page to view all properties and drafts.
+5. **๐๏ธโ๐จ๏ธ View Property Details**
+ - Click on a property card to open the Property Information page.
+ - Access Overview, Plot Info, History, and Documents tabs for detailed information and actions.
+6. **โฌ๏ธ/๐๏ธ Upload/View Documents**
+ - In the Documents tab, upload new documents or view/download existing ones.
+
+---
+
+**๐ ๏ธ Note:** Some features and pages are under development and will be documented as they are completed.
+
+
+---
+
+๐ This is brief documentation. For more details, refer to the codebase or contact the maintainers.
\ No newline at end of file
diff --git a/frontend/mobile-ui/TESTING_CHECKLIST.md b/frontend/mobile-ui/TESTING_CHECKLIST.md
new file mode 100644
index 0000000..ce79a21
--- /dev/null
+++ b/frontend/mobile-ui/TESTING_CHECKLIST.md
@@ -0,0 +1,116 @@
+# FUNCTIONALITY TESTING CHECKLIST
+
+## โ
COMPLETE TESTING GUIDE
+
+### ๐ HomePage Filter Testing
+- [ ] **All Button**: Shows 7 properties from db.json
+- [ ] **New Button**: Shows 2 properties (PRP-2025-003, PRP-2025-005), latest first
+- [ ] **Reviewed Button**: Shows 3 verified properties (PRP-2024-001, PRP-2024-234, PRP-2024-150)
+- [ ] **Draft Button**: Shows 2 draft properties with special layout and Continue buttons
+
+### ๐บ๏ธ Map Component Testing
+- [ ] Map renders on HomePage for All, New, Reviewed filters
+- [ ] Map hidden when Draft filter is active
+- [ ] "View Map" button appears only in Draft mode
+- [ ] Map toggles properly when "View Map" clicked in Draft mode
+- [ ] Map/Land Use tabs switch correctly
+
+### ๐ SearchPropertyPage Testing
+- [ ] Search dropdown has options: All, Location, Owner Name, Phone Number
+- [ ] Shows 3 search results from db.json by default
+- [ ] Property cards display correctly with area, verification status
+- [ ] Voice search button present and clickable
+
+### ๐ LocationPage Testing
+- [ ] Shows location from db.json (123 Gandhi Nagar...)
+- [ ] Phone number displays from db.json (+91 39243 22342)
+- [ ] Map placeholder renders
+
+### ๐ง InboxPage Testing
+- [ ] Shows 5 messages with different types (tax, verification, notification)
+- [ ] Message stats show: 2 Unread, 5 Total, 1 High Priority
+- [ ] Unread messages have visual indicators
+- [ ] Priority colors display correctly (High=Red, Medium=Orange, Low=Green)
+
+### ๐ InsightsPage Testing
+- [ ] Overview section shows 4 insight cards
+- [ ] Property performance shows 3 properties
+- [ ] Trend indicators display correctly (๐๐โก๏ธ)
+- [ ] Quick action buttons render (4 actions)
+
+### ๐ DraftPage Testing
+- [ ] Shows draft properties with delete buttons
+- [ ] Continue buttons functional
+- [ ] Saved timestamps display
+- [ ] Location pins show correctly
+
+### ๐งญ Navigation Testing
+- [ ] Bottom navigation works on: Home, Inbox, Insights, Search pages
+- [ ] Back buttons navigate to previous page correctly
+- [ ] Header icons (Language, Profile, Home) clickable
+- [ ] Page transitions smooth
+
+### ๐ Data Integrity Testing
+- [ ] All HomePage data comes from db.json properties array
+- [ ] Location data comes from db.json location object
+- [ ] Search results come from db.json searchResults array
+- [ ] No hardcoded property data in HomePage
+- [ ] Filter functions work with database flags (isNew, isVerified, isDraft)
+
+### ๐จ UI Consistency Testing
+- [ ] Original date format preserved (e.g., "Oct 13 2025")
+- [ ] No UI changes to property cards (reverted changes)
+- [ ] Original saved date format in Draft mode
+- [ ] Property status badges removed (UI reverted)
+
+## ๐ HOW TO TEST
+
+1. **Start the application:**
+ ```powershell
+ npm run dev
+ ```
+
+2. **Test Homepage Filters:**
+ - Click each filter button (All, New, Reviewed, Draft)
+ - Verify correct number of properties show for each filter
+ - Check Draft mode shows special layout with map toggle
+
+3. **Test Navigation:**
+ - Use bottom navigation to visit each page
+ - Use back buttons to return to previous pages
+ - Test header icons functionality
+
+4. **Test Search:**
+ - Navigate to Search page
+ - Try different search field options
+ - Verify search results display
+
+5. **Test Other Pages:**
+ - Visit Inbox, Insights, Location, Draft pages
+ - Verify content displays correctly
+ - Check all interactive elements
+
+## ๐ EXPECTED RESULTS
+
+### Filter Counts:
+- **All**: 7 properties
+- **New**: 2 properties (sorted by date, latest first)
+- **Reviewed**: 3 properties
+- **Draft**: 2 properties (with special draft layout)
+
+### Data Sources:
+- HomePage properties: `db.json โ properties`
+- Search results: `db.json โ searchResults`
+- Location: `db.json โ location`
+- Other pages: Still use hardcoded data (can be migrated later)
+
+## โก QUICK VERIFICATION
+
+**Essential checks in 2 minutes:**
+1. HomePage filter buttons change property list โ
+2. Draft filter shows different layout with map button โ
+3. Search page shows results โ
+4. Navigation between pages works โ
+5. No console errors โ
+
+If all these pass, the core functionality is working!
\ No newline at end of file
diff --git a/frontend/mobile-ui/db.json b/frontend/mobile-ui/db.json
new file mode 100644
index 0000000..298f329
--- /dev/null
+++ b/frontend/mobile-ui/db.json
@@ -0,0 +1,892 @@
+{
+ "documentTypes": [
+ { "id": 1, "name": "Patta Certificate (Issued by Revenue Department)" },
+ { "id": 2, "name": "Registered Will Document" },
+ { "id": 3, "name": "Un-registered Will Document" },
+ { "id": 4, "name": "Decree by Civil Court" },
+ { "id": 5, "name": "Registered Document" },
+ { "id": 6, "name": "Un-registered Document / Notary document" }
+ ],
+ "ownershipOptions": [
+ {
+ "name": "Vacant Land",
+ "id": "2755"
+ },
+ {
+ "name": "Private",
+ "id": "7db4"
+ },
+ {
+ "name": "Central Government 50%",
+ "id": "6de8"
+ },
+ {
+ "name": "Central Government 75%",
+ "id": "dd6c"
+ },
+ {
+ "name": "State Government",
+ "id": "dc86"
+ }
+ ,
+ {
+ "name": "Central Government 33.33%",
+ "id": "33d3"
+ }
+ ],
+ "propertyTypeOptions": [
+ {
+ "name": "Mixed",
+ "id": "dd67"
+ },
+ {
+ "name": "Residential",
+ "id": "e7a4"
+ },
+ {
+ "name": "Non- Residential",
+ "id": "7f3f"
+ }
+ ],
+ "vacantLandPropertyTypes": [
+ {
+ "name": "Central Government Land",
+ "id": "vlt-1"
+ },
+ {
+ "name": "State Government Land",
+ "id": "vlt-2"
+ },
+ {
+ "name": "Private Land",
+ "id": "vlt-3"
+ }
+ ],
+ "guardianRelationshipOptions": [
+ {
+ "name": "Father",
+ "id": "9531"
+ },
+ {
+ "name": "Mother",
+ "id": "2fa2"
+ },
+ {
+ "name": "Spouse",
+ "id": "d1bd"
+ },
+ {
+ "name": "Husband",
+ "id": "0e1d"
+ },
+ {
+ "name": "Other",
+ "id": "2e47"
+ }
+ ],
+ "genderOptions": [
+ {
+ "name": "MALE",
+ "id": "73d3"
+ },
+ {
+ "name": "FEMALE",
+ "id": "ce22"
+ },
+ {
+ "name": "OTHERS",
+ "id": "8e52"
+ }
+ ],
+ "owners": [],
+ "wardNoOptions": [
+ {
+ "name": "Ward 1",
+ "id": "8508"
+ },
+ {
+ "name": "Ward 2",
+ "id": "a05c"
+ },
+ {
+ "name": "Ward 3",
+ "id": "c0ab"
+ },
+ {
+ "name": "Ward 4",
+ "id": "9fa9"
+ }
+ ],
+ "blockNoOptions": [
+ {
+ "name": "Block A",
+ "id": "1885"
+ },
+ {
+ "name": "Block B",
+ "id": "d9ed"
+ },
+ {
+ "name": "Block C",
+ "id": "a765"
+ },
+ {
+ "name": "Block D",
+ "id": "d600"
+ }
+ ],
+ "streetOptions": [
+ {
+ "name": "Main Street",
+ "id": "c6e7"
+ },
+ {
+ "name": "First Street",
+ "id": "39f1"
+ },
+ {
+ "name": "Second Street",
+ "id": "1dc1"
+ },
+ {
+ "name": "Third Street",
+ "id": "00a9"
+ }
+ ],
+ "reason": [
+ {
+ "id": "1",
+ "label": "New Construction"
+ },
+ {
+ "id": "2",
+ "label": "Renovation"
+ },
+ {
+ "id": "3",
+ "label": "Extension"
+ },
+ {
+ "id": "4",
+ "label": "Demolition"
+ },
+ {
+ "id": "5",
+ "label": "Change of Use"
+ }
+ ],
+ "habitation": [
+ {
+ "id": "1",
+ "label": "Central Park Colony"
+ },
+ {
+ "id": "2",
+ "label": "Sunrise Apartments"
+ },
+ {
+ "id": "3",
+ "label": "Hilltop Residency"
+ },
+ {
+ "id": "4",
+ "label": "Riverfront Homes"
+ },
+ {
+ "id": "5",
+ "label": "Maple Street Villas"
+ }
+ ],
+ "igrsWard": [
+ {
+ "id": "1",
+ "label": "Ward A"
+ },
+ {
+ "id": "2",
+ "label": "Ward B"
+ },
+ {
+ "id": "3",
+ "label": "Ward C"
+ },
+ {
+ "id": "4",
+ "label": "Ward D"
+ },
+ {
+ "id": "5",
+ "label": "Ward E"
+ }
+ ],
+ "igrsLocality": [
+ {
+ "id": "1",
+ "label": "Green Meadows"
+ },
+ {
+ "id": "2",
+ "label": "Lakeview"
+ },
+ {
+ "id": "3",
+ "label": "Sunshine Valley"
+ },
+ {
+ "id": "4",
+ "label": "Riverdale"
+ },
+ {
+ "id": "5",
+ "label": "Maple Gardens"
+ }
+ ],
+ "igrsBlock": [
+ {
+ "id": "1",
+ "label": "Block 12"
+ },
+ {
+ "id": "2",
+ "label": "Block 7"
+ },
+ {
+ "id": "3",
+ "label": "Block 3"
+ },
+ {
+ "id": "4",
+ "label": "Block 9"
+ },
+ {
+ "id": "5",
+ "label": "Block 1"
+ }
+ ],
+ "doorNoFrom": [
+ {
+ "id": "1",
+ "label": "10"
+ },
+ {
+ "id": "2",
+ "label": "101"
+ },
+ {
+ "id": "3",
+ "label": "201"
+ },
+ {
+ "id": "4",
+ "label": "301"
+ },
+ {
+ "id": "5",
+ "label": "401"
+ }
+ ],
+ "doorNoTo": [
+ {
+ "id": "1",
+ "label": "15"
+ },
+ {
+ "id": "2",
+ "label": "110"
+ },
+ {
+ "id": "3",
+ "label": "210"
+ },
+ {
+ "id": "4",
+ "label": "310"
+ },
+ {
+ "id": "5",
+ "label": "410"
+ }
+ ],
+ "floorNumber": [
+ {
+ "id": "1",
+ "label": "Ground Floor"
+ },
+ {
+ "id": "2",
+ "label": "First Floor"
+ },
+ {
+ "id": "3",
+ "label": "Second Floor"
+ },
+ {
+ "id": "4",
+ "label": "Third Floor"
+ },
+ {
+ "id": "5",
+ "label": "Fourth Floor"
+ }
+ ],
+ "buildingClassification": [
+ { "id": "bc-0", "label": "select" },
+ { "id": "bc-1", "label": "ACC Sheet" },
+ { "id": "bc-2", "label": "ACC Sheet Exceeding 10 Feet Height" },
+ { "id": "bc-3", "label": "GI Sheet" },
+ { "id": "bc-4", "label": "Huts" },
+ { "id": "bc-5", "label": "Jack Arch" },
+ { "id": "bc-6", "label": "Madras Terrace" },
+ { "id": "bc-7", "label": "Mangalore/Country Tiled" },
+ { "id": "bc-8", "label": "RCC" },
+ { "id": "bc-9", "label": "RCC High Rise Structure" },
+ { "id": "bc-10", "label": "Slates/Stone Roofed" }
+ ],
+ "igrsClassification": [
+ {
+ "id": "1",
+ "label": "A"
+ },
+ {
+ "id": "2",
+ "label": "B"
+ },
+ {
+ "id": "3",
+ "label": "C"
+ }
+ ],
+ "natureOfUsage": [
+ {
+ "id": "1",
+ "label": "Self Use"
+ },
+ {
+ "id": "2",
+ "label": "Rental"
+ }
+ ],
+ "occupancy": [
+ {
+ "id": "1",
+ "label": "Owner"
+ },
+ {
+ "id": "2",
+ "label": "Tenant"
+ }
+ ],
+ "occupantName": [
+ {
+ "id": "1",
+ "label": "Ravi Kumar"
+ },
+ {
+ "id": "2",
+ "label": "Anita Sharma"
+ },
+ {
+ "id": "3",
+ "label": "Vikas Patel"
+ },
+ {
+ "id": "4",
+ "label": "Priya Singh"
+ },
+ {
+ "id": "5",
+ "label": "Meera Joshi"
+ }
+ ],
+ "unstructuredLand": [
+ {
+ "id": "1",
+ "label": "Yes"
+ },
+ {
+ "id": "2",
+ "label": "No"
+ }
+ ],
+ "length": [
+ {
+ "id": "1",
+ "label": "30"
+ },
+ {
+ "id": "2",
+ "label": "25"
+ },
+ {
+ "id": "3",
+ "label": "28"
+ },
+ {
+ "id": "4",
+ "label": "18"
+ },
+ {
+ "id": "5",
+ "label": "22"
+ }
+ ],
+ "breadth": [
+ {
+ "id": "1",
+ "label": "20"
+ },
+ {
+ "id": "2",
+ "label": "15"
+ },
+ {
+ "id": "3",
+ "label": "18"
+ },
+ {
+ "id": "4",
+ "label": "15"
+ },
+ {
+ "id": "5",
+ "label": "16"
+ }
+ ],
+ "plinthArea": [
+ {
+ "id": "1",
+ "label": "600"
+ },
+ {
+ "id": "2",
+ "label": "375"
+ },
+ {
+ "id": "3",
+ "label": "504"
+ },
+ {
+ "id": "4",
+ "label": "270"
+ },
+ {
+ "id": "5",
+ "label": "352"
+ }
+ ],
+ "buildingPermissionNo": [
+ {
+ "id": "1",
+ "label": "BP-2023-001"
+ },
+ {
+ "id": "2",
+ "label": "BP-2022-015"
+ },
+ {
+ "id": "3",
+ "label": "BP-2021-032"
+ },
+ {
+ "id": "4",
+ "label": "BP-2020-087"
+ },
+ {
+ "id": "5",
+ "label": "BP-2019-021"
+ }
+ ],
+ "floorType": [
+ {
+ "id": "1",
+ "label": "Marble"
+ },
+ {
+ "id": "2",
+ "label": "Tiles"
+ },
+ {
+ "id": "3",
+ "label": "Granite"
+ },
+ {
+ "id": "4",
+ "label": "Concrete"
+ },
+ {
+ "id": "5",
+ "label": "Other"
+ }
+ ],
+ "roofType": [
+ {
+ "id": "1",
+ "label": "Concrete"
+ },
+ {
+ "id": "2",
+ "label": "Metal"
+ },
+ {
+ "id": "3",
+ "label": "Asbestos"
+ },
+ {
+ "id": "4",
+ "label": "Tiles"
+ },
+ {
+ "id": "5",
+ "label": "Other"
+ }
+ ],
+ "wallType": [
+ {
+ "id": "1",
+ "label": "Brick"
+ },
+ {
+ "id": "2",
+ "label": "Stone"
+ },
+ {
+ "id": "3",
+ "label": "Concrete"
+ },
+ {
+ "id": "4",
+ "label": "Wood"
+ },
+ {
+ "id": "5",
+ "label": "Other"
+ }
+ ],
+ "woodType": [
+ {
+ "id": "1",
+ "label": "Teak"
+ },
+ {
+ "id": "2",
+ "label": "Sal"
+ },
+ {
+ "id": "3",
+ "label": "Rosewood"
+ },
+ {
+ "id": "4",
+ "label": "Mango"
+ },
+ {
+ "id": "5",
+ "label": "Other"
+ }
+ ],
+ "properties": [
+ {
+ "id": "PRP-2024-001",
+ "type": "Survey",
+ "description": "New residential property verification required",
+ "address": "123 Gandhi Nagar, Guntur, Andhra Pradesh",
+ "dueDate": "2025-10-13",
+ "status": "High",
+ "phoneNumber": "+91 98765 43210",
+ "area": "35,400 sqft",
+ "propertyType": "Residential",
+ "isVerified": true,
+ "isDraft": false,
+ "isNew": false,
+ "createdDate": "2025-10-10"
+ },
+ {
+ "id": "PRP-2024-002",
+ "type": "Survey",
+ "description": "Commercial property compliance check required",
+ "address": "456 Brodipet, Guntur, Andhra Pradesh",
+ "dueDate": "2025-10-20",
+ "status": "Medium",
+ "phoneNumber": "+91 98765 43211",
+ "area": "3,43,500 sqft",
+ "propertyType": "Commercial",
+ "isVerified": false,
+ "isDraft": true,
+ "isNew": false,
+ "createdDate": "2025-10-08"
+ },
+ {
+ "id": "PRP-2025-003",
+ "type": "Survey",
+ "description": "Apartment complex verification pending final review",
+ "address": "789 Lakshmipuram, Guntur, Andhra Pradesh",
+ "dueDate": "2025-10-10",
+ "status": "Low",
+ "phoneNumber": "+91 98765 43212",
+ "area": "1,32,400 sqft",
+ "propertyType": "Residential",
+ "isVerified": false,
+ "isDraft": false,
+ "isNew": true,
+ "createdDate": "2025-10-13"
+ },
+ {
+ "id": "PRP-2024-234",
+ "type": "Commercial",
+ "description": "456 Brodipet Main Road, Guntur, Andhra Pradesh 522002",
+ "address": "456 Brodipet Main Road, Guntur, Andhra Pradesh 522002",
+ "dueDate": "2025-10-15",
+ "status": "Medium",
+ "phoneNumber": "+91 98765 43213",
+ "area": "3,43,500 sqft",
+ "propertyType": "Commercial",
+ "isVerified": true,
+ "isDraft": false,
+ "isNew": false,
+ "createdDate": "2025-10-05"
+ },
+ {
+ "id": "PRP-2024-324",
+ "type": "Residential",
+ "description": "231 Brodipet Main Road, Guntur, Andhra Pradesh 522002",
+ "address": "231 Brodipet Main Road, Guntur, Andhra Pradesh 522002",
+ "dueDate": "2025-10-18",
+ "status": "Low",
+ "phoneNumber": "+91 98765 43214",
+ "area": "1,32,400 sqft",
+ "propertyType": "Residential",
+ "isVerified": false,
+ "isDraft": true,
+ "isNew": false,
+ "createdDate": "2025-10-07"
+ },
+ {
+ "id": "PRP-2025-005",
+ "type": "Survey",
+ "description": "New office complex verification required",
+ "address": "567 Ring Road, Guntur, Andhra Pradesh",
+ "dueDate": "2025-10-25",
+ "status": "High",
+ "phoneNumber": "+91 98765 43215",
+ "area": "2,45,000 sqft",
+ "propertyType": "Commercial",
+ "isVerified": false,
+ "isDraft": false,
+ "isNew": true,
+ "createdDate": "2025-10-12"
+ },
+ {
+ "id": "PRP-2024-150",
+ "type": "Survey",
+ "description": "Residential villa verification completed",
+ "address": "890 Kothapet, Guntur, Andhra Pradesh",
+ "dueDate": "2025-10-05",
+ "status": "Low",
+ "phoneNumber": "+91 98765 43216",
+ "area": "5,200 sqft",
+ "propertyType": "Residential",
+ "isVerified": true,
+ "isDraft": false,
+ "isNew": false,
+ "createdDate": "2025-09-28"
+ },
+ {
+ "id": "PRP-2024-403",
+ "type": "Survey",
+ "description": "Commercial property assessment draft",
+ "address": "456 Brodipet, Guntur, Andhra Pradesh, 522402",
+ "dueDate": "2025-10-10",
+ "status": "Medium",
+ "phoneNumber": "+91 98765 43217",
+ "area": "4,20,000 sqft",
+ "propertyType": "Commercial",
+ "isVerified": false,
+ "isDraft": true,
+ "isNew": false,
+ "createdDate": "2025-09-30"
+ }
+ ],
+ "location": {
+ "address": "123 Gandhi Nagar, Guntur, Andhra Pradesh 522003",
+ "coordinates": {
+ "lat": 16.2973,
+ "lng": 80.4364
+ },
+ "phoneNumber": "+91 39243 22342"
+ },
+ "searchResults": [
+ {
+ "id": "PRP-2024-001",
+ "type": "Residential",
+ "description": "123 Gandhi Nagar, Guntur, Andhra Pradesh 522003",
+ "address": "123 Gandhi Nagar, Guntur, Andhra Pradesh 522003",
+ "dueDate": "2025-10-13",
+ "status": "High",
+ "area": "35,400 sqft",
+ "isVerified": true,
+ "propertyType": "Residential"
+ },
+ {
+ "id": "PRP-2024-234",
+ "type": "Commercial",
+ "description": "456 Brodipet Main Road, Guntur, Andhra Pradesh 522002",
+ "address": "456 Brodipet Main Road, Guntur, Andhra Pradesh 522002",
+ "dueDate": "2025-10-15",
+ "status": "Medium",
+ "area": "3,43,500 sqft",
+ "isVerified": true,
+ "propertyType": "Commercial"
+ },
+ {
+ "id": "PRP-2024-324",
+ "type": "Residential",
+ "description": "231 Brodipet Main Road, Guntur, Andhra Pradesh 522002",
+ "address": "231 Brodipet Main Road, Guntur, Andhra Pradesh 522002",
+ "dueDate": "2025-10-18",
+ "status": "Low",
+ "area": "1,32,400 sqft",
+ "isVerified": false,
+ "propertyType": "Residential"
+ }
+ ],
+ "searchFields": [
+ {
+ "value": "",
+ "label": "All",
+ "id": "b5f3"
+ },
+ {
+ "value": "location",
+ "label": "Location",
+ "id": "d9f2"
+ },
+ {
+ "value": "owner",
+ "label": "Owner Name",
+ "id": "a646"
+ },
+ {
+ "value": "phone",
+ "label": "Phone Number",
+ "id": "e8a3"
+ }
+ ],
+ "messages": [
+ {
+ "id": "MSG-001",
+ "title": "Property Tax Due - Gandhi Nagar",
+ "description": "Your property tax payment for PRP-2024-001 is due on Oct 15, 2025",
+ "date": "Oct 10, 2025",
+ "type": "tax",
+ "isRead": false,
+ "priority": "high"
+ },
+ {
+ "id": "MSG-002",
+ "title": "Verification Complete - Brodipet",
+ "description": "Property verification for PRP-2024-002 has been completed successfully",
+ "date": "Oct 09, 2025",
+ "type": "verification",
+ "isRead": false,
+ "priority": "medium"
+ },
+ {
+ "id": "MSG-003",
+ "title": "Document Update Required",
+ "description": "Please update ownership documents for property in Lakshmipuram",
+ "date": "Oct 08, 2025",
+ "type": "notification",
+ "isRead": true,
+ "priority": "medium"
+ },
+ {
+ "id": "MSG-004",
+ "title": "Property Assessment Scheduled",
+ "description": "Assessment scheduled for Oct 20, 2025 for your commercial property",
+ "date": "Oct 07, 2025",
+ "type": "notification",
+ "isRead": true,
+ "priority": "low"
+ },
+ {
+ "id": "MSG-005",
+ "title": "Tax Payment Confirmation",
+ "description": "Payment of โน15,000 received for property tax PRP-2024-003",
+ "date": "Oct 05, 2025",
+ "type": "tax",
+ "isRead": true,
+ "priority": "low"
+ }
+ ],
+ "insights": {
+ "overview": [
+ {
+ "id": "total-properties",
+ "title": "Total Properties",
+ "value": "7",
+ "change": "+2",
+ "trend": "up",
+ "period": "This Month"
+ },
+ {
+ "id": "total-tax",
+ "title": "Total Tax Paid",
+ "value": "โน45,000",
+ "change": "+โน5,000",
+ "trend": "up",
+ "period": "This Year"
+ },
+ {
+ "id": "pending-payments",
+ "title": "Pending Payments",
+ "value": "โน12,000",
+ "change": "-โน3,000",
+ "trend": "down",
+ "period": "Due This Month"
+ },
+ {
+ "id": "avg-assessment",
+ "title": "Avg. Assessment Value",
+ "value": "โน25,00,000",
+ "change": "+12%",
+ "trend": "up",
+ "period": "From Last Year"
+ }
+ ],
+ "properties": [
+ {
+ "id": "prop-1",
+ "address": "123 Gandhi Nagar, Guntur",
+ "marketValue": "โน32,00,000",
+ "taxAmount": "โน15,000",
+ "lastAssessment": "Oct 2025",
+ "appreciation": "+8%"
+ },
+ {
+ "id": "prop-2",
+ "address": "456 Brodipet, Guntur",
+ "marketValue": "โน18,50,000",
+ "taxAmount": "โน8,500",
+ "lastAssessment": "Sep 2025",
+ "appreciation": "+5%"
+ },
+ {
+ "id": "prop-3",
+ "address": "789 Lakshmipuram, Guntur",
+ "marketValue": "โน24,75,000",
+ "taxAmount": "โน11,200",
+ "lastAssessment": "Aug 2025",
+ "appreciation": "+15%"
+ }
+ ]
+ },
+ "isgrAdditionalOptions": [
+ { "key": "lifts", "label": "Lifts" },
+ { "key": "toilet", "label": "Toilet" },
+ { "key": "watertap", "label": "Water Tap" },
+ { "key": "cableConnection", "label": "Cable Connection" },
+ { "key": "electricity", "label": "Electricity" },
+ { "key": "attachedBathroom", "label": "Attached Bathroom" },
+ { "key": "waterHarvesting", "label": "Water Harvesting" }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/eslint.config.js b/frontend/mobile-ui/eslint.config.js
new file mode 100644
index 0000000..b19330b
--- /dev/null
+++ b/frontend/mobile-ui/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs['recommended-latest'],
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/frontend/mobile-ui/index.html b/frontend/mobile-ui/index.html
new file mode 100644
index 0000000..e062e51
--- /dev/null
+++ b/frontend/mobile-ui/index.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+ Property Tax Portal
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/package-lock.json b/frontend/mobile-ui/package-lock.json
new file mode 100644
index 0000000..2cc258f
--- /dev/null
+++ b/frontend/mobile-ui/package-lock.json
@@ -0,0 +1,6108 @@
+{
+ "name": "egov-digit",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "egov-digit",
+ "version": "0.0.0",
+ "dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
+ "@mapbox/mapbox-gl-draw": "^1.5.1",
+ "@mui/icons-material": "^7.3.5",
+ "@mui/lab": "^7.0.1-beta.18",
+ "@mui/material": "^7.3.6",
+ "@mui/x-date-pickers": "^8.16.0",
+ "@reduxjs/toolkit": "^2.10.1",
+ "@types/leaflet": "^1.9.21",
+ "@types/react-redux": "^7.1.34",
+ "axios": "^1.12.2",
+ "clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
+ "dayjs": "^1.11.19",
+ "dotenv": "^17.2.3",
+ "leaflet": "^1.9.4",
+ "leaflet-draw": "^1.0.4",
+ "maplibre-gl": "^5.14.0",
+ "react": "^19.2.1",
+ "react-datepicker": "^8.8.0",
+ "react-dom": "^19.2.1",
+ "react-icons": "^5.5.0",
+ "react-leaflet": "^5.0.0",
+ "react-map-gl": "^8.1.0",
+ "react-redux": "^9.2.0",
+ "react-router-dom": "^7.9.4",
+ "react-select": "^5.10.2",
+ "react-toastify": "^11.0.5",
+ "zod": "^4.1.12"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/leaflet-draw": "^1.0.13",
+ "@types/mapbox__mapbox-gl-draw": "^1.4.9",
+ "@types/node": "^24.6.0",
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@vitejs/plugin-react": "^5.0.4",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.22",
+ "globals": "^16.4.0",
+ "json-server": "^1.0.0-beta.3",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.45.0",
+ "vite": "^7.2.4"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.13.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/serialize": "^1.3.3",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.14.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/cache/-/cache-11.14.0.tgz",
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/sheet": "^1.4.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
+ "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.9.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.14.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/react/-/react-11.14.0.tgz",
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.3.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
+ "@emotion/utils": "^1.4.2",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.14.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/styled/-/styled-11.14.1.tgz",
+ "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/is-prop-valid": "^1.3.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.10.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.4.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint/js/-/js-9.39.1.tgz",
+ "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.27.16",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@floating-ui/react/-/react-0.27.16.tgz",
+ "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.6",
+ "@floating-ui/utils": "^0.2.10",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mapbox/geojson-area": {
+ "version": "0.2.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz",
+ "integrity": "sha512-bBqqFn1kIbLBfn7Yq1PzzwVkPYQr9lVUeT8Dhd0NL5n76PBuXzOcuLV7GOSbEB1ia8qWxH4COCvFpziEu/yReA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "wgs84": "0.0.0"
+ }
+ },
+ "node_modules/@mapbox/geojson-normalize": {
+ "version": "0.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz",
+ "integrity": "sha512-82V7YHcle8lhgIGqEWwtXYN5cy0QM/OHq3ypGhQTbvHR57DF0vMHMjjVSQKFfVXBe/yWCBZTyOuzvK7DFFnx5Q==",
+ "license": "ISC",
+ "bin": {
+ "geojson-normalize": "geojson-normalize"
+ }
+ },
+ "node_modules/@mapbox/geojson-rewind": {
+ "version": "0.5.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
+ "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
+ "license": "ISC",
+ "dependencies": {
+ "get-stream": "^6.0.1",
+ "minimist": "^1.2.6"
+ },
+ "bin": {
+ "geojson-rewind": "geojson-rewind"
+ }
+ },
+ "node_modules/@mapbox/jsonlint-lines-primitives": {
+ "version": "2.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
+ "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@mapbox/mapbox-gl-draw": {
+ "version": "1.5.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.5.1.tgz",
+ "integrity": "sha512-DnR/oarZVoIrVHssAn+mtpuGzYH+ebORoPjow46zTBNPod/HQnvIZGtL6hIb5BVWxxH49RC9D20ipxiO9WDRxA==",
+ "license": "ISC",
+ "dependencies": {
+ "@mapbox/geojson-area": "^0.2.2",
+ "@mapbox/geojson-normalize": "^0.0.1",
+ "@mapbox/point-geometry": "^1.1.0",
+ "@turf/projection": "^7.2.0",
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "^5.0.9"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/@mapbox/mapbox-gl-draw/node_modules/nanoid": {
+ "version": "5.1.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/nanoid/-/nanoid-5.1.6.tgz",
+ "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
+ "node_modules/@mapbox/mapbox-gl-supported": {
+ "version": "3.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz",
+ "integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==",
+ "devOptional": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@mapbox/point-geometry": {
+ "version": "1.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
+ "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
+ "license": "ISC"
+ },
+ "node_modules/@mapbox/tiny-sdf": {
+ "version": "2.0.7",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
+ "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@mapbox/unitbezier": {
+ "version": "0.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
+ "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@mapbox/vector-tile": {
+ "version": "2.0.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
+ "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@mapbox/point-geometry": "~1.1.0",
+ "@types/geojson": "^7946.0.16",
+ "pbf": "^4.0.1"
+ }
+ },
+ "node_modules/@mapbox/whoots-js": {
+ "version": "3.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
+ "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@maplibre/maplibre-gl-style-spec": {
+ "version": "24.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.3.1.tgz",
+ "integrity": "sha512-TUM5JD40H2mgtVXl5IwWz03BuQabw8oZQLJTmPpJA0YTYF+B+oZppy5lNMO6bMvHzB+/5mxqW9VLG3wFdeqtOw==",
+ "license": "ISC",
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": "~2.0.2",
+ "@mapbox/unitbezier": "^0.0.1",
+ "json-stringify-pretty-compact": "^4.0.0",
+ "minimist": "^1.2.8",
+ "quickselect": "^3.0.0",
+ "rw": "^1.3.3",
+ "tinyqueue": "^3.0.0"
+ },
+ "bin": {
+ "gl-style-format": "dist/gl-style-format.mjs",
+ "gl-style-migrate": "dist/gl-style-migrate.mjs",
+ "gl-style-validate": "dist/gl-style-validate.mjs"
+ }
+ },
+ "node_modules/@maplibre/mlt": {
+ "version": "1.1.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@maplibre/mlt/-/mlt-1.1.2.tgz",
+ "integrity": "sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==",
+ "license": "(MIT OR Apache-2.0)",
+ "dependencies": {
+ "@mapbox/point-geometry": "^1.1.0"
+ }
+ },
+ "node_modules/@maplibre/vt-pbf": {
+ "version": "4.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@maplibre/vt-pbf/-/vt-pbf-4.1.0.tgz",
+ "integrity": "sha512-9LjFAoWtxdGRns8RK9vG3Fcw/fb3eHMxvAn2jffwn3jnVO1k49VOv6+FEza70rK7WzF8GnBiKa0K39RyfevKUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@mapbox/point-geometry": "^1.1.0",
+ "@mapbox/vector-tile": "^2.0.4",
+ "@types/geojson-vt": "3.2.5",
+ "@types/supercluster": "^7.1.3",
+ "geojson-vt": "^4.0.2",
+ "pbf": "^4.0.1",
+ "supercluster": "^8.0.1"
+ }
+ },
+ "node_modules/@mui/core-downloads-tracker": {
+ "version": "7.3.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz",
+ "integrity": "sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ }
+ },
+ "node_modules/@mui/icons-material": {
+ "version": "7.3.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/icons-material/-/icons-material-7.3.5.tgz",
+ "integrity": "sha512-LciL1GLMZ+VlzyHAALSVAR22t8IST4LCXmljcUSx2NOutgO2XnxdIp8ilFbeNf9wpo0iUFbAuoQcB7h+HHIf3A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@mui/material": "^7.3.5",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/lab": {
+ "version": "7.0.1-beta.19",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/lab/-/lab-7.0.1-beta.19.tgz",
+ "integrity": "sha512-Ekxd2mPnr5iKwrMXjN/y2xgpxPX8ithBBcDenjqNdBt/ZQumrmBl0ifVoqAHsL6lxN6DOgRsWTRc4eOdDiB+0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/system": "^7.3.5",
+ "@mui/types": "^7.4.8",
+ "@mui/utils": "^7.3.5",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material": "^7.3.5",
+ "@mui/material-pigment-css": "^7.3.5",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@mui/material-pigment-css": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/material": {
+ "version": "7.3.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/material/-/material-7.3.6.tgz",
+ "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/core-downloads-tracker": "^7.3.6",
+ "@mui/system": "^7.3.6",
+ "@mui/types": "^7.4.9",
+ "@mui/utils": "^7.3.6",
+ "@popperjs/core": "^2.11.8",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.0",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material-pigment-css": "^7.3.6",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@mui/material-pigment-css": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/private-theming": {
+ "version": "7.3.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/private-theming/-/private-theming-7.3.6.tgz",
+ "integrity": "sha512-Ws9wZpqM+FlnbZXaY/7yvyvWQo1+02Tbx50mVdNmzWEi51C51y56KAbaDCYyulOOBL6BJxuaqG8rNNuj7ivVyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.6",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/styled-engine": {
+ "version": "7.3.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/styled-engine/-/styled-engine-7.3.6.tgz",
+ "integrity": "sha512-+wiYbtvj+zyUkmDB+ysH6zRjuQIJ+CM56w0fEXV+VDNdvOuSywG+/8kpjddvvlfMLsaWdQe5oTuYGBcodmqGzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/sheet": "^1.4.0",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.4.1",
+ "@emotion/styled": "^11.3.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/system": {
+ "version": "7.3.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/system/-/system-7.3.6.tgz",
+ "integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/private-theming": "^7.3.6",
+ "@mui/styled-engine": "^7.3.6",
+ "@mui/types": "^7.4.9",
+ "@mui/utils": "^7.3.6",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/types": {
+ "version": "7.4.9",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/types/-/types-7.4.9.tgz",
+ "integrity": "sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/utils": {
+ "version": "7.3.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/utils/-/utils-7.3.6.tgz",
+ "integrity": "sha512-jn+Ba02O6PiFs7nKva8R2aJJ9kJC+3kQ2R0BbKNY3KQQ36Qng98GnPRFTlbwYTdMD6hLEBKaMLUktyg/rTfd2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/types": "^7.4.9",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-date-pickers": {
+ "version": "8.19.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/x-date-pickers/-/x-date-pickers-8.19.0.tgz",
+ "integrity": "sha512-TQ4FsGUsiGJVs+Ie4q7nHXUmFqZADXL/1hVtZpOKsdr3WQXwpX7C5YmeakZGFR2NZnuv4snFj+WTee3kgyFbyQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.5",
+ "@mui/x-internals": "8.19.0",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
+ "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
+ "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
+ "dayjs": "^1.10.7",
+ "luxon": "^3.0.2",
+ "moment": "^2.29.4",
+ "moment-hijri": "^2.1.2 || ^3.0.0",
+ "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "date-fns": {
+ "optional": true
+ },
+ "date-fns-jalali": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ },
+ "moment-hijri": {
+ "optional": true
+ },
+ "moment-jalaali": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/x-internals": {
+ "version": "8.19.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@mui/x-internals/-/x-internals-8.19.0.tgz",
+ "integrity": "sha512-mMmiyJAN5fW27srXJjhXhXJa+w2xGO45rwcjws6OQc9rdXGdJqRXhBwJd+OT7J1xwSdFIIUhjZRTz1KAfCSGBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.5",
+ "reselect": "^5.1.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@react-leaflet/core": {
+ "version": "3.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@react-leaflet/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
+ "license": "Hippocratic-2.1",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@reduxjs/toolkit/-/toolkit-2.11.0.tgz",
+ "integrity": "sha512-hBjYg0aaRL1O2Z0IqWhnTLytnjDIxekmRxm1snsHjHaKVmIF1HiImWqsq+PuEbn6zdMlkIj9WofK1vR8jjx+Xw==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.47",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
+ "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
+ "node_modules/@tinyhttp/accepts": {
+ "version": "2.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/accepts/-/accepts-2.2.3.tgz",
+ "integrity": "sha512-9pQN6pJAJOU3McmdJWTcyq7LLFW8Lj5q+DadyKcvp+sxMkEpktKX5sbfJgJuOvjk6+1xWl7pe0YL1US1vaO/1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime": "4.0.4",
+ "negotiator": "^0.6.3"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
+ }
+ },
+ "node_modules/@tinyhttp/app": {
+ "version": "2.5.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/app/-/app-2.5.2.tgz",
+ "integrity": "sha512-DcB3Y8GQppLQlO2VxRYF7LzTEAoZb+VRQXuIsErcu2fNaM1xdx6NQZDso5rlZUiaeg6KYYRfU34N4XkZbv6jSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tinyhttp/cookie": "2.1.1",
+ "@tinyhttp/proxy-addr": "2.2.1",
+ "@tinyhttp/req": "2.2.5",
+ "@tinyhttp/res": "2.2.5",
+ "@tinyhttp/router": "2.2.3",
+ "header-range-parser": "1.1.3",
+ "regexparam": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
+ }
+ },
+ "node_modules/@tinyhttp/content-disposition": {
+ "version": "2.2.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/content-disposition/-/content-disposition-2.2.2.tgz",
+ "integrity": "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
+ }
+ },
+ "node_modules/@tinyhttp/content-type": {
+ "version": "0.1.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/content-type/-/content-type-0.1.4.tgz",
+ "integrity": "sha512-dl6f3SHIJPYbhsW1oXdrqOmLSQF/Ctlv3JnNfXAE22kIP7FosqJHxkz/qj2gv465prG8ODKH5KEyhBkvwrueKQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.4"
+ }
+ },
+ "node_modules/@tinyhttp/cookie": {
+ "version": "2.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/cookie/-/cookie-2.1.1.tgz",
+ "integrity": "sha512-h/kL9jY0e0Dvad+/QU3efKZww0aTvZJslaHj3JTPmIPC9Oan9+kYqmh3M6L5JUQRuTJYFK2nzgL2iJtH2S+6dA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
+ }
+ },
+ "node_modules/@tinyhttp/cookie-signature": {
+ "version": "2.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/cookie-signature/-/cookie-signature-2.1.1.tgz",
+ "integrity": "sha512-VDsSMY5OJfQJIAtUgeQYhqMPSZptehFSfvEEtxr+4nldPA8IImlp3QVcOVuK985g4AFR4Hl1sCbWCXoqBnVWnw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/cors": {
+ "version": "2.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/cors/-/cors-2.0.1.tgz",
+ "integrity": "sha512-qrmo6WJuaiCzKWagv2yA/kw6hIISfF/hOqPWwmI6w0o8apeTMmRN3DoCFvQ/wNVuWVdU5J4KU7OX8aaSOEq51A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tinyhttp/vary": "^0.1.3"
+ },
+ "engines": {
+ "node": ">=12.20 || 14.x || >=16"
+ }
+ },
+ "node_modules/@tinyhttp/encode-url": {
+ "version": "2.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/encode-url/-/encode-url-2.1.1.tgz",
+ "integrity": "sha512-AhY+JqdZ56qV77tzrBm0qThXORbsVjs/IOPgGCS7x/wWnsa/Bx30zDUU/jPAUcSzNOzt860x9fhdGpzdqbUeUw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/etag": {
+ "version": "2.1.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/etag/-/etag-2.1.2.tgz",
+ "integrity": "sha512-j80fPKimGqdmMh6962y+BtQsnYPVCzZfJw0HXjyH70VaJBHLKGF+iYhcKqzI3yef6QBNa8DKIPsbEYpuwApXTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/forwarded": {
+ "version": "2.1.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/forwarded/-/forwarded-2.1.2.tgz",
+ "integrity": "sha512-9H/eulJ68ElY/+zYpTpNhZ7vxGV+cnwaR6+oQSm7bVgZMyuQfgROW/qvZuhmgDTIxnGMXst+Ba4ij6w6Krcs3w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/logger": {
+ "version": "2.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/logger/-/logger-2.1.0.tgz",
+ "integrity": "sha512-Ma1fJ9CwUbn9r61/4HW6+nflsVoslpOnCrfQ6UeZq7GGIgwLzofms3HoSVG7M+AyRMJpxlfcDdbH5oFVroDMKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "colorette": "^2.0.20",
+ "dayjs": "^1.11.13",
+ "http-status-emojis": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14.18 || >=16.20"
+ }
+ },
+ "node_modules/@tinyhttp/proxy-addr": {
+ "version": "2.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/proxy-addr/-/proxy-addr-2.2.1.tgz",
+ "integrity": "sha512-BicqMqVI91hHq2BQmnqJUh0FQUnx7DncwSGgu2ghlh+JZG2rHK2ZN/rXkfhrx1rrUw6hnd0L36O8GPMh01+dDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tinyhttp/forwarded": "2.1.2",
+ "ipaddr.js": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/req": {
+ "version": "2.2.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/req/-/req-2.2.5.tgz",
+ "integrity": "sha512-trfsXwtmsNjMcGKcLJ+45h912kLRqBQCQD06ams3Tq0kf4gHLxjHjoYOC1Z9yGjOn81XllRx8wqvnvr+Kbe3gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tinyhttp/accepts": "2.2.3",
+ "@tinyhttp/type-is": "2.2.4",
+ "@tinyhttp/url": "2.1.1",
+ "header-range-parser": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/res": {
+ "version": "2.2.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/res/-/res-2.2.5.tgz",
+ "integrity": "sha512-yBsqjWygpuKAVz4moWlP4hqzwiDDqfrn2mA0wviJAcgvGiyOErtlQwXY7aj3aPiCpURvxvEFO//Gdy6yV+xEpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tinyhttp/content-disposition": "2.2.2",
+ "@tinyhttp/cookie": "2.1.1",
+ "@tinyhttp/cookie-signature": "2.1.1",
+ "@tinyhttp/encode-url": "2.1.1",
+ "@tinyhttp/req": "2.2.5",
+ "@tinyhttp/send": "2.2.3",
+ "@tinyhttp/vary": "^0.1.3",
+ "es-escape-html": "^0.1.1",
+ "mime": "4.0.4"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/router": {
+ "version": "2.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/router/-/router-2.2.3.tgz",
+ "integrity": "sha512-O0MQqWV3Vpg/uXsMYg19XsIgOhwjyhTYWh51Qng7bxqXixxx2PEvZWnFjP7c84K7kU/nUX41KpkEBTLnznk9/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/send": {
+ "version": "2.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/send/-/send-2.2.3.tgz",
+ "integrity": "sha512-o4cVHHGQ8WjVBS8UT0EE/2WnjoybrfXikHwsRoNlG1pfrC/Sd01u1N4Te8cOd/9aNGLr4mGxWb5qTm2RRtEi7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tinyhttp/content-type": "^0.1.4",
+ "@tinyhttp/etag": "2.1.2",
+ "mime": "4.0.4"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/type-is": {
+ "version": "2.2.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/type-is/-/type-is-2.2.4.tgz",
+ "integrity": "sha512-7F328NheridwjIfefBB2j1PEcKKABpADgv7aCJaE8x8EON77ZFrAkI3Rir7pGjopV7V9MBmW88xUQigBEX2rmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tinyhttp/content-type": "^0.1.4",
+ "mime": "4.0.4"
+ },
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/url": {
+ "version": "2.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/url/-/url-2.1.1.tgz",
+ "integrity": "sha512-POJeq2GQ5jI7Zrdmj22JqOijB5/GeX+LEX7DUdml1hUnGbJOTWDx7zf2b5cCERj7RoXL67zTgyzVblBJC+NJWg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/@tinyhttp/vary": {
+ "version": "0.1.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@tinyhttp/vary/-/vary-0.1.3.tgz",
+ "integrity": "sha512-SoL83sQXAGiHN1jm2VwLUWQSQeDAAl1ywOm6T0b0Cg1CZhVsjoiZadmjhxF6FHCCY7OHHVaLnTgSMxTPIDLxMg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ }
+ },
+ "node_modules/@turf/clone": {
+ "version": "7.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@turf/clone/-/clone-7.3.1.tgz",
+ "integrity": "sha512-r7xDOfw9ohA7PhZW+8X9RMsO4szB4YqkhEROaELJyLtQ1bo8VNFtndpZdE6YHQpD7Pjlvlb6i99q8w1QLisEPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@turf/helpers": "7.3.1",
+ "@types/geojson": "^7946.0.10",
+ "tslib": "^2.8.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
+ "node_modules/@turf/helpers": {
+ "version": "7.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@turf/helpers/-/helpers-7.3.1.tgz",
+ "integrity": "sha512-zkL34JVhi5XhsuMEO0MUTIIFEJ8yiW1InMu4hu/oRqamlY4mMoZql0viEmH6Dafh/p+zOl8OYvMJ3Vm3rFshgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "^7946.0.10",
+ "tslib": "^2.8.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
+ "node_modules/@turf/meta": {
+ "version": "7.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@turf/meta/-/meta-7.3.1.tgz",
+ "integrity": "sha512-NWsfOE5RVtWpLQNkfOF/RrYvLRPwwruxhZUV0UFIzHqfiRJ50aO9Y6uLY4bwCUe2TumLJQSR4yaoA72Rmr2mnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@turf/helpers": "7.3.1",
+ "@types/geojson": "^7946.0.10"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
+ "node_modules/@turf/projection": {
+ "version": "7.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@turf/projection/-/projection-7.3.1.tgz",
+ "integrity": "sha512-nDM3LG2j37B1tCpF4xL4rUBrQJcG585IRyDIxL2QEvP1LLv6dcm4fodw70HcGAj05Ux8bJr7IOXQXnobOJrlRA==",
+ "license": "MIT",
+ "dependencies": {
+ "@turf/clone": "7.3.1",
+ "@turf/helpers": "7.3.1",
+ "@turf/meta": "7.3.1",
+ "@types/geojson": "^7946.0.10",
+ "tslib": "^2.8.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson-vt": {
+ "version": "3.2.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
+ "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.7",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
+ "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
+ "license": "MIT",
+ "dependencies": {
+ "hoist-non-react-statics": "^3.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/leaflet": {
+ "version": "1.9.21",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/leaflet/-/leaflet-1.9.21.tgz",
+ "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/leaflet-draw": {
+ "version": "1.0.13",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/leaflet-draw/-/leaflet-draw-1.0.13.tgz",
+ "integrity": "sha512-YU82kilOaU+wPNbqKCCDfHH3hqepN6XilrBwG/mSeZ/z4ewumaRCOah44s3FMxSu/Aa0SVa3PPJvhIZDUA09mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/leaflet": "^1.9"
+ }
+ },
+ "node_modules/@types/mapbox__mapbox-gl-draw": {
+ "version": "1.4.9",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/mapbox__mapbox-gl-draw/-/mapbox__mapbox-gl-draw-1.4.9.tgz",
+ "integrity": "sha512-8OtRdlSFbF/NDKg3CYQZPbI41JUwRPHIOB1f3fc3AG0Wb7CecSuuUG5EG+IsCmTHyzF6cyKpcjR/jcv62U3w6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*",
+ "mapbox-gl": "*"
+ }
+ },
+ "node_modules/@types/mapbox__point-geometry": {
+ "version": "0.1.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
+ "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/node/-/node-24.10.1.tgz",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/pbf": {
+ "version": "3.0.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/pbf/-/pbf-3.0.5.tgz",
+ "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.7",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@types/react-redux": {
+ "version": "7.1.34",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/react-redux/-/react-redux-7.1.34.tgz",
+ "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hoist-non-react-statics": "^3.3.0",
+ "@types/react": "*",
+ "hoist-non-react-statics": "^3.3.0",
+ "redux": "^4.0.0"
+ }
+ },
+ "node_modules/@types/react-redux/node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/supercluster": {
+ "version": "7.1.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/@types/supercluster/-/supercluster-7.1.3.tgz",
+ "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz",
+ "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.48.0",
+ "@typescript-eslint/type-utils": "8.48.0",
+ "@typescript-eslint/utils": "8.48.0",
+ "@typescript-eslint/visitor-keys": "8.48.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.48.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/parser/-/parser-8.48.0.tgz",
+ "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.48.0",
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/typescript-estree": "8.48.0",
+ "@typescript-eslint/visitor-keys": "8.48.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/project-service/-/project-service-8.48.0.tgz",
+ "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.48.0",
+ "@typescript-eslint/types": "^8.48.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz",
+ "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/visitor-keys": "8.48.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz",
+ "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz",
+ "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/typescript-estree": "8.48.0",
+ "@typescript-eslint/utils": "8.48.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/types/-/types-8.48.0.tgz",
+ "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz",
+ "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.48.0",
+ "@typescript-eslint/tsconfig-utils": "8.48.0",
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/visitor-keys": "8.48.0",
+ "debug": "^4.3.4",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/utils/-/utils-8.48.0.tgz",
+ "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.48.0",
+ "@typescript-eslint/types": "8.48.0",
+ "@typescript-eslint/typescript-estree": "8.48.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz",
+ "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.48.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vis.gl/react-mapbox": {
+ "version": "8.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@vis.gl/react-mapbox/-/react-mapbox-8.1.0.tgz",
+ "integrity": "sha512-FwvH822oxEjWYOr+pP2L8hpv+7cZB2UsQbHHHT0ryrkvvqzmTgt7qHDhamv0EobKw86e1I+B4ojENdJ5G5BkyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "mapbox-gl": ">=3.5.0",
+ "react": ">=16.3.0",
+ "react-dom": ">=16.3.0"
+ },
+ "peerDependenciesMeta": {
+ "mapbox-gl": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vis.gl/react-maplibre": {
+ "version": "8.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@vis.gl/react-maplibre/-/react-maplibre-8.1.0.tgz",
+ "integrity": "sha512-PkAK/gp3mUfhCLhUuc+4gc3PN9zCtVGxTF2hB6R5R5yYUw+hdg84OZ770U5MU4tPMTCG6fbduExuIW6RRKN6qQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@maplibre/maplibre-gl-style-spec": "^19.2.1"
+ },
+ "peerDependencies": {
+ "maplibre-gl": ">=4.0.0",
+ "react": ">=16.3.0",
+ "react-dom": ">=16.3.0"
+ },
+ "peerDependenciesMeta": {
+ "maplibre-gl": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vis.gl/react-maplibre/node_modules/@maplibre/maplibre-gl-style-spec": {
+ "version": "19.3.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz",
+ "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==",
+ "license": "ISC",
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": "~2.0.2",
+ "@mapbox/unitbezier": "^0.0.1",
+ "json-stringify-pretty-compact": "^3.0.0",
+ "minimist": "^1.2.8",
+ "rw": "^1.3.3",
+ "sort-object": "^3.0.3"
+ },
+ "bin": {
+ "gl-style-format": "dist/gl-style-format.mjs",
+ "gl-style-migrate": "dist/gl-style-migrate.mjs",
+ "gl-style-validate": "dist/gl-style-validate.mjs"
+ }
+ },
+ "node_modules/@vis.gl/react-maplibre/node_modules/json-stringify-pretty-compact": {
+ "version": "3.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz",
+ "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
+ "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.47",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/arr-union": {
+ "version": "3.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/arr-union/-/arr-union-3.1.0.tgz",
+ "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/assign-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/assign-symbols/-/assign-symbols-1.0.0.tgz",
+ "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.31",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
+ "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/browserslist/-/browserslist-4.28.0.tgz",
+ "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.25",
+ "caniuse-lite": "^1.0.30001754",
+ "electron-to-chromium": "^1.5.249",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.1.4"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bytewise": {
+ "version": "1.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/bytewise/-/bytewise-1.1.0.tgz",
+ "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bytewise-core": "^1.2.2",
+ "typewise": "^1.0.3"
+ }
+ },
+ "node_modules/bytewise-core": {
+ "version": "1.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/bytewise-core/-/bytewise-core-1.2.3.tgz",
+ "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==",
+ "license": "MIT",
+ "dependencies": {
+ "typewise-core": "^1.2"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001757",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
+ "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/cheap-ruler": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/cheap-ruler/-/cheap-ruler-4.0.0.tgz",
+ "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/cookie/-/cookie-1.1.0.tgz",
+ "integrity": "sha512-vXiThu1/rlos7EGu8TuNZQEg2e9TvhH9dmS4T4ZVzB7Ao1agEZ6EG3sn5n+hZRYUgduISd1HpngFzAZiDGm5vQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csscolorparser": {
+ "version": "1.0.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/csscolorparser/-/csscolorparser-1.0.3.tgz",
+ "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.19",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/dayjs/-/dayjs-1.11.19.tgz",
+ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/dot-prop": {
+ "version": "9.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/dot-prop/-/dot-prop-9.0.0.tgz",
+ "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^4.18.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/earcut": {
+ "version": "3.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/earcut/-/earcut-3.0.2.tgz",
+ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
+ "license": "ISC"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.260",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz",
+ "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-escape-html": {
+ "version": "0.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/es-escape-html/-/es-escape-html-0.1.1.tgz",
+ "integrity": "sha512-yUx1o+8RsG7UlszmYPtks+dm6Lho2m8lgHMOsLJQsFI0R8XwUJwiMhM1M4E/S8QLeGyf6MkDV/pWgjQ0tdTSyQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.x"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/eslint/-/eslint-9.39.1.tgz",
+ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.1",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
+ "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.24",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
+ "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eta": {
+ "version": "3.5.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/eta/-/eta-3.5.0.tgz",
+ "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/eta-dev/eta?sponsor=1"
+ }
+ },
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/geojson-vt": {
+ "version": "4.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/geojson-vt/-/geojson-vt-4.0.2.tgz",
+ "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
+ "license": "ISC"
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-value": {
+ "version": "2.0.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/get-value/-/get-value-2.0.6.tgz",
+ "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/gl-matrix": {
+ "version": "3.4.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/gl-matrix/-/gl-matrix-3.4.4.tgz",
+ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
+ "license": "MIT"
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/grid-index": {
+ "version": "1.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/grid-index/-/grid-index-1.1.0.tgz",
+ "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/header-range-parser": {
+ "version": "1.1.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/header-range-parser/-/header-range-parser-1.1.3.tgz",
+ "integrity": "sha512-B9zCFt3jH8g09LR1vHL4pcAn8yMEtlSlOUdQemzHMRKMImNIhhszdeosYFfNW0WXKQtXIlWB+O4owHJKvEJYaA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.22.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/http-status-emojis": {
+ "version": "2.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/http-status-emojis/-/http-status-emojis-2.2.0.tgz",
+ "integrity": "sha512-ompKtgwpx8ff0hsbpIB7oE4ax1LXoHmftsHHStMELX56ivG3GhofTX8ZHWlUaFKfGjcGjw6G3rPk7dJRXMmbbg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immer": {
+ "version": "11.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/immer/-/immer-11.0.0.tgz",
+ "integrity": "sha512-XtRG4SINt4dpqlnJvs70O2j6hH7H0X8fUzFsjMn1rwnETaxwp83HLNimXBjZ78MrKl3/d3/pkzDH0o0Lkxm37Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflection": {
+ "version": "3.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/inflection/-/inflection-3.0.2.tgz",
+ "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
+ "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "license": "MIT"
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "license": "MIT",
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-server": {
+ "version": "1.0.0-beta.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/json-server/-/json-server-1.0.0-beta.3.tgz",
+ "integrity": "sha512-DwE69Ep5ccwIJZBUIWEENC30Yj8bwr4Ax9W9VoIWAYnB8Sj4ReptscO8/DRHv/nXwVlmb3Bk73Ls86+VZdYkkA==",
+ "dev": true,
+ "license": "SEE LICENSE IN ./LICENSE",
+ "dependencies": {
+ "@tinyhttp/app": "^2.4.0",
+ "@tinyhttp/cors": "^2.0.1",
+ "@tinyhttp/logger": "^2.0.0",
+ "chalk": "^5.3.0",
+ "chokidar": "^4.0.1",
+ "dot-prop": "^9.0.0",
+ "eta": "^3.5.0",
+ "inflection": "^3.0.0",
+ "json5": "^2.2.3",
+ "lowdb": "^7.0.1",
+ "milliparsec": "^4.0.0",
+ "sirv": "^2.0.4",
+ "sort-on": "^6.1.0"
+ },
+ "bin": {
+ "json-server": "lib/bin.js"
+ },
+ "engines": {
+ "node": ">=18.3"
+ }
+ },
+ "node_modules/json-server/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-pretty-compact": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
+ "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/kdbush": {
+ "version": "4.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/kdbush/-/kdbush-4.0.2.tgz",
+ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
+ "license": "ISC"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/leaflet-draw": {
+ "version": "1.0.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/leaflet-draw/-/leaflet-draw-1.0.4.tgz",
+ "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==",
+ "license": "MIT"
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lowdb": {
+ "version": "7.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/lowdb/-/lowdb-7.0.1.tgz",
+ "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "steno": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/mapbox-gl": {
+ "version": "3.17.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/mapbox-gl/-/mapbox-gl-3.17.0.tgz",
+ "integrity": "sha512-nCrDKRlr5di6xUksUDslNWwxroJ5yv1hT8pyVFtcpWJOOKsYQxF/wOFTMie8oxMnXeFkrz1Tl1TwA1XN1yX0KA==",
+ "devOptional": true,
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "workspaces": [
+ "src/style-spec",
+ "test/build/vite",
+ "test/build/webpack",
+ "test/build/typings"
+ ],
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+ "@mapbox/mapbox-gl-supported": "^3.0.0",
+ "@mapbox/point-geometry": "^1.1.0",
+ "@mapbox/tiny-sdf": "^2.0.6",
+ "@mapbox/unitbezier": "^0.0.1",
+ "@mapbox/vector-tile": "^2.0.4",
+ "@mapbox/whoots-js": "^3.1.0",
+ "@types/geojson": "^7946.0.16",
+ "@types/geojson-vt": "^3.2.5",
+ "@types/mapbox__point-geometry": "^0.1.4",
+ "@types/pbf": "^3.0.5",
+ "@types/supercluster": "^7.1.3",
+ "cheap-ruler": "^4.0.0",
+ "csscolorparser": "~1.0.3",
+ "earcut": "^3.0.1",
+ "geojson-vt": "^4.0.2",
+ "gl-matrix": "^3.4.4",
+ "grid-index": "^1.1.0",
+ "kdbush": "^4.0.2",
+ "martinez-polygon-clipping": "^0.7.4",
+ "murmurhash-js": "^1.0.0",
+ "pbf": "^4.0.1",
+ "potpack": "^2.0.0",
+ "quickselect": "^3.0.0",
+ "supercluster": "^8.0.1",
+ "tinyqueue": "^3.0.0"
+ }
+ },
+ "node_modules/maplibre-gl": {
+ "version": "5.14.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/maplibre-gl/-/maplibre-gl-5.14.0.tgz",
+ "integrity": "sha512-O2ok6N/bQ9NA9nJ22r/PRQQYkUe9JwfDMjBPkQ+8OwsVH4TpA5skIAM2wc0k+rni5lVbAVONVyBvgi1rF2vEPA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@mapbox/geojson-rewind": "^0.5.2",
+ "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+ "@mapbox/point-geometry": "^1.1.0",
+ "@mapbox/tiny-sdf": "^2.0.7",
+ "@mapbox/unitbezier": "^0.0.1",
+ "@mapbox/vector-tile": "^2.0.4",
+ "@mapbox/whoots-js": "^3.1.0",
+ "@maplibre/maplibre-gl-style-spec": "^24.3.1",
+ "@maplibre/mlt": "^1.1.2",
+ "@maplibre/vt-pbf": "^4.1.0",
+ "@types/geojson": "^7946.0.16",
+ "@types/geojson-vt": "3.2.5",
+ "@types/supercluster": "^7.1.3",
+ "earcut": "^3.0.2",
+ "geojson-vt": "^4.0.2",
+ "gl-matrix": "^3.4.4",
+ "kdbush": "^4.0.2",
+ "murmurhash-js": "^1.0.0",
+ "pbf": "^4.0.1",
+ "potpack": "^2.1.0",
+ "quickselect": "^3.0.0",
+ "supercluster": "^8.0.1",
+ "tinyqueue": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.14.0",
+ "npm": ">=8.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
+ }
+ },
+ "node_modules/martinez-polygon-clipping": {
+ "version": "0.7.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/martinez-polygon-clipping/-/martinez-polygon-clipping-0.7.4.tgz",
+ "integrity": "sha512-jBEwrKtA0jTagUZj2bnmb4Yg2s4KnJGRePStgI7bAVjtcipKiF39R4LZ2V/UT61jMYWrTcBhPazexeqd6JAVtw==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "robust-predicates": "^2.0.4",
+ "splaytree": "^0.1.4",
+ "tinyqueue": "^1.2.0"
+ }
+ },
+ "node_modules/martinez-polygon-clipping/node_modules/tinyqueue": {
+ "version": "1.2.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/tinyqueue/-/tinyqueue-1.2.3.tgz",
+ "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==",
+ "devOptional": true,
+ "license": "ISC"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/milliparsec": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/milliparsec/-/milliparsec-4.0.0.tgz",
+ "integrity": "sha512-/wk9d4Z6/9ZvoEH/6BI4TrTCgmkpZPuSRN/6fI9aUHOfXdNTuj/VhLS7d+NqG26bi6L9YmGXutVYvWC8zQ0qtA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/mime": {
+ "version": "4.0.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/mime/-/mime-4.0.4.tgz",
+ "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa"
+ ],
+ "license": "MIT",
+ "bin": {
+ "mime": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/murmurhash-js": {
+ "version": "1.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
+ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/negotiator/-/negotiator-0.6.4.tgz",
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pbf": {
+ "version": "4.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/pbf/-/pbf-4.0.1.tgz",
+ "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "resolve-protobuf-schema": "^2.1.0"
+ },
+ "bin": {
+ "pbf": "bin/pbf"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/potpack": {
+ "version": "2.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/potpack/-/potpack-2.1.0.tgz",
+ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
+ "license": "ISC"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/protocol-buffers-schema": {
+ "version": "3.6.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
+ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/quickselect": {
+ "version": "3.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/quickselect/-/quickselect-3.0.0.tgz",
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
+ "license": "ISC"
+ },
+ "node_modules/react": {
+ "version": "19.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react/-/react-19.2.1.tgz",
+ "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-datepicker": {
+ "version": "8.10.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-datepicker/-/react-datepicker-8.10.0.tgz",
+ "integrity": "sha512-JIXuA+g+qP3c4MVJpx24o7n1gnv3WV/8A/D6964HucY1FlSEc30+ITPNUfbKZXYHl5rruCtxYCwi2lzn7gaz7g==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react": "^0.27.15",
+ "clsx": "^2.1.1",
+ "date-fns": "^4.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-dom/-/react-dom-19.2.1.tgz",
+ "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.1"
+ }
+ },
+ "node_modules/react-icons": {
+ "version": "5.5.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "19.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-is/-/react-is-19.2.0.tgz",
+ "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==",
+ "license": "MIT"
+ },
+ "node_modules/react-leaflet": {
+ "version": "5.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-leaflet/-/react-leaflet-5.0.0.tgz",
+ "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
+ "license": "Hippocratic-2.1",
+ "dependencies": {
+ "@react-leaflet/core": "^3.0.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
+ "node_modules/react-map-gl": {
+ "version": "8.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-map-gl/-/react-map-gl-8.1.0.tgz",
+ "integrity": "sha512-vDx/QXR3Tb+8/ap/z6gdMjJQ8ZEyaZf6+uMSPz7jhWF5VZeIsKsGfPvwHVPPwGF43Ryn+YR4bd09uEFNR5OPdg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vis.gl/react-mapbox": "8.1.0",
+ "@vis.gl/react-maplibre": "8.1.0"
+ },
+ "peerDependencies": {
+ "mapbox-gl": ">=1.13.0",
+ "maplibre-gl": ">=1.13.0",
+ "react": ">=16.3.0",
+ "react-dom": ">=16.3.0"
+ },
+ "peerDependenciesMeta": {
+ "mapbox-gl": {
+ "optional": true
+ },
+ "maplibre-gl": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.9.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-router/-/react-router-7.9.6.tgz",
+ "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.9.6",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-router-dom/-/react-router-dom-7.9.6.tgz",
+ "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.9.6"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-select": {
+ "version": "5.10.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-select/-/react-select-5.10.2.tgz",
+ "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.0",
+ "@emotion/cache": "^11.4.0",
+ "@emotion/react": "^11.8.1",
+ "@floating-ui/dom": "^1.0.1",
+ "@types/react-transition-group": "^4.4.0",
+ "memoize-one": "^6.0.0",
+ "prop-types": "^15.6.0",
+ "react-transition-group": "^4.3.0",
+ "use-isomorphic-layout-effect": "^1.2.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-toastify": {
+ "version": "11.0.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-toastify/-/react-toastify-11.0.5.tgz",
+ "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19",
+ "react-dom": "^18 || ^19"
+ }
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/regexparam": {
+ "version": "2.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/regexparam/-/regexparam-2.0.2.tgz",
+ "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-protobuf-schema": {
+ "version": "2.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "protocol-buffers-schema": "^3.3.1"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "2.0.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/robust-predicates/-/robust-predicates-2.0.4.tgz",
+ "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==",
+ "devOptional": true,
+ "license": "Unlicense"
+ },
+ "node_modules/rollup": {
+ "version": "4.53.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/set-value": {
+ "version": "2.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "is-extendable": "^0.1.1",
+ "is-plain-object": "^2.0.3",
+ "split-string": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sirv": {
+ "version": "2.0.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/sirv/-/sirv-2.0.4.tgz",
+ "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/sort-asc": {
+ "version": "0.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/sort-asc/-/sort-asc-0.2.0.tgz",
+ "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sort-desc": {
+ "version": "0.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/sort-desc/-/sort-desc-0.2.0.tgz",
+ "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sort-object": {
+ "version": "3.0.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/sort-object/-/sort-object-3.0.3.tgz",
+ "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bytewise": "^1.1.0",
+ "get-value": "^2.0.2",
+ "is-extendable": "^0.1.1",
+ "sort-asc": "^0.2.0",
+ "sort-desc": "^0.2.0",
+ "union-value": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sort-on": {
+ "version": "6.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/sort-on/-/sort-on-6.1.1.tgz",
+ "integrity": "sha512-PB8pVvXAoRBijBCvuKJnmo06D8mSnQlLij0abfB2VdOpfFm29sPGYD4ft2prUPo1AZXTnkn3pP48AppRWyMkrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dot-prop": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/splaytree": {
+ "version": "0.1.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/splaytree/-/splaytree-0.1.4.tgz",
+ "integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/split-string": {
+ "version": "3.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/split-string/-/split-string-3.1.0.tgz",
+ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split-string/node_modules/extend-shallow": {
+ "version": "3.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/extend-shallow/-/extend-shallow-3.0.2.tgz",
+ "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "assign-symbols": "^1.0.0",
+ "is-extendable": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split-string/node_modules/is-extendable": {
+ "version": "1.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/is-extendable/-/is-extendable-1.0.1.tgz",
+ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^2.0.4"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/steno": {
+ "version": "4.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/steno/-/steno-4.0.2.tgz",
+ "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/typicode"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
+ "license": "MIT"
+ },
+ "node_modules/supercluster": {
+ "version": "8.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/supercluster/-/supercluster-8.0.1.tgz",
+ "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
+ "license": "ISC",
+ "dependencies": {
+ "kdbush": "^4.0.2"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tabbable": {
+ "version": "6.3.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/tabbable/-/tabbable-6.3.0.tgz",
+ "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinyqueue": {
+ "version": "3.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm/tinyqueue/-/tinyqueue-3.0.0.tgz",
+ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
+ "license": "ISC"
+ },
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.48.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/typescript-eslint/-/typescript-eslint-8.48.0.tgz",
+ "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.48.0",
+ "@typescript-eslint/parser": "8.48.0",
+ "@typescript-eslint/typescript-estree": "8.48.0",
+ "@typescript-eslint/utils": "8.48.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/typewise": {
+ "version": "1.0.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/typewise/-/typewise-1.0.3.tgz",
+ "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "typewise-core": "^1.2.0"
+ }
+ },
+ "node_modules/typewise-core": {
+ "version": "1.2.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/typewise-core/-/typewise-core-1.2.0.tgz",
+ "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==",
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/union-value": {
+ "version": "1.0.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "arr-union": "^3.1.0",
+ "get-value": "^2.0.6",
+ "is-extendable": "^0.1.1",
+ "set-value": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.2.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
+ "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.2.4",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/vite/-/vite-7.2.4.tgz",
+ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/wgs84": {
+ "version": "0.0.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/wgs84/-/wgs84-0.0.0.tgz",
+ "integrity": "sha512-ANHlY4Rb5kHw40D0NJ6moaVfOCMrp9Gpd1R/AIQYg2ko4/jzcJ+TVXYYF6kXJqQwITvEZP4yEthjM7U6rYlljQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.1.13",
+ "resolved": "https://infyartifactory.jfrog.io/artifactory/api/npm/npm-remote/zod/-/zod-4.1.13.tgz",
+ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/frontend/mobile-ui/package.json b/frontend/mobile-ui/package.json
new file mode 100644
index 0000000..433057a
--- /dev/null
+++ b/frontend/mobile-ui/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "egov-digit",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
+ "@mapbox/mapbox-gl-draw": "^1.5.1",
+ "@mui/icons-material": "^7.3.5",
+ "@mui/lab": "^7.0.1-beta.18",
+ "@mui/material": "^7.3.6",
+ "@mui/x-date-pickers": "^8.16.0",
+ "@reduxjs/toolkit": "^2.10.1",
+ "@types/leaflet": "^1.9.21",
+ "@types/react-redux": "^7.1.34",
+ "axios": "^1.12.2",
+ "clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
+ "dayjs": "^1.11.19",
+ "dotenv": "^17.2.3",
+ "leaflet": "^1.9.4",
+ "leaflet-draw": "^1.0.4",
+ "maplibre-gl": "^5.14.0",
+ "react": "^19.2.1",
+ "react-datepicker": "^8.8.0",
+ "react-dom": "^19.2.1",
+ "react-icons": "^5.5.0",
+ "react-leaflet": "^5.0.0",
+ "react-map-gl": "^8.1.0",
+ "react-redux": "^9.2.0",
+ "react-router-dom": "^7.9.4",
+ "react-select": "^5.10.2",
+ "react-toastify": "^11.0.5",
+ "zod": "^4.1.12"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/leaflet-draw": "^1.0.13",
+ "@types/mapbox__mapbox-gl-draw": "^1.4.9",
+ "@types/node": "^24.6.0",
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@vitejs/plugin-react": "^5.0.4",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.22",
+ "globals": "^16.4.0",
+ "json-server": "^1.0.0-beta.3",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.45.0",
+ "vite": "^7.2.4"
+ },
+ "overrides": {
+ "js-yaml": "^4.1.1"
+ }
+}
diff --git a/frontend/mobile-ui/public/favicon.svg b/frontend/mobile-ui/public/favicon.svg
new file mode 100644
index 0000000..90c8a16
--- /dev/null
+++ b/frontend/mobile-ui/public/favicon.svg
@@ -0,0 +1,4 @@
+
+
+ D
+
diff --git a/frontend/mobile-ui/public/manifest.json b/frontend/mobile-ui/public/manifest.json
new file mode 100644
index 0000000..9cede25
--- /dev/null
+++ b/frontend/mobile-ui/public/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "Property Form Mobile App",
+ "short_name": "PropertyForm",
+ "description": "Mobile application for property form management",
+ "start_url": "/",
+ "display": "standalone",
+ "theme_color": "#d2691e",
+ "background_color": "#f9f1f0",
+ "orientation": "portrait-primary",
+ "scope": "/",
+ "categories": ["business", "productivity"],
+ "icons": [
+ {
+ "src": "/vite.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "any maskable"
+ }
+ ],
+ "prefer_related_applications": false,
+ "screenshots": [
+ {
+ "src": "/screenshot-mobile.png",
+ "sizes": "390x844",
+ "type": "image/png",
+ "form_factor": "narrow"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/public/vite.svg b/frontend/mobile-ui/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/frontend/mobile-ui/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/App.css b/frontend/mobile-ui/src/App.css
new file mode 100644
index 0000000..ad40863
--- /dev/null
+++ b/frontend/mobile-ui/src/App.css
@@ -0,0 +1,17 @@
+
+
+#root {
+ width: 100%;
+ min-height: 100vh;
+ margin: 0;
+ padding: 0;
+ /* Remove text-align: center and min-width: 25% */
+}
+
+/* Add this to ensure forms display properly */
+.App {
+ width: 100%;
+ min-height: 100vh;
+ margin: 0;
+ padding: 0;
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/App.tsx b/frontend/mobile-ui/src/App.tsx
new file mode 100644
index 0000000..4bfe416
--- /dev/null
+++ b/frontend/mobile-ui/src/App.tsx
@@ -0,0 +1,35 @@
+// Main entry point for the React app
+import React from 'react';
+import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-dom';
+import './styles/globals.css';
+import Providers from './Providers';
+import 'react-toastify/dist/ReactToastify.css';
+
+import CitizenRoutes from './app/routes/CitizenRoutes';
+import AgentRoutes from './app/routes/AgentRoutes';
+import { FormRoutes } from './app/routes/FormRoutes';
+import CommonRoutes from './app/routes/CommonRoutes';
+
+// App component sets up global providers and all main routes
+const App: React.FC = () => {
+ return (
+
+ {/* Set up React Router for all app routes */}
+
+
+ {/* Redirect root to landing page */}
+ } />
+ {/* Citizen user routes */}
+ } />
+ {/* Property form routes */}
+ } />
+ {/* Agent user routes */}
+ } />
+ {/* Common routes for all users (fallback) */}
+ } />
+
+
+
+ );
+};
+export default App;
diff --git a/frontend/mobile-ui/src/Providers.tsx b/frontend/mobile-ui/src/Providers.tsx
new file mode 100644
index 0000000..a7c0bfe
--- /dev/null
+++ b/frontend/mobile-ui/src/Providers.tsx
@@ -0,0 +1,41 @@
+// Global providers wrapper for all React context and localization
+import React from 'react';
+import { LanguageProvider } from './context/LanguageContext';
+import { SignUpFormProvider } from './context/SignUpFormContext';
+import { PropertyApplicationsProvider } from './context/PropertyApplicationsContext';
+import { PropertyFormProvider } from './context/PropertyFormContext';
+import { FormModeProvider } from './context/FormModeContext';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { LocalizationProvider as LangProvider } from './services/Citizen/Localization/LocalizationContext';
+import { useAuth } from './context/AuthProvider';
+
+// Props for Providers component (children to render)
+interface ProvidersProps {
+ children: React.ReactNode;
+}
+
+// Providers component wraps the app with all required context providers
+// Includes language, form, property, and localization providers
+const Providers: React.FC = ({ children }) => {
+ // Get user role from Auth context (used for localization)
+ const { role } = useAuth();
+ return (
+
+
+
+
+
+ {/* MUI date localization and app language localization */}
+
+ {children}
+
+
+
+
+
+
+ );
+};
+
+export default Providers;
diff --git a/frontend/mobile-ui/src/app/assets/Agent/Apple_Maps.svg b/frontend/mobile-ui/src/app/assets/Agent/Apple_Maps.svg
new file mode 100644
index 0000000..9175450
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/Apple_Maps.svg
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/PropertyMarker.svg b/frontend/mobile-ui/src/app/assets/Agent/PropertyMarker.svg
new file mode 100644
index 0000000..3c656cb
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/PropertyMarker.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/Rectangle_100.svg b/frontend/mobile-ui/src/app/assets/Agent/Rectangle_100.svg
new file mode 100644
index 0000000..d31f765
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/Rectangle_100.svg
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/account_circle.svg b/frontend/mobile-ui/src/app/assets/Agent/account_circle.svg
new file mode 100644
index 0000000..9dec96e
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/account_circle.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/add_a_photo.svg b/frontend/mobile-ui/src/app/assets/Agent/add_a_photo.svg
new file mode 100644
index 0000000..8b294df
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/add_a_photo.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/camera.svg b/frontend/mobile-ui/src/app/assets/Agent/camera.svg
new file mode 100644
index 0000000..e0fd14a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/camera.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/date_range.svg b/frontend/mobile-ui/src/app/assets/Agent/date_range.svg
new file mode 100644
index 0000000..728bd68
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/date_range.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/delete.svg b/frontend/mobile-ui/src/app/assets/Agent/delete.svg
new file mode 100644
index 0000000..a24559c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/delete.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/edit_square.svg b/frontend/mobile-ui/src/app/assets/Agent/edit_square.svg
new file mode 100644
index 0000000..8199b7a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/edit_square.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/info-icon.svg b/frontend/mobile-ui/src/app/assets/Agent/info-icon.svg
new file mode 100644
index 0000000..a002a27
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/info-icon.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/info_outline.svg b/frontend/mobile-ui/src/app/assets/Agent/info_outline.svg
new file mode 100644
index 0000000..d58a76a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/info_outline.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/notification_icon.svg b/frontend/mobile-ui/src/app/assets/Agent/notification_icon.svg
new file mode 100644
index 0000000..04dae71
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/notification_icon.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/notifications.svg b/frontend/mobile-ui/src/app/assets/Agent/notifications.svg
new file mode 100644
index 0000000..90f16d7
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/notifications.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/open_street_maps.svg b/frontend/mobile-ui/src/app/assets/Agent/open_street_maps.svg
new file mode 100644
index 0000000..84de19f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/open_street_maps.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Agent/undo.svg b/frontend/mobile-ui/src/app/assets/Agent/undo.svg
new file mode 100644
index 0000000..8e3b437
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Agent/undo.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Citizen/home_page/property_pointer_dark.svg b/frontend/mobile-ui/src/app/assets/Citizen/home_page/property_pointer_dark.svg
new file mode 100644
index 0000000..7d0889f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Citizen/home_page/property_pointer_dark.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Citizen/home_page/property_pointer_light.svg b/frontend/mobile-ui/src/app/assets/Citizen/home_page/property_pointer_light.svg
new file mode 100644
index 0000000..4f4bde4
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Citizen/home_page/property_pointer_light.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Citizen/home_page/translate_indic.svg b/frontend/mobile-ui/src/app/assets/Citizen/home_page/translate_indic.svg
new file mode 100644
index 0000000..29820c4
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Citizen/home_page/translate_indic.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Citizen/mycity_page/locateProperty.svg b/frontend/mobile-ui/src/app/assets/Citizen/mycity_page/locateProperty.svg
new file mode 100644
index 0000000..7d44e3e
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Citizen/mycity_page/locateProperty.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Citizen/property_page/activity_zone.svg b/frontend/mobile-ui/src/app/assets/Citizen/property_page/activity_zone.svg
new file mode 100644
index 0000000..f2ca255
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Citizen/property_page/activity_zone.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Citizen/property_page/location.svg b/frontend/mobile-ui/src/app/assets/Citizen/property_page/location.svg
new file mode 100644
index 0000000..7d64903
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/Citizen/property_page/location.svg
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/Citizen/under_construction/tenor.gif b/frontend/mobile-ui/src/app/assets/Citizen/under_construction/tenor.gif
new file mode 100644
index 0000000..2a15c0e
Binary files /dev/null and b/frontend/mobile-ui/src/app/assets/Citizen/under_construction/tenor.gif differ
diff --git a/frontend/mobile-ui/src/app/assets/PropertyMarker.svg b/frontend/mobile-ui/src/app/assets/PropertyMarker.svg
new file mode 100644
index 0000000..c2ead8f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/PropertyMarker.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/activity_zone.svg b/frontend/mobile-ui/src/app/assets/activity_zone.svg
new file mode 100644
index 0000000..8de0fd2
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/activity_zone.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/edit_square.svg b/frontend/mobile-ui/src/app/assets/edit_square.svg
new file mode 100644
index 0000000..7e1dd2f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/edit_square.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/assets/propertyTaxLogo.svg b/frontend/mobile-ui/src/app/assets/propertyTaxLogo.svg
new file mode 100644
index 0000000..1555055
--- /dev/null
+++ b/frontend/mobile-ui/src/app/assets/propertyTaxLogo.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/mobile-ui/src/app/components/BottomBar.tsx b/frontend/mobile-ui/src/app/components/BottomBar.tsx
new file mode 100644
index 0000000..6ac1537
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/BottomBar.tsx
@@ -0,0 +1,108 @@
+// BottomBar is a shared navigation component for the Citizen interface.
+// It provides a fixed bottom navigation bar with quick access to Home, Utilities, Properties, and My City.
+// The component uses localization and dynamic icons to reflect the current route and language.
+// Used in both mobile and desktop views for consistent navigation.
+
+import { useLocation, useNavigate } from 'react-router-dom';
+import HomeIcon from '@mui/icons-material/HomeOutlined';
+import HomeIconFilled from '@mui/icons-material/Home';
+import OfflineBoltIcon from '@mui/icons-material/OfflineBolt';
+import OfflineBoltOutlinedIcon from '@mui/icons-material/OfflineBoltOutlined';
+import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined';
+import ApartmentIcon from '@mui/icons-material/Apartment';
+import CottageOutlinedIcon from '@mui/icons-material/CottageOutlined';
+import CottageIcon from '@mui/icons-material/Cottage';
+import { BottomNavigation, BottomNavigationAction, Paper } from '@mui/material';
+import { useAppSelector } from '../../redux/Hooks';
+import { getMessagesFromSession, useLocalization } from '../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from './Loader';
+
+
+export function BottomBar() {
+ // Get the current language for citizen from Redux store
+ const lang = useAppSelector((state) => state.lang.citizenLang);
+ // Get localization loading state
+ const { loading } = useLocalization();
+ // Get localized messages for the citizen module
+ const messages = getMessagesFromSession("CITIZEN")!;
+
+ // Show loader while localization messages are loading
+ if (loading) {
+ return ;
+ }
+
+ // Navigation configuration for the bottom bar (desired order)
+ const navConfig = [
+ {
+ label: messages['citizen.commons'][lang]['home-btn'],
+ icon: ,
+ iconFilled: ,
+ path: '/citizen',
+ },
+ {
+ label: messages['citizen.commons'][lang]['my-city-btn'],
+ icon: ,
+ iconFilled: ,
+ path: '/under-construction',
+ },
+ {
+ label: messages['citizen.commons'][lang]['properties-btn'],
+ icon: ,
+ iconFilled: ,
+ path: '/citizen/properties',
+ },
+ {
+ label: messages['citizen.commons'][lang]['utility-btn'],
+ icon: ,
+ iconFilled: ,
+ path: '/under-construction',
+ },
+ ];
+
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ // Find the current nav index by matching the most specific path first
+ const matchIndex = [...navConfig]
+ .map((cfg, idx) => ({ idx, len: cfg.path.length, match: location.pathname.startsWith(cfg.path) }))
+ .filter(x => x.match)
+ .sort((a, b) => b.len - a.len)[0]?.idx ?? 0;
+
+ return (
+
+ navigate(navConfig[newValue].path)}
+ >
+ {navConfig.map((cfg, idx) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/Header.tsx b/frontend/mobile-ui/src/app/components/Header.tsx
new file mode 100644
index 0000000..ece792a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/Header.tsx
@@ -0,0 +1,53 @@
+
+// Header is a reusable, styled top bar component for displaying a page or section title.
+// It accepts a main header, an optional sub-header, and an optional icon.
+// Used across both Agent and Citizen screens to provide consistent page headings.
+import { Box, Typography } from "@mui/material";
+import type { FC } from "react";
+import type { HeaderProps } from "../models/Header.model";
+
+
+const Header: FC = ({
+ header, // Main title text to display
+ subHeader, // Optional subtitle or description
+ icon, // Optional icon to display left of the title
+}) => {
+ return (
+ // Outer Box: fixed at the top, styled background, full width
+
+ {/* Inner Box: aligns icon and text horizontally */}
+
+ {icon}
+
+ {/* Main header text */}
+
+ {header}
+
+ {/* Sub-header/description text */}
+
+ {subHeader}
+
+
+
+
+ );
+};
+
+
+export default Header;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/Loader.tsx b/frontend/mobile-ui/src/app/components/Loader.tsx
new file mode 100644
index 0000000..eb80a82
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/Loader.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { Box, styled, keyframes, Typography } from '@mui/material';
+
+// Keyframes for the wave animation
+const wave = keyframes`
+ 0%, 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-20px);
+ }
+`;
+
+// Styled component for each dot
+const Dot = styled(Box)(({ theme }) => ({
+ width: '16px',
+ height: '16px',
+ borderRadius: '50%',
+ backgroundColor: theme.palette.primary.main,
+ animation: `${wave} 1.2s ease-in-out infinite`,
+ '&:nth-of-type(2)': {
+ animationDelay: '0.2s',
+ },
+ '&:nth-of-type(3)': {
+ animationDelay: '0.4s',
+ },
+}));
+
+// Container for the dots
+const LoaderContainer = styled(Box)({
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: '24px',
+ height: '100vh',
+ backgroundColor: '#f5f5f5',
+});
+
+interface LoadingPageProps {
+ message?: string;
+}
+
+const LoadingPage: React.FC = ({ message = 'Double-checking everything for youโฆ' }) => {
+ return (
+
+
+
+
+
+
+
+ {message}
+
+
+ );
+};
+
+export default LoadingPage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/NavBar.tsx b/frontend/mobile-ui/src/app/components/NavBar.tsx
new file mode 100644
index 0000000..e3cb51c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/NavBar.tsx
@@ -0,0 +1,68 @@
+
+// NavBar is a reusable top navigation bar component specifically for the Citizen interface.
+// It provides quick access to navigation actions: going back to the previous page and returning to the citizen home screen.
+// The component uses localization for button labels and adapts to the current language.
+// Used across multiple Citizen screens for consistent navigation and user experience.
+import { Box } from '@mui/material';
+import type { FC } from 'react';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
+import { useNavigate } from 'react-router-dom';
+import { useAppSelector } from '../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../services/Citizen/Localization/LocalizationContext';
+import NavButton from './NavButton';
+import LoadingPage from './Loader';
+
+
+const NavBar: FC = () => {
+ const navigate = useNavigate(); // React Router navigation hook
+ const lang = useAppSelector((state) => state.lang.citizenLang); // Get current citizen language
+ const { loading } = useLocalization(); // Localization loading state
+ const messages = getMessagesFromSession('CITIZEN')!; // Localized messages for citizen module
+
+ // Show loader while localization messages are loading
+ if (loading) {
+ return ;
+ }
+
+ return (
+ // Outer Box: fixed at the top, styled background, horizontal layout
+
+ {/* Previous button: navigates to previous page */}
+ }
+ message={messages['citizen.commons'][lang]['prev-btn']}
+ onClick={() => navigate(-1)}
+ />
+ {/* Home button: navigates to citizen home page */}
+ }
+ message={messages['citizen.commons'][lang]['home-btn']}
+ onClick={() => navigate('/citizen', { replace: true })}
+ />
+
+ );
+};
+
+export default NavBar;
diff --git a/frontend/mobile-ui/src/app/components/NavButton.tsx b/frontend/mobile-ui/src/app/components/NavButton.tsx
new file mode 100644
index 0000000..8b01500
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/NavButton.tsx
@@ -0,0 +1,41 @@
+// NavButton is a reusable button component for navigation actions.
+// It displays an icon and a label, and triggers a callback when clicked.
+// Used in navigation bars and other places where a styled navigation button is needed (e.g., Previous, Home).
+// Accepts icon, message, and onClick handler as props.
+import { Box, Typography } from "@mui/material";
+import type { FC } from "react";
+import type { NavButtonProps } from "../models/NavButton.model";
+
+
+const NavButton: FC = ({ icon, message, onClick }) => {
+ return (
+ // Outer Box: styled container for icon and label
+
+ {/* Icon displayed on the left */}
+ {icon}
+ {/* Navigation label/message */}
+
+ {message}
+
+
+ );
+}
+
+export default NavButton;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/NavHeader.tsx b/frontend/mobile-ui/src/app/components/NavHeader.tsx
new file mode 100644
index 0000000..ed7fef7
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/NavHeader.tsx
@@ -0,0 +1,78 @@
+
+// NavHeader is a reusable top header component for both Citizen and Agent interfaces.
+// It displays a fixed header bar with a styled 'Previous' button, localized label, and icon.
+// The label and localization are determined by the role prop (CITIZEN or AGENT).
+// Used for consistent navigation and UI across screens that require a back/previous action.
+import { type FC } from 'react';
+import { Box, Typography, Button } from '@mui/material';
+import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
+import { useNavigate } from 'react-router-dom';
+import { getAppLocale } from '../../services/Profile/ProfileService';
+import { getMessagesFromSession } from '../../services/Citizen/Localization/LocalizationContext';
+
+
+const NavHeader: FC<{role : string}> = ({role}) => {
+ // Get current language from profile service
+ const lang = getAppLocale();
+ // Get localized messages for the given role (CITIZEN or AGENT)
+ const messages = getMessagesFromSession(role === "CITIZEN" ? "CITIZEN" : "AGENT")!;
+ // React Router navigation hook
+ const navigate = useNavigate();
+
+ return (
+ // Outer Box: fixed header bar, styled background
+
+ {/* Previous button: navigates to previous page, styled as per design */}
+ navigate(-1)}
+ disableElevation
+ sx={{
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ bgcolor: '#fff',
+ border: '2px solid #eedcd2',
+ boxSizing: 'border-box',
+ borderRadius: '20px',
+ width: "160px",
+ height: '50px',
+ boxShadow: 'none',
+ textTransform: 'none',
+ p: 0,
+ '&:hover': {
+ bgcolor: '#fff',
+ border: '2px solid #f3e6df',
+ boxShadow: 'none',
+ },
+ }}
+ >
+ {/* Chevron icon for previous/back action */}
+
+ {/* Localized label for previous/back, based on role */}
+
+ {(role === "CITIZEN" ? messages['citizen.commons'][lang]['prev-btn'] : messages['common'][lang]['previous.btn']) ?? "Previous"}
+
+
+
+ );
+};
+
+export default NavHeader;
diff --git a/frontend/mobile-ui/src/app/components/NotFoundPage.tsx b/frontend/mobile-ui/src/app/components/NotFoundPage.tsx
new file mode 100644
index 0000000..a563099
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/NotFoundPage.tsx
@@ -0,0 +1,105 @@
+
+// NotFoundPage component displays a 404 error page for undefined routes
+import React from "react";
+import { Box, Button, Container, Typography } from "@mui/material";
+import { useNavigate } from "react-router-dom";
+
+
+const NotFoundPage: React.FC = () => {
+ // Hook to programmatically navigate to other routes
+ const navigate = useNavigate();
+
+ return (
+ // Outer Box for full viewport height and centering
+
+ {/* Container to limit max width for content */}
+
+ {/* Centered content box with vertical padding */}
+
+ {/* Circular border with 404 text */}
+
+
+ 404
+
+
+
+ {/* Main heading for not found */}
+
+ Page not found
+
+
+ {/* Description message */}
+
+ The page you are looking for doesn't exist or may have been
+ moved. Please check the URL or return to the Property Tax Portal.
+
+
+ {/* Button to navigate back to the portal home */}
+ navigate("/")}
+ sx={{
+ px: { xs: 3, sm: 4 },
+ py: { xs: 1, sm: 1.2 },
+ fontSize: { xs: "0.9rem", sm: "1rem" },
+ }}
+ >
+ Go to Portal
+
+
+
+
+ );
+};
+
+// Export the NotFoundPage component as default
+export default NotFoundPage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/NotificationCard.tsx b/frontend/mobile-ui/src/app/components/NotificationCard.tsx
new file mode 100644
index 0000000..fe4e425
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/NotificationCard.tsx
@@ -0,0 +1,233 @@
+// NotificationSettings is a reusable settings card for notification preferences.
+// It displays notification categories (different for Citizen and Agent) and allows users to select delivery methods (SMS, WhatsApp, Email).
+// The component uses localization for all labels and adapts to the current language and role.
+// Used in profile/settings screens for both Citizen and Agent interfaces.
+import React, { useState } from 'react';
+import {
+ Box,
+ Typography,
+ Switch,
+ FormControlLabel,
+ Paper,
+ Checkbox,
+} from '@mui/material';
+import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
+import { getAppLocale } from '../../services/Profile/ProfileService';
+import { getMessagesFromSession } from '../../services/Citizen/Localization/LocalizationContext';
+import { COLORS } from '../models/Colors.const';
+
+export const NotificationSettings: React.FC<{ isCitizenProfile: boolean }> = ({
+ isCitizenProfile,
+}) => {
+ // State to track which delivery methods are selected
+ const [deliveryMethods, setDeliveryMethods] = useState({
+ sms: false,
+ whatsapp: false,
+ email: false,
+ });
+
+ // Get current language and localized messages for the role
+ const lang = getAppLocale();
+ const messages = getMessagesFromSession(isCitizenProfile ? 'CITIZEN' : 'AGENT')!;
+
+ // Handler to update delivery method selection
+ const handleCheckboxChange = (event: React.ChangeEvent) => {
+ const { name, checked } = event.target;
+ setDeliveryMethods((prev) => ({
+ ...prev,
+ [name]: checked,
+ }));
+ if (checked) {
+ console.log(`${name} checkbox checked`);
+ }
+ };
+
+ const notificationCategories = isCitizenProfile
+ ? [
+ {
+ key: 'bill-reminders',
+ title: messages['profile'][lang]['bill-reminders'],
+ desc: messages['profile'][lang]['bill-reminders-desc'],
+ },
+ {
+ key: 'license-expiry',
+ title: messages['profile'][lang]['license-expiry'],
+ desc: messages['profile'][lang]['license-expiry-desc'],
+ },
+ {
+ key: 'service-updates',
+ title: messages['profile'][lang]['service-updates'],
+ desc: messages['profile'][lang]['service-updates-desc'],
+ },
+ {
+ key: 'community-news',
+ title: messages['profile'][lang]['community-news'],
+ desc: messages['profile'][lang]['community-news-desc'],
+ },
+ ]
+ : [
+ {
+ key: 'application-updates',
+ title: 'application-updates',
+ desc: 'application-updates-desc',
+ },
+ {
+ key: 'appointment-scheduling',
+ title: 'appointment-scheduling',
+ desc: 'appointment-scheduling-desc',
+ },
+ ];
+
+ // Initialize toggles state with all categories enabled
+ const [toggles, setToggles] = useState(
+ Object.fromEntries(notificationCategories.map((cat) => [cat.key, true]))
+ );
+
+ const handleToggleChange =
+ (key: string) => (event: React.ChangeEvent) => {
+ setToggles((prev) => ({
+ ...prev,
+ [key]: event.target.checked,
+ }));
+ };
+ return (
+ // Paper provides a styled card container for notification settings
+
+ {/* Header with icon and title */}
+
+
+
+ {/* Notification Settings */}
+ {messages['profile'][lang]['notification-settings']}
+
+
+ {/* Notification categories (different for Citizen and Agent) */}
+
+ {notificationCategories.map((item) => (
+ // Each notification category with title, description, and toggle switch
+
+
+
+ {item.title}
+
+
+ {item.desc}
+
+
+
+
+ ))}
+
+
+ {/* Delivery methods section with checkboxes for SMS, WhatsApp, Email */}
+
+
+ {/* Delivery Methods */}
+ {messages['profile'][lang]['delivery-methods']}
+
+
+ {/* SMS Notifications checkbox */}
+
+ }
+ label={
+
+ {/* SMS Notifications */}
+ {messages['profile'][lang]['sms-notifications']}
+
+ }
+ />
+ {/* WhatsApp Notifications checkbox */}
+
+ }
+ label={
+
+ {/* Whatsapp Notifications */}
+ {messages['profile'][lang]['whatsapp-notifications']}
+
+ }
+ />
+ {/* Email Notifications checkbox */}
+
+ }
+ label={
+
+ {/* Email Notifications */}
+ {messages['profile'][lang]['email-notifications']}
+
+ }
+ />
+
+
+
+ );
+};
+
+export default NotificationSettings;
diff --git a/frontend/mobile-ui/src/app/components/Popup/AlertPopup.tsx b/frontend/mobile-ui/src/app/components/Popup/AlertPopup.tsx
new file mode 100644
index 0000000..58ef6c8
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/Popup/AlertPopup.tsx
@@ -0,0 +1,16 @@
+
+// AlertPopup is a wrapper for the Popup component, used to display alert messages
+import type { PopupProps } from "../../models/Popup.model";
+import Popup from "./Popup";
+
+
+// Functional component for alert popups
+export const AlertPopup = ({ title, message, type, open, onClose }: PopupProps) => (
+
+);
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/Popup/InformationPopup.tsx b/frontend/mobile-ui/src/app/components/Popup/InformationPopup.tsx
new file mode 100644
index 0000000..376e1db
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/Popup/InformationPopup.tsx
@@ -0,0 +1,16 @@
+
+// InformationPopup is a wrapper for the Popup component, used to display informational messages
+import type { PopupProps } from "../../models/Popup.model";
+import Popup from "./Popup";
+
+
+// Functional component for information popups
+export const InformationPopup = ({ title, message, type, open, onClose }: PopupProps) => (
+
+);
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/Popup/NotificationPopup.tsx b/frontend/mobile-ui/src/app/components/Popup/NotificationPopup.tsx
new file mode 100644
index 0000000..6fff4cb
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/Popup/NotificationPopup.tsx
@@ -0,0 +1,43 @@
+// NotificationPopup component: renders different popup types based on 'type' prop
+import type { PopupProps } from "../../models/Popup.model";
+import { AlertPopup } from "./AlertPopup";
+import { InformationPopup } from "./InformationPopup";
+import { SuccessPopup } from "./SuccessPopup";
+import { WarningPopup } from "./WarningPopup";
+
+// Renders the appropriate popup component based on the 'type' prop
+// Supported types: 'alert', 'information', 'warning', 'success' (default)
+export const NotificationPopup = ({ title, message, type, open, onClose }: PopupProps) => (
+ // If type is 'alert', render AlertPopup
+ type === 'alert' ?
+ // If type is 'information', render InformationPopup
+ : type === 'information' ?
+ // If type is 'warning', render WarningPopup
+ : type === 'warning' ?
+ // Default: render SuccessPopup
+ :
+);
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/Popup/Popup.tsx b/frontend/mobile-ui/src/app/components/Popup/Popup.tsx
new file mode 100644
index 0000000..6ed9f4a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/Popup/Popup.tsx
@@ -0,0 +1,149 @@
+
+// Popup component displays a modal notification with icon, title, and message
+import React, { useEffect, useState } from 'react';
+import {
+ Modal,
+ Backdrop,
+ Fade,
+ Box,
+ Typography,
+} from '@mui/material';
+import WarningAmberIcon from '@mui/icons-material/WarningAmber';
+import InfoIcon from '@mui/icons-material/Info';
+import ErrorIcon from '@mui/icons-material/Error';
+import CircleNotificationsOutlinedIcon from '@mui/icons-material/CircleNotificationsOutlined';
+import type { PopupProps } from '../../models/Popup.model';
+
+
+// Maps popup type to corresponding icon
+const iconMap: Record = {
+ alert: ,
+ information: ,
+ warning: ,
+ success: ,
+};
+
+
+// Maps popup type to accent color and background
+const stylesMap: Record = {
+ alert: { borderColor: '#0057BD', bg: '#FFFFFF' },
+ information: { borderColor: '#CB9C00', bg: '#FFFFFF' },
+ warning: { borderColor: '#A30222', bg: '#FFFFFF' },
+ success: { borderColor: '#00703C', bg: '#FFFFFF' },
+};
+
+
+// Popup functional component
+export const Popup: React.FC = ({
+ type = 'information', // Type of popup (alert, information, warning, success)
+ title,
+ message,
+ open,
+ duration = 3000, // Duration in ms before auto-close
+}) => {
+ // Internal visibility state for animation and auto-close
+ const [visible, setVisible] = useState(open);
+
+ // Sync visibility with open prop
+ useEffect(() => {
+ setVisible(open);
+ }, [open]);
+
+ // Auto-close after duration if visible
+ useEffect(() => {
+ if (!visible) return;
+ if (duration > 0) {
+ const timer = window.setTimeout(() => {
+ setVisible(false);
+ }, duration);
+ return () => clearTimeout(timer);
+ }
+ }, [visible, duration]);
+
+ // Get accent color for left border
+ const accentColor = stylesMap[type]?.borderColor ?? '#ccc';
+
+ // Render MUI Modal with Fade transition and custom styling
+ return (
+ {
+ setVisible(false);
+ }}
+ closeAfterTransition
+ slots={{ backdrop: Backdrop }}
+ slotProps={{
+ backdrop: {
+ timeout: 200,
+ sx: { backgroundColor: 'transparent' },
+ },
+ }}
+ aria-labelledby="popup-title"
+ aria-describedby="popup-message"
+ sx={{
+ display: 'flex',
+ alignItems: 'flex-start',
+ justifyContent: 'center',
+ pointerEvents: 'none',
+ zIndex: (theme) => theme.zIndex.modal + 100,
+ }}
+ >
+
+
+ {/* Icon for the popup type */}
+ {iconMap[type]}
+
+ {/* Content: title and message */}
+
+ {title && (
+
+ )}
+ {message && (
+
+ )}
+
+
+
+
+ );
+};
+
+// Export the Popup component as default
+export default Popup;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/Popup/SuccessPopup.tsx b/frontend/mobile-ui/src/app/components/Popup/SuccessPopup.tsx
new file mode 100644
index 0000000..516800f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/Popup/SuccessPopup.tsx
@@ -0,0 +1,16 @@
+
+// SuccessPopup is a wrapper for the Popup component, used to display success messages
+import type { PopupProps } from "../../models/Popup.model";
+import Popup from "./Popup";
+
+
+// Functional component for success popups
+export const SuccessPopup = ({ title, message, type, open, onClose }: PopupProps) => (
+
+);
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/Popup/WarningPopup.tsx b/frontend/mobile-ui/src/app/components/Popup/WarningPopup.tsx
new file mode 100644
index 0000000..dd5615c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/Popup/WarningPopup.tsx
@@ -0,0 +1,16 @@
+
+// WarningPopup is a wrapper for the Popup component, used to display warning messages
+import type { PopupProps } from "../../models/Popup.model";
+import Popup from "./Popup";
+
+
+// Functional component for warning popups
+export const WarningPopup = ({ title, message, type, open, onClose }: PopupProps) => (
+
+);
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/ProfileCard.tsx b/frontend/mobile-ui/src/app/components/ProfileCard.tsx
new file mode 100644
index 0000000..47f632b
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/ProfileCard.tsx
@@ -0,0 +1,284 @@
+// ProfileCard is a reusable card component for displaying user profile information.
+// It adapts its layout and fields based on whether the user is a Citizen or Agent.
+// Shows personal details, verification status, contact info, address/jurisdiction, and summary stats.
+// Used in profile screens for both Citizen and Agent interfaces.
+import React, { useEffect, useMemo, useState } from 'react';
+import { Box, Typography, IconButton, Paper } from '@mui/material';
+import edit_square from '../../assets/icons/edit_square.svg';
+import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
+import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined';
+import { getUserFromSession } from '../../context/AuthProvider';
+import { getAppLocale } from '../../services/Profile/ProfileService';
+import { getMessagesFromSession } from '../../services/Citizen/Localization/LocalizationContext';
+import { COLORS } from '../models/Colors.const';
+import authService from '../../services/AuthService';
+import type { Address, UserProfile } from '../models/UserProfile.mode';
+
+export const ProfileCard: React.FC<{
+ isCitizenProfile: boolean;
+ noOfProperties: number;
+ noActiveLicenses: number;
+}> = ({ isCitizenProfile, noOfProperties, noActiveLicenses }) => {
+ const [userProfile, setUserProfile] = useState(null);
+
+ // Get user from session (memoized)
+ const sessionUser = useMemo(() => getUserFromSession(), []);
+ // Determine role (Agent or Citizen)
+ const role = authService.isAgent() ? 'AGENT' : 'CITIZEN';
+
+ // Get current language and localized messages
+ const lang = getAppLocale();
+ const messages = getMessagesFromSession(role)!;
+
+ // Localized labels for profile fields
+ const personalDetailsLabel = messages['profile'][lang]['personal-details'];
+ const personPhone = messages['profile'][lang]['phone-number'];
+ const personAddress = messages['profile'][lang]['residential-address'];
+ const personJurisdiction = messages['profile'][lang]['jurisdiction'];
+ const personProperties = messages['profile'][lang]['properties'];
+ const personLicenses = messages['profile'][lang]['licenses'];
+ const personMemberSince = messages['profile'][lang]['member-since'];
+
+ // Load user profile from session storage on mount or when profile type changes
+ useEffect(() => {
+ const userString = sessionStorage.getItem('user');
+ const user = userString ? JSON.parse(userString) : null;
+ setUserProfile(user);
+ }, [isCitizenProfile, sessionUser]);
+
+ // Determine which profile data to display
+ const displayProfile = userProfile || (sessionUser as UserProfile) || {};
+
+ // Helper to get profile values with fallback
+ const getProfileValue = (
+ profileKey: keyof NonNullable,
+ fallbackKey?: keyof UserProfile
+ ) => {
+ if (displayProfile.profile?.[profileKey]) {
+ return displayProfile.profile[profileKey];
+ }
+ if (fallbackKey && displayProfile[fallbackKey]) {
+ return displayProfile[fallbackKey];
+ }
+ return undefined;
+ };
+
+ // Extract main profile fields
+ const firstName = getProfileValue('firstName', 'firstName') as string;
+ const lastName = getProfileValue('lastName', 'lastName') as string;
+ const fullName =
+ (getProfileValue('fullName', 'fullName') as string) ||
+ `${firstName || ''} ${lastName || ''}`.trim();
+ const phoneNumber = getProfileValue('phoneNumber', 'phoneNumber') as string;
+ const aadharNo = getProfileValue('adhaarNo') as number | undefined;
+ const email = displayProfile.email;
+ const address = displayProfile.profile?.address || displayProfile.address;
+ const ward = displayProfile.ward;
+
+ // Helper to format address for display
+ const formatAddress = (address: Address | undefined): string => {
+ if (!address) return 'Plot 567, 27th Main Road, HSR Layout';
+ const parts = [
+ address.addressLine1,
+ address.addressLine2,
+ address.city,
+ address.state,
+ address.pinCode,
+ ].filter(Boolean);
+ return parts.length > 0 ? parts.join(', ') : 'Address not provided';
+ };
+
+ // Format member since date
+ const formatMemberSince = (dateString: string | undefined): string => {
+ if (!dateString) return 'N/A';
+
+ const date = new Date(dateString);
+ const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'short' };
+ return date.toLocaleDateString('en-US', options);
+ };
+
+ const memberSince = formatMemberSince(displayProfile.createdDate);
+ return (
+ // Paper provides a styled card container for profile info
+
+ {/* Header row with label and edit button */}
+
+
+ {personalDetailsLabel}
+
+
+
+
+
+
+ {/* Main profile info row with avatar, name, verification status, and phone */}
+
+
+
+
+ {fullName || 'Name not available'}
+
+
+
+ {isCitizenProfile ? (
+
+ {messages['profile'][lang]['verified-owner']}
+
+ ) : (
+
+ {messages['profile'][lang]['verified-agent']}
+
+ )}
+
+
+ {/* Masked phone number for citizen profile */}
+ {isCitizenProfile && (
+
+ {aadharNo
+ ? `${aadharNo.toString().slice(0, 4)} **** ***${aadharNo
+ .toString()
+ .slice(-1)}`
+ : ''}
+
+ )}
+
+
+
+ {/* Contact info and address/jurisdiction section */}
+
+
+ {messages['profile'][lang]['email-address']}:
+
+
+ {email || 'Email not available'}
+
+
+
+ {personPhone}:
+
+
+ {phoneNumber || 'Phone not available'}
+
+
+ {/* Citizen: show address, Agent: show jurisdiction */}
+ {isCitizenProfile && (
+ <>
+
+ {personAddress}:
+
+
+ {formatAddress(address)}
+
+ >
+ )}
+
+ {!isCitizenProfile && (
+
+
+ {personJurisdiction}:
+
+ {(ward || ['Ward Nos. 24 && 38 of BBMP', 'Ward Nos. 46 of BBMP']).map(
+ (item: string, index: number) => (
+
+ {item}
+
+ )
+ )}
+
+ )}
+
+
+ {/* Citizen: show summary stats (properties, licenses, member since) */}
+ {isCitizenProfile && (
+
+
+
+ {noOfProperties}
+
+
+ {personProperties}
+
+
+
+
+ {noActiveLicenses}
+
+
+ {personLicenses}
+
+
+
+
+ {personMemberSince}
+
+
+ {memberSince}
+
+
+
+ )}
+
+ );
+};
+
+export default ProfileCard;
diff --git a/frontend/mobile-ui/src/app/components/SupportCard.tsx b/frontend/mobile-ui/src/app/components/SupportCard.tsx
new file mode 100644
index 0000000..bcdb35c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/SupportCard.tsx
@@ -0,0 +1,112 @@
+
+// SupportHelp is a reusable card component for displaying support and help options.
+// It lists multiple support channels (helpdesk, email, portal) with icons, descriptions, and a contact button.
+// Uses localization for all labels and adapts to the current language.
+// Used in profile/settings screens for Citizen interface to provide quick access to support resources.
+import React from "react";
+import { Box, Typography, Button, Paper } from "@mui/material";
+import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
+import MailOutlineIcon from "@mui/icons-material/MailOutline";
+import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
+import LiveHelpOutlinedIcon from "@mui/icons-material/LiveHelpOutlined";
+import { useAppSelector } from "../../redux/Hooks";
+import { getMessagesFromSession } from "../../services/Citizen/Localization/LocalizationContext";
+import { COLORS } from "../models/Colors.const";
+
+
+
+
+export const SupportHelp: React.FC = () => {
+ // Get current language and localized messages for Citizen
+ const lang = useAppSelector(state => state.lang.citizenLang);
+ const messages = getMessagesFromSession("CITIZEN")!;
+
+ // List of support/help items to display
+ const supportItems = [
+ {
+ icon: ,
+ title: messages["profile"][lang]["bbmp-helpdesk"],
+ desc: messages["profile"][lang]["get-help-with-municipal-services"],
+ },
+ {
+ icon: ,
+ title: messages["profile"][lang]["email-support"],
+ desc: `${messages["profile"][lang]["write-to-us"]} support@bbmp.gov.in`,
+ },
+ {
+ icon: ,
+ title: messages["profile"][lang]["citizen-services-portal"],
+ desc: messages["profile"][lang]["browse-services-and-faqs"],
+ },
+ ];
+
+ return(
+ // Paper provides a styled card container for support/help options
+
+ {/* Header with icon and title */}
+
+
+
+ {/* Support & Help */}
+ {messages["profile"][lang]["support-help"]}
+
+
+ {/* List of support/help items with icon, title, description, and contact button */}
+ {supportItems.map((item) => (
+
+
+ {item.icon}
+
+
+ {item.title}
+
+
+ {item.desc}
+
+
+
+
+ {/* contact */}
+ {messages["profile"][lang]["contact"]}
+
+
+ ))}
+
+ );
+}
+
+export default SupportHelp;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/common/ConfirmationDialog.tsx b/frontend/mobile-ui/src/app/components/common/ConfirmationDialog.tsx
new file mode 100644
index 0000000..1855223
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/common/ConfirmationDialog.tsx
@@ -0,0 +1,88 @@
+
+// ConfirmationDialog component displays a modal dialog for confirming or rejecting an action
+import React from 'react';
+import {
+ Dialog,
+ DialogContent,
+ Typography,
+ Button,
+ IconButton,
+ Box,
+} from '@mui/material';
+import CloseIcon from '@mui/icons-material/Close';
+import {
+ confirmDialogPaperStyle,
+ confirmTitleStyle,
+ confirmDescStyle,
+ confirmNoteStyle,
+ confirmButtonBar,
+ rejectBtn,
+ confirmBtn,
+ closeButtonStyle,
+} from '../../../styles/addRequestStyles/ConfirmationDialog';
+
+
+// Props for ConfirmationDialog
+type ConfirmationDialogProps = {
+ open: boolean; // Whether the dialog is open
+ onReject: () => void; // Handler for reject action
+ onConfirm: () => void; // Handler for confirm action
+ onClose?: () => void; // Optional handler for closing the dialog
+};
+
+
+// Functional component for the confirmation dialog
+const ConfirmationDialog: React.FC = ({
+ open,
+ onReject,
+ onConfirm,
+ onClose,
+}) => (
+
+ {/* Close button at the top right */}
+
+
+
+
+ {/* Dialog title */}
+ Confirm Comment
+ {/* Confirmation message */}
+
+ Are you sure you want to Confirm this request?
+
+ {/* Note about irreversibility */}
+
+ This action is irreversible.
+
+ {/* Action buttons: Reject and Confirm */}
+
+
+ Reject
+
+ {
+ onConfirm();
+ }}
+ autoFocus
+ >
+ Confirm
+
+
+
+
+);
+
+// Export the ConfirmationDialog component as default
+export default ConfirmationDialog;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/components/common/RequestDialog.tsx b/frontend/mobile-ui/src/app/components/common/RequestDialog.tsx
new file mode 100644
index 0000000..3d9dd00
--- /dev/null
+++ b/frontend/mobile-ui/src/app/components/common/RequestDialog.tsx
@@ -0,0 +1,153 @@
+
+// RequestDialog component allows users to submit a comment and optionally upload a file
+import React, { useState } from 'react';
+import {
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ Button,
+ Box,
+ IconButton,
+ Typography,
+} from '@mui/material';
+import { Close as CloseIcon, CloudUpload as UploadIcon } from '@mui/icons-material';
+import {
+ requestDialogPaperStyle,
+ requestDialogTitleStyle,
+ requestDialogContentStyle,
+ requestTextFieldStyle,
+ fileUploadBoxStyle,
+ uploadIconStyle,
+ uploadTextStyle,
+ requestDialogActionsStyle,
+ cancelBtnStyle,
+ submitBtnStyle,
+ closeIconButtonStyle,
+} from '../../../styles/addRequestStyles/requestDialog';
+
+
+// Props for RequestDialog
+interface RequestDialogProps {
+ open: boolean; // Whether the dialog is open
+ onClose: () => void; // Handler to close the dialog
+ onSubmit: (comment: string, file?: File) => void; // Handler for submitting the comment and file
+}
+
+
+// Functional component for the request dialog
+const RequestDialog: React.FC = ({
+ open,
+ onClose,
+ onSubmit,
+}) => {
+ // State for the comment input
+ const [comment, setComment] = useState('');
+ // State for the selected file
+ const [selectedFile, setSelectedFile] = useState(undefined);
+
+ // Handle submit button click
+ const handleSubmit = () => {
+ if (comment.trim()) {
+ onSubmit(comment, selectedFile); // Call parent handler
+ setComment(''); // Reset comment
+ setSelectedFile(undefined); // Reset file
+ }
+ };
+
+ // Handle file input change
+ const handleFileChange = (event: React.ChangeEvent) => {
+ const file = event.target.files?.[0];
+ setSelectedFile(file);
+ };
+
+ // Handle dialog close (reset state)
+ const handleClose = () => {
+ setComment('');
+ setSelectedFile(undefined);
+ onClose();
+ };
+
+ return (
+
+ {/* Dialog title with close button */}
+
+
+ Add Comment
+
+
+
+
+
+
+ {/* Dialog content: comment input and file upload */}
+
+ {/* Multiline text field for comment */}
+ setComment(e.target.value)}
+ sx={requestTextFieldStyle}
+ />
+
+ {/* File upload box (clickable) */}
+ document.getElementById('file-input')?.click()}
+ >
+
+
+
+ {selectedFile
+ ? `Selected: ${selectedFile.name}`
+ : 'Click to upload a document (optional)'}
+
+
+
+
+ {/* Dialog actions: Cancel and Submit buttons */}
+
+
+ Cancel
+
+
+ Submit
+
+
+
+ );
+};
+
+// Export the RequestDialog component as default
+export default RequestDialog;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/api/agentApiSlice.ts b/frontend/mobile-ui/src/app/features/Agent/api/agentApiSlice.ts
new file mode 100644
index 0000000..af418ca
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/api/agentApiSlice.ts
@@ -0,0 +1,38 @@
+
+// agentApiSlice sets up the base API configuration for Agent-related requests using Redux Toolkit Query.
+// It handles authentication by attaching a valid token to each request header via AuthService.
+// The base URL points to the backend service for agent operations.
+// Endpoints for agent features should be defined in feature-specific slices that extend this base.
+// Used for all network requests made by Agent screens/components.
+
+import authService from '../../../../services/AuthService';
+
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
+
+// Define a service using a base URL and expected endpoints
+
+export const agentApiSlice = createApi({
+ reducerPath: 'agentApi', // Unique key for agent API slice in Redux store
+ baseQuery: fetchBaseQuery({
+ baseUrl: import.meta.env.VITE_ENUMERATION_HOST, // Backend base URL for agent operations
+
+ // Prepare headers for every request (add auth token, content type)
+ prepareHeaders: async (headers, { getState: _getState }) => {
+ // Use AuthService to get a valid token (handles refresh automatically)
+ const token = await authService.getValidToken();
+ if (token) {
+ headers.set('authorization', `Bearer ${token}`)
+ }
+ headers.set('content-type', 'application/json')
+ return headers
+ },
+ }),
+ // Global tags for cache invalidation (can be extended)
+ // tagTypes: ALL_TAG_TYPES,
+ // Define endpoints in feature-specific API slices that extend this base
+ endpoints: () => ({}),
+})
+
+
+export default agentApiSlice;
+
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/api/fetchProperty.hooks.ts b/frontend/mobile-ui/src/app/features/Agent/api/fetchProperty.hooks.ts
new file mode 100644
index 0000000..8d1528c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/api/fetchProperty.hooks.ts
@@ -0,0 +1,64 @@
+
+/**
+ * fetchPropertyDetails is an async utility for fetching and mapping property details for Agent screens.
+ * It retrieves property data by ID, transforms the response into the required form structure,
+ * and updates the form state. Handles errors by showing a popup.
+ *
+ * @param propID - Property ID to fetch
+ * @param getPropertyById - API function to fetch property by ID
+ * @param updateForm - Callback to update form state with mapped data
+ * @param showErrorPopup - Callback to show error popup on failure
+ */
+export const fetchPropertyDetails = async (
+ propID: string,
+ applicationId: string,
+ getApplicationById: any,
+ getOwnerByPropertyId: any,
+ updateForm: (data: any) => void,
+ showErrorPopup: (message: string) => void
+) => {
+ try {
+ const applicationData = await getApplicationById(applicationId).unwrap();
+ const ownersData = await getOwnerByPropertyId(propID).unwrap();
+ console.log(ownersData);
+
+ updateForm({
+ id: propID,
+ categoryOfOwnership: applicationData.data.Property.OwnershipType || '',
+ propertyType: applicationData.data.Property.PropertyType || '',
+ apartmentName: applicationData.data.Property.ComplexName || '',
+ propertyNo: applicationData.data.Property.PropertyNo || '',
+ propertyAddress: (applicationData.data.Property.Address as any) || undefined,
+ locationData: applicationData.data.Property.GISData
+ ? {
+ gisDataId: applicationData.data.Property.GISData.ID,
+ address: applicationData.data.Property.Address?.Locality || '',
+ coordinates: {
+ lat: applicationData.data.Property.GISData.Latitude || 0,
+ lng: applicationData.data.Property.GISData.Longitude || 0,
+ },
+ drawnShapes:
+ applicationData.data.Property.GISData.Coordinates?.length > 0
+ ? [
+ {
+ type: 'polygon',
+ coordinates: applicationData.data.Property.GISData?.Coordinates.map(
+ (coord: any) => [coord.Longitude, coord.Latitude]
+ ),
+ },
+ ]
+ : [],
+ }
+ : undefined,
+ owners: ownersData.data || [],
+ isgrDetails: (applicationData.data.Property.IGRS as any) || undefined,
+ assessmentDetails: (applicationData.data.Property.AssessmentDetails as any) || undefined,
+ importantNotes: applicationData.data.ImportantNote || '',
+ });
+
+ } catch (error) {
+ // Handle errors and show error popup
+ console.error('Error fetching property details:', error);
+ showErrorPopup('Failed to load property details');
+ }
+};
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/api/homePageApi.ts b/frontend/mobile-ui/src/app/features/Agent/api/homePageApi.ts
new file mode 100644
index 0000000..b0ad7bc
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/api/homePageApi.ts
@@ -0,0 +1,34 @@
+
+// homePageApi defines API endpoints for fetching property lists for the Agent home page.
+// It provides queries for both non-draft (active/finished) and draft properties assigned to the agent.
+// Uses agentApiSlice for base API configuration and authentication.
+// Returns RTK Query hooks for use in Agent screens/components.
+
+import apiSlice from '../../../../redux/apiSlice';
+import { TAG_TYPES } from '../../../../redux/tagTypes';
+import type { HomePageResponse } from '../models/HomePageData.model';
+
+export const allPropertiesApi = apiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ // Fetch non-draft (active/finished) properties assigned to the agent
+ getAllProperties: builder.query({
+ query: ({agentId}) => ({
+ url: `v1/applications/search?assignedAgent=${agentId}&isDraft=false&status=ASSIGNED`,
+ method: 'GET',
+ }),
+ providesTags: [TAG_TYPES.APPLICATION, TAG_TYPES.PROPERTY],
+ }),
+ // Fetch draft properties assigned to the agent
+ getDraftProperties: builder.query({
+ query: ({agentId}) => ({
+ url: `v1/applications/search?assesseeID=${agentId}&isDraft=true`,
+ method: 'GET',
+ }),
+ providesTags: [TAG_TYPES.APPLICATION, TAG_TYPES.PROPERTY],
+ }),
+ }),
+});
+
+
+// RTK Query hooks for use in Agent home page components
+export const { useGetAllPropertiesQuery, useGetDraftPropertiesQuery } = allPropertiesApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/api/propertyData.hooks.ts b/frontend/mobile-ui/src/app/features/Agent/api/propertyData.hooks.ts
new file mode 100644
index 0000000..5c249df
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/api/propertyData.hooks.ts
@@ -0,0 +1,127 @@
+
+// Custom hook for managing property data submission and updates for Agent workflows.
+// Integrates form context and RTK Query mutations for property basics, GIS data, and coordinates.
+// Used in Agent property creation/edit screens to handle async data operations.
+
+import { usePropertyForm } from "../../../../context/PropertyFormContext";
+import { useSubmitCoordinateBatchMutation, useSubmitGISDataMutation, useUpdateCoordinatesMutation, useUpdateGISDataMutation } from "../../../../redux/apis/gisApi";
+import { useSubmitPropertyBasicsMutation, useUpdatePropertyMutation } from "../../../../redux/apis/propertyApi";
+
+
+// usePropertyData provides async functions to save property basics, GIS data, and coordinates.
+// It uses form data from context and exposes three main operations:
+// - savePropertyBasics: create or update property basic info
+// - saveGISData: create or update GIS data for a property
+// - saveCoordinates: create or update coordinate batch for drawn shapes
+export const usePropertyData = () => {
+ const { formData } = usePropertyForm();
+ // RTK Query mutations for property and GIS operations
+ const [submitPropertyBasics] = useSubmitPropertyBasicsMutation();
+ const [submitGISData] = useSubmitGISDataMutation();
+ const [submitCoordinateBatch] = useSubmitCoordinateBatchMutation();
+ const [updateProperty] = useUpdatePropertyMutation();
+ const [updateGISData] = useUpdateGISDataMutation();
+ const [updateCoordinates] = useUpdateCoordinatesMutation();
+
+
+ // Save or update property basic information
+ // If propertyID is provided, updates existing property; otherwise, creates new property
+ const savePropertyBasics = async (propertyID?: string) => {
+ if (propertyID) {
+ const result = await updateProperty({
+ propertyId: propertyID,
+ ownershipType: formData.categoryOfOwnership,
+ propertyType: formData.propertyType,
+ complexName: formData.apartmentName,
+ address: formData.propertyAddress!,
+ propertyNo: formData.propertyNo || '',
+ }).unwrap();
+ // Return updated property ID and number
+
+ return { propertyID, propertyNo: result.data?.PropertyNo };
+ } else {
+ const result = await submitPropertyBasics({
+ ownershipType: formData.categoryOfOwnership,
+ propertyType: formData.propertyType,
+ complexName: formData.apartmentName,
+ }).unwrap();
+ // Return new property ID and number
+
+ return { propertyID: result.data.ID, propertyNo: result.data.PropertyNo };
+ }
+ };
+
+
+ // Save or update GIS data for a property
+ // If gisDataId is provided, updates existing GIS data; otherwise, creates new GIS data
+ const saveGISData = async (propertyID: string, gisDataId?: string) => {
+ const { coordinates } = formData.locationData!;
+
+ if (gisDataId) {
+ await updateGISData({
+ gisDataId,
+ propertyId: propertyID,
+ source: 'GPS',
+ type: 'POLYGON',
+ }).unwrap();
+ return gisDataId;
+ } else {
+ const result = await submitGISData({
+ propertyId: propertyID,
+ latitude: coordinates?.lat!,
+ longitude: coordinates?.lng!,
+ source: 'GPS',
+ type: 'POLYGON',
+ }).unwrap();
+ return result.data.ID;
+ }
+ };
+
+
+ // Save or update coordinates for drawn polygon shapes
+ // If isUpdate is true, updates existing coordinates; otherwise, creates new batch
+ const saveCoordinates = async (gisDataId: string, isUpdate: boolean) => {
+ // Filter drawn shapes to get polygons only
+ const polygonShapes = formData.locationData?.drawnShapes?.filter(
+ (shape) => shape.type === 'polygon'
+ ) || [];
+
+ if (polygonShapes.length === 0) return;
+ // Map polygon coordinates to batch format for API
+ const coordinateBatch = polygonShapes.flatMap((shape) => {
+ if (!shape.coordinates || !Array.isArray(shape.coordinates)) return [];
+ return (shape.coordinates as number[][]).map((coord) => {
+ // Coordinates are stored as [lng, lat] in drawnShapes (see LocationMapWithDrawing.finishShape)
+ const [initialLng, initialLat] = coord;
+ let lat = initialLat;
+ let lng = initialLng;
+ if (typeof lat !== 'number' || typeof lng !== 'number') {
+ // If shape coordinate format is unexpected, try to salvage by swapping
+ const [a, b] = coord as any[];
+ if (typeof a === 'number' && typeof b === 'number') {
+ if (Math.abs(a) > 90 && Math.abs(b) <= 90) {
+ lat = b;
+ lng = a;
+ } else {
+ lat = b;
+ lng = a;
+ }
+ }
+ }
+ return { latitude: lat, longitude: lng, gisDataId };
+ });
+ });
+
+ if (coordinateBatch.length > 0) {
+ if (isUpdate) {
+ await updateCoordinates({ gisDataId, coordinates: coordinateBatch }).unwrap();
+ } else {
+ await submitCoordinateBatch(coordinateBatch).unwrap();
+ }
+ }
+ };
+
+
+ // Expose async save functions for use in Agent property screens
+ return { savePropertyBasics, saveGISData, saveCoordinates };
+};
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/api/reviewPageApi.ts b/frontend/mobile-ui/src/app/features/Agent/api/reviewPageApi.ts
new file mode 100644
index 0000000..71e8f9c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/api/reviewPageApi.ts
@@ -0,0 +1,24 @@
+
+// reviewPageApi defines API endpoints for fetching property applications assigned to an agent for review.
+// Uses agentApiSlice for base API configuration and authentication.
+// Returns RTK Query hook for use in Agent review screens/components.
+
+import { agentApiSlice } from './agentApiSlice';
+import type { ApplicationsResponse } from '../models/ReviewPage.model';
+
+
+// Injects the getApplications endpoint for fetching applications with status AUDIT_VERIFIED assigned to the agent
+export const applicationsApi = agentApiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ getApplications: builder.query({
+ query: ({ AssignedAgent }) => ({
+ url: `/v1/applications/search?AssignedAgent=${AssignedAgent}&status=AUDIT_VERIFIED`,
+ method: 'GET',
+ }),
+ }),
+ }),
+});
+
+
+// RTK Query hook for use in Agent review page components
+export const { useGetApplicationsQuery } = applicationsApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/api/searchPropertyApi.ts b/frontend/mobile-ui/src/app/features/Agent/api/searchPropertyApi.ts
new file mode 100644
index 0000000..3ee9593
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/api/searchPropertyApi.ts
@@ -0,0 +1,25 @@
+
+// API slice for searching properties assigned to an agent
+import { agentApiSlice } from './agentApiSlice';
+import type { SearchPropertyResponse } from '../models/SearchPropertyData.model';
+
+
+// Injects endpoints into the agentApiSlice for property search
+export const searchPropertyApi = agentApiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ // Query to get properties assigned to a specific agent
+ getProperties: builder.query({
+ query: ({ assignedAgent }) => ({
+ url: `/v1/applications/search?assignedAgent=${assignedAgent}`,
+ method: 'GET',
+ headers: {
+ 'X-Tenant-ID': 'pb.amritsar', // Tenant ID header for API
+ },
+ }),
+ }),
+ }),
+});
+
+
+// Export hooks for using the getProperties query
+export const { useGetPropertiesQuery, useLazyGetPropertiesQuery } = searchPropertyApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/components/BottomNavigation.tsx b/frontend/mobile-ui/src/app/features/Agent/components/BottomNavigation.tsx
new file mode 100644
index 0000000..3faddb2
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/components/BottomNavigation.tsx
@@ -0,0 +1,108 @@
+// BottomNavigation renders the bottom tab bar for Agent screens.
+// Displays navigation tabs for Home, Inbox, Notifications, and Search.
+// Highlights the active tab and handles tab changes via props.
+// Uses MUI icons and localized tab labels.
+
+import React from 'react';
+import '../../../../styles/BottomNavigation.css';
+import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
+import SearchIcon from '@mui/icons-material/Search';
+import DraftsOutlinedIcon from '@mui/icons-material/DraftsOutlined';
+import DraftsIcon from '@mui/icons-material/Drafts';
+import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
+import HomeIcon from '@mui/icons-material/Home';
+import NotificationsNoneOutlinedIcon from '@mui/icons-material/NotificationsNoneOutlined';
+import NotificationsIcon from '@mui/icons-material/Notifications';
+
+import { useAppLocalization } from '../../../../services/AgentLocalisation/localisation-search-property';
+
+
+// NavigationTab type defines available tabs
+type NavigationTab = 'home' | 'inbox' | 'notifications' | 'search';
+
+
+// Props for BottomNavigation:
+// - activeTab: currently selected tab
+// - onTabChange: callback for tab change
+interface BottomNavigationProps {
+ activeTab: NavigationTab;
+ onTabChange: (tab: NavigationTab) => void;
+}
+
+
+const BottomNavigation: React.FC = ({
+ activeTab,
+ onTabChange
+}) => {
+ // Get localized tab labels
+ const { homeNavText, inboxNavText, NotificationsNavText, searchNavText } = useAppLocalization();
+
+ // Handle tab click and notify parent
+ const handleTabClick = (tabId: NavigationTab) => {
+ onTabChange(tabId);
+ };
+
+ // Define navigation items with icons and labels
+ const navItems = [
+ {
+ id: 'home' as NavigationTab,
+ label: homeNavText,
+ icon: activeTab === 'home' ? (
+
+ ) : (
+
+ )
+ },
+ {
+ id: 'inbox' as NavigationTab,
+ label: inboxNavText,
+ icon: activeTab === 'inbox' ? (
+
+ ) : (
+
+ )
+ },
+ {
+ id: 'notifications' as NavigationTab,
+ label: NotificationsNavText,
+ icon: activeTab === 'notifications' ? (
+
+ ) : (
+
+ )
+ },
+ {
+ id: 'search' as NavigationTab,
+ label: searchNavText,
+ icon: activeTab === 'search' ? (
+
+ ) : (
+
+ )
+ }
+ ];
+
+ // Render bottom navigation bar with tab buttons
+ return (
+
+ {navItems.map((item) => (
+
(e.currentTarget as HTMLButtonElement).blur()}
+ onClick={() => handleTabClick(item.id)}
+ aria-label={item.label}
+ >
+
+ {item.icon}
+
+ {item.label}
+
+ ))}
+
+ );
+};
+
+
+// Export BottomNavigation for use in Agent screens
+export default BottomNavigation;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/components/ChromeTabs.tsx b/frontend/mobile-ui/src/app/features/Agent/components/ChromeTabs.tsx
new file mode 100644
index 0000000..bb4f264
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/components/ChromeTabs.tsx
@@ -0,0 +1,109 @@
+
+// ChromeTabs renders a tab switcher styled like Chrome browser tabs for Agent screens.
+// Displays two tabs (Map and Landuse) with localized labels.
+// Highlights the selected tab and notifies parent on tab change.
+// Uses MUI styled components for custom appearance.
+
+import React from 'react';
+import { Box, Button, styled } from '@mui/material';
+import { useHomePageLocalization } from '../../../../services/AgentLocalisation/localisation-homepage';
+
+
+// Props for ChromeTabs:
+// - selected: index of currently selected tab (0 or 1)
+// - onTabChange: callback for tab change
+// - height: optional tab height (default 50)
+interface ChromeTabsProps {
+ selected: number;
+ onTabChange: (idx: number) => void;
+ height?: number;
+}
+
+
+const ChromeTabs: React.FC = ({
+ selected,
+ onTabChange,
+ height = 50
+}) => {
+ // Get localized tab labels
+ const { mapTabText, landuseTabText } = useHomePageLocalization();
+
+ // Render two styled tab buttons for Map and Landuse
+ return (
+
+ onTabChange(0)}
+ style={{
+ borderTopLeftRadius: 18,
+ borderTopRightRadius: 0,
+ }}
+ >
+ {mapTabText}
+
+ onTabChange(1)}
+ style={{
+ borderTopLeftRadius: 0,
+ borderTopRightRadius: 18,
+ }}
+ >
+ {landuseTabText}
+
+
+ );
+};
+
+
+// Container for tab buttons, styled for Chrome-like appearance
+const TabsContainer = styled(Box)({
+ display: 'flex',
+ alignItems: 'center',
+ justifyItems:'center',
+ width: '338px',
+ border: '1px solid #000',
+ borderTopLeftRadius: 14,
+ borderTopRightRadius: 14,
+ borderBottomLeftRadius: 0,
+ borderBottomRightRadius: 0,
+ overflow: 'hidden',
+ background: '#fff',
+ paddingBottom:'0.5px'
+});
+
+
+// Styled tab button for ChromeTabs, highlights when selected
+const TabButton = styled(Button, {
+ shouldForwardProp: (prop) => prop !== '$isSelected' && prop !== '$height'
+})<{
+ $isSelected: boolean;
+ $height: number;
+}>(({ $isSelected, $height }) => ({
+ flex: 1,
+ minWidth: 0,
+ height: $height,
+ fontWeight: $isSelected ? 400 : 300,
+ fontSize: 15,
+ background: $isSelected ? '#ededed' : '#fff',
+ color: '#000',
+ border: 'none',
+ borderRadius: 0,
+ boxShadow: 'none',
+ textTransform: 'none',
+ transition: 'background 0.2s',
+ '&:hover': {
+ background: $isSelected ? '#ededed' : '#f5f5f5',
+ },
+ // Remove focus ring
+ '&:focus-visible': {
+ outline: 'none',
+ },
+}));
+
+
+// Export ChromeTabs for use in Agent screens
+export default ChromeTabs;
+
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/components/DetailsCard.tsx b/frontend/mobile-ui/src/app/features/Agent/components/DetailsCard.tsx
new file mode 100644
index 0000000..679b368
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/components/DetailsCard.tsx
@@ -0,0 +1,43 @@
+
+// DetailsCard displays a styled card with a heading and a list of key-value pairs.
+// Used in Agent screens to show property or user details in a readable format.
+// Accepts a heading and an array of items to render.
+
+import React from 'react';
+import '../../../../styles/Agent/DetailsCard.css';
+
+
+// KeyValue type for each row in the details list
+type KeyValue = {
+ key: string;
+ value: React.ReactNode;
+};
+
+
+// Props for DetailsCard:
+// - heading: card title
+// - items: array of key-value pairs to display
+interface DetailsCardProps {
+ heading: string;
+ items: KeyValue[];
+}
+
+
+// Render a card with heading and key-value rows
+const DetailsCard: React.FC = ({ heading, items }) => (
+
+
{heading}
+
+ {items.map(({ key, value }, i) => (
+
+ {key} :
+ {value}
+
+ ))}
+
+
+);
+
+
+// Export DetailsCard for use in Agent screens
+export default DetailsCard;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/components/Layout.tsx b/frontend/mobile-ui/src/app/features/Agent/components/Layout.tsx
new file mode 100644
index 0000000..e510ac9
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/components/Layout.tsx
@@ -0,0 +1,466 @@
+import React, { useState, useEffect, useRef } from 'react';
+import BottomNavigation from '../components/BottomNavigation';
+import '../../../../styles/Layout.css';
+import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
+import KeyboardBackspaceOutlinedIcon from '@mui/icons-material/KeyboardBackspaceOutlined';
+import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
+import LogoutIcon from '@mui/icons-material/Logout';
+import { useAuth } from '../../../../context/AuthProvider';
+import { useNavigate } from 'react-router-dom';
+import { useLanguage } from '../../../../context/LanguageContext';
+import { useAppLocalization } from '../../../../services/AgentLocalisation/localisation-search-property';
+import translateIndicSvg from '../../../assets/Citizen/home_page/translate_indic.svg';
+import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined';
+import { Button, FormControl, InputLabel, MenuItem, Select } from '@mui/material';
+
+type NavigationTab = 'home' | 'inbox' | 'notifications' | 'search';
+
+interface AgentUser {
+ id: string;
+ username: string;
+ email: string;
+ role: string;
+ isActive: boolean;
+ zoneData: {
+ zoneNumber: string;
+ wards: string[];
+ }[];
+ preferredLanguage: string;
+ profile: {
+ firstName: string;
+ lastName: string;
+ fullName: string;
+ phoneNumber: string;
+ adhaarNo: number;
+ gender: string;
+ address: {
+ addressLine1: string;
+ addressLine2: string | null;
+ city: string;
+ state: string;
+ pinCode: string;
+ };
+ department: string;
+ designation: string;
+ };
+ createdDate: string;
+ updatedDate: string;
+ createdBy: string;
+ updatedBy: string;
+}
+
+interface LayoutProps {
+ children: React.ReactNode;
+ activeTab: NavigationTab;
+ onTabChange: (tab: NavigationTab) => void;
+ showHeader?: boolean;
+ showNavigation?: boolean;
+ hideLocationIcon?: boolean;
+ headerProps?: {
+ title?: string;
+ showBackButton?: boolean;
+ onBack?: () => void;
+ showLanguage?: boolean;
+ showProfile?: boolean;
+ showHome?: boolean;
+ onHome?: () => void;
+ locationText?: string;
+ onLanguageSelect?: (locale: string) => void;
+ currentLocale?: string;
+ jurisdictionText?: string;
+ languageText?: string;
+ profileText?: string;
+ logoutText?: string;
+ homeNavText?: string;
+ inboxNavText?: string;
+ insightsNavText?: string;
+ searchNavText?: string;
+ };
+}
+
+const Layout: React.FC = ({
+ children,
+ activeTab,
+ onTabChange,
+ showNavigation = true,
+ hideLocationIcon = false,
+ headerProps = {},
+}) => {
+ const { logout } = useAuth();
+ const { locale, setLocale } = useLanguage();
+ const { jurisdictionText, languageText, profileText, logoutText } =
+ useAppLocalization();
+ // Read agent user from sessionStorage
+ const agentUser: AgentUser | null = (() => {
+ try {
+ const userStr = sessionStorage.getItem('user');
+ return userStr ? JSON.parse(userStr) : null;
+ } catch {
+ return null;
+ }
+ })();
+ const zoneData = agentUser?.zoneData || [];
+ const [selectedZoneIndex, setSelectedZoneIndex] = useState(0);
+ const [selectedWard, setSelectedWard] = useState(
+ zoneData?.[0]?.wards?.[0] || ''
+ );
+
+ const [showProfileDropdown, setShowProfileDropdown] = useState(false);
+ const [showLanguageDropdown, setShowLanguageDropdown] = useState(false);
+ const [showLocationPopup, setShowLocationPopup] = useState(false);
+ const profileRef = useRef(null);
+ const languageRef = useRef(null);
+ const locationPopupRef = useRef(null);
+ const navigate = useNavigate();
+ const languages = [
+ { code: 'en', name: 'English' },
+ { code: 'hi', name: 'เคนเคฟเคเคฆเฅ' },
+ { code: 'kn', name: 'เฒเฒจเณเฒจเฒก' },
+ ];
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const target = event.target as Node;
+
+ // Profile dropdown
+ if (profileRef.current && !profileRef.current.contains(target)) {
+ setShowProfileDropdown(false);
+ }
+
+ // Language dropdown
+ if (languageRef.current && !languageRef.current.contains(target)) {
+ setShowLanguageDropdown(false);
+ }
+
+ // Location popup (with MUI Select support)
+ if (showLocationPopup) {
+ const popup = locationPopupRef.current;
+
+ // If clicking inside the location popup, ignore
+ if (popup && popup.contains(target)) return;
+
+ // If clicking inside MUI Select menu, ignore
+ const popoverMenu = document.querySelector('.MuiPopover-root,.MuiMenu-root');
+ if (popoverMenu && popoverMenu.contains(target)) return;
+
+ // If clicking outside, close
+ setShowLocationPopup(false);
+ }
+ };
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [showLocationPopup]);
+
+ const handleProfileClick = () => {
+ navigate('/profile');
+ };
+
+ const handleLanguageClick = () => {
+ setShowLanguageDropdown(!showLanguageDropdown);
+ };
+
+ const handleLanguageSelect = (langCode: string) => {
+ setLocale(langCode);
+ setShowLanguageDropdown(false);
+ };
+
+ const handleLogout = () => {
+ logout();
+ setShowProfileDropdown(false);
+ navigate('/login');
+ };
+
+ const handleZoneSelect = (i: number) => {
+ setSelectedZoneIndex(i);
+ setSelectedWard(zoneData[i]?.wards?.[0] || '');
+ };
+
+ const handleWardSelect = (ward: string) => {
+ setSelectedWard(ward);
+ };
+
+ const handleJurisdictionConfirm = () => {
+ setShowLocationPopup(false);
+ // Here you can save selected zone/ward if needed elsewhere
+ };
+
+ return (
+
+
+
+
+ {headerProps.showBackButton && headerProps.onBack ? (
+
(e.currentTarget as HTMLButtonElement).blur()}
+ onClick={headerProps.onBack}
+ aria-label="Back"
+ >
+
+
+ ) : !hideLocationIcon ? (
+
+
setShowLocationPopup(true)}
+ style={{ cursor: 'pointer' }}
+ aria-label="Location"
+ >
+
+
+
+
{jurisdictionText}
+
+
+ {zoneData[selectedZoneIndex]?.zoneNumber ?? ''}
+
+
+ {selectedWard}
+
+
+
+
+
+ {/* Location Popup */}
+ {showLocationPopup && (
+
+
+
+
+ Select Zone & Ward
+
+ setShowLocationPopup(false)}
+ >
+ ×
+
+
+
+
+ Zone
+ handleZoneSelect(Number(e.target.value))}
+ size="small"
+ sx={{ background: '#f5faff', borderRadius: 2, fontSize: 16 }}
+ >
+ {zoneData.map((zone, idx) => (
+
+ {zone.zoneNumber}
+
+ ))}
+
+
+
+ Ward
+ handleWardSelect(e.target.value as string)}
+ size="small"
+ sx={{ background: '#f5faff', borderRadius: 2, fontSize: 16 }}
+ >
+ {zoneData[selectedZoneIndex]?.wards?.map((ward) => (
+
+ {ward}
+
+ ))}
+
+
+
+
+ Confirm
+
+
+
+ )}
+
+ ) : null}
+
+
+ {headerProps.title && (
+
+ )}
+
+
+ {headerProps.showLanguage && (
+
+
+
+
+
+ {languageText}
+
+ {showLanguageDropdown && (
+
+ {languages.map((lang) => (
+
handleLanguageSelect(lang.code)}
+ style={{
+ backgroundColor:
+ locale === lang.code ? '#f0f0f0' : 'transparent',
+ fontWeight: locale === lang.code ? 'bold' : 'normal',
+ }}
+ >
+ {lang.name}
+
+ ))}
+
+ )}
+
+ )}
+ {headerProps.showHome && (
+
(e.currentTarget as HTMLElement).blur()}
+ >
+ (e.currentTarget as HTMLButtonElement).blur()}
+ onClick={headerProps.onHome}
+ aria-hidden={true}
+ >
+
+
+
+ )}
+ {headerProps.showProfile && (
+
+
(e.currentTarget as HTMLElement).blur()}
+ >
+
+
+
+ {profileText}
+
+ {showProfileDropdown && (
+
+ )}
+
+ )}
+
+
+ {children}
+
+ {showNavigation && (
+
+ )}
+
+ );
+};
+
+export default Layout;
diff --git a/frontend/mobile-ui/src/app/features/Agent/components/LocationPinMap.tsx b/frontend/mobile-ui/src/app/features/Agent/components/LocationPinMap.tsx
new file mode 100644
index 0000000..ac229dc
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/components/LocationPinMap.tsx
@@ -0,0 +1,242 @@
+
+// LocationPinMap component displays a Leaflet map with a fixed center pin and reverse geocoding
+import React, { useEffect, useRef } from 'react';
+import { MapContainer, TileLayer } from 'react-leaflet';
+import 'leaflet/dist/leaflet.css';
+import L from 'leaflet';
+
+// Fix for default markers not appearing in Leaflet
+delete (L.Icon.Default.prototype as any)._getIconUrl;
+L.Icon.Default.mergeOptions({
+ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
+ iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
+});
+
+
+// Props for LocationPinMap
+interface LocationPinMapProps {
+ center: [number, number]; // Initial center of the map
+ onLocationUpdate: (lat: number, lng: number, address: string) => void; // Callback when location changes
+}
+
+// Helper function to get reverse geocoded address with fallback
+// Tries Photon API, then a simple area lookup, then falls back to coordinates
+const getReverseGeocodedAddress = async (lat: number, lng: number): Promise => {
+ try {
+ // Use Photon API which is CORS-friendly
+ const response = await fetch(
+ `https://photon.komoot.io/reverse?lat=${lat}&lon=${lng}`,
+ {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json'
+ }
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.features && data.features.length > 0) {
+ const feature = data.features[0];
+ if (feature.properties) {
+ const props = feature.properties;
+ const parts = [];
+
+ // Build address from available components
+ if (props.housenumber) parts.push(props.housenumber);
+ if (props.street) parts.push(props.street);
+ if (props.district) parts.push(props.district);
+ if (props.city) parts.push(props.city);
+ if (props.state) parts.push(props.state);
+
+ if (parts.length > 0) {
+ return parts.join(', ');
+ }
+
+ // Fallback to name if available
+ if (props.name) {
+ return props.name;
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.warn('Photon geocoding failed, trying alternative:', error);
+ }
+
+ // Try alternative approach with a simple area lookup
+ try {
+ // For Indian coordinates, provide a basic area estimation
+ if (lat >= 8 && lat <= 37 && lng >= 68 && lng <= 97) {
+ // This is within India bounds
+ const areas = {
+ bangalore: { lat: 12.9716, lng: 77.5946, name: 'Bangalore, Karnataka' },
+ delhi: { lat: 28.6139, lng: 77.2090, name: 'Delhi' },
+ mumbai: { lat: 19.0760, lng: 72.8777, name: 'Mumbai, Maharashtra' },
+ chennai: { lat: 13.0827, lng: 80.2707, name: 'Chennai, Tamil Nadu' },
+ hyderabad: { lat: 17.3850, lng: 78.4867, name: 'Hyderabad, Telangana' },
+ pune: { lat: 18.5204, lng: 73.8567, name: 'Pune, Maharashtra' }
+ };
+
+ // Find closest major city
+ let closestArea = 'Unknown Area, India';
+ let minDistance = Infinity;
+
+ Object.values(areas).forEach(area => {
+ const distance = Math.sqrt(
+ Math.pow(lat - area.lat, 2) + Math.pow(lng - area.lng, 2)
+ );
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestArea = area.name;
+ }
+ });
+
+ return `Near ${closestArea}`;
+ }
+ } catch (error) {
+ console.warn('Area lookup failed:', error);
+ }
+
+ // Final fallback: return formatted coordinates
+ return `Location: ${lat.toFixed(4)}ยฐN, ${lng.toFixed(4)}ยฐE`;
+};
+
+
+const LocationPinMap: React.FC = ({ center, onLocationUpdate }) => {
+ // Reference to the Leaflet map instance
+ const mapRef = useRef(null);
+ // Reference for debouncing location updates
+ const debounceTimeoutRef = useRef(null);
+ // Reference to the map container div
+ const containerRef = useRef(null);
+
+ // ResizeObserver to handle map resizing when container size changes
+ useEffect(() => {
+ const resizeObserver = new ResizeObserver(() => {
+ if (mapRef.current) {
+ setTimeout(() => {
+ mapRef.current?.invalidateSize();
+ }, 100);
+ }
+ });
+
+ if (containerRef.current) {
+ resizeObserver.observe(containerRef.current);
+ }
+
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, []);
+
+ // Aggressively fix map size after mount
+ useEffect(() => {
+ const fixMapSize = () => {
+ if (mapRef.current) {
+ mapRef.current.invalidateSize(true);
+ mapRef.current.getContainer().style.height = '100%';
+ }
+ };
+
+ const intervals = [100, 250, 500, 1000];
+ const timeouts = intervals.map(delay => setTimeout(fixMapSize, delay));
+
+ return () => {
+ timeouts.forEach(clearTimeout);
+ };
+ }, []);
+
+ // Clean up debounce timeout on unmount
+ useEffect(() => {
+ return () => {
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ // Handler for when the map stops moving (user pans/zooms)
+ const handleMapMoveEnd = () => {
+ if (!mapRef.current) return;
+
+ // Get the center coordinates for location display (no pin needed)
+ const mapCenter = mapRef.current.getCenter();
+ const lat = mapCenter.lat;
+ const lng = mapCenter.lng;
+
+ // Clear previous timeout
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+
+ // Debounce the location update
+ debounceTimeoutRef.current = window.setTimeout(async () => {
+ if (!mapRef.current) return;
+
+ try {
+ // Use a more reliable geocoding approach
+ const address = await getReverseGeocodedAddress(lat, lng);
+ onLocationUpdate(lat, lng, address);
+ } catch (error) {
+ console.error('Error getting address:', error);
+ onLocationUpdate(lat, lng, `${lat.toFixed(6)}, ${lng.toFixed(6)}`);
+ }
+ }, 500); // 500ms debounce
+ };
+
+ // Handler for when the map is ready (mounted)
+ const handleMapReady = () => {
+ // Simplified map size fixing with new CSS overrides
+ setTimeout(() => {
+ if (mapRef.current) {
+ mapRef.current.invalidateSize(true);
+ }
+ }, 100);
+ setTimeout(() => {
+ if (mapRef.current) {
+ mapRef.current.invalidateSize(true);
+ }
+ }, 500);
+ };
+
+ return (
+
+ {/* Leaflet MapContainer with no visible marker, just a center pin overlay */}
+
{
+ if (mapInstance) {
+ mapRef.current = mapInstance;
+ mapInstance.on('moveend', handleMapMoveEnd);
+ handleMapReady();
+ }
+ }}
+ zoomControl={false}
+ attributionControl={false}
+ >
+
+
+
+ {/* Center Pin Overlay with Popup - Figma Design */}
+
+
+
+ Move the map to position the pin
+
+
+
+ );
+};
+
+export default LocationPinMap;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/components/Pagination.tsx b/frontend/mobile-ui/src/app/features/Agent/components/Pagination.tsx
new file mode 100644
index 0000000..e312d17
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/components/Pagination.tsx
@@ -0,0 +1,64 @@
+// Pagination component displays page navigation controls for lists with multiple pages.
+// Used in Agent screens to navigate between paginated data sets.
+import React from "react";
+import "../../../../styles/Pagination.css";
+
+// Props for Pagination:
+// - page: current page number
+// - totalPages: total number of pages
+// - onPageChange: callback for changing page
+interface PaginationProps {
+ page: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+}
+
+const Pagination: React.FC = ({ page, totalPages, onPageChange }) => {
+ // Only show pagination controls if there are at least 2 pages
+ if (totalPages < 2) return null;
+
+ return (
+
+ {/* Left Arrow: go to previous page, disabled on first page */}
+ onPageChange(page - 1)}
+ aria-label="Previous page"
+ >
+ <
+
+ {/* Current page indicator */}
+ {page}
+ {/* Next page button, shown if not on last page */}
+ {page < totalPages && (
+ onPageChange(page + 1)}>
+ {page + 1}
+
+ )}
+
+ {/* Ellipsis and last page button, shown if more than one page ahead */}
+ {page < totalPages - 1 && (
+ <>
+ ...
+ onPageChange(totalPages)}>
+ {totalPages}
+
+ >
+ )}
+
+ {/* Right Arrow: go to next page, disabled on last page */}
+ onPageChange(page + 1)}
+ aria-label="Next page"
+ >
+ >
+
+
+ );
+};
+
+// Export Pagination for use in Agent screens
+export default Pagination;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/components/SearchPropertyResultCard.tsx b/frontend/mobile-ui/src/app/features/Agent/components/SearchPropertyResultCard.tsx
new file mode 100644
index 0000000..2f7c359
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/components/SearchPropertyResultCard.tsx
@@ -0,0 +1,174 @@
+// SearchPropertyResultCard displays a property search result with status, address, and location map.
+// Used in Agent screens to show property details and allow viewing location on a map.
+import React from 'react';
+import { Box, Chip, Typography, Button } from '@mui/material';
+import OpenInNewOutlinedIcon from '@mui/icons-material/OpenInNewOutlined';
+import { MapContainer, TileLayer, Marker } from 'react-leaflet';
+import L from 'leaflet';
+
+// Custom SVG icon for map marker (room icon)
+const roomIconSvgString = ` `;
+const markerIcon = L.divIcon({
+ html: roomIconSvgString,
+ className: '',
+ iconSize: [18, 18],
+ iconAnchor: [9, 18],
+});
+
+// Props for SearchPropertyResultCard:
+// - id: property ID
+// - address: property address
+// - isVerified: property verification status
+// - highlight: optional highlight flag
+// - onViewLocation: callback for location view button
+export interface SearchPropertyResultCardProps {
+ id: string;
+ address: string;
+ isVerified: boolean;
+ highlight?: boolean;
+ latlng: { lat: number; lng: number };
+ onViewLocation?: () => void;
+}
+
+// Helper to get status label and colors based on verification
+const getStatusProps = (isVerified: boolean) => {
+ if (isVerified) {
+ return {
+ label: 'Verified',
+ color: '#00703C',
+ bg: '#F5F5F5',
+ };
+ }
+ return {
+ label: 'Pending',
+ color: '#A59400',
+ bg: '#FBEEE8',
+ };
+};
+
+const SearchPropertyResultCard: React.FC = ({
+ id,
+ address,
+ isVerified,
+ latlng,
+ onViewLocation,
+}) => {
+ // Get status label and colors
+ const status = getStatusProps(isVerified);
+
+ return (
+
+ {/* Left section: status, ID, address */}
+
+
+
+ {id}
+
+
+ Address
+
+
+ {address}
+
+
+ {/* Map and view location button */}
+
+
+
+
+
+
+
+ {/* Button to view location in detail */}
+ }
+ sx={{
+ backgroundColor: '#DBFAD3',
+ color: '#000',
+ fontWeight: 450,
+ fontSize: 10,
+ minWidth: 0,
+ borderRadius: '10px',
+ pb: 0.5,
+ }}
+ onClick={onViewLocation}
+ >
+ View Location
+
+
+
+ );
+};
+
+// Export SearchPropertyResultCard for use in Agent screens
+export default SearchPropertyResultCard;
diff --git a/frontend/mobile-ui/src/app/features/Agent/components/StepHeader.tsx b/frontend/mobile-ui/src/app/features/Agent/components/StepHeader.tsx
new file mode 100644
index 0000000..21cd231
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/components/StepHeader.tsx
@@ -0,0 +1,73 @@
+// StepHeader displays a multi-step header with title, subtitle, step progress, and navigation buttons.
+// Used in Agent property workflows to show progress and provide navigation actions.
+import React from 'react';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import '../../../../styles/Agent/StepHeader.css';
+import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
+
+// Props for StepHeader:
+// - title: main header title
+// - subtitle: subheader text
+// - steps: total number of steps (default 6)
+// - activeStep: current active step (default 0)
+// - onPrevious: callback for previous button
+// - previousText: label for previous button
+// - onSaveDraft: callback for save draft button
+// - saveDraftText: label for save draft button
+export interface StepHeaderProps {
+ title: string;
+ subtitle: string;
+ steps?: number;
+ activeStep?: number;
+ onPrevious?: () => void;
+ previousText?: string;
+ onSaveDraft?: () => void;
+ saveDraftText?: string;
+}
+
+const StepHeader: React.FC = ({
+ title,
+ subtitle,
+ steps = 6,
+ activeStep = 0,
+ onPrevious,
+ previousText = "Previous",
+ onSaveDraft,
+ saveDraftText = "Save Draft",
+}) => {
+ // Render step header with title, subtitle, step progress, and navigation buttons
+ return (
+
+ {/* Main header section */}
+
+ {/* Step progress indicator */}
+
+ {[...Array(steps)].map((_, i) => (
+
+ ))}
+
+ {/* Navigation buttons: Previous and Save Draft */}
+
+
+
+ {previousText}
+
+
+
+ {saveDraftText}
+
+
+
+ );
+};
+
+// Export StepHeader for use in Agent screens
+export default StepHeader;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/models/HomePageData.model.ts b/frontend/mobile-ui/src/app/features/Agent/models/HomePageData.model.ts
new file mode 100644
index 0000000..7949438
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/models/HomePageData.model.ts
@@ -0,0 +1,229 @@
+// This file defines TypeScript interfaces for Agent home page data models.
+// Used for typing API responses, property details, and related entities.
+//import type { GISDataResponse } from "../../../../redux/apis/gisApi";
+
+export interface Amenity {
+ ID: string;
+ Type: string;
+ Description: string;
+ ExpiryDate: string;
+ PropertyID: string;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Details for each floor in a property
+export interface FloorDetail {
+ ID: string;
+ FloorNo: number;
+ Classification: string;
+ NatureOfUsage: string;
+ FirmName: string;
+ OccupancyType: string;
+ OccupancyName: string;
+ constructionDate: string;
+ effectiveFromDate: string;
+ UnstructuredLand: string;
+ LengthFt: number;
+ BreadthFt: number;
+ PlinthAreaSqFt: number;
+ BuildingPermissionNo: string;
+ FloorDetailsEntered: boolean;
+ ConstructionDetailsID: string;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Construction details for a property, including all floors
+export interface ConstructionDetails {
+ ID: string;
+ FloorType: string;
+ WallType: string;
+ RoofType: string;
+ WoodType: string;
+ PropertyID: string;
+ FloorDetails: FloorDetail[];
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// GIS coordinate for mapping property location
+export interface GISCoordinate {
+ ID: string;
+ Latitude: number;
+ Longitude: number;
+ GISDataID: string;
+ CreatedAt: string;
+}
+
+// GIS data for a property, including coordinates
+export interface GISData {
+ ID: string;
+ Source: string;
+ Type: string;
+ EntityType: string;
+ PropertyID: string;
+ Coordinates: GISCoordinate[];
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Document metadata for property files
+export interface Document {
+ ID: string;
+ PropertyID: string;
+ DocumentType: string;
+ DocumentName: string;
+ FileStoreID: string | null;
+ UploadDate: string;
+}
+
+// Suggestion for Address interface
+// Address details for a property
+export interface Address {
+ ID: string;
+ Locality: string;
+ ZoneNo: string;
+ WardNo: string;
+ BlockNo: string;
+ Street: string;
+ ElectionWard: string;
+ SecretariatWard: string;
+ PinCode: number;
+ DifferentCorrespondenceAddress: boolean;
+ PropertyID: string;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Assessment details for a property
+export interface AssessmentDetails {
+ ID: string;
+ PropertyID: string;
+ ExtendOfSite: string;
+ IsLandUnderneathBuilding: boolean;
+ IsUnspecifiedShare: boolean;
+ OccupancyCertificateDate: string;
+ OccupancyCertificateNumber: string;
+ ReasonOfCreation: string;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Additional details for a property, including amenities and certifications
+export interface AdditionalDetails {
+ ID: string;
+ FieldName: string;
+ fieldValue: {
+ amenities: string[];
+ certification: {
+ earthquake_resistant: boolean;
+ fire_safety: string;
+ green_building: boolean;
+ };
+ construction_year: number;
+ elevator: boolean;
+ floors: number;
+ security: {
+ access_control: string;
+ cctv: boolean;
+ guard: string;
+ };
+ units_per_floor: number;
+ };
+ PropertyID: string;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Application data for property workflows
+export interface Application {
+ ID: string;
+ ApplicationNo: string;
+ PropertyID: string;
+ Priority: string;
+ TenantID: string;
+ DueDate: string;
+ AssignedAgent: string;
+ Status: string;
+ WorkflowInstanceID: string;
+ AppliedBy: string;
+ AssesseeID: string;
+ Property: PropertyDetails;
+ ApplicationLogs: any | null;
+ IsDraft: boolean;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// IGRS details for property registration
+export interface IGRS {
+ id: string;
+ habitation: string;
+ igrsWard: string;
+ igrsLocality: string;
+ igrsBlock: string;
+ doorNoFrom: string;
+ doorNoTo: string;
+ igrsClassification: string;
+ builtUpAreaPct: number;
+ frontSetback: number;
+ rearSetback: number;
+ sideSetback: number;
+ totalPlinthArea: number;
+ createdAt: string;
+ updatedAt: string;
+ PropertyID: string;
+}
+
+// Main property details object, includes all related entities
+export interface PropertyDetails {
+ ID: string;
+ PropertyNo: string;
+ OwnershipType: string;
+ PropertyType: string;
+ ComplexName: string;
+ Address: Address | null;
+ AssessmentDetails: AssessmentDetails | null;
+ Amenities: Amenity | null;
+ ConstructionDetails: ConstructionDetails | null;
+ AdditionalDetails: AdditionalDetails | null;
+ GISData:{
+ ID: string;
+ Source: string;
+ Type: string;
+ EntityType: string;
+ PropertyID: string;
+ Latitude: number;
+ Longitude: number;
+ Coordinates: Array<{
+ ID: string;
+ Latitude: number;
+ Longitude: number;
+ GISDataID: string;
+ CreatedAt: string;
+ }>;
+ CreatedAt: string;
+ UpdatedAt: string;
+ } | null;
+ CreatedAt: string;
+ UpdatedAt: string;
+ Documents: Document[];
+ IGRS: IGRS | null;
+}
+
+// Pagination info for paginated API responses
+export interface PaginationInfo {
+ page: number;
+ size: number;
+ totalItems: number;
+ totalPages: number;
+}
+
+// API response for Agent home page property list
+export interface HomePageResponse {
+ data: Application[];
+ message: string;
+ pagination: PaginationInfo;
+ success: boolean;
+}
diff --git a/frontend/mobile-ui/src/app/features/Agent/models/PropertyInfoSubmittedData.model.ts b/frontend/mobile-ui/src/app/features/Agent/models/PropertyInfoSubmittedData.model.ts
new file mode 100644
index 0000000..a09c187
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/models/PropertyInfoSubmittedData.model.ts
@@ -0,0 +1,49 @@
+// This file defines TypeScript interfaces for submitted property info data in Agent workflows.
+// Used for typing form submissions, property details, usage, assessment, and documents.
+import type { Owner } from "../../Citizen/models/Owner.model";
+
+// Main data structure for submitted property info
+export interface PropertyInfoSubmittedData {
+ propertyId: string;
+ owner: Owner;
+ propertyDetails: PropertyDetails;
+ usageDetails: UsageDetails;
+ assessmentDetails: AssessmentDetails;
+ documents: Document[];
+}
+
+// Basic property details (type, location, area)
+export interface PropertyDetails {
+ propertyType: string;
+ zoneWard: string;
+ doorNo: string;
+ plotArea: string;
+}
+
+// Usage details for property (survey, GIS, cadastral info)
+export interface UsageDetails {
+ surveyNumber: string;
+ gisDelination: string;
+ gisReference: string;
+ cadastralMap: string;
+ gisRegistration: string;
+}
+
+// Assessment details for property (construction, usage, tax zone)
+export interface AssessmentDetails {
+ constructionYear: number;
+ buildingUsage: string;
+ builtUpArea: string;
+ floorCount: number;
+ propertyTaxZone: string;
+ assessmentStatus: string;
+ notes: string;
+}
+
+// Document metadata for submitted files
+export interface Document {
+ id: string;
+ name: string;
+ type: string;
+ url?: string;
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/models/ReviewPage.model.ts b/frontend/mobile-ui/src/app/features/Agent/models/ReviewPage.model.ts
new file mode 100644
index 0000000..20dc576
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/models/ReviewPage.model.ts
@@ -0,0 +1,58 @@
+// This file defines TypeScript interfaces for Agent review page data models.
+// Used for typing API responses, property details, and application items for review workflows.
+import type {Address, AssessmentDetails, ConstructionDetails, GISData, Amenity, Document, AdditionalDetails} from "./HomePageData.model";
+
+// Property details for review page, includes all related entities
+export interface PropertyDetails {
+ ID: string;
+ PropertyNo: string;
+ OwnershipType: string;
+ PropertyType: string;
+ ComplexName: string;
+ Address: Address;
+ AssessmentDetails: AssessmentDetails;
+ Amenities: Amenity[];
+ ConstructionDetails: ConstructionDetails;
+ AdditionalDetails: AdditionalDetails;
+ GISData: GISData;
+ CreatedAt: string;
+ UpdatedAt: string;
+ Documents: Document;
+ IGRS: any;
+}
+
+// Application item for review workflows
+export interface ApplicationItem {
+ ID: string;
+ ApplicationNo: string;
+ PropertyID: string;
+ Priority: string;
+ TenantID: string;
+ DueDate: string;
+ AssignedAgent: string;
+ Status: string;
+ WorkflowInstanceID: string;
+ AppliedBy: string;
+ AssesseeID: string;
+ Property: PropertyDetails;
+ ApplicationLogs: any;
+ IsDraft: boolean;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Pagination info for paginated API responses
+export interface PaginationInfo {
+ page: number;
+ size: number;
+ totalItems: number;
+ totalPages: number;
+}
+
+// API response for Agent review page property list
+export interface ApplicationsResponse {
+ data: ApplicationItem[];
+ message: string;
+ pagination: PaginationInfo;
+ success: boolean;
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/models/ReviewedPropertyData.model.ts b/frontend/mobile-ui/src/app/features/Agent/models/ReviewedPropertyData.model.ts
new file mode 100644
index 0000000..01da7b0
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/models/ReviewedPropertyData.model.ts
@@ -0,0 +1,8 @@
+// This file defines TypeScript interfaces for reviewed property data in Agent workflows.
+// Used for typing lists of properties that have been reviewed by the agent.
+import type { Property } from "../../../../models/Citizen/Property";
+
+// Main data structure for reviewed properties
+export interface ReviewedPropertyData {
+ reviewedProperties: Property[];
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/models/SearchPropertyData.model.ts b/frontend/mobile-ui/src/app/features/Agent/models/SearchPropertyData.model.ts
new file mode 100644
index 0000000..3d68a5c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/models/SearchPropertyData.model.ts
@@ -0,0 +1,121 @@
+// Represents the address details of a property
+export interface Address {
+ ID: string; // Unique identifier for the address
+ Locality: string; // Locality or neighborhood
+ ZoneNo: string; // Zone number
+ WardNo: string; // Ward number
+ BlockNo: string; // Block number
+ Street: string; // Street name
+ ElectionWard: string; // Election ward
+ SecretariatWard: string; // Secretariat ward
+ PinCode: number; // Postal code
+ DifferentCorrespondenceAddress: boolean; // If correspondence address is different
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+ correspondenceAddress1: string; // Correspondence address line 1
+ correspondenceAddress2: string; // Correspondence address line 2
+ correspondenceAddress3: string; // Correspondence address line 3
+}
+
+// Details related to property assessment
+export interface AssessmentDetails {
+ ID: string; // Unique identifier
+ ReasonOfCreation: string; // Reason for assessment creation
+ OccupancyCertificateNumber: string; // Occupancy certificate number
+ OccupancyCertificateDate: string; // Date of occupancy certificate
+ ExtentOfSite: string; // Site area/extent
+ IsLandUnderneathBuilding: string; // Whether land is underneath building
+ IsUnspecifiedShare: boolean; // If property has unspecified share
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Construction-related details of the property
+export interface ConstructionDetails {
+ ID: string; // Unique identifier
+ FloorType: string; // Type of floor
+ WallType: string; // Type of wall
+ RoofType: string; // Type of roof
+ WoodType: string; // Type of wood used
+ PropertyID: string; // Linked property ID
+ FloorDetails: any; // Details about each floor (structure may vary)
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Any additional custom details for the property
+export interface AdditionalDetails {
+ ID: string; // Unique identifier
+ FieldName: string; // Name of the additional field
+ fieldValue: any; // Value of the field
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Main property data model
+export interface PropertyData {
+ ID: string; // Unique property identifier
+ PropertyNo: string; // Property number
+ OwnershipType: string; // Type of ownership
+ PropertyType: string; // Type/category of property
+ ComplexName: string; // Name of the complex (if any)
+ Address: Address; // Address details
+ AssessmentDetails: AssessmentDetails | null; // Assessment details
+ Amenities: any; // Amenities available (structure may vary)
+ ConstructionDetails: ConstructionDetails | null; // Construction details
+ AdditionalDetails: AdditionalDetails | null; // Additional custom details
+ GISData: any; // Geographic Information System data
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+ Documents: any; // Related documents
+ IGRS: any; // Integrated Grievance Redressal System or similar
+}
+
+// Represents an application related to a property (e.g., assessment, mutation)
+export interface ApplicationData {
+ ID: string; // Unique application identifier
+ ApplicationNo: string; // Application number
+ PropertyID: string; // Linked property ID
+ Priority: string; // Priority of the application
+ TenantID: string; // Tenant or jurisdiction ID
+ DueDate: string; // Due date for processing
+ AssignedAgent: string; // Agent assigned to the application
+ Status: string; // Current status
+ WorkflowInstanceID: string; // Workflow instance identifier
+ AppliedBy: string; // User who applied
+ AssesseeID: string; // Assessee (taxpayer) ID
+ Property: PropertyData; // Property details
+ ApplicationLogs: any; // Logs related to the application
+ IsDraft: boolean; // Whether the application is a draft
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Pagination information for paginated responses
+export interface Pagination {
+ page: number; // Current page number
+ size: number; // Number of items per page
+ totalItems: number; // Total number of items
+ totalPages: number; // Total number of pages
+}
+
+// Response structure for property search API
+export interface SearchPropertyResponse {
+ data: ApplicationData[]; // List of application data
+ message: string; // Response message
+ pagination: Pagination; // Pagination info
+ success: boolean; // Success status
+}
+
+// Parameters for searching properties
+export interface SearchPropertyParams {
+ zoneNo?: string; // Filter by zone number
+ wardNo?: string; // Filter by ward number
+ page?: number; // Page number for pagination
+ size?: number; // Page size for pagination
+ sortBy?: string; // Field to sort by
+ sortOrder?: 'asc' | 'desc'; // Sort order
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Agent/utils/propertyMapper.ts b/frontend/mobile-ui/src/app/features/Agent/utils/propertyMapper.ts
new file mode 100644
index 0000000..fc5d83c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Agent/utils/propertyMapper.ts
@@ -0,0 +1,37 @@
+// Utility to map Application model to PropertyItem type for Agent screens.
+// Used to transform API data into frontend-friendly format for property lists.
+import type { Application } from '../models/HomePageData.model';
+import type { PropertyItem } from '../../../../types';
+
+// Maps an Application object to a PropertyItem for display in property lists.
+export const mapApplicationToPropertyItem = (application: Application): PropertyItem => {
+ const property = application.Property;
+
+
+ // Return mapped property item with normalized fields
+ return {
+ id: application.ID,
+ pId: application.ApplicationNo,
+ propertyNo: property.PropertyNo || 'N/A',
+ type: property.PropertyType || 'RESIDENTIAL',
+ description: `${property.PropertyType} - ${property.OwnershipType}`,
+ address: property.Address
+ ? `${property.Address.Street}, ${property.Address.Locality}`
+ : 'Address not available',
+ phoneNumber: '', // Not available in Application/Property data
+ area: property.AssessmentDetails?.ExtendOfSite || 'N/A',
+ propertyType: property.PropertyType || 'RESIDENTIAL',
+ status: application.Priority || 'MEDIUM',
+ dueDate: application.DueDate || new Date().toISOString(),
+ // isNew: application.Status === 'ASSIGNED',
+ isNew: (application.Status === 'ASSIGNED'),
+ isDraft: application.IsDraft === true,
+ isVerified: application.Status === 'VERIFIED' || application.Status === 'APPROVED' || application.Status === 'AUDIT_VERIFIED',
+ createdDate: application.CreatedAt,
+ gisData: property.GISData ? {
+ latitude: property.GISData.Latitude,
+ longitude: property.GISData.Longitude,
+ coordinates: property.GISData.Coordinates
+ } : undefined,
+ };
+};
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/api/Citizen.api.ts b/frontend/mobile-ui/src/app/features/Citizen/api/Citizen.api.ts
new file mode 100644
index 0000000..688496a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/api/Citizen.api.ts
@@ -0,0 +1,63 @@
+// import { createApi } from '@reduxjs/toolkit/query/react';
+// import type { CitizenHomeData } from '../models/CitizenHome.model';
+// import type { Property } from '../models/Property.model';
+// import type { UrgentAttention } from '../models/UrgentAttention.model';
+// import customBaseQuery from '../../../service/BaseQuery.api';
+
+// export const citizenApi = createApi({
+// reducerPath: 'citizenApi',
+// baseQuery: customBaseQuery,
+// tagTypes: ['Properties', 'CitizenHome'],
+// endpoints: (builder) => ({
+// getCitizenHome: builder.query({
+// query: () => `/api/v1/citizen/home`,
+// transformResponse: (response: any) => {
+// const homeJson = response?.home ?? {};
+
+// const user = homeJson.user ?? {};
+// const activeLicenses = user.activeLicenses ?? 0;
+// const numberOfProperties = user.numberOfProperties ?? 0;
+
+// const properties: Property[] = (homeJson.properties ?? []).map((prop: any) => ({
+// id: prop.id,
+// apartmentName: prop.apartmentName ?? '',
+// address: (prop.address as any) ?? '',
+// enumerationProgress: prop.enumerationProgress ?? 0,
+// locationData: prop.locationData ?? { address: '', coordinates: { lat: 0, lng: 0 }, timestamp: '' },
+// }));
+
+// const urgentAttention: UrgentAttention[] = (homeJson.urgentAttention ?? []).map((item: any) => ({
+// id: item.id,
+// type: item.type,
+// message: item.message,
+// date: item.date,
+// status: item.status,
+// }));
+
+// return { activeLicenses, numberOfProperties, properties, urgentAttention };
+// },
+// providesTags: ['CitizenHome'],
+// }),
+
+// getProperties: builder.query({
+// query: () => `/api/v1/citizen/properties`,
+// transformResponse: (response: any) => {
+// if (response == null) return [];
+// if (response && 'properties' in response) {
+// return response.properties as Property[];
+// } else {
+// return response as Property[];
+// }
+// },
+// providesTags: (result) =>
+// result
+// ? [
+// ...result.map(({ id }) => ({ type: 'Properties' as const, id })),
+// { type: 'Properties', id: 'LIST' },
+// ]
+// : [{ type: 'Properties', id: 'LIST' }],
+// }),
+// }),
+// });
+
+// export const { useGetCitizenHomeQuery, useGetPropertiesQuery } = citizenApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/api/CitizenDocumentPageApi/fileStoreApi.ts b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenDocumentPageApi/fileStoreApi.ts
new file mode 100644
index 0000000..c03c140
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenDocumentPageApi/fileStoreApi.ts
@@ -0,0 +1,52 @@
+// fileStoreApi defines API endpoints for file upload and retrieval for Citizen document workflows.
+// Uses Redux Toolkit Query to provide hooks for file blob fetching and uploading to filestore.
+import { filestoreApiSlice } from '../../../../../redux/fileStoreApiSlice';
+import type { UploadResult } from '../../models/CitizenDocumentPageModel/fileStoreModel';
+
+// Response type for file URL lookup
+export interface FileUrlResponse {
+ url: string;
+ fileStoreId: string;
+}
+
+// Inject endpoints for file blob retrieval and file upload
+export const filestoreApi = filestoreApiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ // Fetch file directly as blob (not URL)
+ getFileBlob: builder.query(
+ {
+ query: ({ fileStoreId, tenantId = "pg" }) => ({
+ url: `/filestore/v1/files/${fileStoreId}?tenantId=${tenantId}`,
+ method: 'GET',
+ responseHandler: (response) => response.blob(), // Get blob directly
+ }),
+ }
+ ),
+
+ // Upload file to filestore using multipart/form-data
+ uploadFileToFilestore: builder.mutation(
+ {
+ query: ({ file, tenantId = "pg", module = "HCM-ADMIN-CONSOLE-CLIENT", tag = "test" }) => {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('tenantId', tenantId);
+ formData.append('module', module);
+ formData.append('tag', tag);
+
+ return {
+ url: '/filestore/v1/files',
+ method: 'POST',
+ body: formData,
+ };
+ },
+ }
+ ),
+ }),
+});
+
+// Export RTK Query hooks for file blob and upload operations
+export const {
+ useGetFileBlobQuery,
+ useLazyGetFileBlobQuery,
+ useUploadFileToFilestoreMutation,
+} = filestoreApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/api/CitizenHomePageApi/CitizenHomePageApi.ts b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenHomePageApi/CitizenHomePageApi.ts
new file mode 100644
index 0000000..bde6020
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenHomePageApi/CitizenHomePageApi.ts
@@ -0,0 +1,28 @@
+// CitizenHomePageApi defines API endpoints for fetching property applications for the Citizen homepage.
+// Uses Redux Toolkit Query to provide hooks for searching applications by assesseeId and draft status.
+import { apiSlice } from '../../../../../redux/apiSlice';
+import type { CitizenApplicationSearchResponse } from './CitizenHomePageModel';
+
+// Inject endpoint for searching citizen applications (homepage)
+export const citizenHomepageApi = apiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ // Fetch applications for 'homepage' by assesseeId & isDraft
+ getCitizenApplications: builder.query<
+ CitizenApplicationSearchResponse,
+ { assesseeId: string; isDraft?: boolean }
+ >({
+ query: ({ assesseeId, isDraft=true }) => ({
+ url: `/v1/applications/search`,
+ params: {
+ assesseeId,
+ isDraft,
+ },
+ method: 'GET',
+ }),
+ }),
+ }),
+});
+
+// Export RTK Query hook for fetching citizen applications
+export const { useGetCitizenApplicationsQuery } = citizenHomepageApi;
+export default citizenHomepageApi;
diff --git a/frontend/mobile-ui/src/app/features/Citizen/api/CitizenHomePageApi/CitizenHomePageModel.ts b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenHomePageApi/CitizenHomePageModel.ts
new file mode 100644
index 0000000..d5ad4f8
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenHomePageApi/CitizenHomePageModel.ts
@@ -0,0 +1,86 @@
+// Model definitions for Citizen homepage application search API response.
+// Each interface represents a part of the response structure for property applications.
+// Used by CitizenHomePageApi to type API data and enable type-safe access in components.
+//
+// Top-level response from /v1/applications/search?assesseeId=...&isDraft=...
+export interface CitizenApplicationSearchResponse {
+ data: CitizenApplicationSummary[];
+ message: string;
+ pagination: {
+ page: number;
+ size: number;
+ totalItems: number;
+ totalPages: number;
+ };
+ success: boolean;
+}
+
+// Application summary for each property application returned in the data array
+export interface CitizenApplicationSummary {
+ ID: string;
+ ApplicationNo: string;
+ PropertyID: string;
+ Priority: string;
+ TenantID: string;
+ DueDate: string;
+ AssignedAgent: string;
+ Status: string;
+ WorkflowInstanceID: string;
+ AppliedBy: string;
+ AssesseeID: string;
+ Property: CitizenPropertySummary;
+ ApplicationLogs: any;
+ IsDraft: boolean;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Property details nested within each application summary
+export interface CitizenPropertySummary {
+ ID: string;
+ PropertyNo: string;
+ OwnershipType: string;
+ PropertyType: string;
+ ComplexName: string;
+ Address: CitizenPropertyAddress;
+ AssessmentDetails: any;
+ Amenities: any[];
+ ConstructionDetails: any;
+ AdditionalDetails: any;
+ GISData: any;
+ CreatedAt: string;
+ UpdatedAt: string;
+ Documents: CitizenPropertyDocument[];
+ IGRS: any;
+ __appId: string;
+}
+
+// Address details for a property, nested within CitizenPropertySummary
+export interface CitizenPropertyAddress {
+ ID: string;
+ Locality: string;
+ ZoneNo: string;
+ WardNo: string;
+ BlockNo: string;
+ Street: string;
+ ElectionWard: string;
+ SecretariatWard: string;
+ PinCode: number;
+ DifferentCorrespondenceAddress: boolean;
+ PropertyID: string;
+ CreatedAt: string;
+ UpdatedAt: string;
+}
+
+// Document metadata for property documents, used in CitizenPropertySummary.Documents
+export interface CitizenPropertyDocument {
+ ID: string;
+ PropertyID: string;
+ DocumentType: string;
+ DocumentName: string;
+ FileStoreID: string;
+ UploadDate: string;
+ action: string;
+ uploadedBy: string;
+ size: string;
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/api/CitizenPropertiesPageApi/CitizenPropertiesPageApi.ts b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenPropertiesPageApi/CitizenPropertiesPageApi.ts
new file mode 100644
index 0000000..1773f36
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenPropertiesPageApi/CitizenPropertiesPageApi.ts
@@ -0,0 +1,28 @@
+// CitizenPropertiesPageApi defines API endpoints for fetching property applications for the Citizen properties page.
+// Uses Redux Toolkit Query to provide hooks for searching applications by assesseeId and draft status.
+import { apiSlice } from '../../../../../redux/apiSlice';
+import type { CitizenApplicationSearchResponse } from '../CitizenHomePageApi/CitizenHomePageModel';
+
+// Inject endpoint for searching citizen properties (properties page)
+export const citizenPropertiesPageApi = apiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ // Fetch applications for 'homepage' by assesseeId & isDraft
+ getCitizenProperties: builder.query<
+ CitizenApplicationSearchResponse,
+ { assesseeId: string; Status: string; isDraft: boolean }
+ >({
+ query: ({ assesseeId, isDraft=true }) => ({
+ url: `/v1/applications/search`,
+ params: {
+ assesseeId,
+ isDraft,
+ },
+ method: 'GET',
+ }),
+ }),
+ }),
+});
+
+// Export RTK Query hook for fetching citizen properties
+export const { useGetCitizenPropertiesQuery } = citizenPropertiesPageApi;
+export default citizenPropertiesPageApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/api/CitizenPropertiesPageApi/CitizenPropertyPageApi.ts b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenPropertiesPageApi/CitizenPropertyPageApi.ts
new file mode 100644
index 0000000..2db6faa
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/api/CitizenPropertiesPageApi/CitizenPropertyPageApi.ts
@@ -0,0 +1,24 @@
+// CitizenPropertyPageApi defines API endpoints for fetching a single property by propertyId for the Citizen property details page.
+// Uses Redux Toolkit Query to provide hooks for retrieving property details.
+import { apiSlice } from '../../../../../redux/apiSlice';
+import type { CitizenPropertyResponse } from '../../models/CitizenPropertiesPageModel/CitizenPropertyPageModel';
+
+// Inject endpoint for fetching a single citizen property by ID
+export const citizenPropertyPageApi = apiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ // Fetch single property by propertyId
+ getCitizenPropertyById: builder.query<
+ CitizenPropertyResponse,
+ { propertyId: string }
+ >({
+ query: ({ propertyId }) => ({
+ url: `/v1/properties/${propertyId}`,
+ method: 'GET',
+ }),
+ }),
+ }),
+});
+
+// Export RTK Query hook for fetching a single citizen property by ID
+export const { useGetCitizenPropertyByIdQuery } = citizenPropertyPageApi;
+export default citizenPropertyPageApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/api/Property.slice.ts b/frontend/mobile-ui/src/app/features/Citizen/api/Property.slice.ts
new file mode 100644
index 0000000..4bd82a4
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/api/Property.slice.ts
@@ -0,0 +1,26 @@
+// import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
+// import type { Property } from '../models/Property.model';
+// import type { PropertyState } from '../models/PropertyState.model';
+
+// const initialState: PropertyState = {
+// selectedProperty: null,
+// };
+
+// const propertySlice = createSlice({
+// name: 'property',
+// initialState,
+// reducers: {
+// setSelectedProperty(state, action: PayloadAction) {
+// state.selectedProperty = action.payload;
+// },
+// clearSelectedProperty(state) {
+// state.selectedProperty = null;
+// },
+// },
+// });
+
+// export const { setSelectedProperty, clearSelectedProperty } = propertySlice.actions;
+// export default propertySlice.reducer;
+
+
+///
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/ChromeTabs.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/ChromeTabs.tsx
new file mode 100644
index 0000000..e159a61
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/ChromeTabs.tsx
@@ -0,0 +1,128 @@
+// ChromeTabs.tsx displays a tab switcher UI for toggling between Map and List views on the Citizen homepage.
+// It uses MUI for UI, localization for tab labels, and styled Button for custom tab appearance.
+// Main responsibilities:
+// - Render two tabs (Map, List) with dynamic selection
+// - Pass selected tab index and change handler via props
+// - Use localization for tab labels
+// Props:
+// selected (number): index of selected tab
+// onTabChange (function): callback for tab change
+// height, customWidth, selectedColor (optional): style overrides
+import React from 'react';
+import { Box, Button, styled } from '@mui/material';
+import { useAppSelector } from '../../../../redux/Hooks';
+import { getMessagesFromSession, useLocalization } from '../../../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from '../../../components/Loader';
+// Props for ChromeTabs: selected tab index, change handler, style overrides
+interface ChromeTabsProps {
+ selected: number;
+ onTabChange: (idx: number) => void;
+ selectedColor?: string;
+ height?: number;
+ customWidth?: string | number;
+}
+
+const SELECTED_BG = "#DFDFDF";
+const UNSELECTED_BG = "#fff";
+const SELECTED_COLOR = "#222";
+const UNSELECTED_COLOR = "#222";
+const BORDER_COLOR = "#222";
+
+// ChromeTabs component: renders two tabs (Map, List) with selection and localization
+const ChromeTabs: React.FC = ({
+ selected,
+ onTabChange,
+ height = 56,
+ customWidth,
+}) => {
+ const lang = useAppSelector(state => state.lang.citizenLang); // Current language
+ const { loading } = useLocalization(); // Global loading state
+ const messages = getMessagesFromSession("CITIZEN")!; // Localized messages
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ // Render tab switcher UI
+ return (
+
+ onTabChange(0)}
+ >
+ {messages['citizen.home'][lang]?.['map-btn'] ?? "Map"}
+
+ onTabChange(1)}
+ >
+ {messages['citizen.home'][lang]?.['list-btn'] ?? "List"}
+
+
+ );
+};
+
+// TabButton: styled MUI Button for tab appearance and selection
+const TabButton = styled(Button, {
+ shouldForwardProp: (prop) => (
+ prop !== '$isSelected' &&
+ prop !== '$height' &&
+ prop !== '$selectedTab' &&
+ prop !== '$borderRadius'
+ )
+})<{
+ $isSelected: boolean;
+ $height: number;
+ $selectedTab: number;
+ $borderRadius: string;
+}>(
+ ({ $isSelected, $height, $borderRadius, theme }) => ({
+ flex: 1, // Each tab fills half the ChromeTabs container
+ height: $height,
+ padding: theme.spacing(0, 2),
+ fontWeight: 600,
+ borderRadius: $borderRadius,
+ fontSize: 18,
+ textTransform: 'none',
+ backgroundColor: $isSelected ? SELECTED_BG : UNSELECTED_BG,
+ color: $isSelected ? SELECTED_COLOR : UNSELECTED_COLOR,
+ border: 'none',
+ borderBottom: $isSelected ? `2px solid ${SELECTED_BG}` : `2px solid transparent`,
+ boxShadow: 'none',
+ margin: 0,
+ letterSpacing: '0.3px',
+ '&:hover': {
+ backgroundColor: $isSelected ? SELECTED_BG : "#F6F6F6"
+ },
+ '&:focus-visible': {
+ outline: `2px solid ${SELECTED_BG}`,
+ outlineOffset: 2
+ },
+ '&:not(:first-of-type)': {
+ marginLeft: 0
+ }
+ })
+);
+
+// Export ChromeTabs for use in Citizen homepage
+export default ChromeTabs;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/EnumOptComponent.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/EnumOptComponent.tsx
new file mode 100644
index 0000000..8ae40cd
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/EnumOptComponent.tsx
@@ -0,0 +1,108 @@
+// EnumOptComponent.tsx displays either enumerated info cards or a progress bar for property enumeration status.
+// It uses MUI for UI, localization for labels, and shows last updated date.
+// Main responsibilities:
+// - Show EnumeratedInfo cards if progress is 100%
+// - Otherwise, show progress bar and last updated info
+// - Use localization for labels/messages
+// Props: property (CitizenPropertyData) - the property whose enumeration status is shown
+import type { FC } from 'react';
+import EnumeratedInfo from './EnumeratedInfo';
+import { Box, LinearProgress, Paper, Typography } from '@mui/material';
+import { useAppSelector } from '../../../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from '../../../components/Loader';
+import type { CitizenPropertyData } from '../models/CitizenPropertiesPageModel/CitizenPropertyPageModel';
+
+interface ExtendedPropertyData extends CitizenPropertyData {
+ enumerationProgress?: number;
+ isDraft?: boolean;
+ applicationStatus?: string;
+}
+
+interface EnumOptComponentProps {
+ property: ExtendedPropertyData;
+}
+
+// EnumOptComponent: shows enumerated info or progress bar for property enumeration
+const EnumOptComponent: FC = ({ property }) => {
+ // Localization, state, and hooks
+ const lang = useAppSelector((state) => state.lang.citizenLang); // Current language
+ const { loading } = useLocalization(); // Global loading state
+ const messages = getMessagesFromSession('CITIZEN')!; // Localized messages
+
+ // No property: render nothing
+ if (!property) return null;
+
+ // Get enumeration progress from property
+ // Default to 50 if not provided and not a draft
+ let progress = 50;
+
+ if (property.enumerationProgress !== undefined) {
+ // If enumerationProgress is -1, it means draft, show 0% progress
+ if (property.enumerationProgress === -1) {
+ progress = 0;
+ } else {
+ console.log(property.enumerationProgress);
+
+ progress = property.enumerationProgress;
+ }
+ }
+
+ // Get last updated date from property
+ const lastUpdated = property.UpdatedAt
+ ? new Date(property.UpdatedAt).toLocaleDateString()
+ : '';
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ // Render enumerated info cards if progress is 100%, else show progress bar
+ return progress === 100 ? (
+ {
+ /* handle view bills */
+ }}
+ onViewLicenses={() => {
+ /* handle view licenses */
+ }}
+ />
+ ) : (
+
+
+ {messages['citizen.my-properties'][lang]['enum-progress']}
+
+
+
+ {progress}%
+
+ {lastUpdated && (
+
+ {messages['citizen.commons'][lang]['last-updated']}: {lastUpdated}
+
+ )}
+
+ );
+};
+
+// Export EnumOptComponent for use in Citizen property details
+export default EnumOptComponent;
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/EnumeratedInfo.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/EnumeratedInfo.tsx
new file mode 100644
index 0000000..5a6c216
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/EnumeratedInfo.tsx
@@ -0,0 +1,139 @@
+// EnumeratedInfo.tsx displays summary cards for bills due and issued licenses on the Citizen dashboard.
+// It uses MUI for UI, localization for labels, and provides buttons to view bills and licenses.
+// Main responsibilities:
+// - Render two cards: bills due and issued licenses
+// - Show total amount and count
+// - Provide buttons for navigation to bills/licenses
+// Props:
+// billsDue (number): count of bills due
+// billsAmount (number): total amount due
+// issuedLicenses (number): count of issued licenses
+// onViewBills, onViewLicenses (function, optional): navigation handlers
+import React from "react";
+import { Box, Button, Typography, Paper } from "@mui/material";
+import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
+import { useAppSelector } from "../../../../redux/Hooks";
+import { getMessagesFromSession, useLocalization } from "../../../../services/Citizen/Localization/LocalizationContext";
+import LoadingPage from "../../../components/Loader";
+
+
+// Props for EnumeratedInfo: bills/amount/licenses and navigation handlers
+interface EnumeratedInfoProps {
+ billsDue: number;
+ billsAmount: number;
+ issuedLicenses: number;
+ onViewBills?: () => void;
+ onViewLicenses?: () => void;
+}
+
+// EnumeratedInfo component: renders summary cards for bills and licenses
+const EnumeratedInfo: React.FC = ({
+ billsAmount,
+ issuedLicenses,
+ onViewBills,
+ onViewLicenses
+}) => {
+ const lang = useAppSelector(state => state.lang.citizenLang); // Current language
+ const { loading } = useLocalization(); // Global loading state
+ const messages = getMessagesFromSession("CITIZEN")!; // Localized messages
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ // Render summary cards for bills and licenses
+ return (
+
+ {/* Bills Card */}
+
+
+ {messages['citizen.my-properties'][lang]['bills-due']}:
+
+
+ {/* {messages['citizen.my-properties'][lang]['total-amount']}: */}
+ Total Amount :
+
+
+ โน{billsAmount?.toLocaleString?.() || billsAmount}
+
+ }
+ >
+ {/* {messages['citizen.my-properties'][lang]['view-bills']} */}
+ View Bills
+
+
+ {/* Licenses Card */}
+
+
+ Issued Licenses:
+
+
+ {issuedLicenses}
+
+ }
+ >
+ {/* {messages['citizen.my-properties'][lang]['view-licenses']} */}
+ View Licences
+
+
+
+ );
+};
+
+// Export EnumeratedInfo for use in Citizen dashboard
+export default EnumeratedInfo;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/ErrorMessage.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/ErrorMessage.tsx
new file mode 100644
index 0000000..edd0f03
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/ErrorMessage.tsx
@@ -0,0 +1,63 @@
+// ErrorMessage.tsx displays a styled error message for the Citizen UI.
+// It uses MUI for UI and shows an error icon, title, and message.
+// Main responsibilities:
+// - Render a centered error card with icon and message
+// - Use Paper for card styling and Typography for text
+// No props required; static error message
+import {type FC} from "react";
+import { Box, Typography, Paper } from "@mui/material";
+import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
+
+// ErrorMessage component: renders a styled error card with icon and message
+const ErrorMessage: FC = () => (
+
+
+ {/* Error icon */}
+
+ {/* Error title */}
+
+ Error
+
+ {/* Error message */}
+
+ Something went wrong
+
+
+
+);
+
+// Export ErrorMessage for use in error boundaries and error states
+export default ErrorMessage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/InfoCard.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/InfoCard.tsx
new file mode 100644
index 0000000..6cced97
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/InfoCard.tsx
@@ -0,0 +1,34 @@
+// InfoCard.tsx displays a styled card with a title and count for summary info on the Citizen dashboard.
+// It uses MUI for UI and is used for showing counts (e.g., bills, licenses, etc.).
+// Main responsibilities:
+// - Render a card with title and count
+// - Style card for dashboard summary
+// Props:
+// title (string): label for the card
+// count (number): value to display
+import React from 'react';
+import { Typography, Paper } from '@mui/material';
+
+// InfoCard component: renders a styled card with title and count
+const InfoCard: React.FC<{ title: string; count: number }> = ({ title, count }) => (
+
+ {/* Card title */}
+ {title}
+ {/* Card count value */}
+ {count}
+
+);
+
+// Export InfoCard for use in dashboard summary sections
+export default InfoCard;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/MapComponent.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/MapComponent.tsx
new file mode 100644
index 0000000..4e7931a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/MapComponent.tsx
@@ -0,0 +1,107 @@
+// MapComponent.tsx displays a Leaflet map centered on a given latitude/longitude with a marker.
+// It uses react-leaflet for map rendering and supports custom tile URL and marker icon.
+// Main responsibilities:
+// - Render a map centered at lat/lng
+// - Show a marker at the property location
+// - Use custom tile URL and icon for map/marker
+// Props:
+// latLng: { lat, lng } - coordinates to center and mark
+// tileUrl: string - URL for map tiles
+// customIcon: L.Icon - Leaflet icon for marker
+import { useEffect, useRef } from 'react';
+import type { FC } from 'react';
+import maplibregl from 'maplibre-gl';
+import 'maplibre-gl/dist/maplibre-gl.css';
+
+type LatLng = { lat: number; lng: number };
+type CustomIconLike = {
+ iconUrl?: string;
+ iconSize?: [number, number];
+ iconAnchor?: [number, number];
+ popupAnchor?: [number, number];
+ className?: string;
+};
+
+const MapComponent: FC<{
+ latLng: LatLng;
+ tileUrl: string; // kept for compatibility with existing callers (tab toggle)
+ customIcon: CustomIconLike;
+}> = ({ latLng, tileUrl, customIcon }) => {
+ const containerRef = useRef(null);
+ const mapRef = useRef(null);
+ const markerRef = useRef(null);
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ // cleanup previous map if any
+ if (mapRef.current) {
+ markerRef.current?.remove();
+ mapRef.current.remove();
+ mapRef.current = null;
+ markerRef.current = null;
+ }
+
+ // Use the tileUrl passed from props as the style
+ mapRef.current = new maplibregl.Map({
+ container: containerRef.current,
+ style: tileUrl,
+ center: [latLng.lng, latLng.lat],
+ zoom: 15,
+ attributionControl: false,
+ scrollZoom: false,
+ doubleClickZoom: false,
+ });
+
+ // create marker element using provided icon-like object (keeps visual same)
+ const el = document.createElement('div');
+ el.style.display = 'inline-block';
+ el.style.lineHeight = '0';
+ el.style.pointerEvents = 'auto';
+
+ const img = document.createElement('img');
+ img.src = customIcon?.iconUrl ?? '';
+ if (customIcon?.iconSize) {
+ img.style.width = `${customIcon.iconSize[0]}px`;
+ img.style.height = `${customIcon.iconSize[1]}px`;
+ } else {
+ img.style.width = '40px';
+ img.style.height = '40px';
+ }
+ img.alt = 'marker';
+ if (customIcon?.className) img.className = customIcon.className;
+
+ el.appendChild(img);
+
+ // position marker with anchor adjustments (maplibre Marker positions by element's top-left)
+ // we'll add margin to mimic iconAnchor behaviour if provided
+ if (customIcon?.iconAnchor) {
+ // convert anchor so element bottom center aligns to coordinates
+ const [ax, ay] = customIcon.iconAnchor;
+ // apply transform so coordinate aligns similarly to Leaflet anchor
+ el.style.transform = `translate(${
+ -ax + (customIcon.iconSize ? customIcon.iconSize[0] / 2 : 20)
+ }px, ${-ay}px)`;
+ } else {
+ el.style.transform = 'translate(-20px, -40px)';
+ }
+ mapRef.current.addControl(new maplibregl.NavigationControl(), 'top-right');
+ markerRef.current = new maplibregl.Marker({ element: el, anchor: 'bottom' })
+ .setLngLat([latLng.lng, latLng.lat])
+ .addTo(mapRef.current);
+
+ return () => {
+ markerRef.current?.remove();
+ markerRef.current = null;
+
+ if (mapRef.current) {
+ mapRef.current.remove();
+ mapRef.current = null;
+ }
+ };
+ }, [latLng.lat, latLng.lng, tileUrl, customIcon]);
+
+ return
;
+};
+
+export default MapComponent;
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/MapView.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/MapView.tsx
new file mode 100644
index 0000000..8816be2
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/MapView.tsx
@@ -0,0 +1,445 @@
+// MapView.tsx displays a Leaflet map with property markers for the Citizen property list/map view.
+// It supports selection, custom marker icons, and property details popups.
+// Main responsibilities:
+// - Render map with property markers and selection
+// - Show property info card on marker select
+// - Handle marker click and details navigation
+// - Use custom icons and panes for selected marker
+// Props:
+// properties: array of property objects
+// selectedMarkerIdx: index of selected marker
+// setSelectedMarkerIdx: callback to set selected marker
+// center, height, showEmptyMessage, zoom, onViewDetails: map options and event handlers
+import React, { useMemo, useEffect, useRef } from 'react';
+import { Box } from '@mui/material';
+import '../../../../styles/Citizen/PropertyMarker.css';
+import maplibregl from 'maplibre-gl';
+import 'maplibre-gl/dist/maplibre-gl.css';
+import { useNavigate } from 'react-router-dom';
+
+import pointerLight from '../../../assets/Citizen/home_page/property_pointer_light.svg';
+import pointerDark from '../../../assets/Citizen/home_page/property_pointer_dark.svg';
+
+// Accept the *application* shape where GISData.Coordinates is under Property
+// ...existing code...
+
+interface GenericProperty {
+ Property?: {
+ GISData?: {
+ Latitude?: number;
+ Longitude?: number;
+ Coordinates?: { Latitude?: number; Longitude?: number }[];
+ };
+ Address?: {
+ BlockNo?: string;
+ PinCode?: string | number;
+ Display?: string;
+ Street?: string;
+ [key: string]: any;
+ };
+ [key: string]: any;
+ };
+ locationData?: {
+ address?: string;
+ coordinates?: {
+ lat?: number;
+ lng?: number;
+ };
+ };
+ PropertyID?: string;
+ id?: string;
+ Status?: string;
+ enumerationProgress?: number;
+ [key: string]: any;
+}
+
+// MapViewProps defines the props for the MapView component
+interface MapViewProps {
+ properties: GenericProperty[];
+ selectedMarkerIdx: number | null;
+ setSelectedMarkerIdx: (idx: number | null) => void;
+ center?: [number, number];
+ height?: number;
+ showEmptyMessage?: boolean;
+ zoom?: number;
+ onViewDetails?: (property: GenericProperty) => void;
+}
+
+const FALLBACK_CENTER: [number, number] = [12.9716, 77.5946];
+
+const STATUS_PROGRESS_MAP: Record = {
+ INITIATED: 20,
+ ASSIGNED: 40,
+ VERIFIED: 60,
+ AUDIT_VERIFIED: 80,
+ APPROVED: 100,
+};
+
+// propertyToMarkerInfo: extracts marker info from property object
+function propertyToMarkerInfo(p: GenericProperty): {
+ lat?: number;
+ lng?: number;
+ address?: any;
+ blockNo?: string;
+ pinCode?: string | number;
+ id?: string;
+ Status?: string;
+ enumerationProgress?: number;
+} {
+ const status = (p.Status || '').toUpperCase();
+ const enumerationProgress = STATUS_PROGRESS_MAP[status] ?? 0;
+
+ if (p?.locationData?.coordinates) {
+ return {
+ lat: p.Property?.GISData?.Latitude,
+ lng: p.Property?.GISData?.Longitude,
+ address: p.locationData.address,
+ blockNo: p?.Property?.Address?.BlockNo,
+ pinCode: p?.Property?.Address?.PinCode,
+ id: p.id,
+ enumerationProgress,
+ };
+ }
+
+ // Check if GISData has direct Latitude/Longitude
+ const gisData = p?.Property?.GISData;
+ let lat: number | undefined;
+ let lng: number | undefined;
+
+ if (gisData) {
+ // First try direct Latitude/Longitude
+ if (gisData.Latitude != null && gisData.Longitude != null) {
+ lat = gisData.Latitude;
+ lng = gisData.Longitude;
+ }
+ // Fallback to Coordinates array
+ else if (gisData.Coordinates?.[0]) {
+ lat = gisData.Coordinates[0].Latitude;
+ lng = gisData.Coordinates[0].Longitude;
+ }
+ }
+
+ const addressObj = p?.Property?.Address;
+ let addressString = '';
+
+ if (addressObj) {
+ // Use Display if available, otherwise construct from parts
+ if (addressObj.Display) {
+ addressString = addressObj.Display;
+ } else if (addressObj.Street || addressObj.Locality) {
+ const parts = [
+ addressObj.Street,
+ addressObj.Locality,
+ addressObj.BlockNo,
+ addressObj.WardNo,
+ ].filter(Boolean);
+ addressString = parts.join(', ');
+ } else {
+ addressString = 'Address not available';
+ }
+ } else {
+ addressString = p?.PropertyID || 'Unknown address';
+ }
+
+ return {
+ lat,
+ lng,
+ address: addressString,
+ blockNo: p?.Property?.Address?.BlockNo,
+ pinCode: p?.Property?.Address?.PinCode,
+ id: p.id || p.PropertyID,
+ enumerationProgress,
+ };
+}
+
+// createMarkerElement: builds a DOM element for MapLibre marker (keeps same HTML/style as Leaflet DivIcon)
+function createMarkerElement(
+ selected: boolean,
+ markerData?: ReturnType
+): HTMLElement {
+ const pointerSize = selected ? 70 : 60;
+ const blockNoStr = markerData?.blockNo
+ ? `Block No: ${markerData.blockNo}
`
+ : '';
+ const pinCodeStr = markerData?.pinCode
+ ? `Pincode: ${markerData.pinCode}
`
+ : '';
+
+ const cardHtml =
+ selected && markerData
+ ? `
+
+
+
${
+ markerData.address ?? 'Unknown address'
+ }
+ ${blockNoStr}
+ ${pinCodeStr}
+
+
+ ${markerData.enumerationProgress ?? 0}% Enumeration process complete
+
+
+
+
+
+ `
+ : '';
+
+ const wrapper = document.createElement('div');
+ wrapper.className = `property-marker${selected ? ' selected' : ''}`;
+ wrapper.innerHTML = `
+
+ ${cardHtml}
+ `;
+
+ // set basic sizing so MapLibre marker placement matches previous behavior
+ wrapper.style.width = `${pointerSize}px`;
+ wrapper.style.height = `${pointerSize + (selected ? 130 : 0)}px`;
+ wrapper.style.display = 'block';
+ wrapper.style.cursor = 'pointer';
+ return wrapper;
+}
+
+// NOTE: Leaflet-specific pane logic removed โ MapLibre markers use element zIndex instead.
+
+// MapView component: renders MapLibre map with property markers and selection logic
+const MapView: React.FC = ({
+ properties,
+ selectedMarkerIdx,
+ setSelectedMarkerIdx,
+ center,
+ height = 400,
+ showEmptyMessage = true,
+ zoom = 15,
+ onViewDetails,
+}) => {
+ const navigate = useNavigate();
+ const mapContainerRef = useRef(null);
+ const mapRef = useRef(null);
+ const markersRef = useRef([]);
+ const markersData = useMemo(() => {
+ return properties
+ .map((p, idx) => ({
+ ...propertyToMarkerInfo(p),
+ idx,
+ original: p,
+ }))
+ .filter((m) => m.lat != null && m.lng != null);
+ }, [properties]);
+
+ // computed center in [lng, lat] for MapLibre
+ const computedCenterLngLat: [number, number] =
+ markersData.length > 0
+ ? [
+ markersData[0].lng ?? FALLBACK_CENTER[1],
+ markersData[0].lat ?? FALLBACK_CENTER[0],
+ ]
+ : center
+ ? [center[1], center[0]]
+ : [FALLBACK_CENTER[1], FALLBACK_CENTER[0]];
+
+ // init map once
+ useEffect(() => {
+ if (!mapContainerRef.current) return;
+
+ // cleanup existing map if any
+ if (mapRef.current) {
+ mapRef.current.remove();
+ mapRef.current = null;
+ }
+
+ const styleUrl =
+ 'https://api.maptiler.com/maps/base-v4/style.json?key=YguiTF06mLtcpSVKIQyc';
+
+ mapRef.current = new maplibregl.Map({
+ container: mapContainerRef.current,
+ style: styleUrl,
+ center: computedCenterLngLat,
+ zoom,
+ attributionControl: false,
+ pitch: 0,
+ bearing: 0,
+ dragRotate: true,
+ pitchWithRotate: true,
+ touchPitch: true,
+ });
+
+ mapRef.current.addControl(new maplibregl.NavigationControl(), 'top-right');
+
+ // prevent default context menu so right-click drag works (keeps parity with previous behavior)
+ const handleContextMenu = (e: Event) => e.preventDefault();
+ mapContainerRef.current.addEventListener('contextmenu', handleContextMenu);
+
+ // cleanup on unmount
+ return () => {
+ if (mapRef.current) {
+ markersRef.current.forEach((m) => m.remove());
+ markersRef.current = [];
+ mapRef.current.remove();
+ mapRef.current = null;
+ }
+ if (mapContainerRef.current) {
+ mapContainerRef.current.removeEventListener('contextmenu', handleContextMenu);
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []); // initialize once
+
+ // update markers when data or selection changes
+ useEffect(() => {
+ const map = mapRef.current;
+ if (!map) return;
+
+ // remove old markers
+ markersRef.current.forEach((m) => m.remove());
+ markersRef.current = [];
+
+ if (markersData.length === 0) {
+ // add a center default marker similar to previous logic
+ const el = document.createElement('div');
+ el.style.width = '20px';
+ el.style.height = '20px';
+ el.style.background = '#1976d2';
+ el.style.borderRadius = '50%';
+ el.style.border = '2px solid white';
+
+ const marker = new maplibregl.Marker({ element: el })
+ .setLngLat(computedCenterLngLat)
+ .addTo(map);
+ markersRef.current.push(marker);
+ return;
+ }
+
+ const bounds = new maplibregl.LngLatBounds();
+
+ markersData.forEach((m, idx) => {
+ const isSelected = selectedMarkerIdx === idx;
+ const el = createMarkerElement(isSelected, isSelected ? m : undefined);
+
+ // set zIndex for selected marker so its card appears above others
+ if (isSelected) {
+ el.style.zIndex = '750';
+ } else {
+ el.style.zIndex = '500';
+ }
+
+ // ensure clicks inside marker elements are visible to listeners
+ el.style.pointerEvents = 'auto';
+
+ const marker = new maplibregl.Marker({ element: el })
+ .setLngLat([m.lng!, m.lat!])
+ .addTo(map);
+
+ // click handler to toggle selection (preserve original behavior)
+ const onMarkerClick = (e: MouseEvent) => {
+ e.stopPropagation();
+ const target = e.target as HTMLElement;
+ // if the click was on the view-details button, let document handler handle it
+ if (target.closest && target.closest('.marker-card-btn-2')) return;
+ setSelectedMarkerIdx(selectedMarkerIdx === idx ? null : idx);
+ };
+ el.addEventListener('click', onMarkerClick);
+
+ markersRef.current.push(marker);
+ bounds.extend([m.lng!, m.lat!]);
+ });
+
+ // NOTE: fitBounds moved to a separate effect that runs only when markersData changes,
+ // so selecting a marker (selectedMarkerIdx change) won't trigger a re-fit/zoom out.
+ }, [markersData, selectedMarkerIdx, setSelectedMarkerIdx]);
+
+ // Fit map to show all markers only when markersData changes (not on selection changes)
+ useEffect(() => {
+ const map = mapRef.current;
+ if (!map || markersData.length === 0) return;
+ const bounds = new maplibregl.LngLatBounds();
+ markersData.forEach((m) => bounds.extend([m.lng!, m.lat!]));
+ // if only one marker, center on it instead of fitting bounds to avoid weird zoom
+ if (markersData.length === 1) {
+ map.easeTo({ center: [markersData[0].lng!, markersData[0].lat!], zoom });
+ } else {
+ map.fitBounds(bounds, { padding: 50, maxZoom: 18, duration: 500 });
+ }
+ }, [markersData, zoom]);
+
+ // Handle "View Details" button click (same document-level listener as before)
+ useEffect(() => {
+ function handleClick(e: MouseEvent) {
+ const target = e.target as HTMLElement;
+ const btn = target.closest
+ ? (target.closest('.marker-card-btn-2') as HTMLElement | null)
+ : null;
+ if (!btn) return;
+ const idAttr = btn.getAttribute('data-prop-id');
+ const markerData = markersData.find((m) => String(m.id) === String(idAttr));
+
+ if (markerData) {
+ // if (onViewDetails) {
+ // onViewDetails(markerData.original);
+ // } else {
+ // navigate(`/citizen/properties/${markerData.id}`, {
+ // state: {
+ // applicationId: markerData.id,
+ // },
+ // });
+ // }
+ }
+ }
+ document.addEventListener('click', handleClick);
+ return () => document.removeEventListener('click', handleClick);
+ }, [markersData, navigate, onViewDetails]);
+
+ return (
+
+
+ {markersData.length === 0 && showEmptyMessage && (
+
+
+ No properties yet. Start by adding one.
+
+
+ )}
+
+ );
+};
+
+export default MapView;
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/NoProperties.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/NoProperties.tsx
new file mode 100644
index 0000000..c9fb624
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/NoProperties.tsx
@@ -0,0 +1,69 @@
+// NoProperties.tsx displays a message when the user has no properties added.
+// It uses MUI for UI, localization for messages, and shows a sad icon and instructions.
+// Main responsibilities:
+// - Render a styled card with icon and localized messages
+// - Show loader if localization is loading
+// No props required; static message for empty property list
+// NoProperties.tsx
+import { Box, Typography } from '@mui/material';
+import { SentimentDissatisfied as SadIcon } from '@mui/icons-material';
+import { useAppSelector } from '../../../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from '../../../components/Loader';
+
+
+// NoProperties component: renders a styled card with icon and localized empty state messages
+const NoProperties: React.FC = () => {
+ const lang = useAppSelector((state) => state.lang.citizenLang); // Current language
+ const { loading: localizationLoading } = useLocalization(); // Global loading state
+ const messages = getMessagesFromSession('CITIZEN')!; // Localized messages
+
+ // Show loader if localization is loading
+ if (localizationLoading) {
+ return ;
+ }
+
+ // Render empty state card with icon and messages
+ return (
+
+ {/* Sad icon for empty state */}
+
+ {/* Main message */}
+
+ {/* No properties found */}
+ {messages['citizen.no-property'][lang]['no-properties-found']}
+
+ {/* Sub-messages */}
+
+ {/* You have not added any property yet. */}
+ {messages['citizen.no-property'][lang]['no-property-added']}
+
+ {/* Add properties to manage your assets! */}
+ {messages['citizen.no-property'][lang]['add-properties']}
+
+
+ );
+};
+
+// Export NoProperties for use in empty property list views
+export default NoProperties;
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/OverviewStack.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/OverviewStack.tsx
new file mode 100644
index 0000000..a6342f0
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/OverviewStack.tsx
@@ -0,0 +1,176 @@
+// OverviewStack.tsx displays a summary stack of property details for the Citizen property details page.
+// It shows property type, construction year, built-up area, plot area, and property value using MUI and localization.
+// Main responsibilities:
+// - Map API property data to display fields
+// - Render summary rows for key property info
+// - Use icons and localization for labels
+// Props: property (CitizenPropertyData) - the property whose summary is shown
+import { Box, Stack, Typography } from '@mui/material';
+import type { FC } from 'react';
+import CropSquareIcon from '@mui/icons-material/CropSquare';
+import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
+import { HouseOutlined } from '@mui/icons-material';
+import { useAppSelector } from '../../../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from '../../../components/Loader';
+import type { CitizenPropertyData } from '../models/CitizenPropertiesPageModel/CitizenPropertyPageModel';
+import activity_zone from "../../../assets/activity_zone.svg"
+
+// Props for OverviewStack: expects property data
+interface OverviewStackProps {
+ property: CitizenPropertyData;
+}
+
+// OverviewStack component: renders summary stack of property details
+const OverviewStack: FC = ({ property }) => {
+ // Localization, state, and hooks
+ const lang = useAppSelector((state) => state.lang.citizenLang); // Current language
+ const { loading } = useLocalization(); // Global loading state
+ const messages = getMessagesFromSession('CITIZEN')!; // Localized messages
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ // No property: render nothing
+ if (!property) return null;
+
+ // Map API property data to display fields for UI
+ const propertyType = property.PropertyType || '-';
+
+ // Construction year - extract from FloorDetails or use default
+ const constructionYear = property.ConstructionDetails?.FloorDetails?.[0]?.constructionDate
+ ? new Date(property.ConstructionDetails.FloorDetails[0].constructionDate).getFullYear()
+ : '-';
+
+ // Built-up Area - sum of all floor plinth areas
+ const builtUpArea =
+ property.ConstructionDetails?.FloorDetails?.reduce(
+ (sum, floor) => sum + (floor.PlinthAreaSqFt || 0),
+ 0
+ ).toFixed(2) + ' sq ft' || '-';
+
+ // Plot Area - from IGRS or AssessmentDetails
+ const plotArea =
+ property.IGRS?.totalPlinthArea
+ ? `${property.IGRS.totalPlinthArea} sq ft`
+ : property.AssessmentDetails?.ExtendOfSite || '-';
+
+ // Property Value - you'll need to calculate or get from another source
+ // const propertyValue = '-';
+ return (
+
+ {/* First Row: Property Type & Construction Year */}
+
+ {/* Property Type */}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['property-type']}
+
+
+ {propertyType}
+
+
+
+ {/* Construction Year */}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['construct-year']}
+
+
+ {constructionYear}
+
+
+
+
+ {/* Second Row: Built-up Area & Plot Area */}
+
+ {/* Built-up Area */}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['built-up-area']}
+
+
+ {builtUpArea}
+
+
+
+ {/* Plot Area */}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['plot-area']}
+
+
+ {plotArea}
+
+
+
+
+ {/* Property Value */}
+
+
+ {/* {messages['citizen.my-properties'][lang]['property-value']}
+ */}
+
+
+ {/* {propertyValue} */}
+
+
+
+ );
+};
+
+// Export OverviewStack for use in property overview sections
+export default OverviewStack;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/PropertyCard.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/PropertyCard.tsx
new file mode 100644
index 0000000..d3e705d
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/PropertyCard.tsx
@@ -0,0 +1,301 @@
+// PropertyCard displays a summary card for a property, including address, status, and a map preview.
+// It supports navigation to property details or form completion, and adapts UI based on property status.
+
+import { type FC } from 'react';
+import { Paper, Typography, Button, Box, Chip } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+import markerSvg from '../../../assets/Citizen/property_page/location.svg';
+import { Edit } from '@mui/icons-material';
+import { useAppSelector } from '../../../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from '../../../components/Loader';
+import MapComponent from './MapComponent';
+import { useFormMode } from '../../../../context/FormModeContext';
+
+// Plain icon-like object (compatible with MapComponent using MapLibre)
+const customIcon = {
+ iconUrl: markerSvg,
+ iconSize: [40, 40] as [number, number],
+ iconAnchor: [20, 40] as [number, number],
+ popupAnchor: [0, -40] as [number, number],
+ className: '',
+};
+
+// Props for PropertyCard
+interface PropertyCardProps {
+ property: any; // Property data object
+ propertyId?: string; // Optional property ID override
+}
+
+// Color mapping for property status badges
+const statusColors: Record = {
+ Enumerated: { bg: '#85B9A1', color: '#fff' },
+ 'Under Enumeration': { bg: '#FFCDB6', color: '#000' },
+ Draft: { bg: '#FFC107', color: '#000' },
+};
+
+// Color mapping for property types
+const typeColors: Record = {
+ Apartment: '#C84C0E',
+ 'Residential Property': '#C84C0E',
+ 'RESIDENTIAL Property': '#C84C0E',
+ 'Primary Property': '#D49C7A',
+ 'Industrial Property': '#53C56C',
+ 'INDUSTRIAL Property': '#53C56C',
+ 'Commercial Property': '#53C56C',
+ 'COMMERCIAL Property': '#53C56C',
+ 'MIXED Property': '#8B4513',
+ 'Mixed Property': '#8B4513',
+};
+
+const PropertyCard: FC = ({ property, propertyId }) => {
+ const navigate = useNavigate();
+ const { setMode } = useFormMode();
+
+ // Compute property type label (e.g., "Residential Property")
+ const type = property.propertyType + ' Property';
+
+ // Determine property status based on enumeration progress
+ // -1: Draft, 100: Enumerated, otherwise: Under Enumeration
+ let status = 'Under Enumeration';
+ if (property.enumerationProgress === -1) {
+ status = 'Draft';
+ } else if (property.enumerationProgress === 100) {
+ status = 'Enumerated';
+ }
+
+ // Get color for property type and status badge
+ const typeColor = typeColors[type] || '#C84C0E';
+ const badge = statusColors[status] || statusColors['Under Enumeration'];
+
+ // Get current language and localization messages
+ const lang = useAppSelector((state) => state.lang.citizenLang);
+ const { loading } = useLocalization();
+ const messages = getMessagesFromSession('CITIZEN')!;
+
+ // Compose address string from propertyAddress or fallback to locationData
+ const address = property.propertyAddress
+ ? [
+ property.propertyAddress.street,
+ property.propertyAddress.locality,
+ property.propertyAddress.wardNo,
+ property.propertyAddress.zoneNo,
+ property.propertyAddress.blockNo,
+ property.propertyAddress.pincode,
+ ]
+ .filter(Boolean)
+ .join(', ')
+ : property.locationData?.address || '';
+
+ // Extract latitude and longitude for map display
+ const latLng = property.GISData
+ ? { lat: property.GISData.Latitude || 0, lng: property.GISData.Longitude || 0 }
+ : { lat: 0, lng: 0 };
+
+ // Prevent click event from bubbling to Paper when clicking on button or map
+ const stopPropagation = (e: React.MouseEvent) => e.stopPropagation();
+
+ // Handle card click: navigate to property details or complete form if draft
+ const handleCardClick = () => {
+ if (status === 'Draft') {
+ handleCompleteForm();
+ } else {
+ // Navigate to property details page with full property data
+ navigate(`/citizen/properties/${property.id}`, {
+ state: {
+ applicationId: property.__appId,
+ property: property,
+ enumerationProgress: property.enumerationProgress,
+ isDraft: property.isDraft,
+ applicationStatus: property.applicationStatus,
+ },
+ });
+ }
+ };
+
+ // Handle view location button click: navigate to map view
+ // const handleViewLocation = (e: React.MouseEvent) => {
+ // e.stopPropagation();
+ // navigate('/citizen/property-location', {
+ // state: { property },
+ // });
+ // };
+
+ // Handle complete form button click (for drafts): set mode and navigate
+ const handleCompleteForm = (e?: React.MouseEvent) => {
+ if (e) {
+ e.stopPropagation();
+ }
+
+ // Resolve property and application ids (prefer passed props, then fallback)
+ const resolvedPropertyId = propertyId;
+ const resolvedApplicationId = property.__appId;
+
+ if (resolvedPropertyId) {
+ localStorage.setItem('propertyId', resolvedPropertyId);
+ }
+ if (resolvedApplicationId) {
+ localStorage.setItem('applicationId', resolvedApplicationId);
+ }
+ // Set form mode to draft
+ setMode('draft');
+ // Navigate to property form
+ navigate('/property-form/property-information');
+ };
+
+ // Show loader while localization is loading
+ if (loading) {
+ return ;
+ }
+
+ // Render the property card UI
+ return (
+
+ {/* Header: Property type and status badge */}
+
+
+ {type}
+
+
+
+ {/* Address label */}
+
+ {messages['citizen.my-properties'][lang]['address']}
+
+ {/* Address value */}
+
+ {address || 'Address not available'}
+
+ {/* Action buttons: Complete Form (Draft) or View Location */}
+
+ {status === 'Draft' && (
+ }
+ sx={{
+ borderRadius: 8,
+ fontWeight: 500,
+ fontSize: 13,
+ color: '#fff',
+ bgcolor: '#C84C0E',
+ textTransform: 'none',
+ px: 1.5,
+ py: 0.5,
+ minHeight: '28px',
+ '&:hover': { bgcolor: '#a03a07' },
+ }}
+ onClick={handleCompleteForm}
+ >
+ Complete Form
+
+ ) // : (
+ // }
+ // sx={{
+ // borderRadius: 8,
+ // fontWeight: 400,
+ // fontSize: 13,
+ // color: '#1A1816',
+ // borderColor: '#D5D5D5',
+ // textTransform: 'none',
+ // px: 1.2,
+ // py: 0.1,
+ // minHeight: '24px',
+ // background: '#f5f5f5',
+ // '&:hover': { background: '#F5F5F5', borderColor: '#E0C9B2' },
+ // }}
+ // onClick={handleViewLocation}
+ // >
+ // {messages['citizen.commons'][lang]['view-location']}
+ //
+ //)
+ }
+
+ {/* Map preview for property location */}
+
+
+
+
+ );
+};
+
+export default PropertyCard;
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/PropertyComponent.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/PropertyComponent.tsx
new file mode 100644
index 0000000..6f5cbf5
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/PropertyComponent.tsx
@@ -0,0 +1,196 @@
+// PropertyComponent.tsx
+// This component displays a card summarizing a property for the citizen user.
+// Shows address details, and provides buttons to view location and details.
+// Props:
+// property: MappedProperty object containing location and details
+// bgcolor: background color for the card
+// propertyDetails: additional property summary info
+// Used in: Citizen property listing views
+
+import React from 'react';
+import { Card, CardContent, Typography, Button, Box, LinearProgress } from '@mui/material';
+import LaunchOutlinedIcon from '@mui/icons-material/LaunchOutlined';
+import { useNavigate } from 'react-router-dom';
+import type { CitizenPropertySummary } from '../../Citizen/api/CitizenHomePageApi/CitizenHomePageModel';
+import { useAppSelector } from '../../../../redux/Hooks';
+import { getMessagesFromSession, useLocalization } from '../../../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from '../../../components/Loader';
+import CottageOutlinedIcon from '@mui/icons-material/CottageOutlined';
+
+export interface MappedProperty {
+ id: string;
+ locationData: {
+ address: string;
+ coordinates: { lat: number; lng: number };
+ BlockNo?: string;
+ Locality?: string;
+ PinCode?: number | string;
+ Street?: string;
+ WardNo?: string;
+ ZoneNo?: string;
+ };
+ status: string;
+ propertyDetails?: CitizenPropertySummary;
+}
+
+interface PropertyCardProps {
+ applicationId: string;
+ property: MappedProperty;
+ bgcolor: string;
+}
+
+const PropertyCard: React.FC = ({
+ applicationId,
+ property,
+ bgcolor,
+}) => {
+ const navigate = useNavigate();
+
+ // Get current language and localization loading state
+ const lang = useAppSelector((state) => state.lang.citizenLang);
+ const { loading } = useLocalization();
+ const messages = getMessagesFromSession('CITIZEN')!;
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ const STATUS_PROGRESS_MAP: Record = {
+ INITIATED: 20,
+ ASSIGNIED: 40,
+ VERIFIED: 60,
+ AUDIT_VERIFIED: 80,
+ APPROVED: 100,
+ };
+
+ const progress = STATUS_PROGRESS_MAP[property.status] ?? 20;
+
+ const viewLocationLabel = messages['citizen.home'][lang]['view-location'];
+
+ const handleViewLocation = () => {
+ navigate('/citizen/property-location', {
+ state: { property },
+ });
+ };
+
+ const handleViewDetails = () => {
+ navigate(
+ `/citizen/properties/${property.id}`,
+ {
+ state: {
+ applicationId : applicationId,
+ }
+ }
+ );
+ };
+
+ const details = property.locationData || {};
+ const propFallback = property.propertyDetails?.Address;
+
+ // Build address string from available fields
+ const addressFields = [
+ details.BlockNo || propFallback?.BlockNo,
+ details.Locality || propFallback?.Locality,
+ details.Street || propFallback?.Street,
+ details.WardNo || propFallback?.WardNo,
+ details.ZoneNo || propFallback?.ZoneNo,
+ (details.PinCode ?? propFallback?.PinCode)?.toString(),
+ ]
+ .filter(Boolean)
+ .join(', ');
+
+ // Render property card UI
+ return (
+
+
+
+
+
+
+
+ {/* Property address header with icon */}
+
+ {/* */}
+
+ {addressFields || 'Address not available'}
+
+
+
+
+ {progress}%
+
+
+ }
+ onClick={handleViewLocation}
+ >
+ {viewLocationLabel}
+
+
+ View Details
+
+
+
+
+ );
+};
+
+// Export PropertyCard for use in citizen property views
+export default PropertyCard;
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/PropertyList.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/PropertyList.tsx
new file mode 100644
index 0000000..ee15494
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/PropertyList.tsx
@@ -0,0 +1,83 @@
+
+// PropertyList displays a list of properties for the citizen user, mapping application summaries to property cards.
+import React from 'react';
+import type { CitizenApplicationSummary } from '../../Citizen/api/CitizenHomePageApi/CitizenHomePageModel';
+import PropertyCard from './PropertyComponent';
+
+
+
+// Helper function to map application summaries to property card data structure
+function mapApplicationsToProperties(applications: CitizenApplicationSummary[]) {
+ return applications.map(app => {
+ const property = app.Property || {};
+ // Extract coordinates from GISData if available
+ const coordsArr = property.GISData?.Coordinates || [];
+ const coordinates = (Array.isArray(coordsArr) && coordsArr.length > 0)
+ ? {
+ lat: coordsArr[0]?.Latitude ?? 0,
+ lng: coordsArr[0]?.Longitude ?? 0,
+ }
+ : { lat: 0, lng: 0 };
+
+ // Compose address string from address fields
+ const addressObj = property.Address || {};
+
+ return {
+ applicationId: app.ID, // Application ID
+ id: property.ID, // Property ID
+ status: app.Status || '', // Application status
+ locationData: {
+ address: [
+ addressObj.BlockNo,
+ addressObj.Locality,
+ addressObj.Street,
+ addressObj.WardNo,
+ addressObj.ZoneNo,
+ addressObj.PinCode
+ ].filter(Boolean).join(', ') || property.ComplexName || '',
+ coordinates,
+ BlockNo: addressObj.BlockNo || '',
+ Locality: addressObj.Locality || '',
+ PinCode: addressObj.PinCode || '',
+ Street: addressObj.Street || '',
+ WardNo: addressObj.WardNo || '',
+ ZoneNo: addressObj.ZoneNo || '',
+ },
+ propertyDetails: property, // Full property details
+ };
+ });
+}
+
+
+// PropertyList: Maps CitizenApplicationSummary[] to PropertyCard shape,
+// passing the mapped property object to each PropertyCard.
+const PropertyList: React.FC<{ properties: CitizenApplicationSummary[] }> = ({ properties }) => {
+ // Map incoming application summaries to property card data
+ const mappedProperties = mapApplicationsToProperties(properties);
+
+ return (
+
+ {mappedProperties.length === 0 ? (
+ // Show message if no properties are found
+
No properties found.
+ ) : (
+ mappedProperties.map(property => {
+ const bgcolor = '#F7E4DB';
+
+ return (
+ // Render a PropertyCard for each mapped property
+
+ );
+ })
+ )}
+
+ );
+};
+
+export default PropertyList;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/PropertyMapSection.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/PropertyMapSection.tsx
new file mode 100644
index 0000000..a393b9f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/PropertyMapSection.tsx
@@ -0,0 +1,118 @@
+// PropertyMapSection.tsx
+// This component displays a map section for a property, allowing users to toggle between different map views (standard and land use).
+// Props:
+// latLng: Object with latitude and longitude of the property
+// customIcon: Leaflet icon to mark the property location
+// Used in: Citizen property detail and location views
+
+import React, { type FC } from 'react';
+import { Box } from '@mui/material';
+
+import { useAppSelector } from '../../../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from '../../../components/Loader';
+import MapComponent from './MapComponent';
+
+// Keep the old URLs for compatibility with existing UI tab toggles (not used by MapLibre style directly)
+const MAP_URL =
+ 'https://api.maptiler.com/maps/base-v4/style.json?key=YguiTF06mLtcpSVKIQyc';
+const LANDUSE_URL =
+ 'https://api.maptiler.com/maps/streets-v2/style.json?key=YguiTF06mLtcpSVKIQyc';
+
+type PropertyMapProps = {
+ latLng: {
+ lat: number;
+ lng: number;
+ };
+ customIcon: {
+ iconUrl?: string;
+ iconSize?: [number, number];
+ iconAnchor?: [number, number];
+ popupAnchor?: [number, number];
+ className?: string;
+ };
+};
+
+const PropertyMapSection: FC = ({ latLng, customIcon }) => {
+ const lang = useAppSelector((state) => state.lang.citizenLang);
+ const { loading } = useLocalization();
+ const messages = getMessagesFromSession('CITIZEN')!;
+
+ const MAP_TABS = [
+ { label: messages['citizen.home'][lang]['map-btn'], key: 'map' },
+ { label: messages['citizen.my-properties'][lang]['land-use'], key: 'landuse' },
+ ];
+
+ const [activeMapTab, setActiveMapTab] = React.useState('map');
+ const tileUrl = activeMapTab === 'landuse' ? LANDUSE_URL : MAP_URL;
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ return (
+
+
+ {MAP_TABS.map((tab, i) => (
+ setActiveMapTab(tab.key)}
+ sx={{
+ flex: 1,
+ textAlign: 'center',
+ cursor: 'pointer',
+ fontWeight: activeMapTab === tab.key ? 700 : 500,
+ fontSize: '18px',
+ color: activeMapTab === tab.key ? '#C84C0E' : '#787878',
+ bgcolor: '#fff',
+ borderTopLeftRadius: i === 0 ? '10px' : 0,
+ borderTopRightRadius: i === MAP_TABS.length - 1 ? '10px' : 0,
+ border: '1.5px solid #E0E0E0',
+ borderBottom: activeMapTab === tab.key ? '3px solid #E26512' : 'none',
+ borderRight: i === 0 ? 'none' : '1.5px solid #E0E0E0',
+ pt: '7px',
+ pb: '3px',
+ transition: 'none',
+ backgroundClip: 'padding-box',
+ }}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+ {/* Pass the selected tileUrl for compatibility; MapComponent uses MapTiler style internally */}
+
+
+
+ );
+};
+
+export default PropertyMapSection;
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/UrgentCard.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/UrgentCard.tsx
new file mode 100644
index 0000000..1895718
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/UrgentCard.tsx
@@ -0,0 +1,39 @@
+// UrgentCard.tsx
+// This component displays a notification card for urgent attention items in the citizen UI.
+// Highlights immediate/pending items with a distinct color and icon.
+// Props:
+// item: UrgentAttention object containing type, status, message, and date
+// Used in: Lists or sections showing urgent notifications to the user
+
+import React from 'react';
+import { Box, Typography } from '@mui/material';
+import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
+import type { UrgentAttention } from '../models/UrgentAttention.model';
+
+const UrgentCard: React.FC<{ item: UrgentAttention }> = ({ item }) => {
+ // Determine if the item requires immediate attention (type or status)
+ const isImmediate = item.type === 'immediate' || item.status === 'pending';
+ return (
+
+ {/* Notification icon with color based on urgency */}
+
+ {/* Notification message */}
+ {item.message}
+ {/* Notification date */}
+ {item.date}
+
+ );
+};
+
+// Export UrgentCard for use in urgent notification lists
+export default UrgentCard;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/index.ts b/frontend/mobile-ui/src/app/features/Citizen/components/index.ts
new file mode 100644
index 0000000..cb9f2eb
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/index.ts
@@ -0,0 +1,8 @@
+export * from "./EnumOptComponent"
+export * from "./PropertyCard"
+export * from "./EnumOptComponent"
+export * from "./ErrorMessage"
+export * from "./MapComponent"
+export * from "./NoProperties"
+export * from "./OverviewStack"
+export * from "./PropertyMapSection"
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/ui/DocumentsTab.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/ui/DocumentsTab.tsx
new file mode 100644
index 0000000..badb85e
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/ui/DocumentsTab.tsx
@@ -0,0 +1,442 @@
+// DocumentsTab.tsx displays and manages property documents for the Citizen property details page.
+// It fetches document metadata, retrieves file blobs, and provides UI for viewing, downloading, editing, and uploading documents.
+// Uses MUI for UI, Redux Toolkit Query for API calls, and localization for labels/messages.
+//
+// Main responsibilities:
+// - Transform API document data for display
+// - Fetch file blobs for each document
+// - Show upload cards for missing documents
+// - Handle view/download/edit actions
+// - Display loading and error states
+//
+// Props: property (CitizenPropertyData) - the property whose documents are shown
+import { Box, Button, IconButton, Paper, Stack, Typography, CircularProgress } from '@mui/material';
+import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
+import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
+// import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
+import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
+import { AddAPhotoOutlined, TaskOutlined } from '@mui/icons-material';
+import type { FC } from 'react';
+import { useState, useEffect } from 'react';
+import { useAppSelector } from '../../../../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../../../services/Citizen/Localization/LocalizationContext';
+import LoadingPage from '../../../../components/Loader';
+import type { CitizenPropertyData } from '../../models/CitizenPropertiesPageModel/CitizenPropertyPageModel';
+import { useLazyGetFileBlobQuery } from '../../api/CitizenDocumentPageApi/fileStoreApi';
+
+const iconColor = '#C84C0E';
+const TENANT_ID = 'pg';
+
+// DocumentDisplay defines the shape of document data for UI rendering
+interface DocumentDisplay {
+ title: string;
+ filename?: string;
+ size?: string;
+ date?: string;
+ documentType?: string;
+ fileStoreId?: string;
+ action?: string;
+ isUpload?: boolean;
+ isChooseFile?: boolean;
+}
+
+// Converts property.Documents from API format to DocumentDisplay[] for UI rendering
+function buildDocumentDisplayList(property: CitizenPropertyData): DocumentDisplay[] {
+ if (!property || !property.Documents || property.Documents.length === 0) {
+ return [];
+ }
+
+ const list: DocumentDisplay[] = [];
+
+ for (const doc of property.Documents) {
+ const hasFile = doc.FileStoreID && doc.FileStoreID.trim() !== '';
+
+ list.push({
+ title: doc.DocumentType || 'Document',
+ filename: doc.DocumentName || '',
+ size: '',
+ date: doc.UploadDate ? new Date(doc.UploadDate).toLocaleDateString() : '',
+ documentType: doc.DocumentType,
+ fileStoreId: doc.FileStoreID,
+ action: doc.action,
+ isUpload: !hasFile,
+ isChooseFile: !hasFile,
+ });
+ }
+
+ return list;
+}
+
+// Props for Documents component: expects property data
+interface DocumentsProps {
+ property: CitizenPropertyData;
+}
+
+// Documents component: displays document cards, handles file fetch/view/download/upload
+const Documents: FC = ({ property }) => {
+ // Localization, state, and hooks
+ const lang = useAppSelector((state) => state.lang.citizenLang); // Current language
+ const { loading } = useLocalization(); // Global loading state
+ const messages = getMessagesFromSession('CITIZEN')!; // Localized messages
+ const [documentDisplayList, setDocumentDisplayList] = useState([]); // UI document list
+ const [fileBlobs, setFileBlobs] = useState>({}); // Cached file blobs
+ const [fileErrors, setFileErrors] = useState>({}); // File fetch errors
+ const [loadingFiles, setLoadingFiles] = useState>({}); // Per-file loading state
+
+ // RTK Query lazy hook for fetching file blobs
+ const [fetchFileBlob] = useLazyGetFileBlobQuery();
+
+ // Build document display list from property data
+ useEffect(() => {
+ if (!property) return;
+ const initialDocs = buildDocumentDisplayList(property);
+ setDocumentDisplayList(initialDocs);
+ }, [property]);
+
+ // Fetch file blobs for each document (sequentially)
+ useEffect(() => {
+ if (documentDisplayList.length === 0) return;
+
+ const fetchFilesSequentially = async () => {
+ const blobs: Record = {};
+ const errors: Record = {};
+ const loading: Record = {};
+
+ for (const doc of documentDisplayList) {
+ // Skip if no fileStoreId or is upload needed
+ if (!doc.fileStoreId || doc.isUpload) continue;
+
+ try {
+ // Set loading state for this file
+ loading[doc.fileStoreId] = true;
+ setLoadingFiles({ ...loading });
+
+ // Fetch file blob from filestore
+ const blob = await fetchFileBlob({
+ fileStoreId: doc.fileStoreId,
+ tenantId: TENANT_ID,
+ }).unwrap();
+
+ if (!blob) {
+ throw new Error('No blob returned from filestore');
+ }
+
+ blobs[doc.fileStoreId] = blob;
+
+ // Update document size in UI
+ const sizeInMB = (blob.size / (1024 * 1024)).toFixed(2);
+ setDocumentDisplayList(prev =>
+ prev.map(d =>
+ d.fileStoreId === doc.fileStoreId
+ ? { ...d, size: `${sizeInMB} MB` }
+ : d
+ )
+ );
+
+ // Update file blob cache
+ setFileBlobs({ ...blobs });
+
+ } catch (error) {
+ // Handle file fetch error
+ console.error(`Error fetching file ${doc.fileStoreId}:`, error);
+ const errorMessage = error instanceof Error ? error.message : 'Failed to fetch file';
+ const apiError = error as { data?: { message?: string } };
+ errors[doc.fileStoreId] = apiError.data?.message || errorMessage;
+ setFileErrors({ ...errors });
+ } finally {
+ // Clear loading state for this file
+ loading[doc.fileStoreId] = false;
+ setLoadingFiles({ ...loading });
+ }
+ }
+ };
+
+ fetchFilesSequentially();
+ }, [documentDisplayList.length, fetchFileBlob]);
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ // No property data: render nothing
+ if (!property) return null;
+
+ // Sort documents: upload-needed first, then normal docs
+ const sortedDocuments =
+ documentDisplayList.length > 0
+ ? [...documentDisplayList].sort((a, b) => {
+ const aNeedsUpload = a.isUpload || a.isChooseFile;
+ const bNeedsUpload = b.isUpload || b.isChooseFile;
+ if (aNeedsUpload === bNeedsUpload) return 0;
+ return aNeedsUpload ? -1 : 1;
+ })
+ : [];
+
+ // View document in new tab using cached blob
+ const handleViewDocument = async (fileStoreId: string) => {
+ try {
+ const blob = fileBlobs[fileStoreId];
+ if (!blob) {
+ console.error('File not found in cache');
+ alert('File is still loading or not available. Please try again.');
+ return;
+ }
+
+ const url = URL.createObjectURL(blob);
+ window.open(url, '_blank');
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ } catch (error) {
+ console.error('Error viewing document:', error);
+ alert('Failed to view document. Please try again.');
+ }
+ };
+
+ // Download document using cached blob
+ const handleDownloadDocument = async (fileStoreId: string, filename: string) => {
+ try {
+ const blob = fileBlobs[fileStoreId];
+ if (!blob) {
+ console.error('File not found in cache');
+ alert('File is still loading or not available. Please try again.');
+ return;
+ }
+
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename || 'document';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ } catch (error) {
+ console.error('Error downloading document:', error);
+ alert('Failed to download document. Please try again.');
+ }
+ };
+
+ // const handleEditDocument = (fileStoreId: string) => {
+ // console.log('Edit document:', fileStoreId);
+ // };
+
+ // Choose file handler for upload (stub)
+ const handleChooseFile = (documentType: string) => {
+ console.log('Choose file for:', documentType);
+ };
+
+ // Render document cards: upload-needed and normal documents
+ return (
+
+
+ {messages['citizen.my-properties'][lang]['documents']}
+
+
+ {sortedDocuments && sortedDocuments.length > 0 ? (
+
+ {sortedDocuments.map((doc, idx) => {
+ const isChooseFile = doc.isUpload || doc.isChooseFile;
+ const borderColor = isChooseFile ? '#C84C0E' : '#e0e0e0';
+ const isFileLoading = loadingFiles[doc.fileStoreId || ''];
+ const hasError = Boolean(fileErrors[doc.fileStoreId || '']);
+
+ // Upload-needed document card
+ if (isChooseFile) {
+ return (
+
+
+
+
+
+ {doc.title}
+
+
+
+
+ Take Image
+
+
+
+ handleChooseFile(doc.documentType || '')}
+ >
+ Choose File
+
+
+ PDF, JPG, PNG (Max 5MB)
+
+
+
+ );
+ }
+
+ // Normal document card: view, download, edit, error, loading
+ return (
+
+
+ {isFileLoading ? (
+
+ ) : hasError ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {doc.title}
+
+ {doc.action === 'PENDING' && !hasError && (
+
+ PENDING APPROVAL
+
+ )}
+ {hasError && (
+
+ FILE NOT FOUND
+
+ )}
+ {/* handleEditDocument(doc.fileStoreId || '')}
+ disabled={isFileLoading || hasError}
+ >
+
+ */}
+
+
+ {doc.filename}
+
+
+
+ {hasError ? (
+ {fileErrors[doc.fileStoreId || '']}
+ ) : isFileLoading ? (
+ 'Loading file...'
+ ) : (
+ <>{doc.size && `${doc.size} โข `}{doc.date}>
+ )}
+
+ handleViewDocument(doc.fileStoreId || '')}
+ disabled={!fileBlobs[doc.fileStoreId || ''] || hasError || isFileLoading}
+ >
+
+
+ handleDownloadDocument(doc.fileStoreId || '', doc.filename || 'document')}
+ disabled={!fileBlobs[doc.fileStoreId || ''] || hasError || isFileLoading}
+ >
+
+
+
+
+
+ );
+ })}
+
+ ) : (
+
+ No documents found.
+
+ )}
+
+ );
+};
+
+export default Documents;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/ui/HistoryCard.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/ui/HistoryCard.tsx
new file mode 100644
index 0000000..b5563ad
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/ui/HistoryCard.tsx
@@ -0,0 +1,77 @@
+import {
+ // Box,
+ // Chip,
+ Paper,
+ Stack,
+ // Tooltip,
+ Typography,
+} from "@mui/material";
+import type { FC } from "react";
+import { useAppSelector } from "../../../../../redux/Hooks";
+import { getMessagesFromSession, useLocalization } from "../../../../../services/Citizen/Localization/LocalizationContext";
+// import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
+import LoadingPage from "../../../../components/Loader";
+import type { CitizenPropertyData } from "../../models/CitizenPropertiesPageModel/CitizenPropertyPageModel";
+
+// Tag color by ownershipType (case-insensitive)
+// const TYPE_COLORS: Record = {
+// owner: "#C84C0E",
+// tenant: "#92731C",
+// };
+
+interface HistoryProps {
+ property: CitizenPropertyData;
+}
+
+const History: FC = ({ property }) => {
+ const lang = useAppSelector((state) => state.lang.citizenLang);
+ const { loading } = useLocalization();
+ const messages = getMessagesFromSession("CITIZEN")!;
+
+ if (loading) {
+ return ;
+ }
+
+ if (!property) return null;
+
+ // Note: The API response doesn't include a 'history' field
+ // You'll need to either:
+ // 1. Add a separate API endpoint to fetch property history
+ // 2. Use property ownership/transaction data from another source
+ // For now, we'll show a placeholder message
+ const hasHistory = false; // Set to true when history data is available
+
+ return (
+
+
+
+ {hasHistory ? (
+
+ {/* When you have history data, map it here */}
+ {/* Example structure:
+ {property.history.map((h, idx) => (
+
+ // History card content
+
+ ))}
+ */}
+
+ ) : (
+
+ {messages['citizen.my-properties'][lang]['no-history'] || 'No ownership history available.'}
+
+ )}
+
+ );
+};
+
+export default History;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/ui/OverviewCard.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/ui/OverviewCard.tsx
new file mode 100644
index 0000000..df29d04
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/ui/OverviewCard.tsx
@@ -0,0 +1,160 @@
+// OverviewCard.tsx displays a summary of property details for the Citizen property details page.
+// It shows government numbers, land use, zoning, and FSI, using MUI for UI and localization for labels.
+// Main responsibilities:
+// - Map API property data to display fields
+// - Render overview and land use/zoning cards
+// - Use OverviewStack for additional summary info
+// - Show loading state and handle missing data
+// Props: property (CitizenPropertyData) - the property whose overview is shown
+import { Box, Paper, Stack, Typography } from "@mui/material";
+import type { FC } from "react";
+import OverviewStack from "../OverviewStack";
+import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
+// import MapIcon from "@mui/icons-material/Map";
+import { useAppSelector } from "../../../../../redux/Hooks";
+import { getMessagesFromSession, useLocalization } from "../../../../../services/Citizen/Localization/LocalizationContext";
+import LoadingPage from "../../../../components/Loader";
+import type { CitizenPropertyData } from "../../models/CitizenPropertiesPageModel/CitizenPropertyPageModel";
+
+// Props for OverviewCard: expects property data
+interface OverviewCardProps {
+ property: CitizenPropertyData;
+}
+
+// OverviewCard component: displays property summary and land use/zoning info
+const OverviewCard: FC = ({ property }) => {
+ // Localization, state, and hooks
+ const lang = useAppSelector((state) => state.lang.citizenLang); // Current language
+ const { loading } = useLocalization(); // Global loading state
+ const messages = getMessagesFromSession("CITIZEN")!; // Localized messages
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ // No property data: render nothing
+ if (!property) {
+ return null;
+ }
+
+ // Map API data to display fields
+ // Note: These fields might not exist in CitizenPropertyData,
+ // you'll need to adjust based on actual data structure
+ // const surveyNumber = property.AdditionalDetails?.fieldValue?.serialNo || 'N/A';
+ const zoneNo = property.Address?.ZoneNo || 'N/A';
+ const ward = property.Address?.WardNo || 'N/A';
+ const currentUse = property.PropertyType || 'N/A';
+ const approvedUse = property.PropertyType || 'N/A';
+ const zoning = property.Address?.ZoneNo || 'N/A';
+ const fsi = property.IGRS?.builtUpAreaPct ? `${property.IGRS.builtUpAreaPct}%` : 'N/A';
+
+ // Render overview and land use/zoning cards
+ return (
+ <>
+ {/* Overview summary card */}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['gov-numbers']}
+
+
+ {/* {messages['citizen.my-properties'][lang]['survey-number']}: {zoneNo} */}
+ Zone No: {zoneNo}
+
+
+ {messages['citizen.my-properties'][lang]['ward']}: {ward}
+
+
+
+ {/* Land Use & Zoning Card - ONLY in Overview */}
+
+
+
+
+
+ {messages['citizen.my-properties'][lang]['land-use-zone']}
+
+
+ {/* }
+ >
+ {messages['citizen.commons'][lang]['view-map']}
+ */}
+
+
+
+
+
+ {messages['citizen.my-properties'][lang]['current-use']}
+
+
+ {currentUse}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['approved-use']}
+
+
+ {approvedUse}
+
+
+
+
+
+
+
+ {messages['citizen.my-properties'][lang]['zoning']}
+
+
+ {zoning}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['fsi']}
+
+
+ {fsi}
+
+
+
+
+ >
+ );
+}
+
+export default OverviewCard;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/ui/PlotInfoCard.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/ui/PlotInfoCard.tsx
new file mode 100644
index 0000000..34cc590
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/ui/PlotInfoCard.tsx
@@ -0,0 +1,197 @@
+// PlotInfoCard.tsx displays plot and building setback information for a property on the Citizen property details page.
+// It shows survey, address, and IGRS setback details using MUI for UI and localization for labels.
+// Main responsibilities:
+// - Map API property data to display fields
+// - Render info grid for plot details
+// - Render building setback grid
+// - Show loading state and handle missing data
+// Props: property (CitizenPropertyData) - the property whose plot info is shown
+import { Box, Divider, Paper, Typography } from "@mui/material";
+import type { FC } from "react";
+import { useAppSelector } from "../../../../../redux/Hooks";
+import { getMessagesFromSession, useLocalization } from "../../../../../services/Citizen/Localization/LocalizationContext";
+import LoadingPage from "../../../../components/Loader";
+import type { CitizenPropertyData } from "../../models/CitizenPropertiesPageModel/CitizenPropertyPageModel";
+import activity_zone from "../../../../assets/activity_zone.svg"
+
+// Props for PlotInfo: expects property data
+interface PlotInfoProps {
+ property: CitizenPropertyData;
+}
+
+// PlotInfo component: displays plot and building setback info for a property
+const PlotInfo: FC = ({ property }) => {
+ // Localization, state, and hooks
+ const lang = useAppSelector((state) => state.lang.citizenLang); // Current language
+ const { loading } = useLocalization(); // Global loading state
+ const messages = getMessagesFromSession("CITIZEN")!; // Localized messages
+
+ // Show loader if localization is loading
+ if (loading) {
+ return ;
+ }
+
+ // No property data: render nothing
+ if (!property) return null;
+
+ // Map API data to display fields
+ // const surveyNumber = property.AdditionalDetails?.fieldValue?.serialNo?.toString() || '-';
+ const complexName = property.ComplexName.toString() || '-';
+ const locality = property.Address?.Locality || '-';
+ const zoneNo = property.Address?.ZoneNo || '-';
+ const wardNo = property.Address?.WardNo || '-';
+ const blockNo = property.Address?.BlockNo || '-';
+ const street = property.Address?.Street || '-';
+
+ // Building Setbacks from IGRS (if available)
+ const frontSetback = property.IGRS?.frontSetback ? `${property.IGRS.frontSetback} ft` : '-';
+ const coverage = property.IGRS?.builtUpAreaPct ? `${property.IGRS.builtUpAreaPct}%` : '-';
+ const rearSetback = property.IGRS?.rearSetback ? `${property.IGRS.rearSetback} ft` : '-';
+ const sideSetback = property.IGRS?.sideSetback ? `${property.IGRS.sideSetback} ft` : '-';
+
+ // Render plot info and building setback grids
+ return (
+
+ {/* Heading with icon */}
+
+
+ {/* svg icon for plot info */}
+
+
+
+ {messages['citizen.my-properties'][lang]['plt-info']}
+
+
+ {/* Info Grid: survey, address, etc. */}
+
+ {/* Survey Number / Locality */}
+
+
+ {/* {messages['citizen.my-properties'][lang]['survey-number']} */}
+ Apartment Name
+
+
+ {complexName}
+
+
+
+
+ Locality
+
+
+ {locality}
+
+
+ {/* Zone No / Ward No */}
+
+
+ Zone No
+
+
+ {zoneNo}
+
+
+
+
+ Ward No
+
+
+ {wardNo}
+
+
+ {/* Block No / Street */}
+
+
+ Block No
+
+
+ {blockNo}
+
+
+
+
+ Street
+
+
+ {street}
+
+
+
+
+
+ {/* Building Setbacks grid */}
+
+ {messages['citizen.my-properties'][lang]['build-setbacks']}
+
+
+ {/* Front Setback / Coverage */}
+
+
+ {messages['citizen.my-properties'][lang]['front-setback']}
+
+
+ {frontSetback}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['coverage']}
+
+
+ {coverage}
+
+
+ {/* Rear Setback / Side Setbacks */}
+
+
+ {messages['citizen.my-properties'][lang]['rear-setback']}
+
+
+ {rearSetback}
+
+
+
+
+ {messages['citizen.my-properties'][lang]['side-setback']}
+
+
+ {sideSetback}
+
+
+
+
+ );
+};
+
+export default PlotInfo;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/components/ui/PropertyList.tsx b/frontend/mobile-ui/src/app/features/Citizen/components/ui/PropertyList.tsx
new file mode 100644
index 0000000..7d88398
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/components/ui/PropertyList.tsx
@@ -0,0 +1,36 @@
+// PropertyList.tsx displays a list of property cards for the Citizen property list page.
+// It receives an array of property summaries and renders a PropertyCard for each.
+// Main responsibilities:
+// - Map over properties and render PropertyCard for each
+// - Pass property data as props to PropertyCard
+// Props: properties (CitizenPropertySummary[]) - array of property summaries to display
+import type { FC } from 'react';
+import { Box } from '@mui/material';
+import PropertyCard from '../PropertyCard';
+import type { CitizenPropertySummary } from '../../api/CitizenHomePageApi/CitizenHomePageModel';
+
+// PropertyList component: renders a list of PropertyCard components for each property
+const PropertyList: FC<{ properties: CitizenPropertySummary[] }> = ({ properties }) => {
+ return (
+ <>
+ {properties.map((prop: CitizenPropertySummary) => (
+
+ {/* */}
+
+
+ ))}
+ >
+ );
+};
+
+// Export PropertyList for use in property list pages
+export default PropertyList;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/AssessmentDetails.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/AssessmentDetails.model.ts
new file mode 100644
index 0000000..e70d6d3
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/AssessmentDetails.model.ts
@@ -0,0 +1,9 @@
+// Represents assessment details for a property
+export type AssessmentDetails = {
+ reason: string; // Reason for assessment creation
+ extentOfSite: number; // Area/extent of the site
+ landUnderBuilding: number; // Area of land under the building
+ isUnspecifiedShare: boolean; // Whether the property has an unspecified share
+ occupancyCertificateDate: string; // Date of occupancy certificate
+ occupancyCertificateNumber: string; // Occupancy certificate number
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/Bill.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/Bill.model.ts
new file mode 100644
index 0000000..7f0d771
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/Bill.model.ts
@@ -0,0 +1,12 @@
+// Represents a property tax bill
+export type Bill = {
+ id: string; // Unique bill identifier
+ propertyId: string; // Linked property ID
+ billNo: string; // Bill number
+ billType: string; // Type of bill (e.g., property tax, penalty)
+ amount: number; // Bill amount
+ status: string; // Bill status (e.g., paid, unpaid)
+ dueDate: string; // Due date for payment
+ createdAt: string; // Creation timestamp
+ updatedAt: string; // Last update timestamp
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/CitizenDocumentPageModel/fileStoreModel.ts b/frontend/mobile-ui/src/app/features/Citizen/models/CitizenDocumentPageModel/fileStoreModel.ts
new file mode 100644
index 0000000..7658381
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/CitizenDocumentPageModel/fileStoreModel.ts
@@ -0,0 +1,8 @@
+// Represents the result of a file upload operation
+export interface UploadResult {
+ files: {
+ fileStoreId: string; // Unique identifier for the uploaded file in the file store
+ tenantId: string; // Tenant or jurisdiction ID associated with the file
+ }[];
+}
+
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/CitizenHome.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/CitizenHome.model.ts
new file mode 100644
index 0000000..47b65d4
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/CitizenHome.model.ts
@@ -0,0 +1,10 @@
+import type { UrgentAttention } from "../models/UrgentAttention.model";
+import type { Property } from "../models/Property.model";
+
+// Represents the main data structure for the citizen home/dashboard view
+export interface CitizenHomeData {
+ activeLicenses: number; // Number of active licenses for the citizen
+ numberOfProperties: number; // Total number of properties owned
+ properties: Property[]; // List of property objects
+ urgentAttention: UrgentAttention[]; // List of items requiring urgent attention
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/CitizenPropertiesPageModel/CitizenPropertyPageModel.ts b/frontend/mobile-ui/src/app/features/Citizen/models/CitizenPropertiesPageModel/CitizenPropertyPageModel.ts
new file mode 100644
index 0000000..daa4fc8
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/CitizenPropertiesPageModel/CitizenPropertyPageModel.ts
@@ -0,0 +1,170 @@
+// Response structure for citizen property API
+export interface CitizenPropertyResponse {
+ success: boolean; // Indicates if the API call was successful
+ message: string; // Response message
+ data: CitizenPropertyData; // Main property data
+}
+
+// Main property data model for citizen view
+export interface CitizenPropertyData {
+ ID: string; // Unique property identifier
+ PropertyNo: string; // Property number
+ OwnershipType: string; // Type of ownership
+ PropertyType: string; // Type/category of property
+ ComplexName: string; // Name of the complex (if any)
+ Address: PropertyAddress; // Address details
+ AssessmentDetails: AssessmentDetails; // Assessment details
+ Amenities: Amenities; // Amenities available
+ ConstructionDetails: ConstructionDetails; // Construction details
+ AdditionalDetails: AdditionalDetails; // Additional custom details
+ GISData: GISData; // Geographic Information System data
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+ Documents: PropertyDocument[]; // Related documents
+ IGRS: IGRSData; // IGRS (Integrated Grievance Redressal System) data
+}
+
+// Address details for a property
+export interface PropertyAddress {
+ ID: string; // Unique address identifier
+ Locality: string; // Locality or neighborhood
+ ZoneNo: string; // Zone number
+ WardNo: string; // Ward number
+ BlockNo: string; // Block number
+ Street: string; // Street name
+ ElectionWard: string; // Election ward
+ SecretariatWard: string; // Secretariat ward
+ PinCode: number; // Postal code
+ DifferentCorrespondenceAddress: boolean; // If correspondence address is different
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Assessment details for the property
+export interface AssessmentDetails {
+ ID: string; // Unique identifier
+ ReasonOfCreation: string; // Reason for assessment creation
+ OccupancyCertificateNumber: string; // Occupancy certificate number
+ OccupancyCertificateDate: string; // Date of occupancy certificate
+ ExtendOfSite: string; // Site area/extent
+ IsLandUnderneathBuilding: boolean; // Whether land is underneath building
+ IsUnspecifiedShare: boolean; // If property has unspecified share
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Amenities available for the property
+export interface Amenities {
+ ID: string; // Unique identifier
+ type: string[]; // Types of amenities
+ Description: string; // Description of amenities
+ ExpiryDate: string; // Expiry date for amenities
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Construction-related details of the property
+export interface ConstructionDetails {
+ ID: string; // Unique identifier
+ FloorType: string; // Type of floor
+ WallType: string; // Type of wall
+ RoofType: string; // Type of roof
+ WoodType: string; // Type of wood used
+ PropertyID: string; // Linked property ID
+ FloorDetails: FloorDetail[]; // Details about each floor
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Details for each floor in the property
+export interface FloorDetail {
+ ID: string; // Unique floor identifier
+ FloorNo: number; // Floor number
+ Classification: string; // Classification of the floor
+ NatureOfUsage: string; // Usage type (e.g., residential, commercial)
+ FirmName: string; // Name of firm (if any)
+ OccupancyType: string; // Type of occupancy
+ OccupancyName: string; // Name of occupant
+ constructionDate: string; // Date of construction
+ effectiveFromDate: string; // Effective from date
+ UnstructuredLand: string; // Unstructured land info
+ LengthFt: number; // Length in feet
+ BreadthFt: number; // Breadth in feet
+ PlinthAreaSqFt: number; // Plinth area in sq ft
+ BuildingPermissionNo: string; // Building permission number
+ FloorDetailsEntered: boolean; // Whether floor details are entered
+ ConstructionDetailsID: string; // Linked construction details ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Any additional custom details for the property
+export interface AdditionalDetails {
+ ID: string; // Unique identifier
+ FieldName: string; // Name of the additional field
+ fieldValue: {
+ DocumentType: number; // Type of document
+ revenueDocumentNo: number; // Revenue document number
+ serialNo: number; // Serial number
+ };
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Geographic Information System (GIS) data for the property
+export interface GISData {
+ ID: string; // Unique GIS data identifier
+ Source: string; // Source of GIS data
+ Type: string; // Type of GIS data
+ Latitude: number; // Latitude coordinate
+ Longitude: number; // Longitude coordinate
+ EntityType: string; // Type of entity (e.g., property)
+ PropertyID: string; // Linked property ID
+ Coordinates: Coordinate[]; // List of coordinates
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+}
+
+// Single coordinate point for GIS data
+export interface Coordinate {
+ ID: string; // Unique coordinate identifier
+ Latitude: number; // Latitude value
+ Longitude: number; // Longitude value
+ GISDataID: string; // Linked GIS data ID
+ CreatedAt: string; // Creation timestamp
+}
+
+// Document associated with the property
+export interface PropertyDocument {
+ ID: string; // Unique document identifier
+ PropertyID: string; // Linked property ID
+ DocumentType: string; // Type of document
+ DocumentName: string; // Name of the document
+ FileStoreID: string; // File store identifier
+ UploadDate: string; // Date of upload
+ action: string; // Action performed (e.g., upload, delete)
+}
+
+// IGRS (Integrated Grievance Redressal System) data for the property
+export interface IGRSData {
+ id: string; // Unique IGRS data identifier
+ habitation: string; // Habitation name
+ igrsWard: string; // IGRS ward
+ igrsLocality: string; // IGRS locality
+ igrsBlock: string; // IGRS block
+ doorNoFrom: string; // Door number (from)
+ doorNoTo: string; // Door number (to)
+ igrsClassification: string; // IGRS classification
+ builtUpAreaPct: number; // Built-up area percentage
+ frontSetback: number; // Front setback
+ rearSetback: number; // Rear setback
+ sideSetback: number; // Side setback
+ totalPlinthArea: number; // Total plinth area
+ createdAt: string; // Creation timestamp
+ updatedAt: string; // Last update timestamp
+ PropertyID: string; // Linked property ID
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/ConstructionDetails.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/ConstructionDetails.model.ts
new file mode 100644
index 0000000..9c2bd5f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/ConstructionDetails.model.ts
@@ -0,0 +1,7 @@
+// Represents construction details for a property
+export type ConstructionDetails = {
+ roofType: string; // Type of roof used in the property
+ wallType: string; // Type of wall construction
+ woodType: string; // Type of wood used (if any)
+ floorType: string; // Type of flooring
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/Document.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/Document.model.ts
new file mode 100644
index 0000000..c5b0095
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/Document.model.ts
@@ -0,0 +1,13 @@
+import type { DocumentFile } from "./DocumentFile.model";
+
+// Represents a legal or property-related document
+export type Document = {
+ no: string; // Document number or identifier
+ files: DocumentFile[]; // List of associated document files
+ courtName: string; // Name of the court (if applicable)
+ documentType: string; // Type/category of the document
+ constructionDate: string; // Date of construction (if relevant)
+ mroProceedingDate: string; // Date of MRO (Mandal Revenue Officer) proceeding
+ mroProceedingNumber: string; // MRO proceeding number
+ testatorAndTwoWitnessesSigned: boolean; // Whether the document is signed by testator and two witnesses
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/DocumentFile.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/DocumentFile.model.ts
new file mode 100644
index 0000000..273bcc2
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/DocumentFile.model.ts
@@ -0,0 +1,8 @@
+// Represents a file associated with a document
+export type DocumentFile = {
+ fileName: string; // Name of the file
+ fileSize: number; // Size of the file in bytes
+ fileType: string; // MIME type of the file
+ fileStoreId: string; // Unique identifier in the file store
+ dateOfUpload: string; // Date when the file was uploaded
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/FloorDetails.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/FloorDetails.model.ts
new file mode 100644
index 0000000..ce5dff1
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/FloorDetails.model.ts
@@ -0,0 +1,18 @@
+// Represents details for a single floor in a property
+export type FloorDetails = {
+ length: number; // Length of the floor (in feet/meters)
+ breadth: number; // Breadth of the floor (in feet/meters)
+ firmName: string; // Name of the firm (if any) occupying the floor
+ occupancy: string; // Type of occupancy (e.g., owner, tenant)
+ plinthArea: number; // Plinth area of the floor
+ floorNumber: string; // Floor number or label
+ occupantName: string; // Name of the occupant
+ natureOfUsage: string; // Usage type (e.g., residential, commercial)
+ constructionDate: string; // Date of construction
+ unstructuredLand: string; // Unstructured land info (if any)
+ effectiveFromDate: string; // Date from which the floor is effective
+ igrsClassification: string; // IGRS classification for the floor
+ buildingPermissionNo: string; // Building permission number
+ floorsDetailsEntered: boolean; // Whether floor details have been entered
+ buildingClassification: string; // Classification of the building
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/History.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/History.model.ts
new file mode 100644
index 0000000..c098c3d
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/History.model.ts
@@ -0,0 +1,11 @@
+// Represents a single item in the property ownership or transaction history
+export type HistoryItem = {
+ id: string; // Unique identifier for the history item
+ propertyId: string; // Linked property ID
+ ownerName: string; // Name of the owner during this period
+ ownershipType: string; // Type of ownership (e.g., freehold, leasehold)
+ description: string; // Description of the transaction or change
+ periodStart: string; // Start date of this ownership/period
+ transactionDate: string; // Date of the transaction
+ createdAt: string; // Creation timestamp
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/IGRSAdditionalDetails.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/IGRSAdditionalDetails.model.ts
new file mode 100644
index 0000000..56113a0
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/IGRSAdditionalDetails.model.ts
@@ -0,0 +1,10 @@
+// Represents additional amenities/details for IGRS (Integrated Grievance Redressal System)
+export type ISGRAdditionalDetails = {
+ lifts: boolean; // Whether lifts are available
+ toilet: boolean; // Whether toilets are available
+ watertap: boolean; // Whether water tap is available
+ electricity: boolean; // Whether electricity is available
+ cableConnection: boolean; // Whether cable connection is available
+ waterHarvesting: boolean; // Whether water harvesting is available
+ attachedBathroom: boolean; // Whether attached bathroom is available
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/IGRSDetails.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/IGRSDetails.model.ts
new file mode 100644
index 0000000..2d555f3
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/IGRSDetails.model.ts
@@ -0,0 +1,9 @@
+// Represents IGRS (Integrated Grievance Redressal System) property details
+export type ISGRDetails = {
+ doorNoTo: string; // Door number (to)
+ igrsWard: string; // IGRS ward
+ igrsBlock: string; // IGRS block
+ doorNoFrom: string; // Door number (from)
+ habitation: string; // Habitation name
+ igrsLocality: string; // IGRS locality
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/License.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/License.model.ts
new file mode 100644
index 0000000..e266fee
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/License.model.ts
@@ -0,0 +1,11 @@
+// Represents a license associated with a property
+export type License = {
+ id: string; // Unique license identifier
+ propertyId: string; // Linked property ID
+ licenseNo: string; // License number
+ licenseType: string; // Type of license (e.g., trade, construction)
+ status: string; // License status (e.g., active, expired)
+ issuedDate: string; // Date when the license was issued
+ expiryDate: string; // Date when the license expires
+ createdAt: string; // Creation timestamp
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/Location.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/Location.model.ts
new file mode 100644
index 0000000..8c135dd
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/Location.model.ts
@@ -0,0 +1,9 @@
+// Represents location information for a property or event
+export type LocationData = {
+ address: string; // Full address as a string
+ coordinates: {
+ lat: number; // Latitude value
+ lng: number; // Longitude value
+ };
+ timestamp: string; // Timestamp when the location was recorded
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/Owner.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/Owner.model.ts
new file mode 100644
index 0000000..a62b953
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/Owner.model.ts
@@ -0,0 +1,10 @@
+// Represents an owner of a property
+export type Owner = {
+ email: string; // Owner's email address
+ gender: string; // Owner's gender
+ mobile: string; // Owner's mobile number
+ aadhaar: string; // Owner's Aadhaar number (unique ID in India)
+ guardian: string; // Name of the guardian (if applicable)
+ ownerName: string; // Name of the property owner
+ guardianRelationship: string; // Relationship to the guardian
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/Property.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/Property.model.ts
new file mode 100644
index 0000000..a0f2f17
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/Property.model.ts
@@ -0,0 +1,56 @@
+import type { AssessmentDetails } from "./AssessmentDetails.model";
+import type { Bill } from "./Bill.model";
+import type { ConstructionDetails } from "./ConstructionDetails.model";
+import type { Document } from "./Document.model";
+import type { FloorDetails } from "./FloorDetails.model";
+import type { HistoryItem } from "./History.model";
+import type { ISGRAdditionalDetails } from "./IGRSAdditionalDetails.model";
+import type { ISGRDetails } from "./IGRSDetails.model";
+import type { License } from "./License.model";
+import type { LocationData } from "./Location.model";
+import type { Owner } from "./Owner.model";
+import type { PropertyAddress } from "./PropertyAddress.model";
+
+// Represents a property and all its associated details
+export interface Property {
+ apartmentName: string; // Name of the apartment (if applicable)
+ approvedUse: string; // Approved use of the property (e.g., residential, commercial)
+ assessmentDetails: AssessmentDetails; // Assessment details for the property
+ bills: Bill[]; // List of bills associated with the property
+ billsAmount: number; // Total amount of all bills
+ billsDue: number; // Total due amount
+ builtUpArea: string; // Built-up area of the property
+ categoryOfOwnership: string; // Category of ownership (e.g., freehold, leasehold)
+ constructionDetails: ConstructionDetails; // Construction details
+ constructionYear: number; // Year of construction
+ coverage: string; // Coverage area
+ currentUse: string; // Current use of the property
+ district: string; // District where the property is located
+ documents: Document[]; // List of property-related documents
+ enumerationProgress: number; // Progress of property enumeration (e.g., for surveys)
+ floors: FloorDetails[]; // Details for each floor
+ frontSetback: string; // Front setback distance
+ fsi: string; // Floor Space Index
+ history: HistoryItem[]; // Ownership or transaction history
+ hobli: string; // Hobli (administrative division)
+ id: string; // Unique property identifier
+ isgrAdditionalDetails: ISGRAdditionalDetails; // Additional IGRS details
+ isgrDetails: ISGRDetails; // IGRS details
+ issuedLicenses: number; // Number of licenses issued
+ licenses: License[]; // List of licenses
+ locationData: LocationData; // Location information
+ owners: Owner[]; // List of property owners
+ plotArea: string; // Plot area
+ propertyAddress: PropertyAddress; // Address details
+ propertyType: string; // Type/category of property
+ propertyValue: string; // Value of the property
+ rearSetback: string; // Rear setback distance
+ sideSetback: string; // Side setback distance
+ subdivision: string; // Subdivision name/number
+ surveyNumber: string; // Survey number
+ taluk: string; // Taluk (administrative division)
+ utilities: any; // Utilities associated with the property
+ village: string; // Village name
+ ward: string; // Ward number or name
+ zoning: string; // Zoning information
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/PropertyAddress.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/PropertyAddress.model.ts
new file mode 100644
index 0000000..b83d5da
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/PropertyAddress.model.ts
@@ -0,0 +1,12 @@
+// Represents the address details for a property
+export type PropertyAddress = {
+ street: string; // Street name
+ wardNo: string; // Ward number
+ zoneNo: string; // Zone number
+ blockNo: string; // Block number
+ pincode: string; // Postal code
+ locality: string; // Locality or neighborhood
+ electionWard: string; // Election ward
+ secretariatWard: string; // Secretariat ward
+ isCorrespondenceAddressDifferent: boolean; // Whether correspondence address is different
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/PropertyContext.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/PropertyContext.model.ts
new file mode 100644
index 0000000..d98983a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/PropertyContext.model.ts
@@ -0,0 +1,11 @@
+import type { Property } from "./Property.model";
+
+// Represents the context type for managing property state in React context
+export interface PropertyContextType {
+ properties: Property[]; // List of all properties
+ selectedProperty: Property | null; // Currently selected property
+ setSelectedProperty: (property: Property | null) => void; // Function to set the selected property
+ reloadProperties: () => void; // Function to reload property list
+ loading: boolean; // Loading state for property operations
+ error: string | null; // Error message, if any
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/PropertyState.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/PropertyState.model.ts
new file mode 100644
index 0000000..5f3c7bb
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/PropertyState.model.ts
@@ -0,0 +1,6 @@
+import type { Property } from "./Property.model";
+
+// Represents the state for property selection (e.g., in Redux or React context)
+export interface PropertyState {
+ selectedProperty: Property | null; // Currently selected property, or null if none
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/Citizen/models/UrgentAttention.model.ts b/frontend/mobile-ui/src/app/features/Citizen/models/UrgentAttention.model.ts
new file mode 100644
index 0000000..2254b38
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/Citizen/models/UrgentAttention.model.ts
@@ -0,0 +1,15 @@
+
+// Status of an urgent attention item
+export type UrgentAttentionStatus = "success" | "pending" | "failed";
+
+// Type of urgent attention required
+export type UrgentAttentionType = "casual" | "immediate";
+
+// Represents an item requiring urgent attention on the citizen dashboard
+export interface UrgentAttention {
+ id: number; // Unique identifier for the urgent attention item
+ type: UrgentAttentionType; // Type of attention required (casual or immediate)
+ message: string; // Message or description of the issue
+ date: string; // ISO date string for when the attention is required
+ status: UrgentAttentionStatus; // Status of the attention item (success, pending, failed)
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/api/documentInfo.api.ts b/frontend/mobile-ui/src/app/features/PropertyForm/api/documentInfo.api.ts
new file mode 100644
index 0000000..cc9123e
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/api/documentInfo.api.ts
@@ -0,0 +1,49 @@
+// documentInfo.api.ts
+// This file defines RTK Query API endpoints for managing document info related to property forms.
+// Endpoints include create, fetch by property ID, and update document info.
+// Uses Redux Toolkit Query for data fetching and mutation, with tag-based cache management.
+// Types:
+// DocumentInfoRequest: request payload for document info
+// DocumentInfoResponse: response payload for create/update
+// DocumentInfoGetResponse: response payload for fetch
+
+import { apiSlice } from '../../../../redux/apiSlice';
+import { TAG_TYPES } from '../../../../redux/tagTypes';
+import type { DocumentInfoRequest, DocumentInfoResponse, DocumentInfoGetResponse } from '../models/documentInfo.model';
+
+export const documentInfoApi = apiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ // Mutation to create new document info for a property
+ createDocumentInfo: builder.mutation({
+ query: (body) => ({
+ url: '/v1/additional-property-details',
+ method: 'POST',
+ body,
+ }),
+ invalidatesTags: [TAG_TYPES.DOCUMENT_INFO],
+ }),
+ // Query to fetch document info by property ID
+ getDocumentInfoByPropertyId: builder.query({
+ query: (propertyId) => ({
+ url: `/v1/additional-property-details/property/${propertyId}?fieldName=DocumentInfo`,
+ method: 'GET',
+ }),
+ providesTags: [TAG_TYPES.DOCUMENT_INFO],
+ }),
+ // Mutation to update document info by document ID
+ updateDocumentInfo: builder.mutation({
+ query: ({ documentId, body }) => ({
+ url: `/v1/additional-property-details/${documentId}`,
+ method: 'PUT',
+ body,
+ }),
+ invalidatesTags: [TAG_TYPES.DOCUMENT_INFO],
+ }),
+ }),
+});
+
+export const {
+ useCreateDocumentInfoMutation,
+ useGetDocumentInfoByPropertyIdQuery,
+ useUpdateDocumentInfoMutation,
+} = documentInfoApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/api/documentUpload.api.ts b/frontend/mobile-ui/src/app/features/PropertyForm/api/documentUpload.api.ts
new file mode 100644
index 0000000..7395775
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/api/documentUpload.api.ts
@@ -0,0 +1,56 @@
+// documentUpload.api.ts
+// This file defines RTK Query API endpoints for managing document uploads related to property forms.
+// Endpoints include fetch, create, update, and delete document upload details.
+// Uses Redux Toolkit Query for data fetching and mutation, with tag-based cache management.
+// Types:
+// DocumentUploadDetailsRequest: request payload for document upload
+// DocumentUploadDetailsResponse: response payload for upload operations
+
+import { apiSlice } from '../../../../redux/apiSlice';
+import { TAG_TYPES } from '../../../../redux/tagTypes';
+import type { DocumentUploadDetailsRequest, DocumentUploadDetailsResponse } from '../models/documentsUpload.model';
+
+export const documentsUploadDetailsApi = apiSlice.injectEndpoints({
+ endpoints: (builder) => ({
+ // Query to fetch document upload details by property ID
+ getDocumentsUploadDetailsByPropertyId: builder.query({
+ query: (propertyId) => ({
+ url: `/v1/documents/property/${propertyId}`,
+ method: 'GET',
+ }),
+ providesTags:[TAG_TYPES.DOCUMENT]
+ }),
+ // Mutation to create new document upload details
+ createDocumentUploadDetails: builder.mutation({
+ query: (body) => ({
+ url: '/v1/documents',
+ method: 'POST',
+ body, // Send array directly
+ }),
+ invalidatesTags:[TAG_TYPES.DOCUMENT]
+ }),
+ // Mutation to update document upload details by document ID
+ updateDocumentUploadDetails: builder.mutation({
+ query: ({ document_id, body }) => ({
+ url: `/v1/documents/${document_id}`,
+ method: 'PUT',
+ body,
+ }),
+ }),
+ // Mutation to delete document upload details by document ID
+ deleteDocumentUploadDetails: builder.mutation<{ message: string }, string>({
+ query: (documentId) => ({
+ url: `/v1/documents/${documentId}`,
+ method: 'DELETE',
+ }),
+ invalidatesTags:[TAG_TYPES.DOCUMENT]
+ }),
+ }),
+});
+
+export const {
+ useGetDocumentsUploadDetailsByPropertyIdQuery,
+ useCreateDocumentUploadDetailsMutation,
+ useUpdateDocumentUploadDetailsMutation,
+ useDeleteDocumentUploadDetailsMutation,
+} = documentsUploadDetailsApi;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/components/ConstructionDetail/ConstructionDropdown.tsx b/frontend/mobile-ui/src/app/features/PropertyForm/components/ConstructionDetail/ConstructionDropdown.tsx
new file mode 100644
index 0000000..4c11710
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/components/ConstructionDetail/ConstructionDropdown.tsx
@@ -0,0 +1,163 @@
+// CustomDropdown is a reusable dropdown component for construction detail forms.
+// It supports validation, custom styling, and controlled open/close behavior.
+import React, { useRef } from 'react';
+import Box from '@mui/material/Box';
+import FormControl from '@mui/material/FormControl';
+import Select from '@mui/material/Select';
+import type { SelectChangeEvent } from '@mui/material/Select';
+import MenuItem from '@mui/material/MenuItem';
+import Typography from '@mui/material/Typography';
+import type { SxProps, Theme } from '@mui/material';
+
+// Option type for dropdown items
+export type DropdownOption = { id: string | number; label: string };
+
+// Props for the CustomDropdown component
+interface CustomDropdownProps {
+ label: string; // Field label
+ name: string; // Field name (for form handling)
+ value?: string; // Selected value
+ options: DropdownOption[]; // List of dropdown options
+ showDropdown: boolean; // Whether the dropdown is open
+ setShowDropdown: (b: boolean) => void; // Function to control dropdown open state
+ onSelect: (field: string, value: string) => void; // Callback when an option is selected
+ // Called by the dropdown before opening so parent can close others
+ closeOtherDropdowns?: () => void;
+ selectText?: string; // Placeholder text
+ required?: boolean; // Whether the field is required
+ error?: string; // Error message (if any)
+ touched?: boolean; // Whether the field has been touched
+ sx?: SxProps; // Custom styles
+ // Called when dropdown is closed (used by parent to mark touched/validate)
+ onBlur?: () => void;
+}
+
+// Style objects for dropdown and label
+const containerSx = { width: '100%', mb: 2 };
+const labelSx = {
+ fontSize: 14,
+ fontWeight: 400,
+ color: '#333333',
+ mb: '6px',
+ textAlign: 'left' as const,
+};
+const formControlSx = {
+ width: '100%',
+ '& .MuiOutlinedInput-root': { borderRadius: 2 },
+};
+const placeholderStyle: React.CSSProperties = { color: '#9E9E9E' };
+
+const CustomDropdown: React.FC = ({
+ label,
+ name,
+ value,
+ options,
+ showDropdown,
+ setShowDropdown,
+ onSelect,
+ closeOtherDropdowns,
+ selectText = 'Select',
+ required = false,
+ error,
+ touched,
+ sx,
+ onBlur,
+}) => {
+ // Use empty string if value is undefined
+ const safeValue = value ?? '';
+ const justSelectedRef = useRef(false);
+
+ // Open dropdown and close others if needed
+ const handleOpen = () => {
+ if (closeOtherDropdowns) closeOtherDropdowns();
+ setShowDropdown(true);
+ };
+
+ // Close dropdown
+ const handleClose = () => {
+ // If the close is happening immediately after a selection, skip onBlur.
+ if (justSelectedRef.current) {
+ justSelectedRef.current = false;
+ setShowDropdown(false);
+ return;
+ }
+
+ if (onBlur) onBlur();
+ setShowDropdown(false);
+ };
+
+ // Handle selection change
+ const handleChange = (e: SelectChangeEvent) => {
+ const selected = e.target.value as string;
+ // mark that the close that follows is caused by selection
+ justSelectedRef.current = true;
+ onSelect(name, selected);
+ // close will be handled; we still call setShowDropdown(false) to make UI responsive
+ setShowDropdown(false);
+ };
+
+ // Render dropdown UI
+ return (
+
+ {/* Field label with required indicator */}
+
+ {label}
+ {required ? * : null}
+
+ :{' '}
+
+
+
+
+
+ selected ? (
+ (selected as string)
+ ) : (
+ {selectText}
+ )
+ }
+ inputProps={{ 'aria-label': label }}
+ sx={{
+ ...(touched && error
+ ? { '& .MuiOutlinedInput-notchedOutline': { borderColor: '#d32f2f' } }
+ : {}),
+ }}
+ >
+ {/* Placeholder option */}
+
+ {selectText}
+
+ {/* Render dropdown options */}
+ {options.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+
+ {/* Error message display */}
+
+ {touched && error ? error : '\u00A0'}
+
+
+ );
+};
+
+// Export CustomDropdown for use in construction detail forms
+export default CustomDropdown;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/components/CountIncrementors.tsx b/frontend/mobile-ui/src/app/features/PropertyForm/components/CountIncrementors.tsx
new file mode 100644
index 0000000..0f24f72
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/components/CountIncrementors.tsx
@@ -0,0 +1,130 @@
+// CountIncrementor component: input with increment/decrement buttons for numeric values
+import React from 'react';
+import { Box, TextField, IconButton, Typography } from '@mui/material';
+import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp';
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+import InputAdornment from '@mui/material/InputAdornment';
+
+// Props for CountIncrementor: label, value, setter, min/max
+interface CountIncrementorProps {
+ label: string;
+ value: number;
+ setValue: (val: number) => void;
+ min?: number;
+ max?: number;
+}
+
+// Main component for count increment/decrement input
+const CountIncrementor: React.FC = ({
+ label,
+ value,
+ setValue,
+ min = 0,
+ max = 100
+}) => {
+ // Handle manual input change in the text field
+ const handleChange = (e: React.ChangeEvent) => {
+ const val = e.target.value === "" ? "" : Number(e.target.value);
+ if (val === "" || (val >= min && val <= max)) setValue(val === "" ? 0 : val);
+ };
+
+ // Increment value (up arrow)
+ const inc = () => {
+ if (value === 0 && min <= 1) setValue(1);
+ else if (typeof value === "number" && value < max) setValue(value + 1);
+ };
+ // Decrement value (down arrow)
+ const dec = () => {
+ if (typeof value === "number" && value > min) setValue(value - 1);
+ else if (value === 0 && min < 0) setValue(min);
+ };
+
+ // Render label, input, and increment/decrement buttons
+ return (
+
+
+ {label} :
+
+
+
+ = max : false}
+ tabIndex={-1}
+ >
+
+
+
+
+
+
+
+ ),
+ }}
+ sx={{
+ borderRadius: "10px",
+ background: "#fff",
+ // remove default number input arrows
+ "& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button": {
+ WebkitAppearance: "none",
+ margin: 0
+ },
+ "& input[type=number]": {
+ MozAppearance: "textfield"
+ },
+ fontSize: 22,
+ fontWeight: 400,
+ color: "#b0b0b0",
+ "& .MuiOutlinedInput-root": {
+ borderRadius: "10px",
+ height: "48px",
+ minWidth: "160px",
+ fontSize: 16,
+ bgcolor: "#fff",
+ borderColor: "#616a6e",
+ "& fieldset": {
+ borderColor: "#616a6e",
+ },
+ "&:hover fieldset": {
+ borderColor: "#b04c00",
+ },
+ "&.Mui-focused fieldset": {
+ borderColor: "#b04c00",
+ },
+ },
+ "&::placeholder": {
+ fontSize: 20,
+ color: "#b0b0b0",
+ opacity: 1,
+ },
+ }}
+ />
+
+ );
+};
+
+export default CountIncrementor;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/components/FloorDetail/FloorCard.tsx b/frontend/mobile-ui/src/app/features/PropertyForm/components/FloorDetail/FloorCard.tsx
new file mode 100644
index 0000000..026cb26
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/components/FloorDetail/FloorCard.tsx
@@ -0,0 +1,96 @@
+
+// Card summarizing a single floor's details in the property form, with edit/delete actions.
+
+import React from 'react';
+import MeetingRoomOutlinedIcon from '@mui/icons-material/MeetingRoomOutlined';
+import deleteIcon from '../../../../assets/Agent/delete.svg';
+import editSquare from '../../../../assets/Agent/edit_square.svg';
+import type { FloorDetails } from '../../../../../context/PropertyFormContext';
+import { useFloorDetailsLocalization } from '../../../../../services/AgentLocalisation/localisation-floor-details';
+import '../../../../../styles/FloorSection.css'
+
+interface FloorCardProps {
+ floor: FloorDetails;
+ index: number;
+ onEdit: (floor: FloorDetails) => void;
+ onDelete: (index: number) => void;
+}
+
+const FloorCard: React.FC = ({ floor, index, onEdit, onDelete }) => {
+ // Get localized labels for floor details
+ const {
+ floorText,
+ classificationText,
+ usageText,
+ plinthAreaLabel,
+ lengthLabel,
+ breadthLabel,
+ deleteText,
+ } = useFloorDetailsLocalization();
+
+ // Render floor card UI
+ return (
+
+ {/* Icon representing a floor */}
+
+
+ {/* Floor details */}
+
+ {floorText}: {floor.floorNumber}
+
+
+ {classificationText}: {floor.buildingClassification}
+
+
+ {usageText}: {floor.natureOfUsage}
+
+
+ {plinthAreaLabel}: {floor.plinthArea}
+
+
+ {lengthLabel}: {floor.length}
+
+
+ {breadthLabel}: {floor.breadth}
+
+
+ {/* Edit and delete buttons for the floor */}
+
+
onDelete(index)}
+ aria-label={deleteText}
+ type="button"
+ >
+
+
+
onEdit(floor)}
+ type="button"
+ aria-label="Edit floor"
+ >
+
+
+
+
+ );
+};
+
+// Export FloorCard for use in floor details forms
+export default FloorCard;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/components/IGRSDetail/IGRSFormTextFiled.tsx b/frontend/mobile-ui/src/app/features/PropertyForm/components/IGRSDetail/IGRSFormTextFiled.tsx
new file mode 100644
index 0000000..cba8bae
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/components/IGRSDetail/IGRSFormTextFiled.tsx
@@ -0,0 +1,98 @@
+
+// Custom text field for IGRS details in property forms. Supports validation, placeholder, error display, and style overrides.
+
+import React from 'react';
+import TextField from '@mui/material/TextField';
+import Typography from '@mui/material/Typography';
+
+interface FormTextFieldProps {
+ label: string;
+ // allow undefined values safely
+ value?: string;
+ onChange: (value: string) => void;
+ onBlur?: () => void;
+ onKeyPress?: (e: React.KeyboardEvent) => void;
+ placeholder?: string;
+ type?: 'text' | 'number';
+ required?: boolean;
+ error?: string;
+ touched?: boolean;
+ sx?: any;
+ inputProps?: React.InputHTMLAttributes;
+}
+
+// Style object for label
+const labelSx = {
+ fontSize: 14,
+ fontWeight: 400,
+ color: '#333333',
+ marginBottom: '6px',
+ textAlign: 'left' as const,
+};
+
+const FormTextField: React.FC = ({
+ label,
+ value,
+ onChange,
+ onBlur,
+ onKeyPress,
+ placeholder,
+ type = 'text',
+ required = false,
+ error,
+ touched,
+ inputProps,
+ sx,
+}) => {
+ // Ensure we never pass undefined to TextField value prop
+ const safeValue = value ?? '';
+
+ // Render text field UI
+ return (
+
+ {/* Field label with required indicator */}
+
+ {label}
+ {required ? * : null}
+ :
+
+
+ onChange(e.target.value)}
+ onBlur={onBlur}
+ onKeyPress={onKeyPress}
+ placeholder={placeholder ?? ''}
+ type={type}
+ inputProps={{ inputMode: type === 'number' ? 'decimal' : 'text', ...inputProps }}
+ error={!!(touched && error)}
+ helperText={touched && error ? error : '\u00A0'}
+ FormHelperTextProps={{ sx: { minHeight: 20 } }}
+ sx={{
+ fontFamily: 'Roboto, sans-serif !important',
+ '& .MuiOutlinedInput-input': {
+ fontFamily: 'Roboto, sans-serif',
+ boxSizing: 'border-box !important',
+ minHeight: '48px',
+ borderRadius: '10px',
+ },
+ '& .MuiOutlinedInput-root': {
+ borderRadius: '10px',
+ fontSize: '16px !important',
+ boxSizing: 'border-box !important',
+ },
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderColor: '#C4C4C4 !important',
+ borderRadius: '10px !important',
+ },
+ ...sx,
+ }}
+ />
+
+ );
+};
+
+// Export FormTextField for use in IGRS detail forms
+export default FormTextField;
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/components/IGRSDetail/IGRSdropdown.tsx b/frontend/mobile-ui/src/app/features/PropertyForm/components/IGRSDetail/IGRSdropdown.tsx
new file mode 100644
index 0000000..43b622b
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/components/IGRSDetail/IGRSdropdown.tsx
@@ -0,0 +1,172 @@
+
+// Custom dropdown for IGRS details in property forms. Supports validation, placeholder, error display, and controlled open/close.
+
+import React, { useRef } from 'react';
+import Box from '@mui/material/Box';
+import FormControl from '@mui/material/FormControl';
+import Select from '@mui/material/Select';
+import type { SelectChangeEvent } from '@mui/material/Select';
+import MenuItem from '@mui/material/MenuItem';
+import Typography from '@mui/material/Typography';
+import type {SxProps, Theme} from '@mui/material/styles';
+
+export type DropdownOption = { id: string | number; label: string };
+
+interface CustomDropdownProps {
+ label: string;
+ name: string;
+ value?: string;
+ options: DropdownOption[];
+ showDropdown: boolean;
+ setShowDropdown: (b: boolean) => void;
+ onSelect: (field: string, value: string) => void;
+ closeOtherDropdowns?: () => void;
+ selectText?: string;
+ required?: boolean;
+ error?: string;
+ touched?: boolean;
+ sx?: SxProps;
+ onBlur?: () => void;
+}
+
+// Style objects for dropdown and label
+const containerSx = {
+ width: '100%',
+ marginBottom: "2px",
+};
+
+const labelSx = {
+ fontSize: 14,
+ fontWeight: 400,
+ color: '#333333',
+ marginBottom: '1.4px',
+ textAlign: 'left' as const,
+};
+
+const formControlSx = {
+ width: '100%',
+ Height: "14px",
+ '& .MuiOutlinedInput-root': {
+ borderRadius: 2,
+ },
+};
+
+const placeholderStyle: React.CSSProperties = {
+ color: '#9E9E9E',
+};
+
+const CustomDropdown: React.FC = ({
+ label,
+ name,
+ value,
+ options,
+ showDropdown,
+ setShowDropdown,
+ onSelect,
+ closeOtherDropdowns,
+ selectText = 'Select',
+ required = false,
+ error,
+ touched,
+ onBlur,
+}) => {
+ // Ref to track if selection is happening (to distinguish blur from selection)
+ const isSelectingRef = useRef(false);
+
+ // Open dropdown and close others if needed
+ const handleOpen = () => {
+ if (closeOtherDropdowns) closeOtherDropdowns();
+ setShowDropdown(true);
+ isSelectingRef.current = false;
+ };
+
+ // Close dropdown and trigger blur if no selection
+ const handleClose = () => {
+ setShowDropdown(false);
+ // Wait a brief moment to see if onChange fires (indicating a selection was made)
+ setTimeout(() => {
+ if (onBlur && !isSelectingRef.current) {
+ onBlur();
+ }
+ isSelectingRef.current = false; // Reset for next time
+ }, 0);
+ };
+
+ // Handle selection change
+ const handleChange = (event: SelectChangeEvent) => {
+ const selected = event.target.value as string;
+ isSelectingRef.current = true; // Mark that selection is happening
+ onSelect(name, selected);
+ setShowDropdown(false);
+ };
+
+ // Use empty string if value is undefined
+ const safeValue = value ?? '';
+
+ // Render dropdown UI
+ return (
+
+ {/* Field label with required indicator */}
+
+ {label}
+ {required ? * : null}
+
+ :
+
+
+
+
+
+ selected ? (selected as string) : {selectText}
+ }
+ inputProps={{ 'aria-label': label }}
+ sx={{
+ height: 48,
+ '& .MuiSelect-select': {
+ display: 'flex',
+ alignItems: 'center',
+ height: '100%',
+ paddingTop: 0,
+ paddingBottom: 0,
+ },
+ ...(touched && error ? { '& .MuiOutlinedInput-notchedOutline': { borderColor: '#d32f2f' } } : {}),
+ }}
+ >
+ {/* Placeholder option */}
+
+ {selectText}
+
+ {/* Render dropdown options */}
+ {options.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+
+ {/* Error message display */}
+
+ {touched && error ? error : '\u00A0'}
+
+
+ );
+};
+
+// Export CustomDropdown for use in IGRS detail forms
+export default CustomDropdown;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/components/OwnerDetail/OwnerCard.tsx b/frontend/mobile-ui/src/app/features/PropertyForm/components/OwnerDetail/OwnerCard.tsx
new file mode 100644
index 0000000..1887b7c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/components/OwnerDetail/OwnerCard.tsx
@@ -0,0 +1,84 @@
+// OwnerCard.tsx
+// This component displays a summary card for a property owner in the property form.
+// Shows owner avatar, name, type (primary/secondary), and provides delete/view actions.
+// Props:
+// name: Owner's name
+// isPrimary: Whether the owner is the primary owner
+// onDelete: Callback to delete the owner
+// onViewOwners: Callback to view all owners
+// primaryOwnerText: Label for primary owner
+// ownerText: Label for secondary owner
+// nameText: Label for name field
+// viewOwnersText: Label for view owners button
+// Used in: Owner details section of property forms
+
+import React from 'react';
+import '../../../../../styles/OwnerCard.css';
+import accountCircle from '../../../../assets/Agent/account_circle.svg';
+import deleteIcon from '../../../../assets/Agent/delete.svg';
+
+interface OwnerCardProps {
+ name: string;
+ isPrimary?: boolean;
+ onDelete?: () => void;
+ onViewOwners?: () => void;
+ primaryOwnerText?: string;
+ ownerText?: string;
+ nameText?: string;
+ viewOwnersText?: string;
+}
+
+const OwnerCard: React.FC = ({
+ name,
+ isPrimary = false,
+ onDelete,
+ onViewOwners,
+ primaryOwnerText = 'Primary Owner',
+ ownerText = 'Owner',
+ nameText = 'Name',
+ viewOwnersText = 'View Owners',
+}) => (
+ // Render owner summary card UI
+
+
+
+ {/* Owner avatar icon */}
+
+
+
+ {/* Owner type (primary/secondary) */}
+
{isPrimary ? primaryOwnerText : ownerText}
+ {/* Owner name */}
+
{nameText}: {name}
+
+ {/* Delete owner button if callback provided */}
+ {onDelete && (
+
+
+
+ )}
+
+ {/* View owners button if callback provided */}
+ {onViewOwners && (
+
+ {viewOwnersText}
+
+ )}
+
+);
+
+// Export OwnerCard for use in owner details forms
+export default OwnerCard;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/components/OwnerDetail/OwnerCardDetail.tsx b/frontend/mobile-ui/src/app/features/PropertyForm/components/OwnerDetail/OwnerCardDetail.tsx
new file mode 100644
index 0000000..a99a2f2
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/components/OwnerDetail/OwnerCardDetail.tsx
@@ -0,0 +1,102 @@
+
+// Card displaying a property owner's details in the property form, with edit/delete actions.
+
+import React from 'react';
+import '../../../../../styles/OwnerCardDetail.css';
+import accountCircle from '../../../../assets/Agent/account_circle.svg';
+import deleteIcon from '../../../../assets/Agent/delete.svg';
+import editSquare from '../../../../assets/Agent/edit_square.svg';
+
+interface OwnerCardProps {
+ name: string;
+ isPrimary?: boolean;
+ onDelete?: () => void;
+ onViewOwners?: () => void;
+ isDetailed: boolean;
+ aadhar?: string;
+ mobile?: string;
+ email?: string;
+ guardian?: string;
+ guardianRelationship?: string;
+ nameText: string;
+ mobileNumberLabel: string;
+ aadhaarLabel: string;
+ emailLabel: string;
+ guardianLabel: string;
+ primaryOwnerText: string;
+ onEdit?: () => void;
+}
+
+const OwnerCardDetail: React.FC = ({
+ name,
+ isPrimary = false,
+ onDelete,
+ isDetailed = false,
+ aadhar,
+ mobile,
+ email,
+ guardian,
+ guardianRelationship,
+ // nameText,
+ // mobileNumberLabel,
+ // aadhaarLabel,
+ // emailLabel,
+ guardianLabel,
+ primaryOwnerText,
+ onEdit,
+}) => {
+ // Render owner detailed card UI
+ return (
+
+
+ {/* Owner avatar icon */}
+
+
+
+ {/* Owner info and details */}
+
+ {/* Owner type (primary/secondary) */}
+
+ {isPrimary ? primaryOwnerText : 'Secondary Owner'}
+
+ {/* Owner details list */}
+
+
Name: {name}
+ {isDetailed && (
+ <>
+ {aadhar &&
Aadhar: {aadhar}
}
+ {mobile &&
Mobile: +91 {mobile}
}
+ {email &&
Email: {email}
}
+ {(guardian || guardianRelationship) && (
+
+ {guardianLabel}: {guardian}{guardianRelationship ? ` (${guardianRelationship})` : ''}
+
+ )}
+ >
+ )}
+
+
+ {/* Top-right Delete Button if callback provided */}
+ {onDelete && (
+
+
+
+ )}
+ {/* Bottom-right Edit Button */}
+
+
+
+
+
+ );
+};
+
+// Export OwnerCardDetail for use in owner details forms
+export default OwnerCardDetail;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/models/AssessmentDetails.model.ts b/frontend/mobile-ui/src/app/features/PropertyForm/models/AssessmentDetails.model.ts
new file mode 100644
index 0000000..e34131b
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/models/AssessmentDetails.model.ts
@@ -0,0 +1,28 @@
+// Request payload for submitting assessment details
+export interface AssessmentDetailsRequest {
+ reasonOfCreation: string; // Reason for creating the assessment
+ occupancyCertificateNumber: string; // Occupancy certificate number
+ occupancyCertificateDate: string; // Date of occupancy certificate
+ extentOfSite: string; // Area/extent of the site
+ isLandUnderneathBuilding: string; // Whether land is underneath building
+ isUnspecifiedShare: boolean; // Whether the property has an unspecified share
+ propertyId: string; // Linked property ID
+}
+
+// Response structure for assessment details API
+export interface AssessmentDetailsResponse {
+ success: boolean; // Indicates if the API call was successful
+ message: string; // Response message
+ data: {
+ ID: string; // Unique assessment ID
+ ReasonOfCreation: string; // Reason for creation
+ OccupancyCertificateNumber: string; // Occupancy certificate number
+ OccupancyCertificateDate: string; // Date of occupancy certificate
+ ExtentOfSite: string; // Site area/extent
+ IsLandUnderneathBuilding: string; // Whether land is underneath building
+ IsUnspecifiedShare: boolean; // Whether property has unspecified share
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+ };
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/models/IgrsDetails.model.ts b/frontend/mobile-ui/src/app/features/PropertyForm/models/IgrsDetails.model.ts
new file mode 100644
index 0000000..8281ceb
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/models/IgrsDetails.model.ts
@@ -0,0 +1,54 @@
+// Request payload for creating IGRS details
+export interface IgrsDetailsRequest {
+ propertyId: string; // Linked property ID
+ habitation: string; // Habitation name
+ igrsWard?: string; // IGRS ward (optional)
+ igrsLocality?: string; // IGRS locality (optional)
+ igrsBlock?: string; // IGRS block (optional)
+ doorNoFrom?: string; // Door number (from, optional)
+ doorNoTo?: string; // Door number (to, optional)
+ igrsClassification?: string; // IGRS classification (optional)
+ builtUpAreaPct?: number; // Built-up area percentage (optional)
+ frontSetback?: number; // Front setback (optional)
+ rearSetback?: number; // Rear setback (optional)
+ sideSetback?: number; // Side setback (optional)
+ totalPlinthArea?: number; // Total plinth area (optional)
+}
+
+// Request payload for updating IGRS details (all fields optional)
+export interface IgrsDetailsUpdateRequest {
+ propertyId?: string; // Linked property ID (optional)
+ habitation?: string; // Habitation name (optional)
+ igrsWard?: string; // IGRS ward (optional)
+ igrsLocality?: string; // IGRS locality (optional)
+ igrsBlock?: string; // IGRS block (optional)
+ doorNoFrom?: string; // Door number (from, optional)
+ doorNoTo?: string; // Door number (to, optional)
+ igrsClassification?: string; // IGRS classification (optional)
+ builtUpAreaPct?: number; // Built-up area percentage (optional)
+ totalPlinthArea?: number; // Total plinth area (optional)
+}
+
+// Response structure for IGRS details API
+export interface IgrsDetailsResponse {
+ data: {
+ id: string; // Unique IGRS details ID
+ habitation: string; // Habitation name
+ igrsWard?: string; // IGRS ward (optional)
+ igrsLocality?: string; // IGRS locality (optional)
+ igrsBlock?: string; // IGRS block (optional)
+ doorNoFrom?: string; // Door number (from, optional)
+ doorNoTo?: string; // Door number (to, optional)
+ igrsClassification?: string; // IGRS classification (optional)
+ builtUpAreaPct?: number; // Built-up area percentage (optional)
+ frontSetback?: number; // Front setback (optional)
+ rearSetback?: number; // Rear setback (optional)
+ sideSetback?: number; // Side setback (optional)
+ totalPlinthArea?: number; // Total plinth area (optional)
+ createdAt: string; // Creation timestamp
+ updatedAt: string; // Last update timestamp
+ PropertyID: string; // Linked property ID
+ };
+ message: string; // Response message
+ success: boolean; // Indicates if the API call was successful
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/models/documentInfo.model.ts b/frontend/mobile-ui/src/app/features/PropertyForm/models/documentInfo.model.ts
new file mode 100644
index 0000000..907cb03
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/models/documentInfo.model.ts
@@ -0,0 +1,41 @@
+// Represents the value of a document info field
+export interface DocumentInfoFieldValue {
+ DocumentType: number | string; // Document type (can be number or string)
+ serialNo: number; // Serial number of the document
+ revenueDocumentNo: number; // Revenue document number
+}
+
+// Request payload for submitting document info
+export interface DocumentInfoRequest {
+ fieldName: string; // Name of the document info field
+ fieldValue: DocumentInfoFieldValue; // Value of the field
+ propertyId: string; // Linked property ID
+}
+
+// Response structure for document info API (single item)
+export interface DocumentInfoResponse {
+ success: boolean; // Indicates if the API call was successful
+ message: string; // Response message
+ data?: {
+ ID: string; // Unique document info ID
+ FieldName: string; // Name of the field
+ fieldValue: DocumentInfoFieldValue; // Value of the field
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+ };
+}
+
+// Response structure for document info API (multiple items)
+export interface DocumentInfoGetResponse {
+ success: boolean; // Indicates if the API call was successful
+ message: string; // Response message
+ data?: Array<{
+ ID: string; // Unique document info ID
+ FieldName: string; // Name of the field
+ fieldValue: DocumentInfoFieldValue; // Value of the field
+ PropertyID: string; // Linked property ID
+ CreatedAt: string; // Creation timestamp
+ UpdatedAt: string; // Last update timestamp
+ }>;
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/features/PropertyForm/models/documentsUpload.model.ts b/frontend/mobile-ui/src/app/features/PropertyForm/models/documentsUpload.model.ts
new file mode 100644
index 0000000..32b233c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/features/PropertyForm/models/documentsUpload.model.ts
@@ -0,0 +1,26 @@
+// Represents details for a single document upload
+export interface DocumentUploadDetails {
+ PropertyId: string; // Linked property ID
+ DocumentType: string; // Type of document
+ DocumentName: string; // Name of the document
+ FileStoreID: string; // File store identifier
+}
+
+// Response structure for document upload API
+export interface DocumentUploadDetailsResponse {
+ message: string; // Response message
+ data: {
+ ID: string; // Unique document upload ID
+ PropertyID: string; // Linked property ID
+ DocumentType: string; // Type of document
+ DocumentName: string; // Name of the document
+ FileStoreID: string; // File store identifier
+ UploadDate: string; // Date of upload
+ action: string; // Action performed (e.g., upload, delete)
+ uploadedBy?: string; // User who uploaded (optional)
+ size?: string; // File size (optional)
+ }[];
+}
+
+// Array type for the document upload request
+export type DocumentUploadDetailsRequest = DocumentUploadDetails[];
diff --git a/frontend/mobile-ui/src/app/models/AlertType.model.ts b/frontend/mobile-ui/src/app/models/AlertType.model.ts
new file mode 100644
index 0000000..44d60d4
--- /dev/null
+++ b/frontend/mobile-ui/src/app/models/AlertType.model.ts
@@ -0,0 +1 @@
+export type AlertType = 'alert' | 'information' | 'warning' | 'success';
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/models/Colors.const.ts b/frontend/mobile-ui/src/app/models/Colors.const.ts
new file mode 100644
index 0000000..0eaa61d
--- /dev/null
+++ b/frontend/mobile-ui/src/app/models/Colors.const.ts
@@ -0,0 +1,9 @@
+export const COLORS = {
+ border: "#00000033",
+ bg: "#fff",
+ toggle: "#C84C0E",
+ text: "#222",
+ profileBg: "#FBEEE8",
+ verified: "#75BA74",
+ contact: "rgba(117, 186, 116, 0.35)",
+};
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/models/Header.model.ts b/frontend/mobile-ui/src/app/models/Header.model.ts
new file mode 100644
index 0000000..3b731c6
--- /dev/null
+++ b/frontend/mobile-ui/src/app/models/Header.model.ts
@@ -0,0 +1,7 @@
+import type { ReactNode } from "react";
+
+export interface HeaderProps {
+ header: string;
+ subHeader: string;
+ icon?: ReactNode;
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/models/NavButton.model.ts b/frontend/mobile-ui/src/app/models/NavButton.model.ts
new file mode 100644
index 0000000..0d47929
--- /dev/null
+++ b/frontend/mobile-ui/src/app/models/NavButton.model.ts
@@ -0,0 +1,7 @@
+import type { ReactNode } from "react";
+
+export interface NavButtonProps {
+ icon: ReactNode;
+ message: string;
+ onClick: () => void;
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/models/Popup.model.ts b/frontend/mobile-ui/src/app/models/Popup.model.ts
new file mode 100644
index 0000000..3b18981
--- /dev/null
+++ b/frontend/mobile-ui/src/app/models/Popup.model.ts
@@ -0,0 +1,10 @@
+import type { AlertType } from "./AlertType.model";
+
+export type PopupProps = {
+ type: AlertType;
+ title: string;
+ message: string;
+ open: boolean;
+ duration?: number;
+ onClose?: () => void;
+};
diff --git a/frontend/mobile-ui/src/app/models/UserProfile.mode.ts b/frontend/mobile-ui/src/app/models/UserProfile.mode.ts
new file mode 100644
index 0000000..b2e154e
--- /dev/null
+++ b/frontend/mobile-ui/src/app/models/UserProfile.mode.ts
@@ -0,0 +1,48 @@
+export interface UserProfile {
+ id?: string;
+ username?: string;
+ email?: string;
+ role?: string;
+ isActive?: boolean;
+ zone?: string[];
+ ward?: string[];
+ preferred_language?: string;
+ createdDate?: string;
+ updatedDate?: string;
+ createdBy?: string;
+ updatedBy?: string;
+ profile?: {
+ firstName?: string;
+ lastName?: string;
+ fullName?: string;
+ phoneNumber?: string;
+ adhaarNo?: number;
+ gender?: string;
+ guardian?: string;
+ guardianType?: string;
+ dateOfBirth?: string;
+ address?: Address;
+ department?: string;
+ designation?: string;
+ workLocation?: string;
+ profilePicture?: string;
+ relationshipToProperty?: string;
+ ownershipShare?: number;
+ isPrimaryOwner?: boolean;
+ isVerified?: boolean;
+ };
+ // Fallback properties for session data
+ firstName?: string;
+ lastName?: string;
+ fullName?: string;
+ phoneNumber?: string;
+ address?: Address;
+}
+
+export interface Address {
+ addressLine1?: string;
+ addressLine2?: string;
+ city?: string;
+ state?: string;
+ pinCode?: string;
+}
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/Agent/AddRequestOrComment.tsx b/frontend/mobile-ui/src/app/pages/Agent/AddRequestOrComment.tsx
new file mode 100644
index 0000000..932688b
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Agent/AddRequestOrComment.tsx
@@ -0,0 +1,293 @@
+// AddRequestOrComment.tsx
+// This page allows an agent to add a request or comment for a property, with file upload support.
+// Features:
+// - Comment input and validation
+// - File upload (drag/drop or browse), preview, and removal
+// - Submits comment and files to backend
+// - Uses localization for all labels and messages
+// - Navigation to previous and home screens
+// Used in: Agent workflow for property verification and feedback
+
+import React, { useRef, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
+import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
+import DownloadIcon from '@mui/icons-material/Download';
+import CloseIcon from '@mui/icons-material/Close';
+import '../../../styles/AddRequestOrComment.css';
+import { uploadFileToFilestore } from '../../../services/AgentLocalisation/fileService';
+import type { UploadResult } from '../../../services/AgentLocalisation/fileService';
+// import { submitCommentRequest } from '../../../services/addCommentService';
+import { useAddCommentLocalization } from '../../../services/AgentLocalisation/localisation-addcomment';
+// import { ApplicationApi } from '../../../redux/apis/ApplicationLog/getApplication';
+import { usePostApplicationLogMutation } from '../../../redux/apis/applicationApi';
+
+// Update UploadedFile interface
+interface UploadedFile {
+ name: string;
+ url: string;
+ filestoreId: string;
+ file?: File;
+}
+
+export function AddRequestOrComment() {
+ // Get propertyId from route params
+ const { propertyId } = useParams<{ propertyId: string }>();
+ const navigate = useNavigate();
+ const fileInputRef = useRef(null);
+
+ // Store files before upload
+ const [uploadedFiles, setUploadedFiles] = useState([]);
+ const [uploading, setUploading] = useState(false);
+ const [error, setError] = useState(null);
+ const [comment, setComment] = useState("");
+
+ // RTK Query mutation hook
+ const [postApplicationLog ] = usePostApplicationLogMutation();
+ const applicationId = localStorage.getItem("applicationId") || "";
+
+ // Use localization hook
+ const {
+ addCommentTitleText,
+ addCommentSubtitleText,
+ previousText,
+ homeText,
+ enterRequestLabelText,
+ dragDropText,
+ browseSystemText,
+ uploadingText,
+ downloadText,
+ attachDocumentsText,
+ cancelText,
+ submitText,
+ fileUploadFailedText,
+ // propertyIdMissingText,
+ // pleaseEnterCommentText,
+ // failedToSubmitText
+ } = useAddCommentLocalization();
+
+ // Store file data in state on select
+ const handleFileChange = (e: React.ChangeEvent) => {
+ if (e.target.files && e.target.files.length > 0) {
+ const file = e.target.files[0];
+ setUploadedFiles([
+ ...uploadedFiles,
+ {
+ name: file.name,
+ url: URL.createObjectURL(file),
+ filestoreId: "",
+ file,
+ },
+ ]);
+ e.target.value = "";
+ }
+ };
+
+ // Store file data in state on drag-drop
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
+ const file = e.dataTransfer.files[0];
+ setUploadedFiles([
+ ...uploadedFiles,
+ {
+ name: file.name,
+ url: URL.createObjectURL(file),
+ filestoreId: "",
+ file,
+ },
+ ]);
+ }
+ };
+ const userName = localStorage.getItem("agentUsername")|| "";
+ const actor = localStorage.getItem("agentRole")|| "AGENT";
+
+ // Remove file from state
+ const handleRemoveFile = (idx: number) => {
+ setUploadedFiles(uploadedFiles.filter((_, i) => i !== idx));
+ if (fileInputRef.current) fileInputRef.current.value = "";
+ };
+
+ // Upload files to filestore on submit
+ const handleSubmit = async () => {
+ setError(null);
+ setUploading(true);
+ try {
+ // Upload each file and get filestoreId
+ const uploadedFileResults = await Promise.all(
+ uploadedFiles.map(async (f) => {
+ if (f.file && !f.filestoreId) {
+ const result: UploadResult = await uploadFileToFilestore(f.file);
+ return {
+ ...f,
+ filestoreId: result.files[0].fileStoreId,
+ };
+ }
+ return f;
+ })
+ );
+ setUploadedFiles(uploadedFileResults);
+
+ // Now call postApplicationLog with filestoreIds
+ const payload: any = {
+ applicationId: applicationId,
+ comments: comment,
+ performedBy: userName,
+ metadata: {},
+ performedDate: new Date().toISOString(),
+ action: "CREATE",
+ actor: actor
+ };
+
+ if (uploadedFileResults.length > 0 && uploadedFileResults[0].filestoreId) {
+ payload.fileStoreId = uploadedFileResults[0].filestoreId;
+ }
+ console.log("Payload for put :", payload);
+
+ // Now send payload
+ await postApplicationLog(payload).unwrap();
+
+ // Clear states after successful submit
+ setComment("");
+ setUploadedFiles([]);
+ if (fileInputRef.current) fileInputRef.current.value = "";
+
+ setUploading(false);
+ // Optionally navigate or show success
+ } catch (err) {
+ setError(fileUploadFailedText);
+ setUploading(false);
+ }
+};
+
+ const handlePrevious = () => {
+ if (propertyId) {
+ navigate(`/verification/${propertyId}`);
+ } else {
+ navigate(-1);
+ }
+ };
+
+ // Navigate to home screen
+ const handleHome = () => {
+ navigate('/agent');
+ };
+
+
+
+ // Render AddRequestOrComment page UI
+ return (
+
+ {/* Header Section */}
+
+
{addCommentTitleText}
+
{addCommentSubtitleText}
+
+ {/* Navigation Section */}
+
+
+ {/* Form Section */}
+
+
+ {enterRequestLabelText}
+
+
+
+ {/* Actions */}
+
+ navigate(-1)}>
+ {cancelText}
+
+
+ {submitText}
+
+
+
+ );
+}
diff --git a/frontend/mobile-ui/src/app/pages/Agent/HomePage.tsx b/frontend/mobile-ui/src/app/pages/Agent/HomePage.tsx
new file mode 100644
index 0000000..6e921a7
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Agent/HomePage.tsx
@@ -0,0 +1,915 @@
+// HomePage component for Agent portal: displays property list, map, filters, and actions
+// Handles fetching, filtering, and paginating property data, as well as locale and notification popups
+import React, { useState, useRef, useEffect, useMemo } from 'react';
+import { useNavigate } from 'react-router-dom';
+import deleteIcon from '../../../assets/AgentAssets/delete.svg';
+import ArrowDropDownOutlinedIcon from '@mui/icons-material/ArrowDropDownOutlined';
+import MapComponent from '../../../components/common/MapComponent';
+import '../../../styles/HomePage.css';
+import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined';
+import AccessAlarmIcon from '@mui/icons-material/AccessAlarm';
+import Layout from '../../features/Agent/components/Layout';
+import type { NavigationTab } from '../../../types';
+import type { PropertyItem } from '../../../types';
+import { fetchData, filterProperties } from '../../../services/dataService';
+import type { DatabaseData } from '../../../services/dataService';
+import CheckBoxRoundedIcon from '@mui/icons-material/CheckBoxRounded';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import '../../../styles/globals.css';
+import LocationMapWithDrawing from '../PropertyForm/LocationMapWithDrawing';
+import Pagination from '../../features/Agent/components/Pagination';
+import { SupportAgentRounded } from '@mui/icons-material';
+import { useHomePageLocalization } from '../../../services/AgentLocalisation/localisation-homepage';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+import dayjs from 'dayjs';
+import ChangeLocalePopup from '../../../components/Popup/ChangeLocalePopup';
+import { setAgentLang } from '../../../redux/LangSlice';
+import { useAppDispatch, useAppSelector } from '../../../redux/Hooks';
+import {
+ useGetAllPropertiesQuery,
+ useGetDraftPropertiesQuery,
+} from '../../features/Agent/api/homePageApi';
+import { mapApplicationToPropertyItem } from '../../features/Agent/utils/propertyMapper';
+import { useFormMode } from '../../../context/FormModeContext';
+import { Button } from '@mui/material';
+import { buttonStyleSx } from '../../../styles/ts-styles/Citizen/HomePage/ContinueButton.style';
+import { useDeleteApplicationMutation } from '../../../redux/apis/applicationApi';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+import LoadingPage from '../../components/Loader';
+
+type CalendarTab = 'All' | 'New' | 'Drafts';
+
+interface HomePageProps {
+ properties?: PropertyItem[];
+ onDraftClick?: (propertyId?: string) => void;
+}
+
+const LOCALE_MISMATCH_POPUP_FLAG = 'citizenLocaleMismatchChecked';
+
+const HomePage: React.FC = () => {
+ // State management for pagination, filters, UI toggles, and data
+ const [page, setPage] = useState(1);
+ const [pageSize] = useState(10);
+
+ const navigate = useNavigate();
+ const [data, setData] = useState(null);
+ const [activeCalendarTab, setActiveCalendarTab] = useState('All');
+ const [selectedDate, setSelectedDate] = useState('');
+ const [showCalendar, setShowCalendar] = useState(false);
+ const [filteredProperties, setFilteredProperties] = useState([]);
+ const { resetForm } = usePropertyForm();
+ const [selectedDateSort, setSelectedDateSort] = useState('');
+ const [activeTab, setActiveTab] = useState('home');
+ const [showDateDropdown, setShowDateDropdown] = useState(false);
+ const dropdownRef = useRef(null);
+ const [showChangeLocalePopup, setShowChangeLocalePopup] = useState(false);
+ const dispatch = useAppDispatch();
+ const loginLocale = localStorage.getItem('loginLocale') || 'en';
+ const lang = useAppSelector((state) => state.lang.agentLang);
+
+ // Fetch non-draft properties from API
+ const {
+ data: nonDraftResponse,
+ isLoading: isLoadingNonDraft,
+ error: nonDraftError,
+ } = useGetAllPropertiesQuery({
+ agentId: localStorage.getItem('user_id') || '',
+ });
+
+ // Fetch draft properties from API
+ const {
+ data: draftResponse,
+ isLoading: isLoadingDraft,
+ error: draftError,
+ } = useGetDraftPropertiesQuery({
+ agentId: localStorage.getItem('user_id') || '',
+ });
+
+ const isLoading = isLoadingNonDraft || isLoadingDraft;
+ const error = nonDraftError || draftError;
+
+ // Combine both API responses into a single property list
+ const allProperties = useMemo(() => {
+ const nonDrafts = nonDraftResponse?.data?.map(mapApplicationToPropertyItem) || [];
+ const drafts = draftResponse?.data?.map(mapApplicationToPropertyItem) || [];
+ return [...nonDrafts, ...drafts];
+ }, [nonDraftResponse, draftResponse]);
+
+ // Handle switching agent language to login locale
+ const handleSwitchToLoginLocale = () => {
+ dispatch(setAgentLang(loginLocale));
+ setShowChangeLocalePopup(false);
+ localStorage.setItem(LOCALE_MISMATCH_POPUP_FLAG, 'true');
+ };
+
+ // Close the locale mismatch popup
+ const handleClosePopup = () => {
+ setShowChangeLocalePopup(false);
+ localStorage.setItem(LOCALE_MISMATCH_POPUP_FLAG, 'true');
+ };
+
+ const { mode, setMode } = useFormMode();
+
+ // Localization: fetches localized strings for UI labels
+ const {
+ agentIdText,
+ helpText,
+ newPropertyText,
+ allTabText,
+ newTabText,
+ draftsTabText,
+ continueText,
+ deleteText,
+ savedText,
+ noPropertiesText,
+ noDataSelectedDateText,
+ dateLabelText,
+ earliestText,
+ oldestText,
+ } = useHomePageLocalization();
+
+ // Helper function to get localized tab text for calendar tabs
+ const getTabText = (tab: CalendarTab): string => {
+ switch (tab) {
+ case 'All':
+ return allTabText;
+ case 'New':
+ return newTabText;
+ case 'Drafts':
+ return draftsTabText;
+ default:
+ return tab;
+ }
+ };
+
+ // Click outside dropdown handler for closing custom dropdowns
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
+ setShowDateDropdown(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ useEffect(() => {
+ if (mode == 'none') {
+ localStorage.removeItem('applicationId');
+ localStorage.removeItem('propertyId');
+ }
+ }, [localStorage.getItem('applicationId'), localStorage.getItem('propertyId')]);
+
+ // Load database data (not directly used in main UI, but may be for location info)
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const fetchedData = await fetchData();
+ setData(fetchedData);
+ } catch (error) {
+ console.error('Error loading data:', error);
+ }
+ };
+ loadData();
+ }, []);
+
+ // Filter properties based on selected tab and date
+ useEffect(() => {
+ let filtered = allProperties;
+
+ if (activeCalendarTab !== 'Drafts') {
+ const filterType = activeCalendarTab.toLowerCase() as 'all' | 'new';
+ filtered = filterProperties(allProperties, filterType);
+ } else {
+ // Show only drafts
+ filtered = allProperties.filter((property) => property.isDraft);
+ }
+
+ if (selectedDate && activeCalendarTab !== 'Drafts') {
+ filtered = filtered.filter((property) => {
+ const propertyDate = new Date(property.dueDate).toDateString();
+ const selectedDateObj = new Date(selectedDate).toDateString();
+ return propertyDate === selectedDateObj;
+ });
+ }
+
+ setFilteredProperties(filtered);
+ }, [allProperties, activeCalendarTab, selectedDate]);
+
+ // Reset to page 1 when filters change
+ useEffect(() => {
+ setPage(1);
+ }, [activeCalendarTab, selectedDate, selectedDateSort]);
+
+ // Handle navigation tab changes (home, search, inbox, notifications)
+ const handleTabChange = (tab: NavigationTab) => {
+ setActiveTab(tab);
+
+ switch (tab) {
+ case 'home':
+ navigate('/agent');
+ break;
+ case 'search':
+ navigate('/agent/search-property');
+ break;
+ case 'inbox':
+ // inbox navigation - to be implemented
+ navigate('/under-construction');
+ break;
+ case 'notifications':
+ // notifications navigation - to be implemented
+ navigate('/under-construction');
+ break;
+ }
+ };
+
+ // Handle calendar tab (All/New/Drafts) selection
+ const handleCalendarTabClick = (tab: CalendarTab) => {
+ setActiveCalendarTab(tab);
+ if (tab !== 'Drafts') {
+ setSelectedDate('');
+ setShowCalendar(false);
+ }
+ };
+
+ // Start new property application
+ const handleNewPropertyClick = () => {
+ resetForm();
+ setMode('new');
+ navigate('/property-form/property-information');
+ };
+
+ // Mutation hook for deleting applications
+ const [deleteApplication] = useDeleteApplicationMutation();
+
+ // Delete a draft property after confirmation
+ const handleDeleteDraft = async (draftId: string) => {
+ if (window.confirm('Are you sure you want to delete this draft?')) {
+ try {
+ const response = await deleteApplication({ applicationId: draftId }).unwrap();
+
+ if (response.success) {
+ setPopup({
+ type: 'success',
+ title: 'Draft Deleted',
+ message: response.message || 'Draft deleted successfully',
+ open: true,
+ });
+ }
+ } catch (error: any) {
+ // setPopup({
+ // type: 'success',
+ // title: 'Error',
+ // message: error?.data?.message || 'Failed to delete draft',
+ // open: true,
+ // });
+ console.log(error);
+
+ }
+ }
+ };
+
+ // Handle date selection from date picker
+ const onDateChange = (date: any) => {
+ if (date === null) {
+ setSelectedDate('');
+ return;
+ }
+ const formattedDate = date ? dayjs(date).format('YYYY-MM-DD') : '';
+ setSelectedDate(formattedDate);
+ };
+
+ // Get CSS class for property status badge
+ const getStatusColor = (status: string) => {
+ switch (status.toLowerCase()) {
+ case 'high':
+ return 'status-high';
+ case 'medium':
+ return 'status-medium';
+ case 'low':
+ return 'status-low';
+ default:
+ return 'status-medium';
+ }
+ };
+
+ // Continue editing a draft property
+ const handleContinueClick = (draftId: string) => {
+ const application = draftResponse?.data.find((app) => app.ID === draftId);
+
+ const propertyId = application!.Property.ID || '';
+ localStorage.setItem('propertyId', propertyId);
+ if (application?.ID) {
+ localStorage.setItem('applicationId', application.ID);
+ }
+
+ setMode('draft');
+ navigate('/property-form/property-information');
+ };
+
+ // View details/verification for a submitted property
+ const handlePropertyClick = (applicationID: string) => {
+ const application = nonDraftResponse?.data.find((app) => app.ID === applicationID);
+ const propertyId = application?.Property.ID || '';
+ localStorage.setItem('propertyId', propertyId);
+
+ if (application?.ID) {
+ localStorage.setItem('applicationId', application.ID);
+ }
+ setMode('verify');
+ navigate('/agent/verification/' + propertyId);
+ };
+
+ // Prepare property locations for map display
+ const propertyLocations = useMemo(() => {
+ // Filter properties based on active tab
+ let propertiesForMap = filteredProperties;
+
+ // For 'All' and 'New' tabs, exclude drafts
+ if (activeCalendarTab === 'All' || activeCalendarTab === 'New') {
+ propertiesForMap = filteredProperties.filter((prop) => !prop.isDraft);
+ }
+
+ return propertiesForMap
+ .filter((prop) => prop.gisData?.latitude && prop.gisData?.longitude)
+ .map((prop) => ({
+ id: prop.id,
+ applicationNo: prop.pId,
+ lat: prop.gisData!.latitude,
+ lng: prop.gisData!.longitude,
+ address: prop.address,
+ status: prop.status,
+ }));
+ }, [filteredProperties, activeCalendarTab]);
+
+ // Handle sorting properties by date (earliest/oldest)
+ const handleDateSortChange = (value: string) => {
+ setSelectedDateSort(value);
+ setShowDateDropdown(false);
+
+ if (value === 'earliest') {
+ const sorted = [...filteredProperties].sort(
+ (a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
+ );
+ setFilteredProperties(sorted);
+ } else if (value === 'oldest') {
+ const sorted = [...filteredProperties].sort(
+ (a, b) => new Date(b.dueDate).getTime() - new Date(a.dueDate).getTime()
+ );
+ setFilteredProperties(sorted);
+ }
+ };
+
+ // State for notification popup
+ const [popup, setPopup] = useState<{
+ type: 'success';
+ title: string;
+ message: string;
+ open: boolean;
+ }>({ type: 'success', title: '', message: '', open: false });
+
+ // Show welcome popup on first login
+ useEffect(() => {
+ const popupFlag = sessionStorage.getItem('showWelcomePopup');
+ if (popupFlag === 'true') {
+ setPopup({
+ type: 'success',
+ title: 'Successfully Logged In!',
+ message: 'Welcome to Property Tax - Agent Portal!',
+ open: true,
+ });
+ sessionStorage.removeItem('showWelcomePopup');
+ }
+ }, []);
+
+ // Show success popup after property creation
+ useEffect(() => {
+ const showSuccess = localStorage.getItem('showSuccessPropCreation');
+ if (showSuccess === 'true') {
+ setPopup({
+ type: 'success',
+ title: 'Success!',
+ message: localStorage.getItem('successMessage') || 'Operation completed successfully!',
+ open: true,
+ });
+ localStorage.removeItem('propertyNo');
+ localStorage.removeItem('showSuccessPropCreation');
+ }
+ }, [localStorage.getItem('showSuccessPropCreation')]);
+
+ // Show locale mismatch popup if agent language differs from login locale
+ useEffect(() => {
+ const alreadyChecked = localStorage.getItem(LOCALE_MISMATCH_POPUP_FLAG);
+ if (alreadyChecked !== 'true' && alreadyChecked !== null) {
+ if (lang && loginLocale && lang !== loginLocale) {
+ setShowChangeLocalePopup(true);
+ }
+ }
+ }, [lang, loginLocale]);
+
+ // Date sorting options for dropdown
+ const dateOptions = useMemo(
+ () => [
+ { id: '', label: dateLabelText },
+ { id: 'earliest', label: earliestText },
+ { id: 'oldest', label: oldestText },
+ ],
+ [dateLabelText, earliestText, oldestText]
+ );
+
+ // CLIENT-SIDE PAGINATION
+ // Calculate total pages based on filtered properties
+ const totalPages = useMemo(() => {
+ return Math.ceil(filteredProperties.length / pageSize);
+ }, [filteredProperties.length, pageSize]);
+
+ // Get paginated properties for current page
+ const paginatedProperties = useMemo(() => {
+ const startIndex = (page - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ return filteredProperties.slice(startIndex, endIndex);
+ }, [filteredProperties, page, pageSize]);
+
+ // Prepare draft property cards for display
+ const draftProperties = paginatedProperties
+ .filter((property) => property.isDraft)
+ .map((property) => ({
+ id: property.id,
+ pid: property.propertyNo,
+ title: `${property.address.split(',')[0]} Property - Draft`,
+ description: property.description,
+ address: property.address,
+ savedDate: `${savedText}: ${new Date(property.createdDate || '').toLocaleDateString(
+ 'en-US',
+ {
+ day: '2-digit',
+ month: 'short',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true,
+ }
+ )}`,
+ dueDate: new Date(property.dueDate).toLocaleDateString('en-US', {
+ month: 'short',
+ day: '2-digit',
+ year: 'numeric',
+ }),
+ }));
+
+ // Show loading state while fetching properties
+ if (isLoading) {
+ return ;
+ }
+
+ // Show error state if property fetch fails
+ if (error) {
+ return (
+
+
+
+ Error loading properties. Please try again later.
+
+
+
+ );
+ }
+
+ // Main render: layout, popups, map, filters, property list, and pagination
+ return (
+ <>
+
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+ {agentIdText}
+
+
+
+
+
+
+ {helpText}
+
+
+
+ {/* Map Section */}
+
+
+
+
+ {/* Add New Property button */}
+
+ handleNewPropertyClick()}
+ >
+ {newPropertyText}
+
+
+
+ {/* Filter and Map Section */}
+
+
+
+
setShowCalendar(true)}
+ width="18"
+ height="18"
+ viewBox="0 0 24 24"
+ fill="currentColor"
+ >
+
+
+
+
setShowCalendar(false)}
+ onAccept={() => setShowCalendar(false)}
+ slotProps={{
+ textField: {
+ style: { display: 'none' },
+ },
+ actionBar: {
+ actions: ['clear', 'accept'],
+ },
+ }}
+ />
+ {selectedDate && (
+ setShowCalendar(true)}
+ >
+ {new Date(selectedDate).toLocaleDateString()}
+
+ )}
+
+
+
navigate('/agent/reviewed-properties')}
+ aria-pressed={false}
+ title="View reviewed properties"
+ >
+
+
+
+
+
+ {/* Custom Dropdown Component */}
+
+
+
setShowDateDropdown(!showDateDropdown)}
+ style={{
+ width: '100px',
+ height: '36px',
+ padding: '6px 32px 6px 12px',
+ borderRadius: '16px',
+ marginRight: '16px',
+ border: '2px solid #C84A00',
+ cursor: 'pointer',
+ backgroundColor: 'white',
+ fontSize: '14px',
+ display: 'flex',
+ backgroundOrigin: 'transparent',
+ color: selectedDateSort ? '#000' : '#666',
+ }}
+ >
+
+ {selectedDateSort
+ ? selectedDateSort === 'earliest'
+ ? earliestText
+ : oldestText
+ : dateLabelText}
+
+
+
+ {showDateDropdown && (
+
+ {dateOptions.map((opt) => (
+
handleDateSortChange(opt.id)}
+ style={{
+ backgroundColor: 'white',
+ cursor: 'pointer',
+ fontSize: '14px',
+ color: opt.id === '' ? '#666' : '#000',
+ borderBottom:
+ opt.id !== dateOptions[dateOptions.length - 1].id
+ ? '1px solid #f0f0f0'
+ : 'none',
+ }}
+ onMouseEnter={(e) =>
+ (e.currentTarget.style.backgroundColor = '#f5f5f5')
+ }
+ onMouseLeave={(e) =>
+ (e.currentTarget.style.backgroundColor =
+ opt.id === selectedDateSort ? '#f5f5f5' : 'white')
+ }
+ >
+ {opt.label}
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Calendar tabs */}
+
+ {(['All', 'New', 'Drafts'] as CalendarTab[]).map((tab) => (
+
handleCalendarTabClick(tab)}
+ >
+ {activeCalendarTab === tab && (
+
+
+
+ )}
+ {getTabText(tab)}
+
+ ))}
+
+
+
+ {/* Property Items List - USING PAGINATED DATA */}
+
+ {activeCalendarTab === 'Drafts' ? (
+ // Draft Properties - use paginatedProperties
+ draftProperties.length > 0 ? (
+ draftProperties.map((draft) => (
+
+
+
{draft.title}
+
{
+ e.stopPropagation();
+ handleDeleteDraft(draft.id);
+ }}
+ aria-label={deleteText}
+ >
+
+
+
+
{draft.pid}
+
+
+ {draft.savedDate}
+
+
+
+
+
+
+
+
+
{draft.address}
+
+
+ {draft.dueDate}
+ {/* {draft.dueDate} */}
+ {
+ handleContinueClick(draft.id);
+ }}
+ >
+ {continueText}
+
+
+
+ ))
+ ) : (
+
+ )
+ ) : // Regular Properties - use paginatedProperties
+ paginatedProperties.length > 0 ? (
+ paginatedProperties.map((item) => {
+ const showGreenTick = !item.isNew && !item.isDraft;
+
+ return (
+
handlePropertyClick(item.id)}
+ >
+
+ {/* Left Section - ID and Status */}
+
+ {/* Property ID */}
+
{item.pId}
+
+ {/* Status Badge - Directly Below ID */}
+
+
+ {item.status}
+
+
+
+ {/* Category ID */}
+
+
Category ID
+
+ {item.description || item.pId}
+
+
+
+ {/* Address */}
+
+
Address
+
{item.address}
+
+
+
+ {/* Right Section - Map and Date */}
+
+ {/* Map Placeholder */}
+
+ {(() => {
+ const loc = propertyLocations.find((pl) => pl.id === item.id);
+ if (
+ loc &&
+ typeof loc.lat === 'number' &&
+ typeof loc.lng === 'number'
+ ) {
+ return (
+
+ {}}
+ readOnly={true}
+ />
+
+ );
+ }
+ return null;
+ })()}
+
+
+ {/* Date Badge */}
+ {!showGreenTick && (
+
+ {new Date(item.dueDate).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })}
+
+ )}
+
+
+
+ );
+ })
+ ) : selectedDate ? (
+
+
{noDataSelectedDateText}
+
+ ) : (
+
+ )}
+
+ {/* Pagination - show only if there are multiple pages */}
+ {filteredProperties.length > 0 && totalPages > 1 && (
+
setPage(newPage)}
+ />
+ )}
+
+
+
+ >
+ );
+};
+
+
+// Export the HomePage component as default
+export default HomePage;
diff --git a/frontend/mobile-ui/src/app/pages/Agent/PropertyFormVerification.tsx b/frontend/mobile-ui/src/app/pages/Agent/PropertyFormVerification.tsx
new file mode 100644
index 0000000..0aac3de
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Agent/PropertyFormVerification.tsx
@@ -0,0 +1,721 @@
+// PropertyFormVerification: Agent view for verifying property form details, showing map, required fields, and navigation
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import { usePropertyApplications } from '../../../context/PropertyApplicationsContext';
+import { usePropertyFormVerificationLocalization } from '../../../services/AgentLocalisation/localisation-propertyFormVerification';
+import '../../../styles/PropertyFormVerification.css';
+import { useFormMode } from '../../../context/FormModeContext';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
+import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
+import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
+//import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined';
+import LocationMapWithDrawing from '../PropertyForm/LocationMapWithDrawing';
+import appleMaps from '../../assets/Agent/Apple_Maps.svg';
+import openStreetMaps from '../../assets/Agent/open_street_maps.svg';
+// Comment/Note Card
+import { useNavigate as useNavigateHook } from 'react-router-dom';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import type { AlertType } from '../../models/AlertType.model';
+import { fetchPropertyDetails } from '../../features/Agent/api/fetchProperty.hooks';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+import { useLazyGetApplicationByIdQuery } from '../../../redux/apis/applicationApi';
+import { useLazyGetOwnersByPropertyIdQuery } from '../../../redux/apis/ownerApi';
+
+// Card component for comment/note actions (application log, send email)
+const CommentOrNoteCard = ({ loc, propertyId }: { loc: any; propertyId: string | undefined }) => {
+ const navigate = useNavigateHook();
+ const appId = localStorage.getItem('applicationId');
+ return (
+
+
+
+ {loc.commentOrAddNoteText}
+
+
+ navigate(`/agent/ApplicationLog/${appId || ''}`)}
+ // onClick={() => navigate('/under-construction')}
+ >
+ {loc.applicationLogText}
+
+ navigate(`/agent/send-email/${propertyId || ''}`)}
+ >
+ {loc.sendEmailText}
+
+
+
+ );
+};
+
+// Helper: Case-insensitive path lookup for nested form data (handles arrays and objects)
+function getValue(obj: any, path: string) {
+ if (!obj || !path) return undefined;
+ const segments = path.split('.');
+ let cur: any = obj;
+ for (const seg of segments) {
+ if (cur == null) return undefined;
+ // Handle array index segment like "owners.0.name"
+ if (Array.isArray(cur)) {
+ const idx = Number(seg);
+ if (!Number.isNaN(idx)) {
+ cur = cur[idx];
+ continue;
+ }
+ // If not a numeric index, use first element as representative
+ cur = cur[0];
+ }
+ if (typeof cur !== 'object') return undefined;
+ // Case-insensitive key lookup to match PropertyFormData casing
+ const key = Object.keys(cur).find((k) => k.toLowerCase() === seg.toLowerCase());
+ if (key === undefined) return undefined;
+ cur = cur[key];
+ }
+ return cur;
+}
+
+// Card component showing address, phone, and form completion progress
+const AddressProgressSection = ({
+ address,
+ phoneNo,
+ formCompletionPercent,
+ texts,
+}: {
+ address: string;
+ phoneNo: string;
+ formCompletionPercent: number;
+ verificationCount: number;
+ missingCount: number;
+ texts: any;
+}) => (
+
+
+
{texts.addressText}:
+
{address || 'N/A'}
+
{texts.phoneNoText}:
+
{phoneNo || 'N/A'}
+
+ {texts.formCompletionText}:
+
+
+
+
{formCompletionPercent}%
+
+
+
+
+
+ {/* {verificationCount} */}
+ {/* {texts.needReviewText} */}
+
+
+ {/* {missingCount} */}
+ {/* {texts.missingText} */}
+
+
+
+
+);
+
+// Main component for property form verification page
+const PropertyFormVerification: React.FC = () => {
+ const { mode, setMode } = useFormMode();
+
+ const navigate = useNavigate();
+ const { propertyId } = useParams();
+ const { formData, updateForm, resetForm } = usePropertyForm();
+ const { fullProperties, properties } = usePropertyApplications();
+ const [getOwnerByPropId] = useLazyGetOwnersByPropertyIdQuery();
+
+ // State for map loading and data loading
+ const [isMapLoading, setIsMapLoading] = useState(true);
+ const [isDataLoaded, setIsDataLoaded] = useState(false);
+
+ // Use localization hook for UI text
+ const texts = usePropertyFormVerificationLocalization();
+
+ // State for notification popup
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ // Show error popup with message
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ // req
+ // const getRequiredFields = () => [
+ // {
+ // key: 'owners',
+ // label: texts.propertyOwnerNameText,
+ // section: texts.ownerInformationText,
+ // desc: texts.fullLegalNameText,
+ // avatar: true,
+ // },
+ // {
+ // key: 'assessmentDetails.ReasonOfCreation',
+ // label: texts.surveyNumberText,
+ // section: texts.propertyDetailsText,
+ // desc: texts.governmentSurveyNumberText,
+ // avatar: true,
+ // },
+ // {
+ // key: 'assessmentDetails.ExtentOfSite',
+ // label: texts.builtUpAreaText,
+ // section: texts.propertyMeasurementsText,
+ // desc: texts.totalBuiltUpAreaText,
+ // avatar: false,
+ // },
+ // ];
+
+ // const getRequiredFields = () => [
+ // { key: 'owners', label: texts.propertyOwnerNameText, section: texts.ownerInformationText, desc: texts.fullLegalNameText, avatar: true },
+ // { key: 'assessmentDetails.ReasonOfCreation', label: texts.surveyNumberText, section: texts.propertyDetailsText, desc: texts.governmentSurveyNumberText, avatar: true },
+ // { key: 'assessmentDetails.ExtentOfSite', label: texts.builtUpAreaText, section: texts.propertyMeasurementsText, desc: texts.totalBuiltUpAreaText, avatar: false },
+ // ];
+
+ // Build required-fields metadata from fetched formData (source of truth)
+ const getRequiredFields = () => {
+ const ownerLabel = (() => {
+ const owners = formData?.owners;
+ if (Array.isArray(owners) && owners.length > 0) {
+ const o: any = owners[0];
+ return o?.Name ?? o?.ownerName ?? o?.name ?? 'Owner Details';
+ }
+ return 'Owner Details';
+ })();
+
+ const reasonLabel =
+ formData?.assessmentDetails?.ReasonOfCreation ?? 'ReasonOfCreation';
+ const extentLabel = formData?.assessmentDetails?.ExtentOfSite ?? 'ExtentOfSite';
+
+ return [
+ {
+ key: 'owners',
+ label: ownerLabel,
+ section: '',
+ desc: '',
+ avatar: true,
+ },
+ {
+ key: 'assessmentDetails.ReasonOfCreation',
+ label: reasonLabel,
+ section: '',
+ desc: '',
+ avatar: true,
+ },
+ {
+ key: 'assessmentDetails.ExtentOfSite',
+ label: extentLabel,
+ section: '',
+ desc: '',
+ avatar: false,
+ },
+ ];
+ };
+ // Metadata for fields needing verification
+ const getVerificationFields = () => [
+ {
+ key: 'constructionDetails.floorType',
+ label: texts.constructionTypeText,
+ section: texts.propertyDetailsText,
+ desc: texts.primaryConstructionMaterialText,
+ },
+ {
+ key: 'assessmentDetails.natureOfUsage',
+ label: texts.propertyUsageText,
+ section: texts.propertyClassificationText,
+ desc: texts.primaryUsageText,
+ },
+ {
+ key: 'isgrAdditionalDetails',
+ label: texts.amenitiesText,
+ section: texts.propertyFeaturesText,
+ desc: texts.selectAmenitiesText,
+ },
+ {
+ key: 'floors',
+ label: texts.floorDetailsText,
+ section: texts.propertyStructureText,
+ desc: texts.floorWiseUsageText,
+ },
+ ];
+
+ // On mount: if propertyId is present, update form with property details
+ useEffect(() => {
+ if (propertyId && fullProperties.length > 0) {
+ const found = fullProperties.find(
+ (p) => p.propertyId === propertyId || p.id === propertyId
+ );
+ if (found && found.property) updateForm(found.property);
+ }
+ // eslint-disable-next-line
+ }, [propertyId, fullProperties]);
+
+ // Get address from the property data (from the list) or fallback to form data
+ const getPropertyAddress = () => {
+ // First try to get address from the properties list (PropertyItem)
+ const propertyItem = properties.find(
+ (p) => p.id === propertyId || p.pId === propertyId
+ );
+ if (propertyItem && propertyItem.address) {
+ return propertyItem.address;
+ }
+
+ // Fallback to constructing from form data
+ const addressParts = [
+ formData.propertyAddress?.Street,
+ formData.propertyAddress?.Locality,
+ formData.propertyAddress?.ZoneNo,
+ formData.propertyAddress?.BlockNo,
+ formData.propertyAddress?.WardNo,
+ formData.propertyAddress?.PinCode,
+ ].filter(Boolean);
+
+ return addressParts.length > 0 ? addressParts.join(', ') : 'Address not available';
+ };
+
+ const [getApplicationById] = useLazyGetApplicationByIdQuery();
+ const address = getPropertyAddress();
+
+ // Required and verification fields for the form
+ const REQUIRED_FIELDS = getRequiredFields();
+ const VERIFICATION_FIELDS = getVerificationFields();
+
+ // Find missing required fields
+ const missingFields = REQUIRED_FIELDS.filter((field) => {
+ //if (field.key === 'owners') return !formData.owners || formData.owners.length === 0;
+ const value = getValue(formData, field.key);
+ return value === undefined || value === '' || value === null;
+ });
+
+ // Human readable list of missing field labels
+ const missingFieldLabels = missingFields.map((f) => f.label || f.key);
+ const missingLabelsStr = missingFieldLabels.join(', ');
+
+ // Find fields that need verification
+ const verificationFields = VERIFICATION_FIELDS.filter((field) => {
+ const value = getValue(formData, field.key);
+ if (field.key === 'isgrAdditionalDetails') {
+ if (!formData.isgrAdditionalDetails) return true;
+ return Object.values(formData.isgrAdditionalDetails).some((v) => v === undefined);
+ }
+ if (field.key === 'floors') return !formData.floors || formData.floors.length === 0;
+ return value !== undefined && value !== '' && value !== null;
+ });
+
+ // Calculate form completion percentage
+ const filledRequired = REQUIRED_FIELDS.length - missingFields.length;
+ const formCompletionPercent = Math.round(
+ (filledRequired / REQUIRED_FIELDS.length) * 100
+ );
+
+ // Navigate to property information form
+ const handleContinueToForm = () => {
+ navigate('/property-form/property-information');
+ };
+
+ // On mount: fetch property details if in verify mode
+ useEffect(() => {
+ let applicationId;
+ let propertyId;
+ if (mode == 'verify') {
+ applicationId = localStorage.getItem('applicationId')!;
+ propertyId = localStorage.getItem('propertyId')!;
+
+ if (!propertyId) {
+ navigate('/agent');
+ }
+
+ setIsMapLoading(true);
+ setIsDataLoaded(false);
+ fetchPropertyDetails(
+ propertyId,
+ applicationId,
+ getApplicationById,
+ getOwnerByPropId,
+ updateForm,
+ showErrorPopup
+ )
+ .then(() => {
+ setIsDataLoaded(true);
+ setTimeout(() => {
+ setIsMapLoading(false);
+ }, 500);
+ })
+ .catch(() => {
+ setIsMapLoading(false);
+ setIsDataLoaded(true);
+ });
+ }
+ }, []);
+
+ // If formData is loaded but not marked as loaded, set as loaded
+ useEffect(() => {
+ // If we have formData but haven't loaded yet, set as loaded
+ if (formData && formData.id && !isDataLoaded) {
+ setIsDataLoaded(true);
+ setIsMapLoading(false);
+ }
+
+ console.log(formData);
+ }, [formData, isDataLoaded]);
+
+ // Handle back navigation: reset form and state
+ const handleBackHome = () => {
+ resetForm();
+ setMode('none');
+ setIsMapLoading(true);
+ setIsDataLoaded(false);
+ localStorage.removeItem('propertyId');
+ localStorage.removeItem('applicationId');
+ navigate(-1);
+ };
+
+ // Get location data from form or create default coordinates from address
+ const getLocationData = () => {
+ const coords = formData.locationData?.coordinates;
+ // Ensure coords is an object with numeric lat and lng
+ if (
+ coords &&
+ typeof (coords as any).lat === 'number' &&
+ typeof (coords as any).lng === 'number'
+ ) {
+ return { lat: (coords as any).lat, lng: (coords as any).lng };
+ }
+
+ // Default to Bangalore coordinates if no valid location data
+ return { lat: 12.9716, lng: 77.5946 };
+ };
+
+ // Open external map providers in a new tab
+ const openGoogleMaps = () => {
+ const loc = getLocationData();
+ const q = `${loc.lat},${loc.lng}`;
+ const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
+ q
+ )}`;
+ window.open(url, '_blank');
+ };
+
+ const openAppleMaps = () => {
+ const loc = getLocationData();
+ const coords = `${loc.lat},${loc.lng}`;
+ const label = address && address !== 'Address not available' ? address : coords;
+ const url = `https://maps.apple.com/?ll=${encodeURIComponent(
+ coords
+ )}&q=${encodeURIComponent(label)}`;
+ window.open(url, '_blank');
+ };
+
+ const openOpenStreetMap = () => {
+ const loc = getLocationData();
+ const url = `https://www.openstreetmap.org/?mlat=${loc.lat}&mlon=${loc.lng}#map=18/${loc.lat}/${loc.lng}`;
+ window.open(url, '_blank');
+ };
+
+ // Localized text for map provider buttons
+ const { googleMapsText, appleMapsText, openStreetMapsText } = useLocalization();
+
+ // Main render: notification popup, header, map, address/progress, missing fields, and actions
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+ {/* Top Header Block */}
+
+
{texts.propertyFormVerificationText}
+
+
{texts.summaryInformationText}
+
+
+ {/* Back to Home Button placed inside header */}
+
+
+
{texts.backToHomeText}
+
+
+
+ {/* Property ID display */}
+
+ {formData.propertyNo && (
+
+ {formData.propertyNo?.toString()}
+
+ )}
+
+
+ {/* Map Container */}
+
+
+ {isMapLoading ? (
+
+ ) : (
+ isDataLoaded && (
+
{
+ /* verification map: no-op for readOnly */
+ }}
+ readOnly={true}
+ addressLabel={address}
+ initialShapes={formData.locationData?.drawnShapes}
+ />
+ )
+ )}
+
+
+ {/* Map Provider Buttons */}
+
+
+
+ {googleMapsText}
+
+
+
+ {/* */}
+ {appleMapsText}
+
+
+
+ {/* */}
+ {openStreetMapsText}
+
+
+
+
+ {/* Address Card */}
+
+
+ {/* Alerts */}
+ {missingFields.length > 0 && (
+
+
+
+
+
+ {missingFields.length} {texts.requiredFieldsMissingText}
+ {missingLabelsStr ? `: ${missingLabelsStr}` : ''}
+
+
+ )}
+ {/* {verificationFields.length > 0 && (
+
+
+
+
+
+ {verificationFields.length} {texts.fieldsNeedVerificationText}
+
+
+ )} */}
+
+ {/* Missing Required Fields */}
+ {missingFields.length > 0 && (
+
+
+
+
+
+ {texts.missingRequiredFieldsText}
+
+ {missingFields.length === 0 && (
+
+ {texts.noRequiredFieldsMissingText}
+
+ )}
+ {missingFields.map((field) => (
+
+
+
{field.label}
+
+ {field.section} {field.desc}
+
+ {/*
{field.desc}
*/}
+
+ {/* {field.avatar &&
} */}
+
+ {texts.requiredText}
+
+
+ ))}
+
+ )}
+
+ {/* Fields Needing Verification */}
+ {/*
+
+ ⚠
+
+ {texts.fieldsNeedingVerificationText}
+
+
+ {verificationFields.length === 0 && (
+
{texts.noFieldsNeedVerificationText}
+ )}
+ {verificationFields.map((field) => (
+
+
+
{field.label}
+
+ {field.section}
+
+ {field.desc}
+
+
+
+ {texts.needsVerificationText}
+
+
+ ))}
+
*/}
+
+ {/* Comment/Add Note Card */}
+
+
+ {/* Continue Button */}
+
+
+ {texts.continueToFormText}
+
+
+
+
+
+ >
+ );
+};
+
+// Export the PropertyFormVerification component as default
+export default PropertyFormVerification;
diff --git a/frontend/mobile-ui/src/app/pages/Agent/PropertyInformationSubmitted.tsx b/frontend/mobile-ui/src/app/pages/Agent/PropertyInformationSubmitted.tsx
new file mode 100644
index 0000000..bb5fb4c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Agent/PropertyInformationSubmitted.tsx
@@ -0,0 +1,376 @@
+// PropertyInformationSubmitted: Agent view for displaying submitted property information summary, map, and details
+import React, { useEffect } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import { usePropertyApplications } from '../../../context/PropertyApplicationsContext';
+import '../../../styles/PropertyInformationSubmitted.css';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import LocationMapWithDrawing from '../PropertyForm/LocationMapWithDrawing';
+import appleMaps from '../../assets/Agent/Apple_Maps.svg';
+import openStreetMaps from '../../assets/Agent/open_street_maps.svg';
+import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
+import rectangle100Icon from '../../assets/Agent/Rectangle_100.svg';
+import { usePropertyInformationSubmittedLocalization } from '../../../services/AgentLocalisation/localisation-propertyInformationSubmitted';
+
+// Main component for property information submitted summary page
+const PropertyInformationSubmitted: React.FC = () => {
+ const navigate = useNavigate();
+ const { propertyId } = useParams();
+ const { formData, updateForm } = usePropertyForm();
+ const { fullProperties, properties } = usePropertyApplications();
+ const localization = usePropertyInformationSubmittedLocalization();
+
+ // On mount: update form data with property details if available
+ useEffect(() => {
+ if (propertyId && fullProperties.length > 0) {
+ const found = fullProperties.find(p =>
+ p.propertyId === propertyId ||
+ p.id === propertyId ||
+ p.applicationNo === propertyId
+ );
+ if (found && found.property) {
+ updateForm(found.property);
+ }
+ }
+ // eslint-disable-next-line
+ }, [propertyId, fullProperties]);
+
+ // Get address from the property data or fallback to form data
+ const getPropertyAddress = () => {
+ const propertyItem = properties.find(p =>
+ p.id === propertyId ||
+ p.pId === propertyId
+ );
+
+ if (propertyItem && propertyItem.address) {
+ return propertyItem.address;
+ }
+
+ const addressParts = [
+ formData.propertyAddress?.Street,
+ formData.propertyAddress?.Locality,
+ formData.propertyAddress?.ZoneNo,
+ formData.propertyAddress?.BlockNo,
+ formData.propertyAddress?.WardNo,
+ formData.propertyAddress?.PinCode
+ ].filter(Boolean);
+
+ return addressParts.length > 0 ? addressParts.join(', ') : 'Address not available';
+ };
+
+ const address = getPropertyAddress();
+
+ // Get phone number from property data (for future use)
+ // const getPhoneNumber = () => {
+ // const propertyItem = properties.find(p => p.id === propertyId || p.pId === propertyId);
+ // if (propertyItem && propertyItem.phoneNumber) {
+ // return propertyItem.phoneNumber;
+ // }
+ // return formData.owners?.[0]?.mobile || '';
+ // };
+
+ // const phoneNo = getPhoneNumber();
+
+ // Navigate back to reviewed properties list
+ const handleBackHome = () => {
+ navigate('/reviewed-properties');
+ };
+
+ // Get location data for map display
+ const getLocationData = () => {
+ if (formData.locationData && formData.locationData.coordinates) {
+ return formData.locationData.coordinates;
+ }
+ return { lat: 12.9716, lng: 77.5946 };
+ };
+
+ // Open external map providers in a new tab
+ const openGoogleMaps = () => {
+ const loc = getLocationData();
+ const q = `${loc.lat},${loc.lng}`;
+ const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(q)}`;
+ window.open(url, '_blank');
+ };
+
+ const openAppleMaps = () => {
+ const loc = getLocationData();
+ const coords = `${loc.lat},${loc.lng}`;
+ const label = address && address !== 'Address not available' ? address : coords;
+ const url = `https://maps.apple.com/?ll=${encodeURIComponent(coords)}&q=${encodeURIComponent(label)}`;
+ window.open(url, '_blank');
+ };
+
+ const openOpenStreetMap = () => {
+ const loc = getLocationData();
+ const url = `https://www.openstreetmap.org/?mlat=${loc.lat}&mlon=${loc.lng}#map=18/${loc.lat}/${loc.lng}`;
+ window.open(url, '_blank');
+ };
+
+ // Extract property form details from documents (fallback to empty fields)
+ const propertyForm = formData.documents?.[0] || {
+ documentType: '',
+ no: '',
+ constructionDate: '',
+ mroProceedingNumber: '',
+ mroProceedingDate: '',
+ courtName: '',
+ testatorAndTwoWitnessesSigned: false
+ };
+
+ // Extract uploaded document details for display
+ const documents =
+ formData.documents?.[0]?.files?.map((file, idx) => ({
+ id: file.fileStoreId || (idx + 1).toString(),
+ name: file.fileName,
+ type: file.fileType,
+ size: file.fileSize,
+ uploadDate: file.dateOfUpload,
+ status: 'uploaded'
+ })) || [];
+
+ // Extract owner details (first owner)
+ const owner = formData.owners?.[0] || {};
+
+ // Extract property address details (with fallback fields)
+ const propertyAddress = formData.propertyAddress || {
+ Locality: '',
+ ZoneNo: '',
+ WardNo: '',
+ BlockNo: '',
+ Street: '',
+ ElectionWard: '',
+ SecretariatWard: '',
+ PinCode: '',
+ DifferentCorrespondenceAddress: false,
+ PropertyId: '',
+ CorrespondenceAddress1: '',
+ CorrespondenceAddress2: '',
+ CorrespondencePincode: ''
+ };
+
+ // Main render: summary cards, map, details, and document list
+ return (
+
+ {/* Property Form Container */}
+
+
+ {/* Header Section */}
+
+
+
+ {localization.previousText}
+
+
+ {localization.propertyInformationSubmittedText}
+
+
+
+ {/* Property ID just above the map */}
+ {propertyId && (
+
{propertyId}
+ )}
+
+ {/* Map and Buttons Section */}
+
+ {/* Map Container */}
+
+ { /* readOnly */ }}
+ readOnly={true}
+ addressLabel={address}
+ />
+
+
+ {/* Map Provider Buttons */}
+
+
+
+ {localization.googleMapsText}
+
+
+
+ {localization.appleMapsText}
+
+
+
+ {localization.openStreetMapsText}
+
+
+
+
+ {/* Lower part - from PropertySummary */}
+
+ {/* Property Details Card */}
+
+
+
{localization.propertyDetailsText}
+
+
+
+
{localization.propertyTypeText} :
+
{propertyForm.documentType || localization.propertyTypeText}
+
+
+
{localization.zoneWardText} :
+
{propertyAddress.ZoneNo || localization.zoneWardText}, {propertyAddress.WardNo || localization.zoneWardText}
+
+
+
{localization.doorNoText} :
+
{formData.isgrDetails?.doorNoFrom || localization.doorNoText}
+
+
+
{localization.plotAreaText} :
+
{formData.assessmentDetails?.ExtentOfSite || '1,200'} {localization.plotAreaText}
+
+
+
+
+ {/* IGSR Details Card */}
+
+
+
{localization.igsrDetailsText}
+
+
+
+
{localization.surveyNumberText}:
+
{formData.isgrDetails?.igrsWard || localization.surveyNumberText}
+
+
+
{localization.subDivisionText}:
+
{formData.isgrDetails?.igrsBlock || localization.subDivisionText}
+
+
+
{localization.gisReferenceText}:
+
{formData.isgrDetails?.igrsLocality || localization.gisReferenceText}
+
+
+
{localization.cadastralMapText}:
+
{formData.isgrDetails?.habitation || localization.cadastralMapText}
+
+
+
{localization.igrsRegistrationText}:
+
{formData.isgrDetails?.doorNoFrom || localization.igrsRegistrationText}
+
+
+
+
+ {/* Owner Information Card */}
+
+
+
{localization.ownerInformationText}
+
+
+
+
{localization.ownerNameText}:
+
{owner.Name || localization.ownerNameText}
+
+
+
{localization.mobileNumberText}:
+
{owner.ContactNo || localization.mobileNumberText}
+
+
+
{localization.emailText}:
+
{owner.Email || localization.emailText}
+
+
+
{localization.addressText}:
+
{propertyAddress.Locality || localization.addressText}
+
+
+
+
+ {/* Assessment Details Card */}
+
+
+
{localization.assessmentDetailsText}
+
+
+
+
{localization.buildingUsageText}:
+
{formData.assessmentDetails?.ReasonOfCreation || localization.buildingUsageText}
+
+
+
{localization.constructionYearText}:
+
{formData.assessmentDetails?.OccupancyCertificateDate || localization.constructionYearText}
+
+
+
{localization.floorCountText}:
+
{formData.floors?.length || '2'} {localization.floorCountText}
+
+
+
{localization.builtupAreaText}:
+
{formData.floors?.reduce((acc, floor) => acc + (Number(floor.plinthArea) || 0), 0) || 2400} {localization.builtupAreaText}
+
+
+
{localization.annualRentalValueText}:
+
{localization.annualRentalValueText}
+
+
+
{localization.propertyTaxZoneText}:
+
{localization.propertyTaxZoneText}
+
+
+
{localization.assessmentStatusText}:
+
{localization.assessmentStatusText}
+
+
+
{localization.notesText} :
+
{localization.notesText}
+
+
+
+
+ {/* Documents */}
+
+
{localization.documentsUploadedText} :
+
+ {documents.map((d) => (
+
+
+
+
+
{d.name}
+
+ ))}
+
+
+
+ {/* Back Button */}
+
+
+ {localization.backText}
+
+
+
+
+
+ );
+};
+
+
+// Export the PropertyInformationSubmitted component as default
+export default PropertyInformationSubmitted;
diff --git a/frontend/mobile-ui/src/app/pages/Agent/ReviewedProperty.tsx b/frontend/mobile-ui/src/app/pages/Agent/ReviewedProperty.tsx
new file mode 100644
index 0000000..f3513c8
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Agent/ReviewedProperty.tsx
@@ -0,0 +1,446 @@
+// ReviewedProperty: Agent view for listing, searching, and filtering reviewed properties with map and pagination
+import React, { useState, useRef, useEffect, useMemo } from 'react';
+import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
+import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
+import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
+import { useNavigate } from 'react-router-dom';
+import Layout from '../../features/Agent/components/Layout';
+import type { NavigationTab, PropertyItem } from '../../../types';
+import LocationMapWithDrawing from '../PropertyForm/LocationMapWithDrawing';
+import Pagination from '../../features/Agent/components/Pagination';
+import { useReviewedPropertyLocalization } from '../../../services/AgentLocalisation/localisation-reviewedProperty';
+import { useGetApplicationsQuery } from '../../features/Agent/api/reviewPageApi';
+import '../../../styles/HomePage.css';
+import '../../../styles/ReviewedProperty.css';
+
+// Props for the custom dropdown filter component
+interface CustomDropdownProps {
+ options: { value: string; label: string }[];
+ value: string;
+ onChange: (value: string) => void;
+ width?: number | string;
+ placeholder?: string;
+ className?: string;
+}
+
+// Custom dropdown component for date sorting
+const CustomDropdown: React.FC = ({
+ options,
+ value,
+ onChange,
+ width = 120,
+ placeholder = 'Select',
+ className = '',
+}) => {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ if (ref.current && !ref.current.contains(event.target as Node)) {
+ setOpen(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const selected = options.find(opt => opt.value === value);
+
+ return (
+
+
setOpen(o => !o)}
+ style={{
+ width: '120px',
+ minWidth: '120px',
+ maxWidth: '120px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ textAlign: 'right',
+ border: '2px solid #C6561A',
+ borderRadius: '32px'
+ }}
+ aria-haspopup="listbox"
+ aria-expanded={open}
+ >
+ {selected ? selected.label : placeholder}
+
+
+
+
+ {open && (
+
+ {options.map(opt => (
+ {
+ onChange(opt.value);
+ setOpen(false);
+ }}
+ role="option"
+ aria-selected={opt.value === value}
+ >
+ {opt.label}
+
+ ))}
+
+ )}
+
+ );
+};
+
+// Main component for reviewed property list page
+const ReviewedProperty: React.FC = () => {
+ const loc = useReviewedPropertyLocalization();
+ const [page, setPage] = useState(1);
+ const [pageSize] = useState(10);
+ const navigate = useNavigate();
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedDateSort, setSelectedDateSort] = useState('');
+
+ // Get assigned agent ID from localStorage (fallback to default)
+ const assignedAgent = localStorage.getItem('agentId') || 'e09421f8-ab1a-4e62-a96b-5b26dff739ef';
+
+ // RTK Query - fetch reviewed property applications
+ const { data: apiResponse, isLoading, error } = useGetApplicationsQuery({
+ AssignedAgent: assignedAgent
+ });
+
+ // Convert API applications to PropertyItem format for display
+ const allProperties = useMemo((): PropertyItem[] => {
+ if (!apiResponse?.data) return [];
+
+ return apiResponse.data.map(app => ({
+ id: app.ID,
+ pId: app.ApplicationNo,
+ description: app.Property?.PropertyType || 'N/A',
+ address: app.Property?.Address
+ ? `${app.Property.Address.Street}, ${app.Property.Address.Locality}, ${app.Property.Address.ZoneNo}`
+ : 'Address not available',
+ status: app.Status,
+ dueDate: new Date(app.DueDate).toLocaleDateString('en-US', {
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ }),
+ isNew: false,
+ isDraft: app.IsDraft,
+ createdDate: app.CreatedAt,
+ type: app.Property?.OwnershipType || 'N/A',
+ phoneNumber: '',
+ area: '',
+ propertyType: app.Property?.PropertyType || 'N/A',
+ isVerified: app.Status === 'AUDIT_VERIFIED',
+ }));
+ }, [apiResponse]);
+
+ // Filter properties by search query and sort by date
+ const filteredProperties = useMemo(() => {
+ let filtered = allProperties;
+
+ if (searchQuery) {
+ const query = searchQuery.toLowerCase();
+ filtered = filtered.filter(
+ p =>
+ p.pId.toLowerCase().includes(query) ||
+ p.address.toLowerCase().includes(query) ||
+ p.description.toLowerCase().includes(query)
+ );
+ }
+
+ // Sort by date if selected
+ if (selectedDateSort === 'earliest') {
+ filtered = [...filtered].sort(
+ (a, b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime()
+ );
+ } else if (selectedDateSort === 'oldest') {
+ filtered = [...filtered].sort(
+ (a, b) => new Date(b.dueDate).getTime() - new Date(a.dueDate).getTime()
+ );
+ }
+
+ return filtered;
+ }, [allProperties, searchQuery, selectedDateSort]);
+
+ // Client-side pagination for property cards
+ const paginatedProperties = useMemo(() => {
+ const startIndex = (page - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ return filteredProperties.slice(startIndex, endIndex);
+ }, [filteredProperties, page, pageSize]);
+
+ // Calculate total number of pages
+ const totalPages = useMemo(() => {
+ return Math.ceil(filteredProperties.length / pageSize);
+ }, [filteredProperties.length, pageSize]);
+
+ // Reset to page 1 when search or sort changes
+ useEffect(() => {
+ setPage(1);
+ }, [searchQuery, selectedDateSort]);
+
+ // Extract property locations for map display
+ const propertyLocations = useMemo(() => {
+ if (!apiResponse?.data) return [];
+
+ return apiResponse.data
+ .filter(app => app.Property?.GISData?.Coordinates && app.Property.GISData.Coordinates.length > 0)
+ .map(app => ({
+ id: app.ID,
+ applicationNo: app.ApplicationNo,
+ lat: app.Property!.GISData!.Coordinates[0].Latitude,
+ lng: app.Property!.GISData!.Coordinates[0].Longitude,
+ address: app.Property?.Address
+ ? `${app.Property.Address.Street}, ${app.Property.Address.Locality}`
+ : 'Address not available',
+ status: app.Status === 'AUDIT_VERIFIED' ? 'Low' : 'High',
+ }));
+ }, [apiResponse]);
+
+ // Handle navigation tab changes (home, search, etc.)
+ const handleTabChange = (tab: NavigationTab) => {
+ switch (tab) {
+ case 'home': navigate('/agent'); break;
+ case 'search': navigate('/agent/search'); break;
+ case 'inbox': break;
+ case 'notifications': break;
+ }
+ };
+
+ // Navigate to property information submitted page
+ const handlePropertyClick = (_propertyId: string) => {
+ navigate('/PropertyInformationSubmitted');
+ };
+
+ // Handle date sort dropdown change
+ const handleDateSortChange = (value: string) => {
+ setSelectedDateSort(value);
+ };
+
+ // Show loading state while fetching properties
+ if (isLoading) {
+ return (
+
+
+
+ Loading reviewed properties...
+
+
+
+ );
+ }
+
+ // Show error state if property fetch fails
+ if (error) {
+ return (
+
+
+
+ Error loading reviewed properties. Please try again later.
+
+
+
+ );
+ }
+
+ // Main render: layout, header, search/filter, property cards, and pagination
+ return (
+
+
+ {/* HEADER */}
+
+
navigate("/agent")}
+ aria-label="Back"
+ style={{
+ background: 'transparent',
+ border: '1px solid #E0E0E0',
+ borderRadius: '20px',
+ padding: '8px 16px',
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px',
+ cursor: 'pointer',
+ marginBottom: '16px',
+ color: '#FC670C',
+ fontSize: '15px',
+ fontWeight: 500
+ }}
+ >
+
+ {loc.previousText}
+
+
{loc.reviewedPropertiesTitleText}
+
+
+ {/* SEARCH + FILTER */}
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder={loc.searchPlaceholderText}
+ className="search-input"
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {/* PROPERTY CARDS */}
+
+ {paginatedProperties.length > 0 ? (
+ paginatedProperties.map((item: PropertyItem) => {
+ const showGreenTick = !item.isNew && !item.isDraft;
+ return (
+
handlePropertyClick(item.id)}
+ >
+
+ {/* LEFT: Info */}
+
+
{item.pId}
+ {showGreenTick && (
+
{loc.verifiedText}
+ )}
+
+
{loc.categoryIdText}
+
{item.description || item.pId}
+
+
+
{loc.addressText}
+
{item.address}
+
+
+
+ {/* RIGHT: Map & Date */}
+
+
+ {(() => {
+ const locItem = propertyLocations.find(pl =>
+ pl.id === item.id ||
+ pl.applicationNo === item.pId
+ );
+ if (locItem && typeof locItem.lat === 'number' && typeof locItem.lng === 'number') {
+ return (
+
+
+ { /* thumbnail no-op */ }}
+ readOnly={true}
+ />
+
+
+ );
+ }
+ return null;
+ })()}
+
+
+ {item.dueDate}
+
+
+
+
+ );
+ })
+ ) : (
+
+
{searchQuery ? loc.noPropertiesFoundText : loc.noPropertiesFoundText}
+
+ )}
+
+ {totalPages > 1 && (
+
setPage(newPage)}
+ />
+ )}
+
+
+
+ );
+};
+
+
+// Export the ReviewedProperty component as default
+export default ReviewedProperty;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/Agent/SearchProperty.tsx b/frontend/mobile-ui/src/app/pages/Agent/SearchProperty.tsx
new file mode 100644
index 0000000..65affca
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Agent/SearchProperty.tsx
@@ -0,0 +1,345 @@
+// SearchProperty page for agents to search and filter property applications
+import React, { useState, useMemo, useEffect } from 'react';
+import {
+ Box,
+ Typography,
+ Button,
+ Select,
+ MenuItem,
+ TextField,
+ IconButton,
+ OutlinedInput,
+ CircularProgress,
+} from '@mui/material';
+import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
+import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined';
+import SearchIcon from '@mui/icons-material/Search';
+import { useNavigate } from 'react-router-dom';
+import SearchPropertyResultCard from '../../features/Agent/components/SearchPropertyResultCard';
+import { ArrowBackIos } from '@mui/icons-material';
+import { useGetPropertiesQuery } from '../../features/Agent/api/searchPropertyApi';
+import type { ApplicationData } from '../../features/Agent/models/SearchPropertyData.model';
+
+// Options for which field to search by
+const SEARCH_FIELD_OPTIONS = [
+ { value: 'zoneNo', label: 'Zone No' },
+ { value: 'wardNo', label: 'Ward No' },
+ { value: 'propertyNo', label: 'Property No' },
+];
+
+// Key for caching applications in localStorage
+const LOCAL_STORAGE_KEY = 'allApplications';
+
+// Main component for property search page
+const SearchProperty: React.FC = () => {
+ const navigate = useNavigate();
+ // State for selected search field, search query, and local application data
+ const [selectedField, setSelectedField] = useState('zoneNo');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [localApplications, setLocalApplications] = useState([]);
+ // Get agent id from localStorage
+ const assignedAgent = localStorage.getItem('user_id') || '';
+
+ // Fetch all applications assigned to agent on mount
+ const {
+ data: apiData,
+ isLoading: apiLoading,
+ isError: apiError,
+ } = useGetPropertiesQuery({ assignedAgent });
+
+ // On successful fetch, store applications in localStorage and state
+ useEffect(() => {
+ if (apiData?.data) {
+ setLocalApplications(apiData.data);
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(apiData.data));
+ }
+ }, [apiData]);
+
+ // On mount: if no API data, restore from localStorage cache
+ useEffect(() => {
+ if ((!apiData || !apiData.data) && !apiLoading) {
+ const cached = localStorage.getItem(LOCAL_STORAGE_KEY);
+ if (cached) {
+ setLocalApplications(JSON.parse(cached));
+ }
+ }
+ }, [apiData, apiLoading]);
+
+ // Filter applications locally based on search field and query
+ const filteredApplications = useMemo(() => {
+ if (!localApplications || localApplications.length === 0) return [];
+ if (!searchQuery.trim()) return localApplications;
+
+ const query = searchQuery.trim().toLowerCase();
+ return localApplications.filter((application) => {
+ const prop = application.Property;
+ if (!prop) return false;
+
+ if (selectedField === 'propertyNo') {
+ const propNo = prop.PropertyNo ?? '';
+ return propNo.toString().toLowerCase().includes(query);
+ }
+
+ const address = prop.Address;
+ if (!address) return false;
+
+ if (selectedField === 'zoneNo') {
+ const zone = address.ZoneNo ?? '';
+ return zone.toString().toLowerCase().includes(query);
+ } else if (selectedField === 'wardNo') {
+ const ward = address.WardNo ?? '';
+ return ward.toString().toLowerCase().includes(query);
+ }
+ return false;
+ });
+ }, [localApplications, searchQuery, selectedField]);
+
+ // Handle Enter key in search input (filtering is automatic)
+ const handleKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ // Local filtering happens automatically, nothing needed.
+ }
+ };
+
+ // Go back to previous page
+ const handleBack = () => navigate(-1);
+ // Navigate to property location view
+ const handleViewLocation = (propertyId: string) => navigate(`agent//location/${propertyId}`);
+
+ // Main UI rendering
+ return (
+
+ {/* Top navigation bar with back button */}
+
+ }
+ >
+ Previous
+
+
+
+ {/* Page heading */}
+
+ Search Property
+
+
+ {/* Dropdown to select which field to search by */}
+
+ Select field
+
+
+ setSelectedField(e.target.value)}
+ IconComponent={(props) => (
+
+ )}
+ input={
+
+ }
+ sx={{
+ fontSize: 16,
+ color: '#222',
+ p: 0,
+ py: 0.7,
+ height: 40,
+ '& fieldset': { borderColor: '#E9AC85', borderRadius: '12px' },
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderColor: '#E9AC85',
+ borderWidth: '2px',
+ },
+ }}
+ MenuProps={{
+ PaperProps: {
+ sx: {
+ bgcolor: '#fff',
+ color: '#222',
+ borderRadius: 2,
+ fontSize: 16,
+ boxShadow: '0 1px 8px rgba(0,0,0,0.13)',
+ },
+ },
+ }}
+ >
+ {SEARCH_FIELD_OPTIONS.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+
+
+
+
+ {/* Search input box */}
+
+ opt.value === selectedField
+ )?.label?.toLowerCase() || ''
+ }`}
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ onKeyPress={handleKeyPress}
+ sx={{
+ bgcolor: '#f0f0f0ff',
+ borderRadius: '8px',
+ '& .MuiOutlinedInput-input': {
+ fontFamily: 'Roboto, sans-serif',
+ boxSizing: 'border-box !important',
+ minHeight: '52px',
+ },
+ '& .MuiOutlinedInput-root': {
+ borderRadius: '8px',
+ fontSize: '16px !important',
+ boxSizing: 'border-box !important',
+ },
+ }}
+ inputProps={{
+ style: {
+ borderRadius: '8px',
+ background: '#f0f0f0ff',
+ },
+ }}
+ />
+
+
+
+
+
+
+
+ {/* Show loading spinner while fetching data */}
+ {apiLoading && (
+
+
+
+ )}
+
+ {/* Show error message if API fetch fails */}
+ {apiError && (
+
+
+ Failed to fetch properties. Showing cached results if available.
+
+
+ )}
+
+ {/* Render filtered search results as cards */}
+
+ {!apiLoading && filteredApplications && filteredApplications.length > 0
+ ? filteredApplications.map((application) => {
+
+ return (
+ handleViewLocation(application.PropertyID)}
+ />
+ );
+ })
+ : !apiLoading && (
+
+ {searchQuery
+ ? 'No properties found'
+ : 'Enter search criteria to filter properties'}
+
+ )}
+
+
+ );
+};
+
+export default SearchProperty;
diff --git a/frontend/mobile-ui/src/app/pages/Agent/SendEmail.tsx b/frontend/mobile-ui/src/app/pages/Agent/SendEmail.tsx
new file mode 100644
index 0000000..830a72c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Agent/SendEmail.tsx
@@ -0,0 +1,296 @@
+// SendEmail.tsx
+// This page allows agents to compose and send emails, including attachments, to citizens or other users.
+// Features:
+// - Add/remove multiple recipients (To field)
+// - Edit/remove sender (From field)
+// - Subject and body input
+// - File attachment support
+// - Navigation to previous and home screens
+// - Uses localization for all labels and messages
+// Used in: Agent workflow for property communication and correspondence
+
+import React, { useState, useRef } from "react";
+import "../../../styles/SendEmail.css";
+import AttachFileIcon from '@mui/icons-material/AttachFile';
+import { FiSend } from 'react-icons/fi';
+import { useNavigate } from "react-router-dom";
+import { ArrowBackIosNew } from "@mui/icons-material";
+import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
+import { useLocalization } from "../../../services/AgentLocalisation/formLocalisation";
+
+
+interface EmailFields {
+ to: string[];
+ from: string;
+ subject: string;
+ body: string;
+ attachment?: File | null;
+}
+
+const DEFAULT_FROM = "agentkumar05@login.com";
+
+const SendEmail: React.FC = () => {
+ // State for email fields, input, and edit mode
+ const [fields, setFields] = useState({
+ to: ["citizen01@login.com"],
+ from: DEFAULT_FROM,
+ subject: "",
+ body: "",
+ attachment: null,
+ });
+ const [inputTo, setInputTo] = useState("");
+ const [fromEdit, setFromEdit] = useState(false);
+ const fromInputRef = useRef(null);
+
+ // Navigation and params
+ // const { propertyId } = useParams();
+ const navigate = useNavigate();
+
+
+// const handlePrevious = () => {
+// if (propertyId) {
+// navigate(`/agent/verification/${propertyId}`);
+// }else{
+// navigate('/agent/verification');
+// }
+// };
+ const handlePrevious = () => {
+ navigate(-1);
+ };
+
+ // Subject/body change
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFields((f) => ({ ...f, [name]: value }));
+ };
+
+ // Attachment
+ const handleAttachment = (e: React.ChangeEvent) => {
+ setFields((f) => ({ ...f, attachment: e.target.files ? e.target.files[0] : null }));
+ };
+
+ // "To" chips
+ const handleToInputChange = (e: React.ChangeEvent) => setInputTo(e.target.value);
+ const handleToInputKeyDown = (e: React.KeyboardEvent) => {
+ if ((e.key === "Enter" || e.key === ",") && inputTo.trim()) {
+ e.preventDefault();
+ if (!fields.to.includes(inputTo.trim()))
+ setFields((f) => ({ ...f, to: [...f.to, inputTo.trim()] }));
+ setInputTo("");
+ } else if (e.key === "Backspace" && !inputTo && fields.to.length > 0) {
+ setFields((f) => ({ ...f, to: f.to.slice(0, -1) }));
+ }
+ };
+ const handleRemoveToChip = (email: string) => {
+ setFields((f) => ({ ...f, to: f.to.filter((e) => e !== email) }));
+ };
+
+ // "From" chip edit/clear
+ const handleRemoveFrom = () => {
+ setFields((f) => ({ ...f, from: "" }));
+ setFromEdit(true);
+ setTimeout(() => fromInputRef.current?.focus(), 0);
+ };
+ const handleEditFrom = () => {
+ setFromEdit(true);
+ setTimeout(() => fromInputRef.current?.focus(), 0);
+ };
+ const handleFromInputChange = (e: React.ChangeEvent) => {
+ setFields((f) => ({ ...f, from: e.target.value }));
+ };
+ const handleFromInputBlur = () => {
+ if (fields.from) setFromEdit(false);
+ };
+ const handleFromInputKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && fields.from) {
+ setFromEdit(false);
+ e.preventDefault();
+ }
+ };
+ const handleHome = () => {
+ navigate("/agent");
+ }
+
+ // Send/discard
+ const handleSend = () => alert("Email data ready for backend:\n" + JSON.stringify(fields, null, 2));
+ const handleDiscard = () => {
+ setFields({
+ to: [],
+ from: "",
+ subject: "",
+ body: "",
+ attachment: null,
+ });
+ setInputTo("");
+ setFromEdit(false);
+ };
+
+ const {
+ SendEmailText,
+InternalCorrspondenceWithSericeManagerText,
+previousText,
+ToText,
+FromText,
+SubjectText,
+ComposeEmailText,
+DiscardText,
+HomeText
+} = useLocalization();
+
+ // Render SendEmail page UI
+ return (
+
+ {/* Header section with title, subtitle, and navigation buttons */}
+
+
+
{SendEmailText}
+
+ {InternalCorrspondenceWithSericeManagerText}
+
+
+
+
+
+ {previousText}
+
+
+
+ {HomeText}
+
+
+ {/*
Send Email
+
Internal Correspondence with Service Manager
*/}
+
+
+ {/* Actions row for attach and send */}
+
+
+
+ );
+};
+
+// Export SendEmail for use in agent communication
+export default SendEmail;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/Agent/index.ts b/frontend/mobile-ui/src/app/pages/Agent/index.ts
new file mode 100644
index 0000000..1f487b6
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Agent/index.ts
@@ -0,0 +1,10 @@
+export * from "./AddRequestOrComment"
+export * from "../PropertyForm/ApplicationLog"
+export * from "../PropertyForm/AssessmentDetails"
+export * from "../PropertyForm/ConstructionDetailsPage"
+export * from "../PropertyForm/DraftPage"
+export * from "./HomePage"
+export * from "../PropertyForm/DocumentsInformation"
+export * from "../PropertyForm/FloorDetailsCards"
+export * from "../PropertyForm/FloorDetailsPage"
+export * from "../PropertyForm/IGRSAdditionalDetailsPage"
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/Citizen/CitizenHomePage.tsx b/frontend/mobile-ui/src/app/pages/Citizen/CitizenHomePage.tsx
new file mode 100644
index 0000000..7f7d952
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Citizen/CitizenHomePage.tsx
@@ -0,0 +1,695 @@
+// CitizenHomePage: Main landing page for citizens, showing property list, map, language/profile controls, and urgent attention
+import React, { useEffect, useState } from 'react';
+import {
+ Box,
+ Container,
+ Typography,
+ Button,
+ TextField,
+ Paper,
+ IconButton,
+ Menu,
+ MenuItem,
+} from '@mui/material';
+import AccountCircleOutlinedIcon from '@mui/icons-material/AccountCircleOutlined';
+import NotificationsActiveIcon from '@mui/icons-material/NotificationsActiveOutlined';
+import SearchIcon from '@mui/icons-material/Search';
+
+import translateIndicSvg from '../../../assets/CitizenAssets/home_page/translate_indic.svg';
+import { useNavigate } from 'react-router-dom';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import { useFormMode } from '../../../context/FormModeContext';
+import { useAppDispatch, useAppSelector } from '../../../redux/Hooks';
+import { setCitizenLang } from '../../../redux/LangSlice';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../services/Citizen/Localization/LocalizationContext';
+import ChangeLocalePopup from '../../../components/Popup/ChangeLocalePopup';
+import type { AlertType } from '../../models/AlertType.model';
+import LoadingPage from '../../components/Loader';
+import { BottomBar } from '../../components/BottomBar';
+// --- REMOVE imported useGetCitizenHomeQuery and CitizenHomeData ---
+// import type { CitizenHomeData } from '../../features/Citizen/models/CitizenHome.model';
+// import { useGetCitizenHomeQuery } from '../../features/Citizen/api/Citizen.api';
+import InfoCard from '../../features/Citizen/components/InfoCard';
+import UrgentCard from '../../features/Citizen/components/UrgentCard';
+import ChromeTabs from '../../features/Citizen/components/ChromeTabs';
+import MapView from '../../features/Citizen/components/MapView';
+import PropertyList from '../../features/Citizen/components/PropertyList';
+import { useGetCitizenApplicationsQuery } from '../../features/Citizen/api/CitizenHomePageApi/CitizenHomePageApi';
+import type { CitizenApplicationSummary } from '../../features/Citizen/api/CitizenHomePageApi/CitizenHomePageModel';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+const NEW_USER_BANGALORE_CENTER: [number, number] = [12.9603, 77.6385];
+const LOCALE_MISMATCH_POPUP_FLAG = 'citizenLocaleMismatchChecked';
+
+// Main component for citizen home page
+const CitizenHomePage: React.FC = () => {
+ // Use RTK Query to get citizen applications (main state)
+
+ // Get assesseeId from localStorage
+ const assesseeId = localStorage.getItem('user_id')!;
+
+
+ // Fetch citizen applications (non-draft)
+ const { data, isLoading, isError } = useGetCitizenApplicationsQuery({ assesseeId, isDraft: false });
+ console.log("Application data from API", data);
+
+ // List of citizen applications
+ const applications: CitizenApplicationSummary[] = data?.data ?? [];
+ // Number of properties for the user
+ const noOfProperties = applications.length;
+
+ // Placeholder for active licenses and urgent attention (to be replaced with backend data)
+ const fakeActiveLicenses = 0;
+ const fakeUrgentAttention: any[] = [];
+
+ // State for selected tab and map marker
+ const [selectedTabIndex, setSelectedTabIndex] = useState(0);
+ const [selectedMarkerIdx, setSelectedMarkerIdx] = useState(null);
+
+ // Dispatcher for the Localization store
+ const dispatch = useAppDispatch();
+
+ // State for showing locale mismatch popup
+ const [showChangeLocalePopup, setShowChangeLocalePopup] = useState(false);
+ // Login locale from localStorage
+ const loginLocale = localStorage.getItem('loginLocale') || 'en';
+
+ // Current language from Redux store
+ const lang = useAppSelector((state) => state.lang.citizenLang);
+
+ // Loading state for localization messages
+ const { loading } = useLocalization();
+
+ // Fetch localized messages from session storage
+ const messages = getMessagesFromSession('CITIZEN');
+
+ // State for search input
+ const [search, setSearch] = useState('');
+ const navigate = useNavigate();
+ const { resetForm } = usePropertyForm();
+ const { setMode } = useFormMode();
+
+ // Language dropdown state and handlers
+ const [langMenuAnchor, setLangMenuAnchor] = useState(null);
+ const handleLangClick = (event: React.MouseEvent) => {
+ setLangMenuAnchor(event.currentTarget);
+ };
+ const handleLangClose = () => setLangMenuAnchor(null);
+ const handleLangSelect = (code: 'en' | 'hi' | 'kn') => {
+ dispatch(setCitizenLang(code));
+ setLangMenuAnchor(null);
+ };
+
+ // Handlers for locale popup actions
+ const handleSwitchToLoginLocale = () => {
+ dispatch(setCitizenLang(loginLocale));
+ setShowChangeLocalePopup(false);
+ localStorage.setItem(LOCALE_MISMATCH_POPUP_FLAG, 'true');
+ };
+ const handleClosePopup = () => {
+ setShowChangeLocalePopup(false);
+ localStorage.setItem(LOCALE_MISMATCH_POPUP_FLAG, 'true');
+ };
+
+ // Initialize language from storage on mount
+ useEffect(() => {
+ const localeOnLogin = localStorage.getItem('citizenLocale') || 'en';
+ dispatch(setCitizenLang(localeOnLogin));
+ }, [dispatch]);
+
+ // Handler for adding a new property
+ const handleAddNewProperty = () => {
+ resetForm();
+ setMode('new');
+ navigate('/property-form/property-information');
+ };
+
+ // Locale mismatch popup logic, should trigger only once per user/session
+ useEffect(() => {
+ const alreadyChecked = localStorage.getItem(LOCALE_MISMATCH_POPUP_FLAG);
+ if (alreadyChecked !== 'true') {
+ if (lang && loginLocale && lang !== loginLocale) {
+ setShowChangeLocalePopup(true);
+ }
+ }
+ }, [lang, loginLocale]);
+
+ // Popup state & effect -- show "Success" popup for 3 seconds on mount
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ title: string;
+ message: string;
+ open: boolean;
+ }>({ type: 'success', title: '', message: '', open: false });
+
+ // Show welcome popup on first login
+ useEffect(() => {
+ // Only show once when the page loads
+ const popupFlag = sessionStorage.getItem('showWelcomePopup');
+ if (popupFlag === 'true') {
+ setPopup({
+ type: 'success',
+ title: 'Successfully Logged In!',
+ message: 'Welcome to Property Tax - Citizen Portal!',
+ open: true,
+ });
+ sessionStorage.removeItem('showWelcomePopup');
+ }
+ }, []);
+
+ // Show success popup after property creation
+ useEffect(() => {
+ const showSuccess = localStorage.getItem('showSuccessPropCreation');
+ if (showSuccess === 'true') {
+ setPopup({
+ type: 'success',
+ title: 'Success!',
+ message: `Property ${localStorage.getItem('propertyNo')} created successfully!`,
+ open: true,
+ });
+ localStorage.removeItem('propertyNo');
+ localStorage.removeItem('showSuccessPropCreation');
+ }
+ }, [localStorage.getItem('showSuccessPropCreation')]);
+
+ // Show loader until messages AND home data are ready
+ if (loading || !messages || isLoading) {
+ return ;
+ }
+
+ // Handle error state from RTK Query
+ if (isError) {
+ // you can show a toast or an error UI. For now keep console and continue with empty data fallback
+ console.error('Failed to load citizen home data');
+ }
+
+ const activeLicensesLabel = messages['citizen.home'][lang]['active-licences'];
+ const noOfPropertiesLabel = messages['citizen.home'][lang]['number-prop'];
+ const languageLabel = messages['citizen.commons'][lang]['home-language'];
+ const profileLabel = messages['citizen.commons'][lang]['home-profile'];
+ const notificationLabel = messages['citizen.commons'][lang]['notification-label'];
+ const myPropertiesLabel = messages['citizen.commons'][lang]['my-properties'];
+ const urgentAttentionLabel = messages['citizen.home'][lang]['urgent-attention'];
+ const addNewPropertyLabel = messages['citizen.home'][lang]['add-new-property'];
+
+ // Use application list as property list source
+ // Main render: popups, header, language/profile controls, map, property list, and urgent attention
+ return (
+ <>
+
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+ {noOfProperties !== 0 ? (
+ /* Existing user content */
+
+
+ {/* Language Section with Dropdown */}
+
+
+
+
+ {languageLabel}
+
+
+
+ handleLangSelect('en')}
+ >
+ English
+
+ handleLangSelect('hi')}
+ >
+ เคนเคฟเคจเฅเคฆเฅ
+
+ handleLangSelect('kn')}
+ >
+ เฒเฒจเณเฒจเฒก
+
+
+
+ {/* Notification Section */}
+
+ {}}
+ sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ p: 0.5,
+ boxShadow: 'none',
+ background: 'none',
+ minWidth: 0,
+ '&:hover': { background: 'none' },
+ }}
+ >
+
+
+
+
+ {notificationLabel}
+
+
+
+ {/* Profile Section */}
+
+ navigate('/profile', { state: { noOfProperties, fakeActiveLicenses} })}
+ sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ p: 0.5,
+ boxShadow: 'none',
+ background: 'none',
+ minWidth: 0,
+ '&:hover': { background: 'none' },
+ }}
+ >
+
+
+
+
+ {profileLabel}
+
+
+
+
+
+ {myPropertiesLabel}
+
+
+
+
+
+
+ {/* Urgent Attention Section (dummy, implement logic if required) */}
+ {Array.isArray(fakeUrgentAttention) &&
+ fakeUrgentAttention.length >= 1 &&
+ fakeUrgentAttention.length <= 3 && (
+
+
+
+
+ {urgentAttentionLabel}
+
+
+ {fakeUrgentAttention.map((item) => (
+
+ ))}
+
+ )}
+
+ {/* Tabs and Map */}
+
+
+
+
+
+
+ {selectedTabIndex === 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ + {addNewPropertyLabel}
+
+
+
+ {/* Urgent Attention Section again (dummy, implement logic if required) */}
+ {Array.isArray(fakeUrgentAttention) &&
+ (fakeUrgentAttention.length === 0 ||
+ fakeUrgentAttention.length > 3) && (
+
+
+
+
+ {urgentAttentionLabel}
+
+
+ {fakeUrgentAttention.map((item) => (
+
+ ))}
+
+ )}
+
+ ) : (
+
+ {/* Top row */}
+
+ {/* Language Section with Dropdown */}
+
+
+
+
+ {languageLabel}
+
+
+
+ handleLangSelect('en')}
+ >
+ English
+
+ handleLangSelect('hi')}
+ >
+ เคนเคฟเคจเฅเคฆเฅ
+
+ handleLangSelect('kn')}
+ >
+ เฒเฒจเณเฒจเฒก
+
+
+
+ {/* Notification Section */}
+
+ {}}
+ sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ p: 0.5,
+ boxShadow: 'none',
+ background: 'none',
+ minWidth: 0,
+ '&:hover': { background: 'none' },
+ }}
+ >
+
+
+
+
+ {notificationLabel}
+
+
+
+ {/* Profile Section */}
+
+ navigate('/profile')}
+ sx={{
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ p: 0.5,
+ boxShadow: 'none',
+ background: 'none',
+ minWidth: 0,
+ '&:hover': { background: 'none' },
+ }}
+ >
+
+
+
+
+ {profileLabel}
+
+
+
+
+
+ {/* Main Title */}
+
+ {myPropertiesLabel}
+
+
+ {/* Map with search bar overlay */}
+
+
+
+ {/* Search bar overlay */}
+
+ setSearch(e.target.value)}
+ InputProps={{
+ disableUnderline: true,
+ style: { fontWeight: 500, fontSize: 18, color: '#222' },
+ endAdornment: ,
+ }}
+ sx={{
+ // Remove any border from root
+ '& .MuiInputBase-root': {
+ border: 'none !important',
+ boxShadow: 'none',
+ outline: 'none',
+ background: 'transparent',
+ },
+ // Remove any border from input itself
+ '& .MuiInput-input': {
+ border: 'none !important',
+ boxShadow: 'none',
+ outline: 'none',
+ background: 'transparent',
+ },
+ // Remove underline from standard variant (extra safety)
+ '& .MuiInput-underline:before, & .MuiInput-underline:after': {
+ borderBottom: 'none !important',
+ },
+ }}
+ />
+
+
+
+
+ {/* Add New Property button */}
+
+
+ + {addNewPropertyLabel}
+
+
+
+ {/* Urgent Attention */}
+
+
+
+
+ {urgentAttentionLabel}
+
+
+
+
+
+ Begin registration of properties to begin enumeration process
+
+
+ {new Date().toISOString().split('T')[0]}
+
+
+
+
+ )}
+
+
+
+ >
+ );
+};
+
+
+// Export the CitizenHomePage component as default
+export default CitizenHomePage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/Citizen/Properties.tsx b/frontend/mobile-ui/src/app/pages/Citizen/Properties.tsx
new file mode 100644
index 0000000..7ddbfd0
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Citizen/Properties.tsx
@@ -0,0 +1,323 @@
+// Properties.tsx
+// This page displays the citizen's properties and drafts, with tab switching, loading, error, and empty states.
+// Features:
+// - Fetches properties and drafts via RTK Query
+// - Tab buttons to switch between properties and drafts
+// - Transforms API data for property cards
+// - Shows loading spinner, error message, or empty state as needed
+// - Renders property list and bottom navigation bar
+// Used in: Citizen workflow for property management and review
+
+import { Box, Button } from '@mui/material';
+import { CottageOutlined } from '@mui/icons-material';
+import { useAppSelector } from '../../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../services/Citizen/Localization/LocalizationContext';
+import PropertyList from '../../features/Citizen/components/ui/PropertyList';
+import Header from '../../components/Header';
+import LoadingPage from '../../components/Loader';
+import { BottomBar } from '../../components/BottomBar';
+import NoProperties from '../../features/Citizen/components/NoProperties';
+import ErrorMessage from '../../features/Citizen/components/ErrorMessage';
+import { useGetCitizenApplicationsQuery } from '../../features/Citizen/api/CitizenHomePageApi/CitizenHomePageApi';
+import type { CitizenApplicationSummary, CitizenPropertySummary } from '../../features/Citizen/api/CitizenHomePageApi/CitizenHomePageModel';
+import { useState } from 'react';
+
+// Status to enumeration progress mapping
+const STATUS_PROGRESS_MAP: Record = {
+ 'INITIATED': 20,
+ 'ASSIGNED': 40,
+ 'AUDIT_VERIFIED': 60,
+ 'VERIFIED': 80,
+ 'APPROVED': 100,
+};
+
+const EMPTY_PROPERTY: CitizenPropertySummary = {
+ __appId: '',
+ ID: '',
+ PropertyNo: '',
+ OwnershipType: '',
+ PropertyType: '',
+ ComplexName: '',
+ Address: {
+ ID: '',
+ Locality: '',
+ ZoneNo: '',
+ WardNo: '',
+ BlockNo: '',
+ Street: '',
+ ElectionWard: '',
+ SecretariatWard: '',
+ PinCode: 0,
+ DifferentCorrespondenceAddress: false,
+ PropertyID: '',
+ CreatedAt: '',
+ UpdatedAt: '',
+ },
+ AssessmentDetails: undefined,
+ Amenities: [],
+ ConstructionDetails: undefined,
+ AdditionalDetails: undefined,
+ GISData: undefined,
+ CreatedAt: '',
+ UpdatedAt: '',
+ Documents: [],
+ IGRS: undefined,
+};
+
+const Properties: React.FC = () => {
+ // State for active tab (properties/drafts)
+ const [activeTab, setActiveTab] = useState<'properties' | 'drafts'>('properties');
+ const lang = useAppSelector((state) => state.lang.citizenLang);
+ const { loading: localizationLoading } = useLocalization();
+ const messages = getMessagesFromSession('CITIZEN')!;
+
+ const assesseeId = localStorage.getItem('user_id')!;
+
+ // Fetch properties (isDraft = false)
+ const {
+ data: propertiesData,
+ isLoading: isLoadingProperties,
+ isError: isErrorProperties,
+ } = useGetCitizenApplicationsQuery({
+ assesseeId,
+ isDraft: false,
+ });
+
+ // Fetch drafts (isDraft = true)
+ const {
+ data: draftsData,
+ isLoading: isLoadingDrafts,
+ isError: isErrorDrafts,
+ } = useGetCitizenApplicationsQuery({
+ assesseeId,
+ isDraft: true,
+ });
+
+ const propertiesApplications: CitizenApplicationSummary[] = propertiesData?.data ?? [];
+ const draftsApplications: CitizenApplicationSummary[] = draftsData?.data ?? [];
+
+ // SAFE transform (pure function, returns new object, no mutation)
+ function transformApplications(applications: CitizenApplicationSummary[], isDraft: boolean) {
+ return applications.map((app) => {
+ // Defensive clone with typed fallback
+ const property = app.Property ? ({ ...EMPTY_PROPERTY, ...app.Property } as CitizenPropertySummary) : { ...EMPTY_PROPERTY };
+ const coordsArr = property.GISData?.Coordinates || [];
+ const coordinates =
+ Array.isArray(coordsArr) && coordsArr.length > 0
+ ? {
+ lat: coordsArr[0]?.Latitude ?? 0,
+ lng: coordsArr[0]?.Longitude ?? 0,
+ }
+ : { lat: 0, lng: 0 };
+
+ const addressObj = (property.Address || {}) as any;
+
+ let enumerationProgress = 20; // Default to INITIATED
+ if (isDraft) {
+ enumerationProgress = -1;
+ } else {
+ const status = app.Status?.toUpperCase() || 'INITIATED';
+ enumerationProgress = STATUS_PROGRESS_MAP[status] || 20;
+ }
+
+ const out = {
+ // keep original property fields first (so PropertyList props still available)
+ ...property,
+ // UI-only computed fields
+ id: property.ID || app.ID,
+ propertyType: property.PropertyType || 'Residential',
+ enumerationProgress,
+ isDraft,
+ applicationStatus: app.Status,
+ propertyAddress: {
+ street: addressObj.Street || '',
+ locality: addressObj.Locality || '',
+ wardNo: addressObj.WardNo || '',
+ zoneNo: addressObj.ZoneNo || '',
+ blockNo: addressObj.BlockNo || '',
+ pincode: addressObj.PinCode || '',
+ },
+ locationData: {
+ coordinates,
+ address: [
+ addressObj.BlockNo,
+ addressObj.Locality,
+ addressObj.Street,
+ addressObj.WardNo,
+ addressObj.ZoneNo,
+ addressObj.PinCode,
+ ]
+ .filter(Boolean)
+ .join(', ') || property.ComplexName || '',
+ },
+ // helper unique id for debugging/dedupe
+ __appId: app.ID ?? `${property.ID || 'unknown'}-${isDraft ? 'd' : 'p'}`,
+ };
+
+ // freeze to detect accidental mutation in downstream components
+ try { Object.freeze(out); } catch (e) { /* noop */ }
+
+ return out;
+ });
+ }
+
+ // dedupe by property ID (or app id) to ensure no overlaps
+ function dedupeById(items: any[]) {
+ const seen = new Set();
+ const result: any[] = [];
+ for (const it of items) {
+ const key = it.id || it.PropertyID || it.__appId || JSON.stringify(it);
+ if (!seen.has(key)) {
+ seen.add(key);
+ result.push(it);
+ }
+ }
+ return result;
+ }
+
+ // Build lists (no memo) and dedupe
+ const transformedProperties = transformApplications(propertiesApplications, false);
+ const transformedDrafts = transformApplications(draftsApplications, true);
+
+ if (typeof window !== 'undefined') {
+ // check intersection (overlap)
+ const propIds = new Set(transformedProperties.map(p => p.id));
+ const overlap = transformedDrafts.filter(d => propIds.has(d.id)).map(d => d.id);
+ if (overlap.length) {
+ console.warn('[PropertiesView] overlap between properties and drafts (IDs):', overlap);
+ }
+ }
+
+ // dedupe to be safe (prefer drafts to appear in drafts only - remove from properties if overlap)
+ // remove from properties any item whose id also appears in drafts
+ const draftIds = new Set(transformedDrafts.map(d => d.id));
+ const propertiesFiltered = transformedProperties.filter(p => !draftIds.has(p.id));
+ const propertiesFinal = dedupeById(propertiesFiltered);
+ const draftsFinal = dedupeById(transformedDrafts);
+
+ const currentData = activeTab === 'properties' ? propertiesFinal : draftsFinal;
+
+ const isLoading = activeTab === 'properties' ? isLoadingProperties : isLoadingDrafts;
+ const isError = activeTab === 'properties' ? isErrorProperties : isErrorDrafts;
+
+ if (localizationLoading) {
+ return ;
+ }
+
+ return (
+
+ {/* Header with localized title and subtitle */}
+ }
+ />
+
+ {/* Main content */}
+
+ {/* Tab Buttons for properties/drafts */}
+
+ setActiveTab('properties')}
+ >
+ Properties
+
+ setActiveTab('drafts')}
+ >
+ Drafts
+
+
+
+ {/* Loading State */}
+ {isLoading && }
+
+ {/* Error State */}
+ {!isLoading && isError && }
+
+ {/* Empty State */}
+ {!isLoading && !isError && currentData.length === 0 && (
+
+ )}
+
+ {/* Properties/Drafts List */}
+ {!isLoading && !isError && currentData.length > 0 && (
+ // Force remount of PropertyList when tab changes so any internal state is reset
+
+ )}
+
+
+
+
+ );
+};
+
+export default Properties;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/Citizen/PropertyDetails.tsx b/frontend/mobile-ui/src/app/pages/Citizen/PropertyDetails.tsx
new file mode 100644
index 0000000..9b0bfd0
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Citizen/PropertyDetails.tsx
@@ -0,0 +1,367 @@
+// PropertyDetails.tsx
+// This page displays detailed information for a single citizen property, including address, status, map, progress, and tabs for overview, plot info, history, and documents.
+// Features:
+// - Fetches property details via RTK Query using propertyId from route
+// - Shows address, status badge, and map location
+// - View location button navigates to map view
+// - Progress bar and tabbed content (overview, plot info, history, documents)
+// - Handles loading and error states
+// - Uses localization for all labels and messages
+// Used in: Citizen workflow for property review and details
+
+import React from 'react';
+import { Box, Typography, Chip, Button, Tabs, Tab, Paper } from '@mui/material';
+import { useNavigate, useParams, useLocation } from 'react-router-dom';
+
+import markerSvg from '../../../assets/CitizenAssets/property_page/location.svg';
+import { CottageOutlined } from '@mui/icons-material';
+
+import { useAppSelector } from '../../../redux/Hooks';
+import {
+ getMessagesFromSession,
+ useLocalization,
+} from '../../../services/Citizen/Localization/LocalizationContext';
+import Header from '../../components/Header';
+import NavBar from '../../components/NavBar';
+import PropertyMapSection from '../../features/Citizen/components/PropertyMapSection';
+import EnumOptComponent from '../../features/Citizen/components/EnumOptComponent';
+import OverviewCard from '../../features/Citizen/components/ui/OverviewCard';
+import PlotInfo from '../../features/Citizen/components/ui/PlotInfoCard';
+import History from '../../features/Citizen/components/ui/HistoryCard';
+import Documents from '../../features/Citizen/components/ui/DocumentsTab';
+import LoadingPage from '../../components/Loader';
+import { useGetCitizenPropertyByIdQuery } from '../../features/Citizen/api/CitizenPropertiesPageApi/CitizenPropertyPageApi';
+import type { CitizenPropertyData } from '../../features/Citizen/models/CitizenPropertiesPageModel/CitizenPropertyPageModel';
+
+// Replace Leaflet icon creation with a plain icon-like object (no Leaflet dependency)
+const customIcon = {
+ iconUrl: markerSvg,
+ iconSize: [40, 40] as [number, number],
+ iconAnchor: [20, 40] as [number, number],
+ popupAnchor: [0, -40] as [number, number],
+ className: '',
+};
+
+const statusStyles: Record = {
+ 'Under Enumeration': { bg: '#FFCDB6', color: '#000' },
+ Enumerated: { bg: '#85B9A1', color: '#fff' },
+ Draft: { bg: '#FFC107', color: '#000' },
+ Private: { bg: '#F8E8DB', color: '#D49C7A' },
+ Government: { bg: '#E8FAE6', color: '#53C56C' },
+};
+
+const STATUS_PROGRESS_MAP: Record = {
+ INITIATED: 20,
+ ASSIGNED: 40,
+ VERIFIED: 60,
+ AUDIT_VERIFIED: 80,
+ APPROVED: 100,
+};
+
+interface ExtendedPropertyData extends CitizenPropertyData {
+ enumerationProgress?: number;
+ isDraft?: boolean;
+ applicationStatus?: string;
+}
+
+const PropertyDetails: React.FC = () => {
+ const [tab, setTab] = React.useState(0);
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { propertyId } = useParams<{ propertyId: string }>();
+
+ // Get state passed from PropertyCard
+ const {
+ applicationId: passedAppId,
+ enumerationProgress: passedProgress,
+ isDraft,
+ applicationStatus,
+ } = location.state || {};
+
+ // Fetch property details using RTK Query
+ const { data, isLoading, isError } = useGetCitizenPropertyByIdQuery({
+ propertyId: propertyId || '',
+ });
+ const lang = useAppSelector((state) => state.lang.citizenLang);
+ const { loading: localizationLoading } = useLocalization();
+ const messages = getMessagesFromSession('CITIZEN')!;
+
+ // Show loader while data is being fetched
+ if (isLoading || localizationLoading) {
+ return ;
+ }
+
+ if (isError || !data?.success) {
+ return (
+
+
+ Property data not found.
+
+ navigate('/citizen/properties')}
+ >
+ Back to Properties
+
+
+ );
+ }
+
+ const property = data.data;
+ const address = [
+ property.Address.Street,
+ property.Address.Locality,
+ property.Address.WardNo,
+ property.Address.ZoneNo,
+ property.Address.BlockNo,
+ property.Address.PinCode,
+ ]
+ .filter(Boolean)
+ .join(', ');
+
+ // Use the enumeration progress from the passed state
+ // If not passed, fall back to mapping application status
+ let enumerationProgress = 20; // Default to INITIATED
+
+ if (passedProgress == undefined) {
+ enumerationProgress = passedProgress;
+ console.log(enumerationProgress);
+
+ } else {
+ // Fallback logic if state wasn't passed
+ if (isDraft) {
+ enumerationProgress = -1; // Draft
+ } else {
+ const status = applicationStatus?.toUpperCase() || 'INITIATED';
+ enumerationProgress = STATUS_PROGRESS_MAP[status] || 20;
+ console.log(status);
+
+ }
+ }
+
+ let status = 'Under Enumeration';
+ if (enumerationProgress === -1) {
+ status = 'Draft';
+ } else if (enumerationProgress === 100) {
+ status = 'Enumerated';
+ } else {
+ status = 'Under Enumeration'; // All other statuses (20%, 40%, 60%, 80%)
+ }
+
+ const badge = statusStyles[status] || { bg: '#F8E8DB', color: '#D49C7A' };
+
+ // Get coordinates from GISData
+ const latLng = property.GISData
+ ? { lat: property.GISData.Latitude || 0, lng: property.GISData.Longitude || 0 }
+ : { lat: 0, lng: 0 };
+
+ const extendedProperty: ExtendedPropertyData = {
+ ...property,
+ enumerationProgress,
+ isDraft,
+ applicationStatus,
+ };
+
+ return (
+
+ }
+ />
+
+
+
+
+
+ {property.PropertyNo || 'N/A'}
+
+
+
+
+
+
+ {messages['citizen.my-properties'][lang]['address']}
+
+
+ {address}
+
+
+
+
+
+
+ {/* View Location Button */}
+ {/*
+ }
+ sx={{
+ alignContent: 'center',
+ bgcolor: '#c84c0e',
+ color: '#ffffffff ',
+ borderRadius: '6px',
+ fontWeight: 500,
+ fontStyle: 'regular',
+ fontSize: 12,
+ textTransform: 'none',
+ p: '4px 10px',
+ minHeight: 20,
+ '&:hover': { bgcolor: '#d0f5d3' },
+ }}
+ onClick={() => {
+ localStorage.setItem('applicationId', passedAppId || '');
+ navigate('/citizen/application-logs/' + passedAppId);
+ }}
+ >
+ View Application Log
+
+ }
+ sx={{
+ alignContent: 'center',
+ bgcolor: '#DBFAD3',
+ color: '#000',
+ borderRadius: '10px',
+ fontWeight: 400,
+ fontStyle: 'regular',
+ fontSize: 12,
+ textTransform: 'none',
+ p: '4px 10px',
+ minHeight: 24,
+ '&:hover': { bgcolor: '#d0f5d3' },
+ }}
+ onClick={() =>
+ navigate('/citizen/property-location', {
+ state: {
+ property: extendedProperty,
+ },
+ })
+ }
+ >
+ {messages['citizen.commons'][lang]['view-location']}
+
+ */}
+
+ }
+ sx={{
+ alignContent: 'center',
+ bgcolor: '#c84c0e',
+ color: '#ffffffff ',
+ borderRadius: '6px',
+ fontWeight: 500,
+ fontStyle: 'regular',
+ fontSize: 12,
+ textTransform: 'none',
+ p: '4px 10px',
+ minHeight: 20,
+ '&:hover': { bgcolor: '#d0f5d3' },
+ }}
+ onClick={() => {
+ localStorage.setItem('applicationId', passedAppId || '');
+ navigate('/citizen/application-logs/' + passedAppId);
+ }}
+ >
+ View Application Log
+
+
+
+
+
+ setTab(val)}
+ variant="fullWidth"
+ TabIndicatorProps={{ style: { display: 'none' } }}
+ sx={{
+ bgcolor: '#fff',
+ borderRadius: '20px',
+ alignItems: 'center',
+ minHeight: 44,
+ mb: 1,
+ '& .MuiTab-root': {
+ fontWeight: 400,
+ color: '#000',
+ fontSize: 14,
+ textTransform: 'none',
+ borderRadius: '10px',
+ minHeight: 27,
+ '&.Mui-selected': {
+ width: 77,
+ color: '#000',
+ alignSelf: 'center',
+ height: 24,
+ bgcolor: '#FBEEE8',
+ },
+ },
+ }}
+ >
+
+
+
+
+
+
+
+ {tab === 0 && }
+ {tab === 1 && }
+ {tab === 2 && }
+ {tab === 3 && }
+
+
+ );
+};
+
+// Export PropertyDetails for use in citizen property details view
+export default PropertyDetails;
diff --git a/frontend/mobile-ui/src/app/pages/Citizen/PropertyLocationView.tsx b/frontend/mobile-ui/src/app/pages/Citizen/PropertyLocationView.tsx
new file mode 100644
index 0000000..947f0fd
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Citizen/PropertyLocationView.tsx
@@ -0,0 +1,120 @@
+// PropertyLocationView.tsx
+// This page displays the map location for a citizen's property, with address and navigation controls.
+// Features:
+// - Gets property data from route state
+// - Redirects back if no property data is present
+// - Shows address and map location using LocationMapWithDrawing
+// - Back button to return to previous page
+// Used in: Citizen workflow for viewing property location on a map
+
+import { useLocation, useNavigate } from 'react-router-dom';
+import '../../../styles/PropertyFormVerification.css';
+
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import LocationMapWithDrawing from '../../../components/LocationMapWithDrawing';
+import { useEffect } from 'react';
+
+const PropertyLocationView: React.FC = () => {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const property = location.state?.property as any;
+ const address = property
+ ? [
+ property.Address?.Street,
+ property.Address?.Locality,
+ property.Address?.WardNo,
+ property.Address?.ZoneNo,
+ property.Address?.BlockNo,
+ property.Address?.PinCode,
+ ]
+ .filter(Boolean)
+ .join(', ')
+ : '';
+
+ useEffect(() => {
+ if (!property) navigate(-1);
+ }, [property, navigate]);
+
+ if (!property) return null;
+
+ const coordinates = property.GISData || property.propertyDetails.GISData;
+ const lat = coordinates?.Latitude;
+ const lng = coordinates?.Longitude;
+
+ if (!lat || !lng) {
+ return (
+
+
+
Property Location
+
+ View Property Location
+
+
navigate(-1)}>
+
+
Previous
+
+
+
+
+
+
Error:
+
+ No location coordinates available for this property.
+
+
+
+
+
+ );
+ }
+
+ const handleBack = () => navigate(-1);
+
+ return (
+
+
+
Property Location
+
+ View Property Location
+
+
+
+
+
+
+
+
Address:
+
+ {property.locationData?.address || address || 'N/A'}
+
+
+
+
+
+ {
+ console.log('Location updated:', { lat, lng, address });
+ }}
+ />
+
+
+
+ );
+};
+
+export default PropertyLocationView;
diff --git a/frontend/mobile-ui/src/app/pages/LoginPage.tsx b/frontend/mobile-ui/src/app/pages/LoginPage.tsx
new file mode 100644
index 0000000..754388e
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/LoginPage.tsx
@@ -0,0 +1,673 @@
+// LoginPage.tsx
+// This component renders the login page for both agents and citizens.
+// Handles login logic, validation, localization, and UI switching between agent and citizen tabs.
+import { useEffect, useState } from 'react';
+import {
+ Box,
+ Typography,
+ TextField,
+ IconButton,
+ Paper,
+ Button,
+ // Alert,
+ Link,
+ Fade,
+} from '@mui/material';
+import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
+import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
+import LoginTabs from '../../components/Login-Screen/LoginTabs';
+import { PhoneLoginService } from '../../services/PhoneLoginService';
+import { useSignUpForm } from '../../context/SignUpFormContext';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import authService from '../../services/AuthService';
+import { LoginHeaderWithLanguage } from '../../components/Login-Screen/LoginHeaderWithLanguage';
+import { LanguageSettingsModal } from '../../components/Login-Screen/LanguageSettingsModal';
+import { useLoginLocalization } from '../../services/Citizen/Localization/useLocalization';
+import type { AlertType } from '../../app/models/AlertType.model';
+import '../../styles/PasswordField.css';
+import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
+import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
+import { NotificationPopup } from '../../app/components/Popup/NotificationPopup';
+
+const PRIMARY_ORANGE = '#C84C0E';
+const PAGE_BG = '#E5E5E5';
+const CONTAINER_BG = '#FFFFFF';
+
+// Helper to validate agent username format
+function isValidUsername(username: string) {
+ if (username.length < 2) return false;
+ return /^[A-Za-z][A-Za-z0-9._-]*[A-Za-z0-9]$/.test(username);
+}
+
+// Helper to validate Indian phone number format
+function isValidPhone(phone: string) {
+ return /^[6-9]\d{9}$/.test(phone);
+}
+
+// Props for LoginScreen component
+interface LoginFormProps {
+ onLoginSuccess: () => void;
+}
+
+/**
+ * LoginScreen component
+ * Renders login UI for agent and citizen, manages state, validation, and handles login logic.
+ */
+export default function LoginScreen({ onLoginSuccess }: LoginFormProps) {
+ // Set default login locale if not present
+ localStorage.setItem('loginLocale', localStorage.getItem('loginLocale') || 'en');
+ // State for language modal and selected language
+ const [langModalOpen, setLangModalOpen] = useState(false);
+ const [selectedLanguage, setSelectedLanguage] = useState(
+ localStorage.getItem('loginLocale') || 'en'
+ );
+
+ // Localization hook for login page
+ const { t } = useLoginLocalization(selectedLanguage);
+
+ // const [error, setError] = useState('');
+ // const [showAlert, setShowAlert] = useState(false);
+
+ // Context and navigation hooks
+ const { setUsername: setFormUsername, setOtp } = useSignUpForm();
+ const [searchParams, setSearchParams] = useSearchParams();
+ const initialRole = (searchParams.get('role')?.toLowerCase() === 'agent' ? 'agent' : 'citizen') as 'agent' | 'citizen';
+ const navigate = useNavigate();
+ const [loginType, setLoginType] = useState<'agent' | 'citizen'>(initialRole);
+ const [fadeKey, setFadeKey] = useState(0);
+
+
+
+ // State for agent login fields
+ const [agentUsername, setAgentUsername] = useState('');
+ const [showAgentUsernameError, setShowAgentUsernameError] = useState(false);
+ const [password, setPassword] = useState('');
+ const [showPassword, setShowPassword] = useState(false);
+
+ // State for citizen login fields
+ const [phoneNumber, setPhoneNumber] = useState('');
+ const [showPhoneError, setShowPhoneError] = useState(false);
+
+ // State for notification popup
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+
+
+ // Update URL search params when login type changes
+ useEffect(() => {
+ const popup = sessionStorage.getItem("signupSuccessMessage");
+ if (popup) {
+ setPopup({
+ type: 'success',
+ open: true,
+ title: 'Signup Successful',
+ message: popup,
+ duration: 3000,
+ });
+ sessionStorage.removeItem("signupSuccessMessage");
+ }
+
+
+ const roleParam = loginType === 'agent' ? 'Agent' : 'Citizen';
+ setSearchParams({ role: roleParam }, { replace: true });
+ }, [loginType, setSearchParams]);
+
+ // Helper to show error notification popup
+ function showErrorPopup(message: string, duration = 3000, title?: string,) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: title || 'Error Encountered!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ /**
+ * Handles agent login: validates input, calls authService, and navigates on success.
+ */
+ const handleAgentLogin = async () => {
+ if (agentUsername === '' || password === '') {
+ showErrorPopup('Please enter both username and password.', 3000, 'Incomplete Details');
+ return;
+ }
+ if (!isValidUsername(agentUsername)) {
+ setShowAgentUsernameError(true);
+ showErrorPopup(
+ 'Username must start with an alphabet and end with a letter/number.',
+ 3000,
+ 'Invalid Username'
+ );
+ return;
+ }
+ setShowAgentUsernameError(false);
+ try {
+ const credentials = {
+ username: agentUsername,
+ password: password,
+ };
+ const success = await authService.loginWithPassword(credentials);
+ if (success) {
+ // Store agent username in localStorage
+ localStorage.setItem('agentUsername', agentUsername);
+
+ sessionStorage.setItem('showWelcomePopup', 'true');
+ navigate('/agent');
+ onLoginSuccess();
+ } else {
+ showErrorPopup('Check User credentials!', 3000, 'Login Failed');
+ }
+ } catch {
+ showErrorPopup('An unexpected error occurred!', 3000, 'Error');
+ }
+ };
+
+ /**
+ * Handles citizen login: validates phone, sends OTP, and navigates to OTP verification.
+ */
+ const handleCitizenLogin = async () => {
+ if (!isValidPhone(phoneNumber)) {
+ setShowPhoneError(true);
+ showErrorPopup('Invalid Phone Number', 3000, 'Login failed');
+ return;
+ }
+ setShowPhoneError(false);
+ try {
+ const result = await PhoneLoginService.sendOtp(phoneNumber);
+ if (result.success) {
+ // Store citizen phone number in localStorage
+ sessionStorage.setItem('citizenPhoneNumber', phoneNumber);
+
+ setOtp('8472');
+ setFormUsername(result.data.users[0].username);
+ navigate('/otp-verification', { state: { phone: phoneNumber } });
+ } else {
+ showErrorPopup(t('login-failed-number', 'Login failed, Check your number.'), 3000, 'Number Not Registered');
+ }
+ } catch {
+ showErrorPopup(t('error-occurred', 'An error occurred. Please try again.'), 3000, 'Something went wrong.');
+ }
+ };
+
+ // Handler for switching between agent and citizen tabs
+ const handleTabSwitch = (tab: 'agent' | 'citizen') => {
+ setLoginType(tab);
+ setFadeKey(fadeKey + 1);
+ setShowAgentUsernameError(false);
+ setShowPhoneError(false);
+ setPassword('');
+ setPhoneNumber('');
+ setAgentUsername('');
+ };
+
+ // Render the login page UI
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+ setLangModalOpen(true), t }}
+ />
+ setLangModalOpen(false)}
+ onConfirm={() => setLangModalOpen(false)}
+ />
+
+
+
+
+
+
+
+
+
+ {t('user-name', 'User Name')}
+
+
+ *
+
+
+
+
+
+ {
+ setAgentUsername(e.target.value);
+ setShowAgentUsernameError(false);
+ }}
+ onBlur={(e) => {
+ if (e.target.value && !isValidUsername(e.target.value)) {
+ setShowAgentUsernameError(true);
+ }
+ }}
+ error={showAgentUsernameError}
+ />
+
+ {showAgentUsernameError
+ ? t(
+ 'user-name-condition',
+ 'Username must start with an alphabet and end with a letter/number.'
+ )
+ : t('enter-username-to-login', 'Enter username to login')}
+
+
+
+
+
+
+ {t('password', 'Password')}
+
+
+ *
+
+
+ {/* setPassword(e.target.value)}
+ /> */}
+
+ setPassword(e.target.value)}
+ className="pass-reveal"
+ style={{
+ width: '100%',
+ minHeight: '48px',
+ padding: '8.5px 40px 8.5px 14px',
+ fontFamily: 'Roboto, sans-serif',
+ fontSize: '14px',
+ borderRadius: '10px',
+ border: '1px solid rgba(0, 0, 0, 0.23)',
+ outline: 'none',
+ boxSizing: 'border-box',
+ transition: 'border-color 0.2s',
+ }}
+ onFocus={(e) => {
+ e.target.style.borderColor = '#C84C0E';
+ }}
+ onBlur={(e) => {
+ e.target.style.borderColor = 'rgba(0, 0, 0, 0.23)';
+ }}
+ />
+ setShowPassword(!showPassword)}
+ style={{
+ position: 'absolute',
+ right: '10px',
+ top: '50%',
+ transform: 'translateY(-50%)',
+ background: 'none',
+ border: 'none',
+ cursor: 'pointer',
+ padding: '4px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ color: 'rgba(0, 0, 0, 0.54)',
+ }}
+ >
+ {showPassword ? (
+ // Eye open icon (password visible)
+
+ ) : (
+ // Eye closed icon (password hidden)
+
+ )}
+
+
+
+
+ {t('forgot-password', 'Forgot Password')}
+
+
+
+
+
+
+ {/* {t('verify', 'Verify')} */}
+ Login
+
+
+
+
+
+
+
+
+
+
+ {t('phone-number', 'Phone Number')}
+
+
+ *
+
+
+
+
+
+ {
+ const val = e.target.value.replace(/\D/g, '');
+ setPhoneNumber(val);
+ setShowPhoneError(false);
+ }}
+ onBlur={(e) => {
+ if (!isValidPhone(e.target.value)) {
+ setShowPhoneError(true);
+ } else {
+ setShowPhoneError(false);
+ }
+ }}
+ error={showPhoneError}
+ />
+
+ {showPhoneError
+ ? t(
+ 'enter-valid-number',
+ 'Please enter a valid 10-digit Indian phone number'
+ )
+ : t(
+ 'enter-phone-number-used-to-register',
+ 'Enter Phone Number used to register'
+ )}
+
+
+
+ {/* {showAlert && (
+
+
+
+ }
+ severity="warning"
+ sx={{
+ background: '#FFEFD8',
+ color: '#444',
+ mt: 2,
+ mb: 2,
+ fontFamily: 'Roboto, sans-serif',
+ borderRadius: '12px',
+ px: 2,
+ py: 1,
+ fontSize: 15,
+ alignItems: 'center',
+ }}
+ >
+ {error ? error : 'Login failed, Check your number.'}
+
+ )} */}
+
+
+
+ {t('verify', 'Verify')}
+
+
+ navigate('/register')}
+ sx={{
+ color: '#000000',
+ fontFamily: 'Roboto, sans-serif',
+ textTransform: 'none',
+ fontSize: 12,
+ justifyContent: 'center',
+ alignItems: 'center',
+ display: 'flex',
+ mt: 1.5,
+ }}
+ endIcon={ }
+ >
+
+ {t('register-as-new-user', 'Register as New User')}
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/mobile-ui/src/app/pages/Profile.tsx b/frontend/mobile-ui/src/app/pages/Profile.tsx
new file mode 100644
index 0000000..e3a7034
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/Profile.tsx
@@ -0,0 +1,107 @@
+// Profile.tsx
+// This component renders the user profile page for both agents and citizens.
+// Displays profile info, notification settings, support, and logout functionality.
+import { type FC } from 'react';
+import { Logout } from '@mui/icons-material';
+import { Box, IconButton, Typography } from '@mui/material';
+import { useLocation, useNavigate } from 'react-router-dom';
+import { useAuth } from '../../context/AuthProvider';
+import { getAppLocale } from '../../services/Profile/ProfileService';
+import { getMessagesFromSession } from '../../services/Citizen/Localization/LocalizationContext';
+import NavHeader from '../components/NavHeader';
+import ProfileCard from '../components/ProfileCard';
+import NotificationSettings from '../components/NotificationCard';
+import SupportHelp from '../components/SupportCard';
+import authService from '../../services/AuthService';
+
+/**
+ * ProfilePage component
+ * Renders the profile page, showing user info, notification settings, support (for citizens), and logout button.
+ */
+const ProfilePage: FC = () => {
+ // Get logout function from auth context
+ const { logout } = useAuth();
+
+ // Determine user role (agent or citizen)
+ const role = authService.isAgent() ? 'AGENT' : 'CITIZEN';
+
+ // Boolean flag for citizen profile
+ const isCitizenProfile: boolean = authService.isCitizen();
+
+ // Get navigation state for property/license counts
+ const location = useLocation();
+ const noOfProperties = location.state?.noOfProperties ?? 0;
+ const noActiveLicenses = location.state?.noActiveLicenses ?? 0;
+
+ // Get current language and localized messages
+ const lang = getAppLocale();
+ const messages = getMessagesFromSession(isCitizenProfile ? 'CITIZEN' : 'AGENT')!;
+ const navigate = useNavigate();
+
+ // Localized label for logout button
+ const logOutLabel = messages['profile'][lang]['log-out'];
+
+ // Render the profile page UI
+ return (
+
+
+ {/* Profile card with user info and stats */}
+
+
+ {/* Notification settings card */}
+
+
+ {/* Support/help card for citizens only */}
+ {isCitizenProfile &&
}
+
+ {/* Logout button */}
+
{
+ logout();
+ navigate('/login');
+ }}
+ >
+
+ {logOutLabel}
+ {/* Log Out */}
+
+
+
+
+
+
+ );
+};
+
+export default ProfilePage;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/ApplicationLog.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/ApplicationLog.tsx
new file mode 100644
index 0000000..9aa9d1a
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/ApplicationLog.tsx
@@ -0,0 +1,265 @@
+// ApplicationLog.tsx
+// Property Form Application Log page
+// Displays a timeline of application events, comments, and status updates for a property
+// Features:
+// - Shows mock log data (replaceable with backend fetch)
+// - Localized UI using useApplicationLogLocalization
+// - Navigation to Add Request/Comment, Verification, and Home
+// - Timeline UI with error, agent, and admin highlights
+// - Downloadable files and extra details for some log items
+// Used in: Property form workflow for tracking application progress and history
+
+import React, { useEffect, useState } from 'react';
+import { ArrowBackIosNew, Download } from '@mui/icons-material';
+import DescriptionIcon from '@mui/icons-material/Description';
+import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined';
+import VisibilityIcon from '@mui/icons-material/Visibility';
+import '../../../styles/ApplicationLog.css';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useApplicationLogLocalization } from '../../../services/AgentLocalisation/localisation-applicationLog';
+import { ApplicationApi } from '../../../redux/apis/ApplicationLog/getApplication';
+
+// LogItem: structure for each timeline event
+interface LogItem {
+ id: number;
+ performedBy: string;
+ actor: string;
+ date?: string;
+ title: string;
+ // description?: string;
+ files?: { name: string; url: string }[];
+ extra?: React.ReactNode;
+ isError?: boolean;
+ isAgent?: boolean;
+ isAdmin?: boolean;
+}
+
+// Mock data generator function (to be called with localized text)
+const mapLogData = (data: any[]): LogItem[] =>
+ [...data]
+ .sort((a, b) => new Date(a.PerformedDate).getTime() - new Date(b.PerformedDate).getTime())
+ .map((item, idx) => ({
+ id: idx + 1,
+ actor: item.Actor,
+ title: item.Comments,
+ Comments: item.Comments,
+ performedBy: item.PerformedBy,
+ date: item.PerformedDate,
+ }));
+
+export const ApplicationLog: React.FC = () => {
+ // State for log items and loading indicator
+ const [logItems, setLogItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+ // Navigation and route params
+ const navigate = useNavigate();
+ const { propertyId } = useParams();
+
+ const applicationId = localStorage.getItem('applicationId');
+ // Use RTK Query hook to fetch application data
+ const { data } = ApplicationApi.useGetApplicationByPropertyIdQuery(
+ applicationId ?? '',
+ { refetchOnMountOrArgChange: true }
+ );
+ console.log('Application data:', data);
+ const applicationLogs = data?.data.ApplicationLogs || [];
+
+ // Localization
+ const loc = useApplicationLogLocalization();
+
+ const handleAddRequest = () => {
+ if (propertyId) {
+ navigate(`/agent/add-comment/${propertyId}`);
+ } else {
+ navigate('/agent/add-comment');
+ }
+ };
+
+ const handlePrevious = () => {
+ navigate(-1);
+ };
+
+ const handleHome = () => {
+ navigate('/agent');
+ };
+
+ useEffect(() => {
+ const mapped = mapLogData(applicationLogs);
+ setLogItems(mapped);
+ setLoading(false);
+
+ // Log each log item with labels
+ mapped.forEach((item, idx) => {
+ console.log(`Log Item #${idx + 1}:`);
+ console.log(` Actor: ${item.actor}`);
+ console.log(` Performed By: ${item.performedBy}`);
+ console.log(` Comments: ${item.title}`);
+ console.log(` Date: ${item.date}`);
+ // Add more fields if needed
+ });
+ }, [applicationLogs]);
+ // Check if any log item actor is 'citizen'
+ const isCitizenActor = logItems.some(item => item.actor?.toUpperCase() === 'CITIZEN');
+
+ return (
+
+
+
+
+
+ {loc.applicationLogTitle}
+
+
+ {loc.commentHistorySubtitle}{' '}
+ {loc.viewedByApplicantText}
+
+
+
+
+
+ {loc.previousText}
+
+
+
+ {loc.homeText}
+
+
+
+
+
+
+ {loading && (
+
+ โป {loc.loadingText}
+
+ )}
+ {!loading && (
+ <>
+ {logItems.map((item, idx) => {
+ // Find the corresponding log object for this item
+ const logObj = applicationLogs.find(
+ (log) =>
+ log.Actor === item.actor &&
+ log.PerformedBy === item.performedBy &&
+ log.Comments === item.title &&
+ log.PerformedDate === item.date
+ );
+ return (
+
+
+
{idx + 1}
+
+
+ {item.actor}
+
+
{item.performedBy}
+ {item.title &&
{item.title}
}
+ {logObj?.FileStoreID && (
+
+
+ {/* Show file name if present */}
+ {(() => {
+ let fileName = 'Document';
+ if (logObj?.Metadata) {
+ try {
+ const meta = JSON.parse(logObj.Metadata);
+ if (meta?.file?.name) fileName = meta.file.name;
+ } catch (e) {
+ console.error('Error parsing metadata JSON:', e);
+ }
+ }
+ return (
+ fileName && (
+
+
+
+
+ {fileName}
+
+ )
+ );
+ })()}
+
+
+ )}
+ {/* Download card if FileStoreID is present for this log */}
+ {logObj?.FileStoreID && (
+
+ {isCitizenActor ? (
+ {
+ const url = `${
+ import.meta.env.VITE_FILESTORE_HOST
+ }/filestore/v1/files/${
+ logObj.FileStoreID
+ }?tenantId=pg`;
+ window.open(url, '_blank');
+ }}
+ >
+ View Doc
+
+ ) : (
+ {
+ const url = `${
+ import.meta.env.VITE_FILESTORE_HOST
+ }/filestore/v1/files/${
+ logObj.FileStoreID
+ }?tenantId=pg`;
+ window.open(url, '_blank');
+ }}
+ >
+ Download Doc
+
+ )}
+
+ )}
+ {item.date && (
+
+ {new Date(item.date).toLocaleDateString()}
+
+ )}
+ {item.extra}
+
+
+
+ );
+ })}
+ >
+ )}
+
+ {/* Add Request button below the timeline box, not inside it */}
+
+ {!isCitizenActor && (
+
+ {loc.addRequestText}
+
+ )}
+
+
+
+ );
+};
+
+export default ApplicationLog;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/AssessmentDetails.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/AssessmentDetails.tsx
new file mode 100644
index 0000000..358f134
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/AssessmentDetails.tsx
@@ -0,0 +1,706 @@
+// AssessmentDetails.tsx
+// Property Form Assessment Details page
+// Collects and manages assessment details for a property, including reason, certificate info, site extent, and land share
+// Features:
+// - Form for entering assessment details (reason, certificate number/date, extent, land share)
+// - Uses localization for labels and error messages
+// - Fetches and updates assessment details via RTK Query
+// - Handles form validation, error popups, and draft saving
+// - Dynamic dropdowns and custom input components
+// - Responsive UI with MUI components
+// Used in: Property form workflow for property assessment step
+
+
+import React, { useEffect, useState, useRef } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import Button from '@mui/material/Button';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Checkbox from '@mui/material/Checkbox';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+import dayjs from 'dayjs';
+import { useNavigate } from 'react-router-dom';
+import { useFormMode } from '../../../context/FormModeContext';
+import JsonService, {
+ getUnitOfMeasurementOptions,
+} from '../../../services/jsonServerApiCalls';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import { useAssessmentDetailsLocalization } from '../../../services/AgentLocalisation/localisation-AssessmentDetails';
+import CalendarIcon from '../../assets/Agent/date_range.svg';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import CustomDropdown from '../../features/PropertyForm/components/IGRSDetail/IGRSdropdown';
+import type { DropdownOption } from '../../features/PropertyForm/components/IGRSDetail/IGRSdropdown';
+import FormTextField from '../../features/PropertyForm/components/IGRSDetail/IGRSFormTextFiled';
+import { uniformInputSx, verifyButtonSx } from './styles/sharedStyles';
+import type { AlertType } from '../../models/AlertType.model';
+import {
+ useCreateAssessmentDetailsMutation,
+ useUpdateAssessmentDetailsMutation,
+} from '../../../redux/apis/AssessmentDetails.api';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+type LocalState = {
+ reason: string;
+ occupancyCertificateNumber: string;
+ occupancyCertificateDate: string;
+ extentOfSite: string;
+ landUnderBuilding: string;
+ isUnspecifiedShare: boolean;
+};
+
+const containerStyle = {
+ width: '100%',
+ margin: '0 auto',
+ minHeight: '100vh',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ bgcolor: '#fff',
+};
+
+const headerStyle = {
+ backgroundColor: '#F9E6E0',
+ padding: '16px',
+};
+
+const formContentStyle = {
+ flex: 1,
+ px: '8%',
+ py: 3,
+ backgroundColor: '#FFFFFF',
+};
+
+const formSubmitSx = {
+ padding: '16px 0',
+ backgroundColor: '#FFFFFF',
+};
+
+const labelSx = {
+ fontSize: 14,
+ fontWeight: 400,
+ color: '#333333',
+ textAlign: 'left' as const,
+};
+
+const AssessmentDetails: React.FC = () => {
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const { formData, updateForm } = usePropertyForm();
+
+ const propertyId = formData.id ?? '';
+
+ const [createAssessmentDetails] = useCreateAssessmentDetailsMutation();
+ const [updateAssessmentDetails] = useUpdateAssessmentDetailsMutation();
+
+ const {
+ reasonCreationLabel,
+ occupancyCertificateNumberLabel,
+ occupancyCertificateDateLabel,
+ unspecifiedShareLabel,
+ selectOption,
+ // draftSavedAlert,
+ reasonRequiredError,
+ certificateNumberRequiredError,
+ certificateDateRequiredError,
+ extentSiteRequiredError,
+ positiveNumberRequiredError,
+ landUnderneathRequiredError,
+ reasonOptions: reasonTranslations,
+ // propertyFormTitle,
+ // newPropertyFormTitle,
+ assessmentDetailsSubtitle,
+ previousText,
+ saveDraftText,
+ enterExtentSitePlaceholder,
+ enterLandUnderneathPlaceholder,
+ } = useAssessmentDetailsLocalization();
+
+ const { nextButtonText, onlyNumbersAreAllowedMSG } = useLocalization();
+
+ const [localData, setLocalData] = useState({
+ reason: formData.assessmentDetails?.ReasonOfCreation || '',
+ occupancyCertificateNumber:
+ formData.assessmentDetails?.OccupancyCertificateNumber || '',
+ occupancyCertificateDate: formData.assessmentDetails?.OccupancyCertificateDate || '',
+ extentOfSite: formData.assessmentDetails?.ExtentOfSite || '',
+ landUnderBuilding:
+ formData.assessmentDetails?.IsLandUnderneathBuilding?.toString() || '',
+ isUnspecifiedShare: formData.assessmentDetails?.IsUnspecifiedShare || false,
+ });
+
+ const [reasonsFromJson, setReasonsFromJson] = useState([]);
+ const [reasons, setReasons] = useState([]);
+ const [showReasonDropdown, setShowReasonDropdown] = useState(false);
+
+ // Error and touched state
+ const [errors, setErrors] = useState>({
+ reason: '',
+ occupancyCertificateNumber: '',
+ occupancyCertificateDate: '',
+ extentOfSite: '',
+ landUnderBuilding: '',
+ isUnspecifiedShare: '',
+ } as Record);
+
+ const [touched, setTouched] = useState>({
+ reason: false,
+ occupancyCertificateNumber: false,
+ occupancyCertificateDate: false,
+ extentOfSite: false,
+ landUnderBuilding: false,
+ isUnspecifiedShare: false,
+ });
+
+ // OnlyNumbers warnings (for positive number fields)
+ const [showNumberWarnings, setShowNumberWarnings] = useState>({
+ extentOfSite: false,
+ landUnderBuilding: false,
+ });
+
+ // close other dropdowns helper
+ const closeAllDropdowns = () => {
+ setShowReasonDropdown(false);
+ };
+
+ // unit of measurement MDMS
+ const [unitOfMeasurement, setUnitOfMeasurement] = useState('');
+
+ useEffect(() => {
+ getUnitOfMeasurementOptions()
+ .then((data) => {
+ setUnitOfMeasurement(data?.unitOfmeasurement || '');
+ })
+ .catch((e) => {
+ console.log(e);
+ });
+ }, []);
+
+ useEffect(() => {
+ JsonService.getReasons()
+ .then((data) => {
+ const jsonReasons: DropdownOption[] = (data || []).map((item: any) => ({
+ id: Number(item.id),
+ label: item.label,
+ }));
+ setReasonsFromJson(jsonReasons);
+
+ if (reasonTranslations && reasonTranslations.length > 0) {
+ const translated = jsonReasons.map((r, idx) => ({
+ id: r.id,
+ label: reasonTranslations[idx] || r.label,
+ }));
+ setReasons(translated);
+ } else {
+ setReasons(jsonReasons);
+ }
+ })
+ .catch(() => {
+ setReasons([]);
+ });
+ }, [reasonTranslations]);
+
+ useEffect(() => {
+ if (
+ reasonTranslations &&
+ reasonTranslations.length > 0 &&
+ reasonsFromJson.length > 0
+ ) {
+ const translated = reasonsFromJson.map((r, idx) => ({
+ id: r.id,
+ label: reasonTranslations[idx] || r.label,
+ }));
+ setReasons(translated);
+ } else if (reasonsFromJson.length > 0) {
+ setReasons(reasonsFromJson);
+ }
+ }, [reasonTranslations, reasonsFromJson]);
+
+ // click outside detection for reason dropdown
+ const reasonDropdownRef = useRef(null);
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (
+ reasonDropdownRef.current &&
+ !reasonDropdownRef.current.contains(event.target as Node)
+ ) {
+ setShowReasonDropdown(false);
+ }
+ }
+ if (showReasonDropdown) {
+ document.addEventListener('mousedown', handleClickOutside);
+ }
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, [showReasonDropdown]);
+
+ function validateField(field: string, value: string) {
+ if (field === 'reason') return !value ? reasonRequiredError : '';
+ if (field === 'occupancyCertificateNumber') {
+ if (!value.trim()) return certificateNumberRequiredError;
+ const ocRegex = /^OC-\d{4}-\d{3}(-REVISED)?$/;
+ if (!ocRegex.test(value.trim()))
+ return 'Invalid format. Example: OC-0000-000';
+ return '';
+ }
+ if (field === 'extentOfSite' || field === 'landUnderBuilding') {
+ if (!value || value.trim() === '')
+ return field === 'extentOfSite'
+ ? extentSiteRequiredError
+ : landUnderneathRequiredError;
+ if (!/^\d*\.?\d*$/.test(value)) return onlyNumbersAreAllowedMSG;
+ if (value === '.') return 'Must contain at least one digit';
+ const num = Number(value);
+ if (isNaN(num) || num <= 0) return positiveNumberRequiredError;
+ return '';
+ }
+ if (field === 'occupancyCertificateDate') {
+ if (!value) return certificateDateRequiredError;
+ return '';
+ }
+ return '';
+ }
+
+ // Number keydown block ('-', 'e', '+')
+ const handleNumberKeyDown = (e: React.KeyboardEvent) => {
+ if (['-', 'e', '+'].includes(e.key)) e.preventDefault();
+ };
+
+ const handleFieldChange = (field: keyof LocalState) => (value: string) => {
+ if (field === 'extentOfSite' || field === 'landUnderBuilding') {
+ // Only allow digits and one dot for float
+ const isValid = /^\d*\.?\d*$/.test(value);
+ let sanitized = value.replace(/[^0-9.]/g, '');
+ // Only allow one dot
+ const parts = sanitized.split('.');
+ if (parts.length > 2) sanitized = parts[0] + '.' + parts.slice(1).join('');
+ if (!isValid && value !== '') {
+ setShowNumberWarnings((prev) => ({ ...prev, [field]: true }));
+ return;
+ }
+ setLocalData((prev) => ({ ...prev, [field]: sanitized }));
+ if (showNumberWarnings[field]) {
+ if (/^\d*\.?\d*$/.test(sanitized) || sanitized === '')
+ setShowNumberWarnings((prev) => ({ ...prev, [field]: false }));
+ }
+ if (touched[field]) setErrors((prev) => ({ ...prev, [field]: '' }));
+ } else if (field === 'occupancyCertificateNumber') {
+ // Accept only uppercase letters, digits and dash
+ const partialOcRegex = /^[A-Z0-9\-]*$/;
+ const ocRegex = /^OC-\d{4}-\d{3}(-REVISED)?$/;
+ if (!partialOcRegex.test(value) && value !== '') {
+ setErrors((prev) => ({
+ ...prev,
+ [field]: 'Only uppercase letters, digits and dash (-) allowed.',
+ }));
+ setTouched((prev) => ({ ...prev, [field]: true }));
+ return;
+ }
+ // Check full format as user types
+ let errorMsg = '';
+ if (value && !ocRegex.test(value.trim())) {
+ errorMsg = 'Invalid format. Example: OC-0000-000';
+ }
+ setLocalData((prev) => ({ ...prev, [field]: value }));
+ setErrors((prev) => ({ ...prev, [field]: errorMsg }));
+ setTouched((prev) => ({ ...prev, [field]: true }));
+ } else {
+ setLocalData((prev) => ({ ...prev, [field]: value }));
+ setTouched((prev) => ({ ...prev, [field]: true }));
+ setErrors((prev) => ({ ...prev, [field]: validateField(field as string, value) }));
+ }
+ };
+
+ // --- LEADING ZEROES LOGIC ON BLUR ---
+ const handleBlur = (field: keyof LocalState) => {
+ let newValue = localData[field];
+
+ // Remove leading zeros for positive number fields on blur
+ if (
+ (field === 'extentOfSite' || field === 'landUnderBuilding') &&
+ typeof newValue === 'string' &&
+ newValue !== ''
+ ) {
+ // '006.3' => '6.3', '007' => '7', '000' => '0', '.7' untouched
+ if (/^\d+(\.\d*)?$/.test(newValue)) {
+ const parts = newValue.split('.');
+ parts[0] = String(Number(parts[0]));
+ newValue = parts.length > 1 ? parts.join('.') : parts[0];
+ }
+ setLocalData((prev) => ({ ...prev, [field]: newValue }));
+ }
+ setTouched((prev) => ({ ...prev, [field]: true }));
+ setErrors((prev) => ({
+ ...prev,
+ [field]: validateField(
+ field as string,
+ typeof newValue === 'string' ? newValue : ''
+ ),
+ }));
+ };
+
+ const handleCheckboxChange = (
+ _event: React.ChangeEvent,
+ checked: boolean
+ ) => {
+ setLocalData((prev) => ({ ...prev, isUnspecifiedShare: checked }));
+ };
+
+ const handleReasonSelect = (label: string) => {
+ setLocalData((prev) => ({ ...prev, reason: label }));
+ setErrors((prev) => ({ ...prev, reason: '' }));
+ setTouched((prev) => ({ ...prev, reason: true }));
+ setShowReasonDropdown(false);
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const validation = {
+ reason: validateField('reason', localData.reason),
+ occupancyCertificateNumber: validateField(
+ 'occupancyCertificateNumber',
+ localData.occupancyCertificateNumber
+ ),
+ occupancyCertificateDate: validateField(
+ 'occupancyCertificateDate',
+ localData.occupancyCertificateDate
+ ),
+ extentOfSite: validateField('extentOfSite', localData.extentOfSite),
+ landUnderBuilding: validateField('landUnderBuilding', localData.landUnderBuilding),
+ isUnspecifiedShare: '',
+ };
+
+ setErrors(validation as any);
+ setTouched({
+ reason: true,
+ occupancyCertificateNumber: true,
+ occupancyCertificateDate: true,
+ extentOfSite: true,
+ landUnderBuilding: true,
+ isUnspecifiedShare: true,
+ });
+
+ if (Object.values(validation).some(Boolean)) return;
+
+ const requestBody = {
+ reasonOfCreation: localData.reason,
+ occupancyCertificateNumber: localData.occupancyCertificateNumber,
+ occupancyCertificateDate: localData.occupancyCertificateDate,
+ extentOfSite: localData.extentOfSite,
+ isLandUnderneathBuilding: localData.landUnderBuilding,
+ isUnspecifiedShare: localData.isUnspecifiedShare,
+ propertyId,
+ };
+
+ try {
+ let resp;
+ if (formData.assessmentDetails?.ID) {
+ resp = await updateAssessmentDetails({
+ id: formData.assessmentDetails.ID,
+ body: requestBody,
+ }).unwrap();
+ } else {
+ resp = await createAssessmentDetails(requestBody).unwrap();
+ }
+
+ if (resp) {
+ updateForm({
+ assessmentDetails: {
+ ID: resp?.data.ID,
+ ReasonOfCreation: resp?.data.ReasonOfCreation,
+ OccupancyCertificateNumber: resp?.data.OccupancyCertificateNumber,
+ OccupancyCertificateDate: resp?.data.OccupancyCertificateDate,
+ ExtentOfSite: resp?.data.ExtentOfSite,
+ IsLandUnderneathBuilding: resp?.data.IsLandUnderneathBuilding,
+ IsUnspecifiedShare: resp?.data.IsUnspecifiedShare,
+ },
+ });
+ }
+
+ // Optionally, show success popup or navigate
+ navigate('/property-form/igrs-details');
+ } catch (error) {
+ showErrorPopup('Failed to save assessment details.');
+ }
+ };
+
+ useEffect(() => {
+ if (formData.assessmentDetails) {
+ setLocalData({
+ reason: formData.assessmentDetails?.ReasonOfCreation || '',
+ occupancyCertificateNumber:
+ formData.assessmentDetails?.OccupancyCertificateNumber || '',
+ occupancyCertificateDate:
+ formData.assessmentDetails?.OccupancyCertificateDate || '',
+ extentOfSite: formData.assessmentDetails?.ExtentOfSite || '',
+ landUnderBuilding:
+ formData.assessmentDetails?.IsLandUnderneathBuilding?.toString() || '',
+ isUnspecifiedShare: formData.assessmentDetails?.IsUnspecifiedShare || false,
+ });
+ }
+ }, [formData.assessmentDetails]);
+
+ const handleGoBack = () => navigate(-1);
+
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ const handleSaveDraft = () => {
+ updateForm({
+ assessmentDetails: {
+ ReasonOfCreation: localData.reason,
+ OccupancyCertificateNumber: localData.occupancyCertificateNumber,
+ OccupancyCertificateDate: localData.occupancyCertificateDate,
+ ExtentOfSite: localData.extentOfSite.toString(),
+ IsLandUnderneathBuilding: localData.landUnderBuilding || '',
+ IsUnspecifiedShare: localData.isUnspecifiedShare,
+ },
+ });
+ };
+
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
+
+
+
+ handleReasonSelect(value)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectOption}
+ required
+ error={errors.reason}
+ touched={touched.reason}
+ onBlur={() => handleBlur('reason')}
+ />
+
+ handleBlur('occupancyCertificateNumber')}
+ placeholder={occupancyCertificateNumberLabel}
+ type="text"
+ required
+ error={
+ touched.occupancyCertificateNumber
+ ? errors.occupancyCertificateNumber
+ : ''
+ }
+ touched={touched.occupancyCertificateNumber}
+ />
+
+
+
+ {occupancyCertificateDateLabel}
+ *
+ :
+
+ {
+ const value = date ? dayjs(date).format('YYYY-MM-DD') : '';
+ setLocalData((prev) => ({
+ ...prev,
+ occupancyCertificateDate: value,
+ }));
+ setTouched((prev) => ({ ...prev, occupancyCertificateDate: true }));
+ setErrors((prev) => ({
+ ...prev,
+ occupancyCertificateDate: validateField(
+ 'occupancyCertificateDate',
+ value
+ ),
+ }));
+ }}
+ disableFuture
+ format="DD/MM/YYYY"
+ slots={{
+ openPickerIcon: () => (
+
+ ),
+ }}
+ slotProps={{
+ textField: {
+ required: true,
+ placeholder: occupancyCertificateDateLabel,
+ error:
+ touched.occupancyCertificateDate &&
+ !!errors.occupancyCertificateDate,
+ helperText: touched.occupancyCertificateDate
+ ? errors.occupancyCertificateDate
+ : '',
+ onBlur: () => {
+ setTouched((prev) => ({
+ ...prev,
+ occupancyCertificateDate: true,
+ }));
+ setErrors((prev) => ({
+ ...prev,
+ occupancyCertificateDate: validateField(
+ 'occupancyCertificateDate',
+ localData.occupancyCertificateDate
+ ),
+ }));
+ },
+ inputProps: {
+ style: { marginTop: '-8px' },
+ },
+ sx: {
+ width: '100%',
+ '& .MuiOutlinedInput-root': {
+ borderRadius: '12px',
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderRadius: '12px',
+ },
+ },
+ '& .MuiOutlinedInput-input': {
+ padding: '12px 14px',
+ },
+ },
+ FormHelperTextProps: {
+ sx: {
+ color: '#D32F2F',
+ marginTop: '8px',
+ minHeight: '20px',
+ },
+ },
+ },
+ actionBar: { actions: ['clear', 'accept'] },
+ }}
+ maxDate={dayjs()}
+ />
+
+
+ handleBlur('extentOfSite')}
+ placeholder={enterExtentSitePlaceholder}
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.extentOfSite
+ ? onlyNumbersAreAllowedMSG
+ : errors.extentOfSite
+ }
+ touched={touched.extentOfSite || showNumberWarnings.extentOfSite}
+ />
+
+ handleBlur('landUnderBuilding')}
+ placeholder={enterLandUnderneathPlaceholder}
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.landUnderBuilding
+ ? onlyNumbersAreAllowedMSG
+ : errors.landUnderBuilding
+ }
+ touched={
+ touched.landUnderBuilding || showNumberWarnings.landUnderBuilding
+ }
+ />
+
+
+ handleCheckboxChange(_e as any, checked)}
+ />
+ }
+ label={unspecifiedShareLabel}
+ />
+
+
+
+
+ {mode === 'verify' ? 'Verify' : nextButtonText}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default AssessmentDetails;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/ConstructionDetailsPage.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/ConstructionDetailsPage.tsx
new file mode 100644
index 0000000..65f1bd7
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/ConstructionDetailsPage.tsx
@@ -0,0 +1,472 @@
+// ConstructionDetailsPage.tsx
+// Property Form Construction Details page
+// Collects and manages construction details for a property (floor, roof, wall, wood types)
+// Features:
+// - Form for entering construction details using dropdowns
+// - Fetches and updates construction details via RTK Query
+// - Uses localization for labels and dropdown options
+// - Handles form validation, error popups, and draft saving
+// - Responsive UI with MUI components and custom dropdowns
+// Used in: Property form workflow for construction details step
+import React, { useEffect, useState } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Button from '@mui/material/Button';
+import { useNavigate } from 'react-router-dom';
+import { useFormMode } from '../../../context/FormModeContext';
+import JsonService from '../../../services/jsonServerApiCalls';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useAssessmentDetailsLocalization } from '../../../services/AgentLocalisation/localisation-AssessmentDetails';
+import CustomDropdown from '../../features/PropertyForm/components/ConstructionDetail/ConstructionDropdown';
+import type { DropdownOption } from '../../features/PropertyForm/components/ConstructionDetail/ConstructionDropdown';
+import { verifyButtonSx } from './styles/sharedStyles';
+import type { AlertType } from '../../models/AlertType.model';
+import {
+ useCreateConstructionDetailsMutation,
+ useUpdateConstructionDetailsMutation,
+ useLazyGetConstructionDetailsByPropertyIdQuery,
+ type ConstructionDetailsRequest,
+} from '../../../redux/apis/contructionAPI';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+// Inline styles for layout and UI
+const containerStyle = {
+ width: '100%',
+ margin: '0',
+ minHeight: '100vh',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ bgcolor: '#fff',
+};
+
+const headerStyle = { backgroundColor: '#F9E6E0' };
+const formContentStyle = { flex: 1, padding: '24px', backgroundColor: '#FFFFFF' };
+const formFieldWrapper = { marginBottom: -1.5 };
+const formSubmitStyle = { padding: '16px 0', backgroundColor: '#FFFFFF' };
+
+type LocalData = {
+ floorType: string;
+ roofType: string;
+ wallType: string;
+ woodType: string;
+ constructionId: string;
+};
+
+const ConstructionDetailsPage: React.FC = () => {
+ // Contexts and hooks for form mode, navigation, and property form data
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const { formData, updateForm } = usePropertyForm();
+ const {
+ newPropertyForm,
+ constructionDetails,
+ floorTypeText,
+ roofTypeText,
+ wallTypeText,
+ woodTypeText,
+ selectText,
+ translateDropdownOptions,
+ nextButtonText,
+ } = useLocalization();
+
+ // RTK Query hooks for fetching and mutating construction details
+ const [createConstructionDetails, { isLoading: isCreating }] =
+ useCreateConstructionDetailsMutation();
+ const [updateConstructionDetails, { isLoading: isUpdating }] =
+ useUpdateConstructionDetailsMutation();
+ const [triggerGetByPropertyId, { isLoading: isFetching }] =
+ useLazyGetConstructionDetailsByPropertyIdQuery();
+
+ // Localization hooks for labels, dropdowns, and UI text
+ // Local state for form fields, dropdowns, and popup
+ const [localData, setLocalData] = useState({
+ floorType: formData.constructionDetails?.floorType || '',
+ roofType: formData.constructionDetails?.roofType || '',
+ wallType: formData.constructionDetails?.wallType || '',
+ woodType: formData.constructionDetails?.woodType || '',
+ constructionId: formData.constructionDetails?.id?.toString() || '',
+ });
+
+ const [floorTypes, setFloorTypes] = useState([]);
+ const [roofTypes, setRoofTypes] = useState([]);
+ const [wallTypes, setWallTypes] = useState([]);
+ const [woodTypes, setWoodTypes] = useState([]);
+
+ const [rawFloorTypes, setRawFloorTypes] = useState([]);
+ const [rawRoofTypes, setRawRoofTypes] = useState([]);
+ const [rawWallTypes, setRawWallTypes] = useState([]);
+ const [rawWoodTypes, setRawWoodTypes] = useState([]);
+
+ // dropdown open states (pass to CustomDropdown so parent can enforce exclusive open)
+ const [showFloorTypeDropdown, setShowFloorTypeDropdown] = useState(false);
+ const [showRoofTypeDropdown, setShowRoofTypeDropdown] = useState(false);
+ const [showWallTypeDropdown, setShowWallTypeDropdown] = useState(false);
+ const [showWoodTypeDropdown, setShowWoodTypeDropdown] = useState(false);
+
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>(
+ {
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ }
+ );
+ const propertyId = formData.id;
+ // Effect: Fetch existing construction details if propertyId is present
+ useEffect(() => {
+ const fetchExistingData = async () => {
+ if (propertyId) {
+ try {
+ const result = await triggerGetByPropertyId(propertyId).unwrap();
+ if (result.data && result.data.length > 0) {
+ const existingData = result.data[0];
+ // Map PascalCase to camelCase
+ const mappedData = {
+ floorType: existingData.FloorType || '',
+ roofType: existingData.RoofType || '',
+ wallType: existingData.WallType || '',
+ woodType: existingData.WoodType || '',
+ constructionId: existingData.ID || '',
+ };
+
+ setLocalData(mappedData);
+ updateForm({
+ constructionDetails: {
+ ...mappedData,
+ id: mappedData.constructionId,
+ },
+ });
+ }
+ } catch (error) {
+ console.error('Failed to fetch construction details:', error);
+ }
+ }
+ };
+
+ fetchExistingData();
+ }, [propertyId]);
+
+ // Effect: Fetch raw dropdown lists from JSON service
+ useEffect(() => {
+ JsonService.getFloorTypes().then(setRawFloorTypes);
+ JsonService.getRoofTypes().then(setRawRoofTypes);
+ JsonService.getWallTypes().then(setRawWallTypes);
+ JsonService.getWoodTypes().then(setRawWoodTypes);
+ }, []);
+
+ // Effect: Translate dropdown options on language change
+ useEffect(() => {
+ setFloorTypes(translateDropdownOptions(rawFloorTypes));
+ setRoofTypes(translateDropdownOptions(rawRoofTypes));
+ setWallTypes(translateDropdownOptions(rawWallTypes));
+ setWoodTypes(translateDropdownOptions(rawWoodTypes));
+ }, [rawFloorTypes, rawRoofTypes, rawWallTypes, rawWoodTypes, translateDropdownOptions]);
+
+ // Effect: Sync local state with context data
+ useEffect(() => {
+ if (formData.constructionDetails) {
+ setLocalData({
+ floorType: formData.constructionDetails.floorType || '',
+ roofType: formData.constructionDetails.roofType || '',
+ wallType: formData.constructionDetails.wallType || '',
+ woodType: formData.constructionDetails.woodType || '',
+ constructionId: formData.constructionDetails.id?.toString() || '',
+ });
+ }
+ }, [formData.constructionDetails]);
+
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ // showErrorPopup: Displays warning popup for errors
+ // helper to close others before opening
+ const closeAllDropdowns = () => {
+ setShowFloorTypeDropdown(false);
+ setShowRoofTypeDropdown(false);
+ setShowWallTypeDropdown(false);
+ setShowWoodTypeDropdown(false);
+ };
+
+ // --- NEW: touched and field error state for dropdowns ---
+ const [fieldErrors, setFieldErrors] = useState>({
+ floorType: '',
+ roofType: '',
+ wallType: '',
+ woodType: '',
+ });
+ const [touchedFields, setTouchedFields] = useState>({
+ floorType: false,
+ roofType: false,
+ wallType: false,
+ woodType: false,
+ });
+ const markTouched = (name: string) =>
+ setTouchedFields((prev) => ({ ...prev, [name]: true }));
+
+ const clearError = (name: string) =>
+ setFieldErrors((prev) => ({ ...prev, [name]: '' }));
+
+ // Called when a dropdown option is selected
+ const handleDropdownSelect = (field: string, value: string) => {
+ setLocalData((prev) => ({ ...prev, [field]: value }));
+ clearError(field);
+ markTouched(field);
+ };
+
+ // Called when a dropdown is closed (user clicked away) โ if value is empty we show required error
+ const handleBlurDropdown = (field: string) => {
+ markTouched(field);
+ const currentValue = (localData as any)[field] as string;
+ if (!currentValue || currentValue === '') {
+ const labelMap: Record = {
+ floorType: floorTypeText,
+ roofType: roofTypeText,
+ wallType: wallTypeText,
+ woodType: woodTypeText,
+ };
+ setFieldErrors((prev) => ({
+ ...prev,
+ [field]: `${labelMap[field]} is required`,
+ }));
+ } else {
+ clearError(field);
+ }
+ };
+ // --- END NEW ---
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ // Validation (also set field errors for UI)
+ const missing = [
+ { key: 'floorType', label: floorTypeText },
+ { key: 'roofType', label: roofTypeText },
+ { key: 'wallType', label: wallTypeText },
+ { key: 'woodType', label: woodTypeText },
+ ].filter((f) => !(localData as any)[f.key]);
+
+ if (missing.length > 0) {
+ // mark touched and set errors for missing fields
+ const newFieldErrors: Record = { ...fieldErrors };
+ const newTouched = { ...touchedFields };
+ missing.forEach((m) => {
+ newTouched[m.key] = true;
+ newFieldErrors[m.key] = `${m.label} is required`;
+ });
+ setTouchedFields(newTouched);
+ setFieldErrors(newFieldErrors);
+
+ showErrorPopup('Please fill in all construction details');
+ return;
+ }
+
+ if (!propertyId) {
+ showErrorPopup('Property ID is missing. Please complete previous steps.');
+ return;
+ }
+
+ try {
+ const constructionPayload: ConstructionDetailsRequest = {
+ floorType: localData.floorType,
+ wallType: localData.wallType,
+ roofType: localData.roofType,
+ woodType: localData.woodType,
+ propertyId: propertyId,
+ };
+
+ let result;
+ if (localData.constructionId) {
+ // UPDATE existing construction details
+ result = await updateConstructionDetails({
+ id: localData.constructionId,
+ data: constructionPayload,
+ }).unwrap();
+ } else {
+ // CREATE new construction details
+ result = await createConstructionDetails(constructionPayload).unwrap();
+ }
+
+ // Update context with the saved data
+ updateForm({
+ constructionDetails: {
+ ...result.data,
+ id: result.data.ID,
+ },
+ });
+
+ // Navigate to next page after a short delay
+ setTimeout(() => {
+ navigate('/property-form/floor-details-cards');
+ }, 1500);
+ } catch (error: any) {
+ console.error('Failed to save construction details:', error);
+ const errorMessage =
+ error?.data?.message || 'Failed to save construction details. Please try again.';
+ showErrorPopup(errorMessage);
+ }
+ };
+
+ // handleSubmit: Validates and submits form, creates/updates construction details via API
+ const handleGoBack = () => navigate(-1);
+
+ // handleGoBack: Navigates to previous page
+ const handleSaveDraft = () => {
+ updateForm({
+ constructionDetails: {
+ floorType: localData.floorType,
+ roofType: localData.roofType,
+ wallType: localData.wallType,
+ woodType: localData.woodType,
+ id: localData.constructionId,
+ },
+ });
+ };
+
+ // handleSaveDraft: Saves current form data as draft in context
+ const { previousText, saveDraftText } = useAssessmentDetailsLocalization();
+
+ const isSubmitting = isCreating || isUpdating;
+
+ // UI rendering: WarningPopup, header, form with dropdowns and submit button
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
+
+
+
+
+ handleBlurDropdown('floorType')}
+ />
+
+
+
+ handleBlurDropdown('roofType')}
+ />
+
+
+
+ handleBlurDropdown('wallType')}
+ />
+
+
+
+ handleBlurDropdown('woodType')}
+ />
+
+
+
+
+ {isSubmitting ? 'Submitting...' : mode === 'verify' ? 'Verify' : nextButtonText}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ConstructionDetailsPage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/DocumentUpload.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/DocumentUpload.tsx
new file mode 100644
index 0000000..aba4111
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/DocumentUpload.tsx
@@ -0,0 +1,673 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import '../../../styles/DocumentUpload.css';
+import { useFormMode } from '../../../context/FormModeContext';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import {
+ uploadFileToFilestore,
+ getFileFromFilestore,
+} from '../../../services/AgentLocalisation/fileService';
+import { useDocumentUploadLocalization } from '../../../services/AgentLocalisation/localisation-documentupload';
+import {
+ useCreateDocumentUploadDetailsMutation,
+ useGetDocumentsUploadDetailsByPropertyIdQuery,
+ useDeleteDocumentUploadDetailsMutation,
+} from '../../features/PropertyForm/api/documentUpload.api';
+
+import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
+import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
+import AddAPhotoOutlinedIcon from '@mui/icons-material/AddAPhotoOutlined';
+import TaskAltOutlinedIcon from '@mui/icons-material/TaskAltOutlined';
+import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
+import TaskOutlinedIcon from '@mui/icons-material/TaskOutlined';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import type { AlertType } from '../../models/AlertType.model';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+// Type for uploaded document state
+interface UploadedDocument {
+ id: string;
+ name: string;
+ size: string;
+ date: string;
+ status: 'uploaded' | 'pending';
+ fileStoreId?: string;
+ fileType?: string;
+ documentType: string;
+ isNewlyAdded?: boolean;
+}
+
+// Error type for API errors
+interface ApiError {
+ data?: {
+ message?: string;
+ };
+ message?: string;
+}
+
+// Main component for document upload step in property form
+export const DocumentUpload: React.FC = () => {
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const { formData, updateForm } = usePropertyForm();
+
+ // Property ID from form data
+ const PROPERTY_ID = formData.id ?? '';
+
+ // Fetch existing uploaded documents for this property
+ const {
+ data: existingDocuments,
+ isLoading: isFetchingDocuments,
+ error: fetchError,
+ } = useGetDocumentsUploadDetailsByPropertyIdQuery(PROPERTY_ID, {
+ skip: mode !== 'draft' && mode !== 'verify',
+ });
+
+ const {
+ propertyFormText,
+ newPropertyFormText,
+ documentUploadText,
+ documentsUploadedText,
+ uploadedSuccessfullyText,
+ fileTypesText,
+ chooseFileText,
+ takePhotoText,
+ // verifyText,
+ confirmText,
+ twoNonJudicialStampPapersText,
+ notarizedAffidavitText,
+ deathCertificateText,
+ thirdPartyVerificationText,
+ pattaCertificateText,
+ mroProceedingsText,
+ willDeedText,
+ decreeDocumentText,
+ registeredDocumentText,
+ photoOfPropertyText,
+ // draftSavedText,
+ fileUploadFailedText,
+ fileNotFoundText,
+ downloadFailedText,
+ unableToOpenFileText,
+ previousText,
+ saveDraftText,
+ } = useDocumentUploadLocalization();
+
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ // State for uploaded documents
+ const [uploadedDocs, setUploadedDocs] = useState([]);
+
+ function showErrorPopup(
+ message: string,
+ duration = 3000,
+ type: AlertType = 'warning',
+ title: string = 'Warning'
+ ) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type,
+ open: true,
+ title,
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ // Populate uploadedDocs from API or form data on mount/update
+ useEffect(() => {
+ if (
+ existingDocuments?.data &&
+ Array.isArray(existingDocuments.data) &&
+ existingDocuments.data.length > 0
+ ) {
+ const mappedDocs: UploadedDocument[] = existingDocuments.data.map((doc) => ({
+ id: doc.ID,
+ name: doc.DocumentName,
+ size: doc.size || 'N/A',
+ date: new Date(doc.UploadDate).toLocaleDateString(),
+ status: 'uploaded' as const,
+ fileStoreId: doc.FileStoreID,
+ fileType: doc.DocumentName.endsWith('.pdf')
+ ? 'application/pdf'
+ : doc.DocumentName.endsWith('.png')
+ ? 'image/png'
+ : doc.DocumentName.endsWith('.jpg') || doc.DocumentName.endsWith('.jpeg')
+ ? 'image/jpeg'
+ : '',
+ documentType: doc.DocumentType,
+ isNewlyAdded: false,
+ }));
+ setUploadedDocs(mappedDocs);
+ } else if (formData.documents?.[0]?.files) {
+ const mappedDocs = formData.documents[0].files.map((file, idx) => ({
+ id: file.fileStoreId || (idx + 1).toString(),
+ name: file.fileName,
+ size: `${(file.fileSize / (1024 * 1024)).toFixed(1)} MB`,
+ date: file.dateOfUpload,
+ status: 'uploaded' as const,
+ fileStoreId: file.fileStoreId,
+ fileType: file.fileType,
+ documentType: file.documentType || '',
+ isNewlyAdded: false,
+ }));
+ setUploadedDocs(mappedDocs);
+ }
+ }, [existingDocuments, formData.documents]);
+
+ // State for current upload type, error, and file input ref
+ const [currentUploadType, setCurrentUploadType] = useState(null);
+ const [error, setError] = useState(null);
+ const fileInputRef = useRef(null);
+ // RTK mutations for uploading and deleting documents
+ const [createDocumentUpload, { isLoading: isSubmitting }] =
+ useCreateDocumentUploadDetailsMutation();
+ const [deleteDocument, { isLoading: isDeleting }] =
+ useDeleteDocumentUploadDetailsMutation();
+
+ // Show loading state while fetching documents
+ if (isFetchingDocuments) {
+ return (
+
+ );
+ }
+
+ // Log fetch error if present
+ if (fetchError) {
+ console.error('Error fetching documents:', fetchError);
+ }
+
+ // Navigate back to previous page
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ // Save current document state as draft in form
+ const handleSaveDraft = () => {
+ updateForm({
+ documents: [
+ {
+ ...(formData.documents?.[0] || {}),
+ documentType: formData.documents?.[0]?.documentType || '',
+ serialNoLabel: formData.documents?.[0]?.serialNoLabel || '',
+ revenueDocumentNumber: formData.documents?.[0]?.revenueDocumentNumber || '',
+ files: uploadedDocs.map((doc) => ({
+ fileStoreId: doc.fileStoreId || doc.id,
+ fileName: doc.name,
+ fileSize:
+ Number(doc.size.replace(' MB', '').replace('N/A', '0')) * 1024 * 1024,
+ dateOfUpload: doc.date,
+ fileType:
+ doc.fileType ||
+ (doc.name.endsWith('.pdf')
+ ? 'application/pdf'
+ : doc.name.endsWith('.png')
+ ? 'image/png'
+ : doc.name.endsWith('.jpg')
+ ? 'image/jpeg'
+ : ''),
+ documentType: doc.documentType,
+ })),
+ },
+ ],
+ });
+ };
+
+ // Trigger file input for choosing a file
+ const handleChooseFile = (documentType: string) => {
+ setCurrentUploadType(documentType);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ fileInputRef.current.removeAttribute('capture');
+ fileInputRef.current.click();
+ }
+ };
+
+ // Trigger file input for taking a photo
+ const handleTakePhoto = (documentType: string) => {
+ setCurrentUploadType(documentType);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ fileInputRef.current.setAttribute('capture', 'environment');
+ fileInputRef.current.click();
+ }
+ };
+
+ // Handle file selection and upload to filestore
+ const handleFileSelected = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file || !currentUploadType) return;
+
+ // Add file type validation
+ const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
+ const allowedExtensions = ['.pdf', '.jpg', '.jpeg', '.png'];
+
+ const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
+
+ if (!allowedTypes.includes(file.type) && !allowedExtensions.includes(fileExtension)) {
+ showErrorPopup('Invalid file type. Only PDF, JPG, and PNG files are allowed.');
+ setCurrentUploadType(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ return;
+ }
+
+ try {
+ const result = await uploadFileToFilestore(file);
+
+ // Convert file size
+ const fileSizeInKB = file.size / 1024;
+ const formattedSize =
+ fileSizeInKB >= 1024
+ ? `${(fileSizeInKB / 1024).toFixed(2)} MB`
+ : `${fileSizeInKB.toFixed(2)} KB`;
+
+ const uploadedDoc: UploadedDocument = {
+ id: result.files[0].fileStoreId,
+ name: file.name,
+ size: formattedSize,
+ date: new Date().toLocaleDateString(),
+ status: 'uploaded',
+ fileStoreId: result.files[0].fileStoreId,
+ fileType: file.type,
+ documentType: currentUploadType,
+ isNewlyAdded: true,
+ };
+
+ setUploadedDocs((prev) => [...prev, uploadedDoc]);
+ setCurrentUploadType(null);
+ } catch (err) {
+ console.error('Error uploading file:', err);
+ showErrorPopup(fileUploadFailedText);
+ setCurrentUploadType(null);
+ }
+ };
+
+ // Download a document from filestore
+ const handleDownloadDocument = async (docId: string) => {
+ const doc = uploadedDocs.find((d) => d.id === docId);
+ if (!doc || !doc.fileStoreId) {
+ showErrorPopup(fileNotFoundText);
+ return;
+ }
+ try {
+ const blob = await getFileFromFilestore(doc.fileStoreId);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = doc.name;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ a.remove();
+ } catch (err) {
+ console.error('Error downloading document:', err);
+ showErrorPopup(downloadFailedText);
+ }
+ };
+
+ // View a document in a new tab
+ const handleViewDocument = async (docId: string) => {
+ const doc = uploadedDocs.find((d) => d.id === docId);
+ if (!doc || !doc.fileStoreId) {
+ showErrorPopup(fileNotFoundText);
+ return;
+ }
+ try {
+ const blob = await getFileFromFilestore(doc.fileStoreId);
+ const url = window.URL.createObjectURL(blob);
+ window.open(url, '_blank');
+ } catch (err) {
+ console.error('Error viewing document:', err);
+ showErrorPopup(unableToOpenFileText);
+ }
+ };
+
+ // Remove a document (delete from server if not newly added)
+ const handleRemoveDocument = async (docId: string) => {
+ const doc = uploadedDocs.find((d) => d.id === docId);
+
+ if (doc?.isNewlyAdded) {
+ setUploadedDocs((prev) => prev.filter((d) => d.id !== docId));
+ return;
+ }
+
+ if (!window.confirm('Are you sure you want to delete this document?')) {
+ return;
+ }
+
+ try {
+ await deleteDocument(docId).unwrap();
+ setUploadedDocs((prev) => prev.filter((d) => d.id !== docId));
+ } catch (err) {
+ const error = err as ApiError;
+ console.error('Failed to delete document:', err);
+ showErrorPopup(error?.data?.message || 'Failed to delete document. Please try again.');
+ }
+ };
+
+ // Confirm and submit all uploaded documents
+ const handleConfirm = async () => {
+ setError(null);
+
+ const newlyAddedDocs = uploadedDocs.filter((doc) => doc.isNewlyAdded === true);
+
+ if (newlyAddedDocs.length === 0) {
+ updateForm({
+ documents: [
+ {
+ documentType: formData.documents?.[0]?.documentType || '',
+ serialNoLabel: formData.documents?.[0]?.serialNoLabel || '',
+ revenueDocumentNumber: formData.documents?.[0]?.revenueDocumentNumber || '',
+ files: uploadedDocs.map((doc) => ({
+ fileStoreId: doc.fileStoreId || doc.id,
+ fileName: doc.name,
+ fileSize:
+ Number(doc.size.replace(' MB', '').replace('N/A', '0')) * 1024 * 1024,
+ dateOfUpload: doc.date,
+ fileType:
+ doc.fileType ||
+ (doc.name.endsWith('.pdf')
+ ? 'application/pdf'
+ : doc.name.endsWith('.png')
+ ? 'image/png'
+ : doc.name.endsWith('.jpg')
+ ? 'image/jpeg'
+ : ''),
+ documentType: doc.documentType,
+ })),
+ },
+ ],
+ });
+
+ navigate('/property-form/summary');
+ return;
+ }
+
+ const documentPayload = newlyAddedDocs.map((doc) => ({
+ PropertyId: PROPERTY_ID,
+ DocumentType: doc.documentType,
+ DocumentName: doc.name,
+ FileStoreID: doc.fileStoreId || '',
+ Size: doc.size,
+ UploadDate: new Date().toISOString(),
+ }));
+
+ try {
+ const response = await createDocumentUpload(documentPayload).unwrap();
+ console.log('Documents created successfully:', response);
+
+ updateForm({
+ documents: [
+ {
+ documentType: formData.documents?.[0]?.documentType || '',
+ serialNoLabel: formData.documents?.[0]?.serialNoLabel || '',
+ revenueDocumentNumber: formData.documents?.[0]?.revenueDocumentNumber || '',
+ files: uploadedDocs.map((doc) => ({
+ fileStoreId: doc.fileStoreId || doc.id,
+ fileName: doc.name,
+ fileSize:
+ Number(doc.size.replace(' MB', '').replace('N/A', '0')) * 1024 * 1024,
+ dateOfUpload: doc.date,
+ fileType:
+ doc.fileType ||
+ (doc.name.endsWith('.pdf')
+ ? 'application/pdf'
+ : doc.name.endsWith('.png')
+ ? 'image/png'
+ : doc.name.endsWith('.jpg')
+ ? 'image/jpeg'
+ : ''),
+ documentType: doc.documentType,
+ })),
+ },
+ ],
+ });
+
+ navigate('/property-form/summary');
+ } catch (err) {
+ const error = err as ApiError;
+ console.error('Failed to save documents:', err);
+ setError(error?.data?.message || 'Failed to save documents. Please try again.');
+ }
+ };
+
+ // List of required/pending document types
+ const pendingDocuments = [
+ twoNonJudicialStampPapersText,
+ notarizedAffidavitText,
+ deathCertificateText,
+ thirdPartyVerificationText,
+ pattaCertificateText,
+ mroProceedingsText,
+ willDeedText,
+ decreeDocumentText,
+ registeredDocumentText,
+ photoOfPropertyText,
+ ];
+
+ // Limit for max uploads and list of uploaded document types
+ const canUploadMore = uploadedDocs.length < 12;
+ const uploadedDocTypes = uploadedDocs.map((d) => d.documentType);
+
+ // Main render: upload controls, uploaded docs, pending docs, and confirm button
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
handleBack()}
+ onSaveDraft={() => handleSaveDraft()}
+ saveDraftText={saveDraftText}
+ previousText={previousText}
+ />
+
+
+
+
+ {uploadedDocs.length}/12 {documentsUploadedText}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {uploadedDocs.map((doc) => (
+
+
{doc.documentType}
+
+
+
+
+ {uploadedSuccessfullyText}
+
+
+
+
+
+
+
+
+
+ {doc.name}
+
+
+ {doc.size} • {doc.date}
+
+
+
+
+ handleRemoveDocument(doc.id)}
+ type="button"
+ disabled={isDeleting}
+ style={{ alignItems: 'end', width: '90px', height: '20px' }}
+ >
+
+
+
+
+
+
+
+ handleViewDocument(doc.id)}
+ type="button"
+ style={{ alignItems: 'end' }}
+ >
+
+
+ handleDownloadDocument(doc.id)}
+ type="button"
+ >
+
+
+
+
+
+ ))}
+
+
+
+ {pendingDocuments.map(
+ (docType, index) =>
+ !uploadedDocTypes.includes(docType) && (
+
+
+
+
+
+ canUploadMore && handleChooseFile(docType.replace('*', ''))
+ }
+ disabled={!canUploadMore}
+ type="button"
+ >
+ {chooseFileText}
+
+
{fileTypesText}
+
+
+ canUploadMore && handleTakePhoto(docType.replace('*', ''))
+ }
+ disabled={!canUploadMore}
+ type="button"
+ title={takePhotoText}
+ >
+
+
+
+
+ )
+ )}
+
+
+
+
+ {isSubmitting
+ ? 'Submitting...'
+ : mode === 'verify'
+ ? 'Verify'
+ : confirmText}
+
+
+
+
+ >
+ );
+};
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/DocumentsInformation.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/DocumentsInformation.tsx
new file mode 100644
index 0000000..25fbbf1
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/DocumentsInformation.tsx
@@ -0,0 +1,544 @@
+// DocumentsInformation.tsx
+// Property Form Documents Information page
+// Collects and manages document details for a property (type, serial number, revenue document number)
+// Features:
+// - Form for entering document information using dropdowns and text fields
+// - Fetches and updates document info via RTK Query
+// - Uses localization for labels, dropdown options, and error messages
+// - Handles form validation, error popups, and draft saving
+// - Responsive UI with MUI components and custom dropdowns
+// Used in: Property form workflow for document information step
+import React, { useEffect, useState } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Button from '@mui/material/Button';
+import { useNavigate } from 'react-router-dom';
+import { useFormMode } from '../../../context/FormModeContext';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import { useDocumentsLocalization } from '../../../services/AgentLocalisation/localisation-documents';
+import JsonService from '../../../services/jsonServerApiCalls';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import CustomDropdown, {
+ type DropdownOption,
+} from '../../features/PropertyForm/components/IGRSDetail/IGRSdropdown';
+import FormTextField from '../../features/PropertyForm/components/IGRSDetail/IGRSFormTextFiled';
+import { uniformInputSx, verifyButtonSx } from './styles/sharedStyles';
+import type { AlertType } from '../../models/AlertType.model';
+import {
+ useCreateDocumentInfoMutation,
+ useGetDocumentInfoByPropertyIdQuery,
+ useUpdateDocumentInfoMutation,
+} from '../../features/PropertyForm/api/documentInfo.api';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+// ApiError: interface for API error responses
+interface ApiError {
+ data?: {
+ errors?: string[];
+ message?: string;
+ };
+ message?: string;
+}
+
+// Inline styles for layout and UI
+const containerSx = {
+ width: '100%',
+ margin: '0 auto',
+ minHeight: '100vh',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ bgcolor: '#fff',
+};
+
+const headerSx = {
+ backgroundColor: '#F9E6E0',
+ padding: '16px',
+};
+
+const formContentSx = {
+ flex: 1,
+ px: '8%',
+ py: 3,
+ backgroundColor: '#FFFFFF',
+};
+
+const formSubmitSx = { padding: '16px 0', backgroundColor: '#FFFFFF' };
+
+const DocumentsInformation: React.FC = () => {
+ // Contexts and hooks for form mode, navigation, and property form data
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const { formData, updateForm } = usePropertyForm();
+
+ const PROPERTY_ID = formData.id ?? (localStorage.getItem('propertyId') || '');
+
+ // Fetch existing document info
+ const { data: existingDocInfo, isLoading: isFetchingDocInfo } =
+ mode === 'verify' || mode === 'draft'
+ ? useGetDocumentInfoByPropertyIdQuery(PROPERTY_ID)
+ : { data: null, isLoading: false };
+
+ const {
+ propertyFormTitle,
+ newPropertyFormTitle,
+ documentsSubtitle,
+ documentTypeLabel,
+ enterNumberPlaceholder,
+ // saveDraftSuccessMsg,
+ documentTypeOptions,
+ selectPlaceholder,
+ saveDraftText,
+ previousText,
+ } = useDocumentsLocalization();
+
+ const {
+ nextButtonText,
+ ThisFieldIsRequiredMSG,
+ SerialNoText,
+ RevenueDocumentNumberText,
+ onlyNumbersAreAllowedMSG,
+ } = useLocalization();
+
+ const [createDocumentInfo, { isLoading: isCreating }] = useCreateDocumentInfoMutation();
+ const [updateDocumentInfo, { isLoading: isUpdating }] = useUpdateDocumentInfoMutation();
+
+ const isSubmitting = isCreating || isUpdating;
+
+ const [documentInfoId, setDocumentInfoId] = useState(null);
+
+ const [localData, setLocalData] = useState({
+ documentType: formData.documents?.[0]?.documentType || '',
+ serialNoLabel: formData.documents?.[0]?.serialNoLabel || '',
+ revenueDocumentNumber: formData.documents?.[0]?.revenueDocumentNumber || '',
+ });
+
+ const [errors, setErrors] = useState({
+ documentType: '',
+ serialNoLabel: '',
+ revenueDocumentNumber: '',
+ });
+
+ const [touched, setTouched] = useState({
+ documentType: false,
+ serialNoLabel: false,
+ revenueDocumentNumber: false,
+ });
+
+ const [showNumberWarnings, setShowNumberWarnings] = useState({
+ serialNoLabel: false,
+ revenueDocumentNumber: false,
+ });
+
+ // dropdown open state
+ const [showDocumentTypeDropdown, setShowDocumentTypeDropdown] = useState(false);
+
+ // API-provided options (raw)
+ const [apiOptions, setApiOptions] = useState<{ id: string | number; name: string }[]>(
+ []
+ );
+
+ // normalize into DropdownOption[] for CustomDropdown
+ const dropdownOptions: DropdownOption[] = React.useMemo(
+ () =>
+ (documentTypeOptions && documentTypeOptions.length
+ ? documentTypeOptions.map((label, idx) => ({ id: idx + 1, label: label ?? '' }))
+ : apiOptions.map((o) => ({ id: o.id, label: o.name ?? '' }))) as DropdownOption[],
+ [documentTypeOptions, apiOptions]
+ );
+
+ // Effect: Fetch document types from API and set dropdown options
+ useEffect(() => {
+ let mounted = true;
+ (async () => {
+ try {
+ const data = await JsonService.getDocumentTypes();
+ if (!mounted) return;
+ if (Array.isArray(data) && data.length) {
+ setApiOptions(data as { id: string | number; name: string }[]);
+ }
+ } catch {
+ // ignore, we will rely on localization options
+ }
+ })();
+ return () => {
+ mounted = false;
+ };
+ }, []);
+
+ // Effect: Prepopulate form when existing document info is fetched
+ useEffect(() => {
+ if (
+ existingDocInfo?.success &&
+ existingDocInfo?.data &&
+ Array.isArray(existingDocInfo.data)
+ ) {
+ const documentInfoEntries = existingDocInfo.data.filter(
+ (item) => item.FieldName === 'DocumentInfo'
+ );
+
+ if (documentInfoEntries.length > 0) {
+ const latestEntry = documentInfoEntries.reduce((latest, current) => {
+ const latestDate = new Date(latest.UpdatedAt);
+ const currentDate = new Date(current.UpdatedAt);
+ return currentDate > latestDate ? current : latest;
+ });
+
+ setDocumentInfoId(latestEntry.ID);
+
+ let docTypeOption;
+ if (dropdownOptions && dropdownOptions.length > 0 && latestEntry.fieldValue) {
+ if (typeof latestEntry.fieldValue.DocumentType === 'number') {
+ docTypeOption = dropdownOptions.find(
+ (opt) => Number(opt.id) === Number(latestEntry.fieldValue.DocumentType)
+ );
+ } else {
+ docTypeOption = dropdownOptions.find(
+ (opt) => opt.label === latestEntry.fieldValue.DocumentType
+ );
+ }
+
+ setLocalData({
+ documentType: docTypeOption?.label || '',
+ serialNoLabel: latestEntry.fieldValue.serialNo?.toString() || '',
+ revenueDocumentNumber:
+ latestEntry.fieldValue.revenueDocumentNo?.toString() || '',
+ });
+ } else {
+ setLocalData({
+ documentType: '',
+ serialNoLabel: latestEntry.fieldValue.serialNo?.toString() || '',
+ revenueDocumentNumber:
+ latestEntry.fieldValue.revenueDocumentNo?.toString() || '',
+ });
+ }
+ } else {
+ showErrorPopup('No DocumentInfo entries found');
+ }
+ }
+ }, [existingDocInfo, dropdownOptions]);
+
+ // ===============
+ // Numeric Validation and Input Handling
+ // ===============
+ const validateField = (name: keyof typeof localData, value: string) => {
+ if (!value || value.trim() === '') return ThisFieldIsRequiredMSG;
+ if (['serialNoLabel', 'revenueDocumentNumber'].includes(name)) {
+ // Only allow whole numbers (no decimals)
+ if (!/^\d+$/.test(value)) return onlyNumbersAreAllowedMSG;
+ const num = Number(value);
+ if (isNaN(num) || num <= 0) return 'Must be a positive number';
+ }
+ return '';
+ };
+
+ // ENHANCED: strip leading zeros from number fields on blur!
+ const handleBlur = (name: keyof typeof localData) => {
+ let newValue = localData[name];
+
+ // Strip leading zeros for these fields on blur
+ if (
+ (name === 'serialNoLabel' || name === 'revenueDocumentNumber') &&
+ newValue !== ''
+ ) {
+ if (/^\d+(\.\d*)?$/.test(newValue)) {
+ const parts = newValue.split('.');
+ parts[0] = String(Number(parts[0]));
+ newValue = parts.length > 1 ? parts.join('.') : parts[0];
+ }
+ setLocalData((prev) => ({ ...prev, [name]: newValue }));
+ }
+
+ setTouched((prev) => ({ ...prev, [name]: true }));
+ setErrors((prev) => ({ ...prev, [name]: validateField(name, newValue) }));
+ };
+
+ // Prevent '-', 'e', '+', '.'
+ const handleNumberKeyDown = (e: React.KeyboardEvent) => {
+ const forbidden = ['-', 'e', '+', '.'];
+ if (forbidden.includes(e.key)) {
+ e.preventDefault();
+ }
+ };
+
+ const handleFieldChange = (field: keyof typeof localData) => (value: string) => {
+ let sanitized = value;
+ if (field === 'serialNoLabel' || field === 'revenueDocumentNumber') {
+ // Only allow digits and one dot for float
+ // const isValid = /^\d*\.?\d*$/.test(value);
+ sanitized = value.replace(/[^0-9]/g, '');
+
+ const isValid = /^\d*$/.test(value);
+ if (!isValid && value !== '') {
+ setShowNumberWarnings((prev) => ({ ...prev, [field]: true }));
+ return;
+ }
+ }
+ setLocalData((prev) => ({ ...prev, [field]: sanitized }));
+ if (field === 'serialNoLabel' || field === 'revenueDocumentNumber') {
+ if (showNumberWarnings[field]) {
+ if (/^\d*$/.test(sanitized) || sanitized === '')
+ setShowNumberWarnings((prev) => ({ ...prev, [field]: false }));
+ }
+ }
+ if (touched[field]) setErrors((prev) => ({ ...prev, [field]: '' }));
+ };
+
+ // handleDocumentTypeSelect: Handles selection from document type dropdown
+ const handleDocumentTypeSelect = (_field: string, value: string) => {
+ setLocalData((prev) => ({ ...prev, documentType: value }));
+ setErrors((prev) => ({ ...prev, documentType: '' }));
+ setTouched((prev) => ({ ...prev, documentType: true }));
+ setShowDocumentTypeDropdown(false);
+ };
+
+ // Local state for popup messages
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ function showErrorPopup(
+ message: string,
+ duration = 3000,
+ type: AlertType = 'information',
+ title: string = 'Information'
+ ) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type,
+ open: true,
+ title,
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ // handleSubmit: Validates and submits form, creates/updates document info via API
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const submitErrors = {
+ documentType: validateField('documentType', localData.documentType),
+ serialNoLabel: validateField('serialNoLabel', localData.serialNoLabel),
+ revenueDocumentNumber: validateField(
+ 'revenueDocumentNumber',
+ localData.revenueDocumentNumber
+ ),
+ };
+
+ setErrors({
+ documentType: submitErrors.documentType || '',
+ serialNoLabel: submitErrors.serialNoLabel || '',
+ revenueDocumentNumber: submitErrors.revenueDocumentNumber || '',
+ });
+ setTouched({ documentType: true, serialNoLabel: true, revenueDocumentNumber: true });
+
+ if (Object.values(submitErrors).some(Boolean)) return;
+
+ try {
+ const selectedOption = dropdownOptions.find(
+ (opt) => opt.label === localData.documentType
+ );
+ const documentTypeId = selectedOption?.id || 0;
+
+ const payload = {
+ fieldName: 'DocumentInfo',
+ fieldValue: {
+ DocumentType: Number(documentTypeId),
+ serialNo: Number(localData.serialNoLabel),
+ revenueDocumentNo: Number(localData.revenueDocumentNumber),
+ },
+ propertyId: PROPERTY_ID,
+ };
+
+ let response;
+
+ if (documentInfoId) {
+ response = await updateDocumentInfo({
+ documentId: documentInfoId,
+ body: payload,
+ }).unwrap();
+ } else {
+ response = await createDocumentInfo(payload).unwrap();
+ }
+
+ console.log(response);
+
+ // Update form context
+ updateForm({
+ documents: [
+ {
+ ...localData,
+ files: formData.documents?.[0]?.files || [],
+ },
+ ],
+ });
+
+ // Navigate to next page
+ navigate('/property-form/documents-upload');
+ } catch (err) {
+ const error = err as ApiError;
+ showErrorPopup(
+ error?.data?.errors?.[0] ||
+ error?.message ||
+ 'Failed to save document information. Please try again.'
+ );
+ }
+ };
+
+ // handleSaveDraft: Saves current form data as draft in context
+ const handleSaveDraft = () => {
+ updateForm({
+ documents: [
+ {
+ ...localData,
+ files: formData.documents?.[0]?.files || [],
+ },
+ ],
+ });
+ };
+
+ // handleBack: Navigates to previous page
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ if (isFetchingDocInfo) {
+ return (
+
+
+
Loading document information...
+
+
+ );
+ }
+
+ // UI rendering: WarningPopup, header, form with dropdowns and submit button
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
+
+
+
+ setShowDocumentTypeDropdown(false)}
+ selectText={selectPlaceholder || 'Select'}
+ required
+ error={errors.documentType}
+ touched={touched.documentType}
+ onBlur={() => handleBlur('documentType')}
+ />
+
+
+
+ handleBlur('serialNoLabel')}
+ placeholder=""
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.serialNoLabel
+ ? onlyNumbersAreAllowedMSG
+ : errors.serialNoLabel
+ }
+ touched={touched.serialNoLabel || showNumberWarnings.serialNoLabel}
+ />
+
+
+ handleBlur('revenueDocumentNumber')}
+ placeholder={enterNumberPlaceholder}
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.revenueDocumentNumber
+ ? onlyNumbersAreAllowedMSG
+ : errors.revenueDocumentNumber
+ }
+ touched={
+ touched.revenueDocumentNumber ||
+ showNumberWarnings.revenueDocumentNumber
+ }
+ />
+
+
+
+
+ {isSubmitting
+ ? 'Submitting...'
+ : mode === 'verify'
+ ? 'Verify'
+ : nextButtonText}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default DocumentsInformation;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/DraftPage.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/DraftPage.tsx
new file mode 100644
index 0000000..d9dd31c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/DraftPage.tsx
@@ -0,0 +1,86 @@
+// DraftPage.tsx
+// Property Form Drafts page
+// Displays draft property applications and provides navigation back to previous page
+// Features:
+// - Loads draft data from backend (fetchData)
+// - Shows tabs for Calendar, Map, and filter options (All, New, Reviewed, Drafts)
+// - Uses custom Layout component for consistent UI
+// - Handles back navigation via prop or browser history
+// Used in: Property form workflow for managing and viewing drafts
+
+import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import '../../../styles/DraftPage.css';
+import Layout from '../../features/Agent/components/Layout';
+import { fetchData } from '../../../services/dataService';
+import type { DatabaseData } from '../../../services/dataService';
+
+// DraftPageProps: props for optional propertyId and custom back handler
+interface DraftPageProps {
+ propertyId?: string;
+ onBack?: () => void;
+}
+
+const DraftPage: React.FC = ({ onBack }) => {
+ // Navigation and local state for fetched data
+ const navigate = useNavigate();
+ const [, setData] = useState(null);
+
+ useEffect(() => {
+ // Load draft data from backend
+ const loadData = async () => {
+ try {
+ const fetchedData = await fetchData();
+ setData(fetchedData);
+ } catch (error) {
+ console.error('Error loading data:', error);
+ }
+ };
+
+ loadData();
+ }, []);
+
+ const handleBack = () => {
+ // Handles back navigation via prop or browser history
+ if (onBack) {
+ onBack();
+ } else {
+ navigate(-1);
+ }
+ };
+
+ return (
+ // Layout with header and navigation hidden
+ {}} // No tab change needed on draft page
+ showHeader={false}
+ showNavigation={false}
+ headerProps={{
+ title: 'Drafts',
+ showBackButton: true,
+ onBack: handleBack,
+ showLanguage: false,
+ showProfile: false,
+ }}
+ >
+
+ {/* Tabs */}
+
+ Calendar
+ Map
+
+
+ {/* Filter Tabs */}
+
+ All
+ New
+ Reviewed
+ Drafts
+
+
+
+ );
+};
+
+export default DraftPage;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/FloorDetailsCards.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/FloorDetailsCards.tsx
new file mode 100644
index 0000000..f00b07f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/FloorDetailsCards.tsx
@@ -0,0 +1,381 @@
+// FloorDetailsCards.tsx
+// Property Form Floor Details page
+// Displays and manages a list of floor details for a property
+// Features:
+// - Loads floor details from backend and updates context
+// - Allows adding, editing, and deleting floor entries
+// - Shows error and loading states
+// - Uses custom FloorCard component for each floor
+// - Responsive UI with MUI components and localization
+// Used in: Property form workflow for floor details step
+import React, { useEffect, useState } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Button from '@mui/material/Button';
+import Typography from '@mui/material/Typography';
+import { useNavigate } from 'react-router-dom';
+import { FaPlus } from 'react-icons/fa';
+import { useFormMode } from '../../../context/FormModeContext';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import type { FloorDetails } from '../../../context/PropertyFormContext';
+import { useFloorDetailsLocalization } from '../../../services/AgentLocalisation/localisation-floor-details';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import FloorCard from '../../features/PropertyForm/components/FloorDetail/FloorCard';
+import { verifyButtonSx } from './styles/sharedStyles';
+import type { AlertType } from '../../models/AlertType.model';
+
+import {
+ useGetFloorDetailsByConstructionIdQuery,
+ useDeleteFloorDetailsMutation,
+} from '../../../redux/apis/floorApi';
+import CircularProgress from '@mui/material/CircularProgress';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+const containerSx = {
+ width: '100%',
+ // maxWidth: 375,
+ margin: '0 auto',
+ minHeight: '100vh',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ bgcolor: '#fff',
+};
+
+const headerSx = {
+ backgroundColor: '#F9E6E0',
+ padding: '16px',
+};
+
+const contentSx = {
+ flex: 1,
+ px: '8%',
+ py: 3,
+ backgroundColor: '#FFFFFF',
+ display: 'flex',
+ flexDirection: 'column' as const,
+};
+
+const addButtonSx = {
+ display: 'flex',
+ alignItems: 'center',
+ gap: 1,
+ backgroundColor: '#F7E4DB',
+ color: '#6D4531',
+ borderRadius: 2,
+ padding: '8px 16px',
+ fontSize: 16,
+ fontWeight: 500,
+ textTransform: 'none',
+ width: '46%',
+ '&:hover': { backgroundColor: '#edd8ca' },
+};
+
+const cardListSx = {
+ mt: 2.5,
+ mb: 1.5,
+ display: 'flex',
+ flexDirection: 'column' as const,
+ gap: 2,
+};
+
+const errorSx = {
+ color: 'red',
+ mt: 2,
+ fontSize: '1rem',
+};
+
+const FloorDetailsCards: React.FC = () => {
+ // Contexts and hooks for form mode, navigation, and property form data
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const { formData, updateForm } = usePropertyForm();
+
+ // const floorId = (formData.floors as any)?.floorId as string | undefined;
+ // const isEditMode = Boolean(floorId);
+
+ const {
+ saveDraftText,
+ atLeastOneFloorMsg,
+ previousText,
+ propertyFormTitle,
+ newPropertyFormTitle,
+ floorDetailsSubtitle,
+ } = useFloorDetailsLocalization();
+
+ const { addFloorText, nextButtonText, NoFloorDetailsFoundMSG } = useLocalization();
+
+ const [error, setError] = useState(null);
+
+ const constructionDetailsId = formData.constructionDetails!.id?.toString();
+ const {
+ data: floorDetailsData,
+ isLoading,
+ error: apiError,
+ refetch,
+ } = useGetFloorDetailsByConstructionIdQuery(constructionDetailsId!);
+
+ const [deleteFloor, { isLoading: isDeleting }] = useDeleteFloorDetailsMutation();
+
+ // Updated convert function to handle the actual API response structure
+ const convertApiToLocal = (apiData: any) => {
+ return {
+ floorNumber: (apiData.FloorNo || apiData.floorNo || '').toString(),
+ buildingClassification: apiData.Classification || apiData.classification || '',
+ igrsClassification: 'A', // Default or map from API
+ natureOfUsage: apiData.NatureOfUsage || apiData.natureOfUsage || '',
+ firmName: apiData.FirmName || apiData.firmName || '',
+ occupancy: apiData.OccupancyType || apiData.occupancyType || '',
+ constructionDate: apiData.ConstructionDate || apiData.constructionDate || '',
+ effectiveFromDate: apiData.EffectiveFromDate || apiData.effectiveFromDate || '',
+ unstructuredLand: apiData.UnstructuredLand || apiData.unstructuredLand || '',
+ length: (apiData.LengthFt || apiData.lengthFt || 0).toString(),
+ breadth: (apiData.BreadthFt || apiData.breadthFt || 0).toString(),
+ plinthArea: (apiData.PlinthAreaSqFt || apiData.plinthAreaSqFt || 0).toString(),
+ buildingPermissionNo:
+ apiData.BuildingPermissionNo || apiData.buildingPermissionNo || '',
+ floorsDetailsEntered:
+ apiData.FloorDetailsEntered || apiData.floorDetailsEntered || true,
+ occupantName: apiData.OccupancyName || apiData.occupancyName || '',
+ };
+ };
+
+ // Update local context when API data changes
+ useEffect(() => {
+ // Check different possible data structures
+ let floors: any[] = [];
+
+ if (floorDetailsData?.data && Array.isArray(floorDetailsData.data)) {
+ floors = floorDetailsData.data;
+ } else if (Array.isArray(floorDetailsData)) {
+ floors = floorDetailsData;
+ } else if (floorDetailsData && typeof floorDetailsData === 'object') {
+ // Check if the data is directly an array or has floors property
+ const dataKeys = Object.keys(floorDetailsData);
+ console.log('Data keys:', dataKeys);
+ }
+
+ if (floors.length > 0) {
+ const convertedFloors = floors.map(convertApiToLocal);
+ updateForm({ floors: convertedFloors });
+ } else {
+ // showErrorPopup("No floors found, setting empty array");
+ updateForm({ floors: [] });
+ }
+ }, [floorDetailsData]);
+
+ const handleAddFloor = () => {
+ // Navigate without any state - this creates a new empty floor
+ navigate('/property-form/floor-details');
+ };
+
+ const handleDeleteFloor = async (idx: number) => {
+ try {
+ // Get the floor ID from API data
+ let floors: any[] = [];
+
+ if (floorDetailsData?.data && Array.isArray(floorDetailsData.data)) {
+ floors = floorDetailsData.data;
+ } else if (Array.isArray(floorDetailsData)) {
+ floors = floorDetailsData;
+ }
+
+ const floorToDelete = floors[idx];
+ const floorId = floorToDelete?.ID || floorToDelete?.id;
+
+ if (floorId) {
+ await deleteFloor(floorId).unwrap();
+ refetch(); // Refresh the data
+ } else {
+ // Fallback to local deletion if no API ID
+ const newFloors = (formData.floors ?? []).filter((_, i) => i !== idx);
+ updateForm({ floors: newFloors });
+ }
+ } catch (error: any) {
+ console.error('Failed to delete floor:', error);
+ const errorMessage =
+ error?.data?.message || 'Failed to delete floor. Please try again.';
+ showErrorPopup(errorMessage);
+ }
+ };
+
+ const handleEditFloor = (floor: FloorDetails) => {
+ // Find the corresponding API floor data to get the ID
+ const floorIndex =
+ formData.floors?.findIndex(
+ (f) => f.floorNumber === floor.floorNumber && f.plinthArea === floor.plinthArea
+ ) ?? -1;
+
+ let floors: any[] = [];
+ if (floorDetailsData?.data && Array.isArray(floorDetailsData.data)) {
+ floors = floorDetailsData.data;
+ } else if (Array.isArray(floorDetailsData)) {
+ floors = floorDetailsData;
+ }
+
+ const apiFloor = floors[floorIndex];
+ const floorId = apiFloor?.ID || apiFloor?.id;
+
+ // Navigate with floorId - this will trigger edit mode and prefill data
+ navigate('/property-form/floor-details', {
+ state: {
+ floorId: floorId,
+ },
+ });
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!formData.floors || formData.floors.length === 0) {
+ setError(atLeastOneFloorMsg);
+ return;
+ }
+ setError(null);
+ navigate('/property-form/documents');
+ };
+
+ const handleGoBack = () => {
+ navigate(-1);
+ };
+
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ const handleSaveDraft = () => {
+ updateForm({
+ floors: formData.floors,
+ });
+ };
+
+ // Loading state
+ if (isLoading) {
+ return (
+
+
+ Loading floor details...
+
+ );
+ }
+
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
+
+
+
+ {formData.floors && formData.floors.length > 0 ? (
+
+ {formData.floors.map((floor, idx) => (
+
+ ))}
+
+ ) : (
+
+ {apiError
+ ? 'Error loading floor details from server'
+ : NoFloorDetailsFoundMSG || 'No floor details found'}
+
+ )}
+
+ {error && {error} }
+
+ {/* Show API error if exists */}
+ {apiError && (
+
+ API Error:{' '}
+ {(apiError as any)?.data?.message || 'Failed to load floor details'}
+
+ )}
+
+
+
+ }
+ onClick={handleAddFloor}
+ sx={addButtonSx}
+ disabled={isLoading}
+ >
+ {addFloorText}
+
+
+
+ {mode === 'verify' ? 'Verify' : nextButtonText}
+
+
+
+
+
+ >
+ );
+};
+
+export default FloorDetailsCards;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/FloorDetailsPage.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/FloorDetailsPage.tsx
new file mode 100644
index 0000000..49db4c0
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/FloorDetailsPage.tsx
@@ -0,0 +1,992 @@
+// FloorDetailsPage.tsx
+// Property Form Floor Details entry page
+// Collects and manages details for a single floor in a property
+// Features:
+// - Form for entering floor details (number, classification, usage, dimensions, dates, etc.)
+// - Fetches and updates floor details via RTK Query
+// - Handles form validation, error/success popups, and draft saving
+// - Responsive UI with MUI components and custom dropdowns
+// - Supports both add and edit modes based on navigation state
+// Used in: Property form workflow for floor details entry/edit step
+import React, { useEffect, useState } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Button from '@mui/material/Button';
+import Typography from '@mui/material/Typography';
+//import Checkbox from '@mui/material/Checkbox';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+import dayjs from 'dayjs';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useFormMode } from '../../../context/FormModeContext';
+import { usePropertyForm, type FloorDetails } from '../../../context/PropertyFormContext';
+import { useFloorDetailsLocalization } from '../../../services/AgentLocalisation/localisation-floor-details';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import CustomDropdown, {
+ type DropdownOption,
+} from '../../features/PropertyForm/components/IGRSDetail/IGRSdropdown';
+import FormTextField from '../../features/PropertyForm/components/IGRSDetail/IGRSFormTextFiled';
+import { uniformInputSx, verifyButtonSx } from './styles/sharedStyles';
+import type { AlertType } from '../../models/AlertType.model';
+import CalendarIcon from '../../assets/Agent/date_range.svg';
+import {
+ useCreateFloorDetailsMutation,
+ useGetFloorDetailsByIdQuery,
+ useUpdateFloorDetailsMutation,
+ type FloorDetailsRequest,
+} from '../../../redux/apis/floorApi';
+import { CircularProgress } from '@mui/material';
+import { getUnitOfMeasurementOptions } from '../../../services/jsonServerApiCalls';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+// Existing validateFloor logic (unchanged)
+const validateFloor = (data: any, texts: any, isRequiredText: string) => {
+ const errors: Partial> = {};
+
+ errors.floorNumber =
+ !data.floorNumber || data.floorNumber === ''
+ ? `${texts.floorNumberLabel} ${isRequiredText}.`
+ : '';
+ errors.buildingClassification =
+ !data.buildingClassification || data.buildingClassification === ''
+ ? `${texts.buildingClassificationLabel} ${isRequiredText}.`
+ : '';
+ errors.igrsClassification =
+ !data.igrsClassification || data.igrsClassification === ''
+ ? `${texts.igrsClassificationLabel} ${isRequiredText}.`
+ : '';
+ errors.natureOfUsage =
+ !data.natureOfUsage || data.natureOfUsage === ''
+ ? `${texts.natureOfUsageLabel} ${isRequiredText}.`
+ : '';
+ errors.occupancy =
+ !data.occupancy || data.occupancy === ''
+ ? `${texts.occupancyLabel} ${isRequiredText}.`
+ : '';
+ errors.unstructuredLand =
+ !data.unstructuredLand || data.unstructuredLand === ''
+ ? `${texts.unstructuredLandLabel} ${isRequiredText}.`
+ : '';
+
+ // Alphabets only (optional)
+ if (data.firmName && /[^a-zA-Z\s]/.test(data.firmName)) {
+ errors.firmName = `${texts.firmNameLabel} - Only alphabets allowed`;
+ }
+ if (data.occupantName && /[^a-zA-Z\s]/.test(data.occupantName)) {
+ errors.occupantName = `${texts.occupantNameLabel} - Only alphabets allowed`;
+ }
+
+ // numeric validations
+ const numericCheck = (val: string, label: string, required = true) => {
+ if (!val || val === '') {
+ return required ? `${label} ${isRequiredText}.` : '';
+ }
+ if (!/^\d*\.?\d*$/.test(val)) return `${label} must be a valid number`;
+ if (val === '.') return `${label} must contain at least one digit`;
+ const num = Number(val);
+ if (isNaN(num) || num <= 0) return `${label} must be a positive number`;
+ return '';
+ };
+
+ errors.length = numericCheck(data.length, texts.lengthLabel, true);
+ errors.breadth = numericCheck(data.breadth, texts.breadthLabel, true);
+ errors.plinthArea = numericCheck(data.plinthArea, texts.plinthAreaLabel, true);
+ errors.buildingPermissionNo = numericCheck(
+ data.buildingPermissionNo,
+ texts.buildingPermissionNoLabel,
+ false
+ );
+
+ // Date validations
+ const today = dayjs().endOf('day');
+ if (!data.constructionDate || data.constructionDate === '') {
+ errors.constructionDate = `${texts.constructionDateLabel} ${isRequiredText}.`;
+ } else if (dayjs(data.constructionDate).isAfter(today)) {
+ errors.constructionDate = 'Date cannot be in the future';
+ }
+
+ if (!data.effectiveFromDate || data.effectiveFromDate === '') {
+ errors.effectiveFromDate = `${texts.effectiveFromDateLabel} ${isRequiredText}.`;
+ } else if (dayjs(data.effectiveFromDate).isAfter(today)) {
+ errors.effectiveFromDate = 'Date cannot be in the future';
+ }
+
+ Object.keys(errors).forEach((k) => {
+ if (!errors[k as keyof typeof errors]) delete errors[k as keyof typeof errors];
+ });
+
+ return errors;
+};
+
+const containerSx = {
+ width: '100%',
+ // maxWidth: 480,
+ margin: '0 auto',
+ minHeight: '100vh',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ bgcolor: '#fff',
+};
+
+const headerSx = {
+ backgroundColor: '#F9E6E0',
+ padding: '16px',
+};
+const formContentSx = { flex: 1, px: '8%', py: 3, backgroundColor: '#FFFFFF' };
+const formSubmitSx = { padding: '16px 0', backgroundColor: '#FFFFFF' };
+
+const FloorDetailsPage: React.FC = () => {
+ // Contexts and hooks for form mode, navigation, location, and property form data
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const { formData, updateForm } = usePropertyForm();
+
+ const floorId = (location.state as any)?.floorId as string | undefined;
+ const isEditMode = Boolean(floorId);
+
+ const [createFloorDetails] = useCreateFloorDetailsMutation();
+ const [updateFloorDetails] = useUpdateFloorDetailsMutation();
+
+ const {
+ data: existingFloorData,
+ isLoading: isLoadingFloorData,
+ error: loadError,
+ } = useGetFloorDetailsByIdQuery(floorId!, {
+ skip: !floorId,
+ });
+
+ const { saveDraftText } = useFloorDetailsLocalization();
+ const floorLoc = useFloorDetailsLocalization();
+ const loc = useLocalization();
+
+ const {
+ floorNumberLabel,
+ buildingClassificationLabel,
+ igrsClassificationLabel,
+ natureOfUsageLabel,
+ firmNameLabel,
+ occupancyLabel,
+ constructionDateLabel,
+ effectiveFromDateLabel,
+ unstructuredLandLabel,
+ lengthLabel,
+ breadthLabel,
+ plinthAreaLabel,
+ buildingPermissionNoLabel,
+ //cloneFloorText,
+ addFloorRequiredMsg,
+ selectPlaceholder,
+ floorNumberOptions,
+ buildingClassificationOptions,
+ natureOfUsageOptions,
+ occupancyOptions,
+ unstructuredLandOptions,
+ occupantNameLabel,
+ previousText,
+ propertyFormTitle,
+ newPropertyFormTitle,
+ floorDetailsSubtitle,
+ } = floorLoc;
+
+ const {
+ nextButtonText,
+ onlyNumbersAllowedInText,
+ isRequiredText,
+ OnlyalphabetsareallowedMSG,
+ } = loc;
+
+ const convertApiToLocal = (apiData: any) => {
+ const data = apiData?.data || apiData;
+ return {
+ floorNumber: (data.FloorNo || data.floorNo || '').toString(),
+ buildingClassification: data.Classification || data.classification || '',
+ igrsClassification: 'A',
+ natureOfUsage: data.NatureOfUsage || data.natureOfUsage || '',
+ firmName: data.FirmName || data.firmName || '',
+ occupancy: data.OccupancyType || data.occupancyType || '',
+ constructionDate: data.ConstructionDate || data.constructionDate || '',
+ effectiveFromDate: data.EffectiveFromDate || data.effectiveFromDate || '',
+ unstructuredLand: data.UnstructuredLand || data.unstructuredLand || '',
+ length: (data.LengthFt || data.lengthFt || '').toString(),
+ breadth: (data.BreadthFt || data.breadthFt || '').toString(),
+ plinthArea: (data.PlinthAreaSqFt || data.plinthAreaSqFt || '').toString(),
+ buildingPermissionNo: data.BuildingPermissionNo || data.buildingPermissionNo || '',
+ floorsDetailsEntered: data.FloorDetailsEntered || data.floorDetailsEntered || true,
+ occupantName: data.OccupancyName || data.occupancyName || '',
+ };
+ };
+
+ const getInitialLocalData = () => ({
+ floorNumber: '',
+ buildingClassification: '',
+ igrsClassification: 'A',
+ natureOfUsage: '',
+ firmName: '',
+ occupancy: '',
+ constructionDate: '',
+ effectiveFromDate: '',
+ unstructuredLand: '',
+ length: '',
+ breadth: '',
+ plinthArea: '',
+ buildingPermissionNo: '',
+ floorsDetailsEntered: true,
+ occupantName: '',
+ });
+
+ const [localData, setLocalData] = useState(getInitialLocalData());
+ const [unitOfMeasurement, setUnitOfMeasurement] = useState('');
+ useEffect(() => {
+ getUnitOfMeasurementOptions()
+ .then((data) => {
+ setUnitOfMeasurement(data?.unitOfmeasurement || '');
+ })
+ .catch(() => {});
+ }, []);
+
+ useEffect(() => {
+ if (isEditMode && existingFloorData) {
+ const convertedData = convertApiToLocal(existingFloorData);
+ setLocalData(convertedData);
+ } else if (!isEditMode) {
+ setLocalData(getInitialLocalData());
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [existingFloorData, isEditMode]);
+
+ const toOptions = (arr: string[] | undefined): DropdownOption[] =>
+ (arr || []).map((label, i) => ({ id: i, label: label ?? '' }));
+
+ const [floorNumberOpts] = useState(toOptions(floorNumberOptions));
+ const [buildingClassificationOpts] = useState(
+ toOptions(buildingClassificationOptions)
+ );
+ const [natureOfUsageOpts] = useState(toOptions(natureOfUsageOptions));
+ const [occupancyOpts] = useState(toOptions(occupancyOptions));
+ const [unstructuredLandOpts] = useState(
+ toOptions(unstructuredLandOptions)
+ );
+
+ const [showFloorNumberDropdown, setShowFloorNumberDropdown] = useState(false);
+ const [showBuildingClassificationDropdown, setShowBuildingClassificationDropdown] =
+ useState(false);
+ const [showNatureOfUsageDropdown, setShowNatureOfUsageDropdown] = useState(false);
+ const [showOccupancyDropdown, setShowOccupancyDropdown] = useState(false);
+ const [showUnstructuredLandDropdown, setShowUnstructuredLandDropdown] = useState(false);
+
+ const closeAllDropdowns = () => {
+ setShowFloorNumberDropdown(false);
+ setShowBuildingClassificationDropdown(false);
+ setShowNatureOfUsageDropdown(false);
+ setShowOccupancyDropdown(false);
+ setShowUnstructuredLandDropdown(false);
+ };
+
+ const [fieldErrors, setFieldErrors] = useState>({});
+ const [touchedFields, setTouchedFields] = useState>({});
+ const [error, setError] = useState(null);
+ const [showNumberWarnings, setShowNumberWarnings] = useState>({
+ length: false,
+ breadth: false,
+ plinthArea: false,
+ buildingPermissionNo: false,
+ });
+
+ const markTouched = (name: string) =>
+ setTouchedFields((prev) => ({ ...prev, [name]: true }));
+
+ const handleDropdownSelect = (field: keyof typeof localData) => (val: string) => {
+ setLocalData((prev) => ({ ...prev, [field]: val }));
+ setFieldErrors((prev) => ({ ...prev, [field]: '' }));
+ markTouched(field as string);
+ closeAllDropdowns();
+ };
+
+ const sanitizeNumberValue = (raw: string) => {
+ let v = raw.replace(/[^0-9.]/g, '');
+ if ((v.match(/\./g) || []).length > 1) {
+ const parts = v.split('.');
+ v = parts[0] + '.' + parts.slice(1).join('');
+ }
+ return v;
+ };
+
+ const handleNumberKeyDown = (e: React.KeyboardEvent) => {
+ const forbidden = ['-', 'e', 'E', '+'];
+ if (forbidden.includes(e.key)) {
+ e.preventDefault();
+ }
+ };
+
+ const handleFieldChange = (field: keyof typeof localData) => (value: string) => {
+ let newValue = value;
+
+ if (field === 'firmName' || field === 'occupantName') {
+ if (/[^a-zA-Z\s]/.test(value)) {
+ setFieldErrors((prev) => ({ ...prev, [field]: OnlyalphabetsareallowedMSG }));
+ }
+ newValue = value.replace(/[^a-zA-Z\s]/g, '');
+ }
+
+ if (
+ ['length', 'breadth', 'plinthArea', 'buildingPermissionNo'].includes(
+ field as string
+ )
+ ) {
+ newValue = sanitizeNumberValue(value);
+ if (!/^\d*\.?\d*$/.test(newValue)) {
+ setShowNumberWarnings((prev) => ({ ...prev, [field as string]: true }));
+ } else {
+ setShowNumberWarnings((prev) => ({ ...prev, [field as string]: false }));
+ }
+
+ if (
+ newValue === '' &&
+ touchedFields[field as string] &&
+ field !== 'buildingPermissionNo'
+ ) {
+ const labelMap: any = {
+ length: lengthLabel,
+ breadth: breadthLabel,
+ plinthArea: plinthAreaLabel,
+ };
+ setFieldErrors((prev) => ({
+ ...prev,
+ [field]: `${labelMap[field as string]} ${isRequiredText}.`,
+ }));
+ }
+ }
+
+ setLocalData((prev) => ({ ...prev, [field]: newValue }));
+ if (touchedFields[field as string])
+ setFieldErrors((prev) => ({ ...prev, [field]: '' }));
+ };
+
+ const handleDateChange =
+ (field: 'constructionDate' | 'effectiveFromDate') => (date: any) => {
+ const value = date ? dayjs(date).format('YYYY-MM-DD') : '';
+ setLocalData((prev) => ({ ...prev, [field]: value }));
+ markTouched(field);
+ setFieldErrors((prev) => ({ ...prev, [field]: '' }));
+ };
+
+ // ---- LEADING ZERO REMOVAL ON BLUR ----
+ const handleBlurField = (field: keyof typeof localData) => {
+ markTouched(field as string);
+
+ let value = localData[field];
+
+ // Remove leading zeros for numeric fields on blur
+ if (
+ ['length', 'breadth', 'plinthArea', 'buildingPermissionNo'].includes(field as string) &&
+ typeof value === 'string' &&
+ value !== ''
+) {
+ if (/^\d+(\.\d*)?$/.test(value)) {
+ const parts = value.split('.');
+ parts[0] = String(Number(parts[0]));
+ value = parts.length > 1 ? parts.join('.') : parts[0];
+ setLocalData((prev) => ({ ...prev, [field]: value }));
+ }
+}
+
+ // Retain the rest of your validation logic
+ if (field === 'floorNumber' && (!value || value === '')) {
+ setFieldErrors((prev) => ({
+ ...prev,
+ [field]: `${floorNumberLabel} ${isRequiredText}.`,
+ }));
+ return;
+ }
+
+ if (field === 'buildingClassification' && (!value || value === '')) {
+ setFieldErrors((prev) => ({
+ ...prev,
+ [field]: `${buildingClassificationLabel} ${isRequiredText}.`,
+ }));
+ return;
+ }
+
+ if (field === 'natureOfUsage' && (!value || value === '')) {
+ setFieldErrors((prev) => ({
+ ...prev,
+ [field]: `${natureOfUsageLabel} ${isRequiredText}.`,
+ }));
+ return;
+ }
+
+ if (field === 'occupancy' && (!value || value === '')) {
+ setFieldErrors((prev) => ({
+ ...prev,
+ [field]: `${occupancyLabel} ${isRequiredText}.`,
+ }));
+ return;
+ }
+
+ if (field === 'unstructuredLand' && (!value || value === '')) {
+ setFieldErrors((prev) => ({
+ ...prev,
+ [field]: `${unstructuredLandLabel} ${isRequiredText}.`,
+ }));
+ return;
+ }
+
+ if (
+ ['length', 'breadth', 'plinthArea'].includes(field as string) &&
+ !value
+ ) {
+ const labelMap: any = {
+ length: lengthLabel,
+ breadth: breadthLabel,
+ plinthArea: plinthAreaLabel,
+ };
+ setFieldErrors((prev) => ({
+ ...prev,
+ [field]: `${labelMap[field as string]} ${isRequiredText}.`,
+ }));
+ }
+ };
+
+ const convertLocalToApi = (localData: any): FloorDetailsRequest => ({
+ floorNo: Number(localData.floorNumber),
+ classification: localData.buildingClassification,
+ natureOfUsage: localData.natureOfUsage,
+ firmName: localData.firmName,
+ occupancyType: localData.occupancy,
+ occupancyName: localData.occupantName,
+ constructionDate: localData.constructionDate,
+ effectiveFromDate: localData.effectiveFromDate,
+ unstructuredLand: localData.unstructuredLand,
+ lengthFt: Number(localData.length),
+ breadthFt: Number(localData.breadth),
+ plinthAreaSqFt: Number(localData.plinthArea),
+ buildingPermissionNo: localData.buildingPermissionNo,
+ floorDetailsEntered: true,
+ constructionDetailsId: formData.constructionDetails!.id!.toString(),
+ });
+
+ // handleAddFloor: Validates and submits form, creates/updates floor details via API
+ const handleAddFloor = async (e?: React.FormEvent) => {
+ if (e) e.preventDefault();
+ const textsForValidation = {
+ floorNumberLabel,
+ buildingClassificationLabel,
+ igrsClassificationLabel,
+ natureOfUsageLabel,
+ firmNameLabel,
+ occupancyLabel,
+ constructionDateLabel,
+ effectiveFromDateLabel,
+ unstructuredLandLabel,
+ lengthLabel,
+ breadthLabel,
+ plinthAreaLabel,
+ buildingPermissionNoLabel,
+ isRequiredText,
+ };
+
+ const validation = validateFloor(localData, textsForValidation, isRequiredText);
+ setFieldErrors(validation);
+
+ if (Object.keys(validation).length > 0) {
+ setError(addFloorRequiredMsg);
+ return;
+ }
+ setError(null);
+
+ const newFloor: FloorDetails = {
+ ...localData,
+ length: Number(localData.length),
+ breadth: Number(localData.breadth),
+ plinthArea: Number(localData.plinthArea),
+ buildingPermissionNo: localData.buildingPermissionNo,
+ occupantName: localData.occupantName,
+ };
+
+ try {
+ const apiData = convertLocalToApi(localData);
+ if (isEditMode && floorId) {
+ const result = await updateFloorDetails({ id: floorId, data: apiData }).unwrap();
+ console.log(result);
+
+ const floorIndex =
+ formData.floors?.findIndex((f, idx) => {
+ return (
+ idx === formData.floors?.indexOf(f) &&
+ f.floorNumber === localData.floorNumber
+ );
+ }) ?? -1;
+
+ if (floorIndex !== -1) {
+ const updated = [...(formData.floors ?? [])];
+ updated[floorIndex] = newFloor;
+ updateForm({ floors: updated });
+ }
+ } else {
+ const result = await createFloorDetails(apiData).unwrap();
+ console.log(result);
+
+ updateForm({ floors: [...(formData.floors ?? []), newFloor] });
+ }
+ setLocalData(getInitialLocalData());
+ setFieldErrors({});
+ navigate('/property-form/floor-details-cards');
+ } catch (error: any) {
+ console.error('Failed to save floor details:', error);
+ const errorMessage =
+ error?.data?.message || 'Failed to save floor details. Please try again.';
+ showErrorPopup(errorMessage);
+ setError(errorMessage);
+ }
+ };
+
+ const handleGoBack = () => navigate(-1);
+
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ const handleSaveDraft = () => {
+ updateForm({ floors: formData.floors });
+ };
+
+ if (isEditMode && isLoadingFloorData) {
+ return (
+
+
+ Loading floor details...
+
+ );
+ }
+
+ if (isEditMode && loadError) {
+ return (
+
+
+ Failed to load floor details. Please try again.
+
+ navigate(-1)} sx={{ mt: 2 }}>
+ Go Back
+
+
+ );
+ }
+
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
+
+ e.preventDefault()}>
+
+
+
+ {
+ closeAllDropdowns();
+ setShowFloorNumberDropdown(b);
+ }}
+ onSelect={(_n, v) => handleDropdownSelect('floorNumber')(v)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectPlaceholder}
+ required
+ error={fieldErrors.floorNumber}
+ touched={!!touchedFields.floorNumber}
+ onBlur={() => handleBlurField('floorNumber')}
+ />
+
+
+
+ {
+ closeAllDropdowns();
+ setShowBuildingClassificationDropdown(b);
+ }}
+ onSelect={(_n, v) =>
+ handleDropdownSelect('buildingClassification')(v)
+ }
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectPlaceholder}
+ required
+ error={fieldErrors.buildingClassification}
+ touched={!!touchedFields.buildingClassification}
+ onBlur={() => handleBlurField('buildingClassification')}
+ />
+
+
+ {
+ closeAllDropdowns();
+ setShowNatureOfUsageDropdown(b);
+ }}
+ onSelect={(_n, v) => handleDropdownSelect('natureOfUsage')(v)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectPlaceholder}
+ required
+ error={fieldErrors.natureOfUsage}
+ touched={!!touchedFields.natureOfUsage}
+ onBlur={() => handleBlurField('natureOfUsage')}
+ />
+
+ handleBlurField('firmName')}
+ placeholder=""
+ type="text"
+ required={false}
+ error={
+ fieldErrors.firmName ||
+ (touchedFields.firmName ? fieldErrors.firmName : '')
+ }
+ touched={!!touchedFields.firmName}
+ sx={{ ...uniformInputSx }}
+ />
+
+ {
+ closeAllDropdowns();
+ setShowOccupancyDropdown(b);
+ }}
+ onSelect={(_n, v) => handleDropdownSelect('occupancy')(v)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectPlaceholder}
+ required
+ error={fieldErrors.occupancy}
+ touched={!!touchedFields.occupancy}
+ onBlur={() => handleBlurField('occupancy')}
+ />
+ handleBlurField('occupantName')}
+ placeholder=""
+ type="text"
+ required={false}
+ error={fieldErrors.occupantName}
+ touched={!!touchedFields.occupantName}
+ sx={{ ...uniformInputSx }}
+ />
+
+
+
+ {constructionDateLabel}
+ *
+ :
+
+ (
+
+ ),
+ }}
+ slotProps={{
+ textField: {
+ placeholder: ' ',
+ onBlur: () => handleBlurField('constructionDate'),
+ inputProps: {
+ style: { marginTop: '-8px' },
+ },
+ sx: {
+ width: '60%',
+ '& .MuiOutlinedInput-root': {
+ borderRadius: '12px',
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderRadius: '12px',
+ },
+ },
+ '& .MuiOutlinedInput-input': {
+ padding: '12px 14px',
+ },
+ },
+ },
+ actionBar: { actions: ['clear', 'accept'] },
+ }}
+ maxDate={dayjs()}
+ />
+ {fieldErrors.constructionDate && (
+
+ {fieldErrors.constructionDate}
+
+ )}
+
+
+
+ {effectiveFromDateLabel}
+ *
+ :
+
+ (
+
+ ),
+ }}
+ slotProps={{
+ textField: {
+ placeholder: ' ',
+
+ onBlur: () => handleBlurField('effectiveFromDate'),
+ inputProps: {
+ style: { marginTop: '-8px' },
+ },
+ sx: {
+ width: '60%',
+ '& .MuiOutlinedInput-root': {
+ borderRadius: '12px',
+ '& .MuiOutlinedInput-notchedOutline': {
+ borderRadius: '12px',
+ },
+ },
+ '& .MuiOutlinedInput-input': {
+ padding: '12px 14px',
+ },
+ },
+ },
+ actionBar: { actions: ['clear', 'accept'] },
+ }}
+ maxDate={dayjs()}
+ />
+ {fieldErrors.effectiveFromDate && (
+
+ {fieldErrors.effectiveFromDate}
+
+ )}
+
+
+
+ {
+ closeAllDropdowns();
+ setShowUnstructuredLandDropdown(b);
+ }}
+ onSelect={(_n, v) => handleDropdownSelect('unstructuredLand')(v)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectPlaceholder}
+ required
+ error={fieldErrors.unstructuredLand}
+ touched={!!touchedFields.unstructuredLand}
+ onBlur={() => handleBlurField('unstructuredLand')}
+ />
+
+
+ handleBlurField('length')}
+ placeholder=""
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ touchedFields.length
+ ? fieldErrors.length
+ : showNumberWarnings.length
+ ? `${onlyNumbersAllowedInText} ${lengthLabel}.`
+ : ''
+ }
+ touched={!!touchedFields.length || !!showNumberWarnings.length}
+ sx={{ ...uniformInputSx }}
+ />
+
+ handleBlurField('breadth')}
+ placeholder=""
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ touchedFields.breadth
+ ? fieldErrors.breadth
+ : showNumberWarnings.breadth
+ ? `${onlyNumbersAllowedInText} ${breadthLabel}.`
+ : ''
+ }
+ touched={!!touchedFields.breadth || !!showNumberWarnings.breadth}
+ sx={{ ...uniformInputSx }}
+ />
+
+ handleBlurField('plinthArea')}
+
+ placeholder=""
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown,
+ onFocus:() => markTouched('plinthArea')
+ }}
+ error={
+ touchedFields.plinthArea
+ ? fieldErrors.plinthArea
+ : showNumberWarnings.plinthArea
+ ? `${onlyNumbersAllowedInText} ${plinthAreaLabel}.`
+ : ''
+ }
+ touched={!!touchedFields.plinthArea || !!showNumberWarnings.plinthArea}
+ sx={{ ...uniformInputSx }}
+ />
+ handleBlurField('buildingPermissionNo')}
+
+ placeholder=""
+ type="number"
+ required={false}
+ inputProps={{ onKeyDown: handleNumberKeyDown,
+ onFocus: () => markTouched('buildingPermissionNo')
+ }}
+ error={
+ touchedFields.buildingPermissionNo
+ ? fieldErrors.buildingPermissionNo
+ : showNumberWarnings.buildingPermissionNo
+ ? `${onlyNumbersAllowedInText} ${buildingPermissionNoLabel}.`
+ : ''
+ }
+ touched={
+ !!touchedFields.buildingPermissionNo ||
+ !!showNumberWarnings.buildingPermissionNo
+ }
+ sx={{ ...uniformInputSx }}
+ />
+ {/*
+
+ {cloneFloorText}
+ */}
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {mode === 'verify' ? 'Verify' : nextButtonText}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default FloorDetailsPage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/IGRSAdditionalDetailsPage.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/IGRSAdditionalDetailsPage.tsx
new file mode 100644
index 0000000..2f4712e
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/IGRSAdditionalDetailsPage.tsx
@@ -0,0 +1,276 @@
+import React, { useEffect, useState } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import Checkbox from '@mui/material/Checkbox';
+import FormControlLabel from '@mui/material/FormControlLabel';
+import Button from '@mui/material/Button';
+import { useNavigate } from 'react-router-dom';
+import { useFormMode } from '../../../context/FormModeContext';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+// JsonService & getIsgrAdditionalOptions removed if amenity options are static!
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useAssessmentDetailsLocalization } from '../../../services/AgentLocalisation/localisation-AssessmentDetails';
+import { verifyButtonSx } from './styles/sharedStyles';
+import type { AlertType } from '../../models/AlertType.model';
+import {
+ useGetAmenitiesByPropertyIdQuery,
+ useCreateAmenityMutation,
+ useUpdateAmenityMutation,
+} from '../../../redux/apis/amenitiesApi';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+type IsgrOption = {
+ key: string;
+ label: string;
+};
+
+type LocalData = {
+ lifts: boolean;
+ toilet: boolean;
+ watertap: boolean;
+ cableConnection: boolean;
+ electricity: boolean;
+ attachedBathroom: boolean;
+ waterHarvesting: boolean;
+};
+
+// Amenity options - static mapping. If you need to fetch options, uncomment fetch block below.
+const OPTIONS: IsgrOption[] = [
+ { key: 'lifts', label: 'Lift' },
+ { key: 'toilet', label: 'Toilets' },
+ { key: 'watertap', label: 'Water Tap' },
+ { key: 'cableConnection', label: 'Cable Connection' },
+ { key: 'electricity', label: 'Electricity' },
+ { key: 'attachedBathroom', label: 'Attached Bathroom' },
+ { key: 'waterHarvesting', label: 'Water Harvesting' },
+];
+
+const AMENITY_TYPE_MAPPING: Record = {
+ lifts: 'Lift',
+ toilet: 'Toilets',
+ watertap: 'Water Tap',
+ cableConnection: 'Cable Connection',
+ electricity: 'Electricity',
+ attachedBathroom: 'Attached Bathroom',
+ waterHarvesting: 'Water Harvesting',
+};
+
+const containerSx = { width: '100%', margin: '0 auto', minHeight: '100vh', display: 'flex', flexDirection: 'column', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", bgcolor: '#fff' };
+const headerSx = { backgroundColor: '#F9E6E0', padding: '16px' };
+const formContentSx = { flex: 1, px: '8%', py: 3, backgroundColor: '#FFFFFF' };
+const checkboxWrapperSx = { display: 'flex', flexDirection: 'column', gap: 1 };
+const checkboxLabelSx = { fontSize: 16, color: '#000', fontWeight: 300, textAlign: 'left' };
+const formSubmitSx = { padding: '16px 0', backgroundColor: '#FFFFFF' };
+
+const ISGRAdditionalDetailsPage: React.FC = () => {
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const { formData, updateForm } = usePropertyForm();
+ const {
+ nextButtonText,
+ translateDropdown,
+ // IGRSDetailsText,
+ newPropertyForm,
+ previousText,
+ } = useLocalization();
+ const { propertyFormTitle, saveDraftText } = useAssessmentDetailsLocalization();
+
+ const [localData, setLocalData] = useState({
+ lifts: formData.isgrAdditionalDetails?.lifts || false,
+ toilet: formData.isgrAdditionalDetails?.toilet || false,
+ watertap: formData.isgrAdditionalDetails?.watertap || false,
+ cableConnection: formData.isgrAdditionalDetails?.cableConnection || false,
+ electricity: formData.isgrAdditionalDetails?.electricity || false,
+ attachedBathroom: formData.isgrAdditionalDetails?.attachedBathroom || false,
+ waterHarvesting: formData.isgrAdditionalDetails?.waterHarvesting || false,
+ });
+
+ console.log(formData);
+
+
+ const hasLoadedAmenities = React.useRef(false);
+
+ // API hooks
+ const { data: existingAmenities } =
+ useGetAmenitiesByPropertyIdQuery(formData.id || '', { skip: !formData.id });
+ const [createAmenity] = useCreateAmenityMutation();
+ const [updateAmenity] = useUpdateAmenityMutation();
+
+ // Use static amenity options (or fetch if dynamic)
+ const options = OPTIONS;
+
+ // Convert local form data to array of API amenity types
+ const getSelectedAmenityTypes = (data: LocalData): string[] =>
+ Object.entries(data)
+ .filter(([_, isSelected]) => isSelected)
+ .map(([fieldName]) => AMENITY_TYPE_MAPPING[fieldName as keyof LocalData]);
+
+ // Convert API amenity types to local form data
+ const convertApiDataToLocalData = (amenityTypes: string[]): LocalData => {
+ const result: LocalData = {
+ lifts: false,
+ toilet: false,
+ watertap: false,
+ cableConnection: false,
+ electricity: false,
+ attachedBathroom: false,
+ waterHarvesting: false,
+ };
+ const reverseMapping: Record = {};
+ Object.entries(AMENITY_TYPE_MAPPING).forEach(([field, apiType]) => {
+ reverseMapping[apiType.toLowerCase()] = field as keyof LocalData;
+ });
+ amenityTypes.forEach((apiType) => {
+ const fieldName = reverseMapping[apiType.toLowerCase()];
+ if (fieldName && fieldName in result) result[fieldName] = true;
+ });
+ return result;
+ };
+
+ // Centralized handler for all checkboxes
+ const handleCheckboxChange =
+ (name: keyof LocalData) =>
+ (_event: React.ChangeEvent, checked: boolean) => {
+ setLocalData((prev) => ({ ...prev, [name]: checked }));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!formData.id) {
+ showErrorPopup('Property ID is missing');
+ return;
+ }
+
+ const selectedTypes = getSelectedAmenityTypes(localData);
+
+ try {
+ // Use already loaded amenities, no refetch!
+ const existingAmenity = existingAmenities?.data;
+ if (existingAmenity && existingAmenity.ID) {
+ await updateAmenity({
+ amenityId: existingAmenity.ID,
+ property_id: formData.id,
+ type: selectedTypes,
+ }).unwrap();
+ } else {
+ await createAmenity({
+ property_id: formData.id,
+ type: selectedTypes,
+ }).unwrap();
+ }
+
+ updateForm({ isgrAdditionalDetails: { ...localData } });
+ navigate('/property-form/construction-details');
+ } catch (err: any) {
+ showErrorPopup(err?.data?.message || 'Failed to save amenities');
+ }
+ };
+
+ const handleGoBack = () => navigate(-1);
+
+ // Popup state
+ const [popup, setPopup] = useState<{ type: AlertType; open: boolean; title: string; message: string; duration: number; }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() =>
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ }), 10
+ );
+ }
+
+ // Load existing amenities once when API data is first available (no formData dependency needed)
+ useEffect(() => {
+ if (
+ existingAmenities?.data.type &&
+ existingAmenities.data.type.length > 0 &&
+ !hasLoadedAmenities.current
+ ) {
+ const convertedData = convertApiDataToLocalData(existingAmenities.data.type);
+ setLocalData(convertedData);
+ updateForm({ isgrAdditionalDetails: convertedData });
+ hasLoadedAmenities.current = true;
+ }
+ }, [existingAmenities, updateForm]);
+
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
+
+
+ {options.map((option) => (
+
+ }
+ label={
+
+ {translateDropdown(option.label)}
+
+ }
+ sx={{ alignItems: 'center', gap: 1 }}
+ />
+ ))}
+
+
+
+ {mode === 'verify' ? 'Verify' : nextButtonText}
+
+
+
+
+
+ >
+ );
+};
+
+export default ISGRAdditionalDetailsPage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/IGRSDetailsPage.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/IGRSDetailsPage.tsx
new file mode 100644
index 0000000..f245247
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/IGRSDetailsPage.tsx
@@ -0,0 +1,690 @@
+// ISGRDetailsPage.tsx
+// Property Form ISGR Details page
+// Collects and manages IGRS and building details for a property (habitation, ward, locality, classification, area, setbacks, etc.)
+// Features:
+// - Form for entering IGRS details using dropdowns and text fields
+// - Fetches and updates IGRS details via RTK Query
+// - Handles form validation, error popups, and draft saving
+// - Responsive UI with MUI components and localization
+// - Dynamic dropdowns populated from backend services
+// Used in: Property form workflow for IGRS details step
+import React, { useEffect, useState } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Button from '@mui/material/Button';
+import { useNavigate } from 'react-router-dom';
+import { useFormMode } from '../../../context/FormModeContext';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import JsonService, {
+ getUnitOfMeasurementOptions,
+} from '../../../services/jsonServerApiCalls';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useAssessmentDetailsLocalization } from '../../../services/AgentLocalisation/localisation-AssessmentDetails';
+import CustomDropdown from '../../features/PropertyForm/components/IGRSDetail/IGRSdropdown';
+import type { DropdownOption } from '../../features/PropertyForm/components/IGRSDetail/IGRSdropdown';
+import FormTextField from '../../features/PropertyForm/components/IGRSDetail/IGRSFormTextFiled';
+import { uniformInputSx, verifyButtonSx } from './styles/sharedStyles';
+import type { AlertType } from '../../models/AlertType.model';
+import {
+ useGetIgrsDetailsByIdQuery,
+ useCreateIgrsDetailsMutation,
+ useUpdateIgrsDetailsMutation,
+} from '../../../redux/apis/IgrsDetails.api';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+// Inline styles for layout and UI
+const containerSx = {
+ width: '100%',
+ // maxWidth: 480,
+ margin: '0 auto',
+ minHeight: '100vh',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ bgcolor: '#fff',
+};
+
+const headerSx = { backgroundColor: '#F9E6E0', padding: '16px' };
+const formContentSx = { flex: 1, px: '8%', py: 3, backgroundColor: '#FFFFFF' };
+const formSubmitSx = { padding: '16px 0', backgroundColor: '#FFFFFF' };
+
+const ISGRDetailsPage: React.FC = () => {
+ // Contexts and hooks for form mode, navigation, and property form data
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const { formData, updateForm } = usePropertyForm();
+ const propertyId = formData.id;
+ const igrsId = formData.isgrDetails?.id;
+
+ // RTK Query hooks for fetching and mutating IGRS details
+ const { data: igrsDetailsData } = useGetIgrsDetailsByIdQuery(igrsId!, {
+ skip: !propertyId || !igrsId,
+ });
+
+ const [createIgrsDetails] = useCreateIgrsDetailsMutation();
+ const [updateIgrsDetails] = useUpdateIgrsDetailsMutation();
+
+ // Localization hooks for labels, dropdowns, and UI text
+ const {
+ habitationText,
+ IGRSwardText,
+ IGRSLocalityText,
+ IGRSBlockText,
+ IGRSDoorNoFromText,
+ IGRSDoorNoToText,
+ nextButtonText,
+ selectText,
+ IGRSClassification,
+ builtUpArea,
+ frontSetBack,
+ rearSetBack,
+ sideSetBack,
+ totalPlintArea,
+ //IGRSAndBuildingDetailsText,
+ newPropertyForm,
+ previousText,
+ translateDropdownOptions,
+ onlyNumbersAreAllowedMSG,
+ ThisFieldIsRequiredMSG,
+ PercentagePlaceholder,
+ } = useLocalization();
+
+ const { saveDraftText, propertyFormTitle } = useAssessmentDetailsLocalization();
+
+ const [habitations, setHabitations] = useState([]);
+ const [wards, setWards] = useState([]);
+ const [localities, setLocalities] = useState([]);
+ const [blocks, setBlocks] = useState([]);
+ const [doorNoFrom, setDoorNoFrom] = useState([]);
+ const [doorNoTo, setDoorNoTo] = useState([]);
+ const [igrsClassification, setIgrsClassification] = useState([]);
+
+ const [rawHabitations, setRawHabitations] = useState([]);
+ const [rawWards, setRawWards] = useState([]);
+ const [rawLocalities, setRawLocalities] = useState([]);
+ const [rawBlocks, setRawBlocks] = useState([]);
+ const [rawDoorNoFrom, setRawDoorNoFrom] = useState([]);
+ const [rawDoorNoTo, setRawDoorNoTo] = useState([]);
+ const [rawIgrsClassification, setRawIgrsClassification] = useState(
+ []
+ );
+
+ const [showHabitationDropdown, setShowHabitationDropdown] = useState(false);
+ const [showIgrsWardDropdown, setShowIgrsWardDropdown] = useState(false);
+ const [showIgrsLocalityDropdown, setShowIgrsLocalityDropdown] = useState(false);
+ const [showIgrsBlockDropdown, setShowIgrsBlockDropdown] = useState(false);
+ const [showDoorNoFromDropdown, setShowDoorNoFromDropdown] = useState(false);
+ const [showDoorNoToDropdown, setShowDoorNoToDropdown] = useState(false);
+ const [showIgrsClassificationDropdown, setShowIgrsClassificationDropdown] =
+ useState(false);
+
+ const [unitOfMeasurement, setUnitOfMeasurement] = useState('');
+ // Effect: Fetch unit of measurement from MDMS
+ useEffect(() => {
+ getUnitOfMeasurementOptions()
+ .then((data) => {
+ setUnitOfMeasurement(data?.unitOfmeasurement || '');
+ })
+ .catch(() => {
+ console.log('Failed to fetch Unit of Measurement Options');
+ });
+ }, []);
+
+ useEffect(() => {
+ if (formData.isgrDetails && propertyId) {
+ updateForm({
+ isgrDetails: {
+ id: formData.isgrDetails?.id,
+ habitation: formData.isgrDetails?.habitation ?? '',
+ igrsWard: formData.isgrDetails?.igrsWard ?? '',
+ igrsLocality: formData.isgrDetails?.igrsLocality ?? '',
+ igrsBlock: formData.isgrDetails?.igrsBlock ?? '',
+ doorNoFrom: formData.isgrDetails?.doorNoFrom ?? '',
+ doorNoTo: formData.isgrDetails?.doorNoTo ?? '',
+ igrsClassification: formData.isgrDetails?.igrsClassification ?? '',
+ builtUpAreaPct: formData.isgrDetails?.builtUpAreaPct ?? '',
+ frontSetback: formData.isgrDetails?.frontSetback ?? '',
+ rearSetback: formData.isgrDetails?.rearSetback ?? '',
+ sideSetback: formData.isgrDetails?.sideSetback ?? '',
+ totalPlinthArea: formData.isgrDetails?.totalPlinthArea ?? '',
+ },
+ });
+ }
+ }, [igrsDetailsData]);
+
+ const [errors, setErrors] = useState>({});
+ const [touched, setTouched] = useState>({});
+ const [showNumberWarnings, setShowNumberWarnings] = useState>(
+ {}
+ );
+
+ useEffect(() => {
+ JsonService.getHabitations().then(setRawHabitations);
+ JsonService.getIgrsWards().then(setRawWards);
+ JsonService.getIgrsLocalities().then(setRawLocalities);
+ JsonService.getIgrsBlocks().then(setRawBlocks);
+ JsonService.getDoorNoFrom().then(setRawDoorNoFrom);
+ JsonService.getDoorNoTo().then(setRawDoorNoTo);
+ JsonService.getIgrsClassifications().then(setRawIgrsClassification);
+ }, []);
+
+ useEffect(() => {
+ const normalize = (arr: DropdownOption[]) =>
+ (translateDropdownOptions(arr) || []).map((o) => ({
+ id: o.id,
+ label: o.label ?? '',
+ }));
+
+ setHabitations(normalize(rawHabitations));
+ setWards(normalize(rawWards));
+ setLocalities(normalize(rawLocalities));
+ setBlocks(normalize(rawBlocks));
+ setDoorNoFrom(normalize(rawDoorNoFrom));
+ setDoorNoTo(normalize(rawDoorNoTo));
+ setIgrsClassification(normalize(rawIgrsClassification));
+ }, [
+ rawHabitations,
+ rawWards,
+ rawLocalities,
+ rawBlocks,
+ rawDoorNoFrom,
+ rawDoorNoTo,
+ rawIgrsClassification,
+ translateDropdownOptions,
+ ]);
+
+ // === Validation helper
+ const validateField = (name: string, value: string): string => {
+ if (!value || value.trim() === '')
+ return ThisFieldIsRequiredMSG || 'This field is required';
+ if (
+ [
+ 'builtUpAreaPct',
+ 'frontSetback',
+ 'rearSetback',
+ 'sideSetback',
+ 'totalPlinthArea',
+ ].includes(name)
+ ) {
+ if (!/^\d*\.?\d*$/.test(value)) return 'Must be a valid number';
+ if (value === '.') return 'Must contain at least one digit';
+ const num = Number(value);
+ if (isNaN(num) || num <= 0) return 'Must be a positive';
+ if (name === 'builtUpAreaPct' && num > 100) return 'Cannot exceed 100';
+ }
+ return '';
+ };
+
+ // === ENHANCED: strip leading zeros from number fields on blur!
+ const handleBlur = (name: string) => {
+ setTouched((prev) => ({ ...prev, [name]: true }));
+ let value = String(formData.isgrDetails?.[name as keyof typeof formData.isgrDetails] ?? '');
+
+ // Strip leading zeros for numeric fields on blur
+ if (
+ [
+ 'builtUpAreaPct',
+ 'frontSetback',
+ 'rearSetback',
+ 'sideSetback',
+ 'totalPlinthArea',
+ ].includes(name) &&
+ value !== ''
+ ) {
+ if (/^\d+(\.\d*)?$/.test(value)) {
+ const parts = value.split('.');
+ parts[0] = String(Number(parts[0]));
+ value = parts.length > 1 ? parts.join('.') : parts[0];
+ // Update in formData via updateForm!
+ updateForm({
+ isgrDetails: {
+ ...formData.isgrDetails,
+ [name]: value,
+ }
+ });
+ }
+ }
+
+ const error: string = validateField(name, value);
+ setErrors((prev) => ({ ...prev, [name]: error }));
+ };
+
+ const handleNumberKeyDown = (e: React.KeyboardEvent) => {
+ const forbidden = ['-', 'e', '+'];
+ if (forbidden.includes(e.key)) {
+ e.preventDefault();
+ }
+ };
+
+ const handleInputChange = (field: string) => (value: string) => {
+ const isValid = /^\d*\.?\d*$/.test(value);
+ if (!isValid && value !== '') {
+ setShowNumberWarnings((prev) => ({ ...prev, [field]: true }));
+ return;
+ }
+
+ updateForm({
+ isgrDetails: {
+ ...formData.isgrDetails,
+ [field]: value,
+ },
+ });
+
+ if (showNumberWarnings[field]) {
+ if (isValid) setShowNumberWarnings((prev) => ({ ...prev, [field]: false }));
+ }
+ if (touched[field]) setErrors((prev) => ({ ...prev, [field]: '' }));
+ };
+
+ // closeAllDropdowns: Helper to close all dropdowns before opening one
+ const closeAllDropdowns = () => {
+ setShowHabitationDropdown(false);
+ setShowIgrsWardDropdown(false);
+ setShowIgrsLocalityDropdown(false);
+ setShowIgrsBlockDropdown(false);
+ setShowDoorNoFromDropdown(false);
+ setShowDoorNoToDropdown(false);
+ setShowIgrsClassificationDropdown(false);
+ };
+
+ // handleDropdownSelect: Handles selection from dropdowns
+ const handleDropdownSelect = (field: string) => (val: string) => {
+ updateForm({
+ isgrDetails: {
+ ...formData.isgrDetails,
+ [field]: val,
+ },
+ });
+ setErrors((prev) => ({ ...prev, [field]: '' }));
+ setTouched((prev) => ({ ...prev, [field]: true }));
+ };
+
+ // handleSubmit: Validates and submits IGRS details, creates/updates via API
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ const fieldsToValidate = [
+ 'habitation',
+ 'igrsWard',
+ 'igrsLocality',
+ 'igrsBlock',
+ 'doorNoFrom',
+ 'doorNoTo',
+ 'igrsClassification',
+ 'builtUpAreaPct',
+ 'frontSetback',
+ 'rearSetback',
+ 'sideSetback',
+ 'totalPlinthArea',
+ ];
+
+ const submitErrors: Record = {};
+ fieldsToValidate.forEach((field) => {
+ const value =
+ String(formData.isgrDetails?.[field as keyof typeof formData.isgrDetails] ?? '');
+ const error = validateField(field, value);
+ submitErrors[field] = error;
+ });
+
+ setErrors(submitErrors);
+ setTouched(fieldsToValidate.reduce((acc, k) => ({ ...acc, [k]: true }), {}));
+
+ if (Object.values(submitErrors).some(Boolean)) return;
+
+ if (!propertyId) {
+ showErrorPopup('Property ID is required.');
+ return;
+ }
+
+ const requestBody = {
+ propertyId: propertyId,
+ habitation: formData.isgrDetails?.habitation ?? '',
+ igrsWard: formData.isgrDetails?.igrsWard ?? '',
+ igrsLocality: formData.isgrDetails?.igrsLocality ?? '',
+ igrsBlock: formData.isgrDetails?.igrsBlock ?? '',
+ doorNoFrom: formData.isgrDetails?.doorNoFrom ?? '',
+ doorNoTo: formData.isgrDetails?.doorNoTo ?? '',
+ igrsClassification: formData.isgrDetails?.igrsClassification ?? '',
+ builtUpAreaPct: Number(formData.isgrDetails?.builtUpAreaPct || 0),
+ frontSetback: Number(formData.isgrDetails?.frontSetback || 0),
+ rearSetback: Number(formData.isgrDetails?.rearSetback || 0),
+ sideSetback: Number(formData.isgrDetails?.sideSetback || 0),
+ totalPlinthArea: Number(formData.isgrDetails?.totalPlinthArea || 0),
+ };
+
+ try {
+ let resp;
+ if (igrsId) {
+ resp = await updateIgrsDetails({
+ id: igrsId,
+ body: requestBody,
+ }).unwrap();
+ } else {
+ resp = await createIgrsDetails(requestBody).unwrap();
+ }
+
+ if (resp?.data) {
+ updateForm({
+ isgrDetails: {
+ id: resp.data.id || '',
+ habitation: resp.data.habitation || '',
+ igrsWard: resp.data.igrsWard || '',
+ igrsLocality: resp.data.igrsLocality || '',
+ igrsBlock: resp.data.igrsBlock || '',
+ doorNoFrom: resp.data.doorNoFrom || '',
+ doorNoTo: resp.data.doorNoTo || '',
+ igrsClassification: resp.data.igrsClassification || '',
+ builtUpAreaPct: resp.data.builtUpAreaPct ?? 0,
+ frontSetback: resp.data.frontSetback ?? 0,
+ rearSetback: resp.data.rearSetback ?? 0,
+ sideSetback: resp.data.sideSetback ?? 0,
+ totalPlinthArea: resp.data.totalPlinthArea ?? 0,
+ },
+ });
+ }
+
+ navigate('/property-form/igrs-additional-details');
+ } catch (error) {
+ console.error('Error saving IGRS details:', error);
+ showErrorPopup('Failed to save IGRS details.');
+ }
+ };
+
+ // Popup state and showErrorPopup: Manages warning popup for errors
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ // handleGoBack: Navigates to previous page
+ const handleGoBack = () => navigate(-1);
+ // handleSaveDraft: Saves current IGRS details as draft in context
+ const handleSaveDraft = () => {
+ //save draft function
+ };
+
+ const getFieldValue = (field: string) => {
+ const v = formData.isgrDetails?.[field as keyof typeof formData.isgrDetails];
+ return v ?? '';
+ };
+
+ // UI rendering: WarningPopup, header, form with dropdowns, text fields, and submit button
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
+
+
+
+
+
+ handleDropdownSelect('habitation')(val)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectText}
+ required
+ error={errors.habitation}
+ touched={touched.habitation}
+ onBlur={() => handleBlur('habitation')}
+ />
+
+
+
+ handleDropdownSelect('igrsWard')(val)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectText}
+ required
+ error={errors.igrsWard}
+ touched={touched.igrsWard}
+ onBlur={() => handleBlur('igrsWard')}
+ />
+
+
+
+ handleDropdownSelect('igrsLocality')(val)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectText}
+ required
+ error={errors.igrsLocality}
+ touched={touched.igrsLocality}
+ onBlur={() => handleBlur('igrsLocality')}
+ />
+
+ handleDropdownSelect('igrsBlock')(val)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectText}
+ required
+ error={errors.igrsBlock}
+ touched={touched.igrsBlock}
+ onBlur={() => handleBlur('igrsBlock')}
+ />
+
+
+
+ handleDropdownSelect('doorNoFrom')(val)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectText}
+ required
+ error={errors.doorNoFrom}
+ touched={touched.doorNoFrom}
+ onBlur={() => handleBlur('doorNoFrom')}
+ />
+
+
+
+ handleDropdownSelect('doorNoTo')(val)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectText}
+ required
+ error={errors.doorNoTo}
+ touched={touched.doorNoTo}
+ onBlur={() => handleBlur('doorNoTo')}
+ />
+
+
+
+ handleDropdownSelect('igrsClassification')(val)}
+ closeOtherDropdowns={closeAllDropdowns}
+ selectText={selectText}
+ required
+ error={errors.igrsClassification}
+ touched={touched.igrsClassification}
+ onBlur={() => handleBlur('igrsClassification')}
+ />
+
+ handleBlur('builtUpAreaPct')}
+ placeholder={PercentagePlaceholder}
+ type="number"
+ required
+ inputProps={{ min: 0, max: 100, onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.builtUpAreaPct
+ ? onlyNumbersAreAllowedMSG
+ : errors.builtUpAreaPct
+ }
+ touched={touched.builtUpAreaPct || showNumberWarnings.builtUpAreaPct}
+ />
+
+
+ handleBlur('frontSetback')}
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.frontSetback
+ ? onlyNumbersAreAllowedMSG
+ : errors.frontSetback
+ }
+ touched={touched.frontSetback || showNumberWarnings.frontSetback}
+ />
+ handleBlur('rearSetback')}
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.rearSetback
+ ? onlyNumbersAreAllowedMSG
+ : errors.rearSetback
+ }
+ touched={touched.rearSetback || showNumberWarnings.rearSetback}
+ />
+
+
+ handleBlur('sideSetback')}
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.sideSetback
+ ? onlyNumbersAreAllowedMSG
+ : errors.sideSetback
+ }
+ touched={touched.sideSetback || showNumberWarnings.sideSetback}
+ />
+
+ handleBlur('totalPlinthArea')}
+ type="number"
+ required
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ error={
+ showNumberWarnings.totalPlinthArea
+ ? onlyNumbersAreAllowedMSG
+ : errors.totalPlinthArea
+ }
+ touched={touched.totalPlinthArea || showNumberWarnings.totalPlinthArea}
+ />
+
+
+
+ {mode === 'verify' ? 'Verify' : nextButtonText}
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ISGRDetailsPage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/LocationMapWithDrawing.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/LocationMapWithDrawing.tsx
new file mode 100644
index 0000000..c8e378c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/LocationMapWithDrawing.tsx
@@ -0,0 +1,1101 @@
+// LocationMapWithDrawing.tsx
+// Interactive map component for property location selection and polygon drawing
+// Features:
+// - Uses MapLibre GL for map rendering and drawing
+// - Allows users to draw polygons to mark property boundaries
+// - Supports reverse geocoding to fetch address for selected location
+// - Handles both read-only and interactive modes
+// - Exposes control methods to parent via refs
+// - Responsive UI with custom overlays and popups
+// Used in: Property form workflow for selecting and drawing property location
+
+import React, { useState, useEffect, useRef } from 'react';
+import maplibregl from 'maplibre-gl';
+import 'maplibre-gl/dist/maplibre-gl.css';
+import '../../../styles/CustomDrawingToolbar.css';
+import locatePropertyIcon from '../../../assets/CitizenAssets/mycity_page/locateProperty.svg';
+
+interface LocationMapWithDrawingProps {
+ center: [number, number];
+ onLocationUpdate: (lat: number, lng: number, address: string) => void;
+ addressLabel?: string;
+ onShapeDrawn?: (shapeData: any) => void;
+ initialShapes?: Array<{
+ type: 'polyline' | 'rectangle' | 'polygon' | 'point';
+ coordinates: number[] | number[][];
+ area?: number;
+ }>;
+ readOnly?: boolean;
+ // optional external map ref to allow parent to control map (pan/zoom)
+ externalMapRef?: React.MutableRefObject;
+ // external ref to call finish or start from parent
+ externalFinishRef?: React.MutableRefObject<{
+ finish?: () => void;
+ start?: () => void;
+ clear?: () => void;
+ } | null>;
+ onClearAll?: () => void;
+ startDrawing?: boolean;
+ // notify parent when user starts moving the map (used to hide center instruction)
+ onMapMoveStart?: () => void;
+ // notify parent when the map locks/unlocks a selected location (true when locked)
+ onLocationLockChange?: (locked: boolean) => void;
+ // (deprecated) showCenterInstruction removed; center pin overlay is always visible
+}
+
+// Shape data interface
+interface ShapeData {
+ type: 'rectangle' | 'polygon';
+ coordinates: number[] | number[][];
+ area?: number;
+ id: string;
+ address?: string;
+ addedAt?: string;
+}
+
+// Drawing tool types
+type DrawingTool = 'polygon' | null;
+
+// Helper function for reverse geocoding
+const getReverseGeocodedAddress = async (lat: number, lng: number): Promise => {
+ try {
+ const response = await fetch(
+ `https://photon.komoot.io/reverse?lat=${lat}&lon=${lng}`,
+ {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ },
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.features && data.features.length > 0) {
+ const feature = data.features[0];
+ if (feature.properties) {
+ const props = feature.properties;
+ const parts = [];
+
+ if (props.housenumber) parts.push(props.housenumber);
+ if (props.street) parts.push(props.street);
+ if (props.district) parts.push(props.district);
+ if (props.city) parts.push(props.city);
+ if (props.state) parts.push(props.state);
+
+ if (parts.length > 0) {
+ return parts.join(', ');
+ }
+
+ if (props.name) {
+ return props.name;
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.warn('Geocoding failed:', error);
+ }
+
+ return `Location: ${lat.toFixed(4)}ยฐN, ${lng.toFixed(4)}ยฐE`;
+};
+
+const LocationMapWithDrawing: React.FC = ({
+ center,
+ onLocationUpdate,
+ onShapeDrawn,
+ initialShapes = [],
+ readOnly = false,
+ addressLabel,
+ externalMapRef,
+ externalFinishRef,
+ onClearAll,
+ startDrawing,
+ onMapMoveStart,
+ onLocationLockChange,
+}) => {
+ const mapContainerRef = useRef(null);
+ const mapRef = useRef(null);
+ const debounceTimeoutRef = useRef(null);
+ const isDrawingRef = useRef(false);
+ const activeToolRef = useRef(null);
+ const programmaticPanRef = useRef(false);
+ const markersRef = useRef([]);
+ const polygonLayersRef = useRef([]);
+ const lastCentroidShapeIdRef = useRef(null);
+
+ const [activeTool, setActiveTool] = useState(null);
+ const shapesVisible = true;
+ const [isDrawing, setIsDrawing] = useState(false);
+ const [drawnShapes, setDrawnShapes] = useState([]);
+ const [currentPath, setCurrentPath] = useState<[number, number][]>([]);
+ const [polygonCentroid, setPolygonCentroid] = useState<[number, number] | null>(null);
+ const [locationLocked, setLocationLocked] = useState(false);
+ const [mapReady, setMapReady] = useState(false);
+ // Clear all drawn shapes and reset selection
+ const clearAllShapes = () => {
+ setDrawnShapes([]);
+ setCurrentPath([]);
+ setPolygonCentroid(null);
+ setLocationLocked(false);
+
+ // Remove all markers
+ markersRef.current.forEach((marker) => marker.remove());
+ markersRef.current = [];
+
+ // Remove all polygon layers
+ if (mapRef.current) {
+ polygonLayersRef.current.forEach((layerId) => {
+ if (mapRef.current!.getLayer(layerId)) {
+ mapRef.current!.removeLayer(layerId);
+ }
+ if (mapRef.current!.getLayer(`${layerId}-outline`)) {
+ mapRef.current!.removeLayer(`${layerId}-outline`);
+ }
+ if (mapRef.current!.getSource(layerId)) {
+ mapRef.current!.removeSource(layerId);
+ }
+ });
+ polygonLayersRef.current = [];
+ // clear remembered centroid shape id
+ lastCentroidShapeIdRef.current = null;
+ }
+
+ // Notify parent if provided
+ if (onLocationLockChange) {
+ try {
+ onLocationLockChange(false);
+ } catch (e) {
+ /* ignore */
+ }
+ }
+ if (onClearAll) {
+ try {
+ onClearAll();
+ } catch (e) {
+ /* ignore */
+ }
+ }
+ };
+
+ // Calculate polygon area (simple approximation)
+ const calculatePolygonArea = (coordinates: [number, number][]): number => {
+ if (coordinates.length < 3) return 0;
+
+ let area = 0;
+ for (let i = 0; i < coordinates.length; i++) {
+ const j = (i + 1) % coordinates.length;
+ area += coordinates[i][0] * coordinates[j][1];
+ area -= coordinates[j][0] * coordinates[i][1];
+ }
+ return Math.abs(area / 2) * 111320 * 111320; // Rough conversion to square meters
+ };
+
+ // Add polygon to map
+ const addPolygonToMap = (shape: ShapeData) => {
+ if (!mapRef.current || shape.type !== 'polygon') return;
+
+ // Guard: ensure map style is loaded before adding sources/layers
+ if (!mapRef.current.isStyleLoaded()) {
+ console.warn('Map style not loaded yet, deferring polygon add');
+ return;
+ }
+
+ const coords = shape.coordinates as number[][];
+ const sourceId = shape.id;
+
+ // Convert coordinates to GeoJSON format [lng, lat]
+ const coordinates = coords.map(([lng, lat]) => [lng, lat]);
+ coordinates.push(coordinates[0]); // Close the polygon
+
+ const geojson = {
+ type: 'Feature' as const,
+ geometry: {
+ type: 'Polygon' as const,
+ coordinates: [coordinates],
+ },
+ properties: {
+ id: shape.id,
+ address: shape.address,
+ area: shape.area,
+ },
+ };
+
+ // Add source
+ if (!mapRef.current.getSource(sourceId)) {
+ mapRef.current.addSource(sourceId, {
+ type: 'geojson',
+ data: geojson as any,
+ });
+ }
+
+ // Add fill layer
+ const fillLayerId = sourceId;
+ if (!mapRef.current.getLayer(fillLayerId)) {
+ mapRef.current.addLayer({
+ id: fillLayerId,
+ type: 'fill',
+ source: sourceId,
+ paint: {
+ 'fill-color': readOnly ? '#C84C0E' : '#7f1d1d',
+ 'fill-opacity': readOnly ? 0.2 : 0.3,
+ },
+ });
+ polygonLayersRef.current.push(fillLayerId);
+ }
+
+ // Add outline layer
+ const outlineLayerId = `${sourceId}-outline`;
+ if (!mapRef.current.getLayer(outlineLayerId)) {
+ mapRef.current.addLayer({
+ id: outlineLayerId,
+ type: 'line',
+ source: sourceId,
+ paint: {
+ 'line-color': readOnly ? '#C84C0E' : '#7f1d1d',
+ 'line-width': readOnly ? 3 : 4,
+ },
+ });
+ polygonLayersRef.current.push(outlineLayerId);
+ }
+ };
+
+ // Add marker to map
+ const addMarkerToMap = (lng: number, lat: number, iconUrl: string) => {
+ if (!mapRef.current) {
+ console.warn('Map ref not available when trying to add marker');
+ return null;
+ }
+
+ // Create image element for the marker
+ const img = document.createElement('img');
+ img.src = iconUrl;
+ img.style.width = '36px';
+ img.style.height = '36px';
+ img.style.cursor = 'pointer';
+ img.alt = 'Location marker';
+
+ // Create wrapper div
+ const el = document.createElement('div');
+ el.className = 'custom-marker';
+ el.style.width = '36px';
+ el.style.height = '36px';
+ el.appendChild(img);
+
+ const marker = new maplibregl.Marker({
+ element: el,
+ anchor: 'bottom',
+ offset: [0, 0],
+ })
+ .setLngLat([lng, lat])
+ .addTo(mapRef.current);
+
+ markersRef.current.push(marker);
+ return marker;
+ };
+
+ // Helper function to compute polygon centroid
+ const computePolygonCentroid = (coords: [number, number][]) => {
+ // coords are [lat, lng]
+ const pts = coords.map(([lat, lng]) => ({ x: lng, y: lat }));
+ let twiceArea = 0;
+ let xSum = 0;
+ let ySum = 0;
+ for (let i = 0; i < pts.length; i++) {
+ const j = (i + 1) % pts.length;
+ const cross = pts[i].x * pts[j].y - pts[j].x * pts[i].y;
+ twiceArea += cross;
+ xSum += (pts[i].x + pts[j].x) * cross;
+ ySum += (pts[i].y + pts[j].y) * cross;
+ }
+ const area = twiceArea / 2;
+ if (Math.abs(area) < 1e-9) {
+ // fallback to average of points
+ const avg = pts.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), {
+ x: 0,
+ y: 0,
+ });
+ return { lat: avg.y / pts.length, lng: avg.x / pts.length };
+ }
+ const cx = xSum / (6 * area);
+ const cy = ySum / (6 * area);
+ return { lat: cy, lng: cx };
+ };
+
+ // Compute centroid using map projection (pixel coordinates) for better accuracy
+ // on very small polygons. Falls back to geographic centroid if map not ready.
+ const computePolygonCentroidProjected = (coords: [number, number][]) => {
+ // coords are [lat, lng]
+ try {
+ const map = mapRef.current;
+ if (!map || !map.loaded()) {
+ return computePolygonCentroid(coords);
+ }
+
+ // Project geographic coordinates to screen (pixel) coordinates
+ const projected = coords.map(([lat, lng]) => {
+ const p = map.project([lng, lat]);
+ return { x: p.x, y: p.y };
+ });
+
+ // Compute centroid in projected space
+ let twiceArea = 0;
+ let xSum = 0;
+ let ySum = 0;
+ for (let i = 0; i < projected.length; i++) {
+ const j = (i + 1) % projected.length;
+ const cross = projected[i].x * projected[j].y - projected[j].x * projected[i].y;
+ twiceArea += cross;
+ xSum += (projected[i].x + projected[j].x) * cross;
+ ySum += (projected[i].y + projected[j].y) * cross;
+ }
+ const area = twiceArea / 2;
+ if (Math.abs(area) < 1e-6) {
+ // fallback to average
+ const avg = projected.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), {
+ x: 0,
+ y: 0,
+ });
+ const cx = avg.x / projected.length;
+ const cy = avg.y / projected.length;
+ const un = map.unproject([cx, cy]);
+ return { lat: un.lat, lng: un.lng };
+ }
+ const cx = xSum / (6 * area);
+ const cy = ySum / (6 * area);
+
+ // Unproject back to geographic coordinates (use array form)
+ const un = map.unproject([cx, cy]);
+ return { lat: un.lat, lng: un.lng };
+ } catch (e) {
+ // anything fails, fallback to geographic centroid
+ return computePolygonCentroid(coords);
+ }
+ };
+
+ // Initialize shapes from props
+ useEffect(() => {
+ if (initialShapes.length > 0) {
+ // Filter out any legacy polylines (we no longer support polyline drawing)
+ const shapes = initialShapes
+ .filter((shape) => shape.type !== 'polyline')
+ .map(
+ (shape, index) =>
+ ({
+ id: `initial_${shape.type}_${index}`,
+ type: shape.type as 'rectangle' | 'polygon',
+ coordinates: shape.coordinates,
+ area: (shape as any).area,
+ address: (shape as any).address,
+ } as ShapeData)
+ );
+ setDrawnShapes(shapes);
+
+ // If there's a polygon in initial shapes, calculate and set its centroid
+ const polygon = shapes.find((shape) => shape.type === 'polygon');
+ if (polygon && polygon.coordinates) {
+ const coords = polygon.coordinates as number[][];
+ // Convert from [lng, lat] to [lat, lng] format for centroid calculation
+ const latLngCoords: [number, number][] = coords.map(([lng, lat]) => [lat, lng]);
+ const centroid = computePolygonCentroidProjected(latLngCoords);
+ setPolygonCentroid([centroid.lat, centroid.lng]);
+ }
+ }
+ }, [initialShapes]);
+
+ // If we get initialShapes and the map is already ready, ensure they are rendered on the map
+ useEffect(() => {
+ if (!mapRef.current || !mapReady) return;
+ if (initialShapes.length === 0) return;
+ if (readOnly) return;
+
+ // Convert incoming shapes same as above
+ const shapes = initialShapes
+ .filter((shape) => shape.type !== 'polyline')
+ .map(
+ (shape, index) =>
+ ({
+ id: `initial_${shape.type}_${index}`,
+ type: shape.type as 'rectangle' | 'polygon',
+ coordinates: shape.coordinates,
+ area: (shape as any).area,
+ address: (shape as any).address,
+ } as ShapeData)
+ );
+
+ // Remove any existing polygon layers first
+ polygonLayersRef.current.forEach((layerId) => {
+ try {
+ if (!mapRef.current) return;
+ if (mapRef.current.getLayer(layerId)) mapRef.current.removeLayer(layerId);
+ if (mapRef.current.getLayer(`${layerId}-outline`))
+ mapRef.current.removeLayer(`${layerId}-outline`);
+ if (mapRef.current.getSource(layerId)) mapRef.current.removeSource(layerId);
+ } catch (e) {
+ // ignore
+ }
+ });
+ polygonLayersRef.current = [];
+
+ shapes.forEach((s) => {
+ if (s.type === 'polygon') addPolygonToMap(s);
+ });
+ }, [initialShapes, mapReady]);
+
+ // start drawing mode when requested by parent
+ useEffect(() => {
+ if (typeof startDrawing !== 'undefined' && startDrawing) {
+ setActiveTool('polygon');
+ setIsDrawing(true);
+ }
+ }, [startDrawing]);
+
+ // keep refs in sync for event listeners attached in callbacks
+ useEffect(() => {
+ isDrawingRef.current = isDrawing;
+ }, [isDrawing]);
+ useEffect(() => {
+ activeToolRef.current = activeTool;
+ }, [activeTool]);
+
+ // Update polygon centroid when drawn shapes change
+ useEffect(() => {
+ // If we are programmatically panning the map (flyTo) after finishShape,
+ // skip recomputing centroid until the pan finishes to avoid marker jumping.
+ if (programmaticPanRef.current) {
+ // console.log('Skipping centroid update while programmatic pan is active');
+ return;
+ }
+
+ const polygon = drawnShapes.find((shape) => shape.type === 'polygon');
+ if (polygon && polygon.coordinates) {
+ // if the centroid was just set from finishShape for this shape id, skip recomputing to avoid marker jump
+ if (
+ lastCentroidShapeIdRef.current &&
+ polygon.id === lastCentroidShapeIdRef.current
+ ) {
+ // clear the ref after skipping once so future edits will update centroid
+ lastCentroidShapeIdRef.current = null;
+ return;
+ }
+ const coords = polygon.coordinates as number[][];
+ // Convert from [lng, lat] to [lat, lng] format for centroid calculation
+ const latLngCoords: [number, number][] = coords.map(([lng, lat]) => [lat, lng]);
+ const centroid = computePolygonCentroidProjected(latLngCoords);
+ setPolygonCentroid([centroid.lat, centroid.lng]);
+ } else {
+ // No polygon found, clear the centroid
+ setPolygonCentroid(null);
+ }
+ }, [drawnShapes]);
+
+ // Note: point/pin drawing removed - only polygon drawing supported
+
+ // Handle path click (for lines and polygons)
+ const handlePathClick = (lat: number, lng: number) => {
+ setCurrentPath((prev) => {
+ const newPoint: [number, number] = [lat, lng];
+ // If last point equals new point (within tiny epsilon), skip adding
+ const last = prev[prev.length - 1];
+ const eps = 1e-7;
+ if (
+ last &&
+ Math.abs(last[0] - newPoint[0]) < eps &&
+ Math.abs(last[1] - newPoint[1]) < eps
+ ) {
+ return prev;
+ }
+ const newPath: [number, number][] = [...prev, newPoint];
+ return newPath;
+ });
+ };
+
+ // Finish drawing polygon
+ const finishShape = async () => {
+ if (currentPath.length < 3) return; // Need at least 3 points for a polygon
+ // Remove consecutive near-duplicate points to keep only real vertices
+ const eps = 1e-7;
+ const filteredPath: [number, number][] = [];
+ for (const pt of currentPath) {
+ const last = filteredPath[filteredPath.length - 1];
+ if (last && Math.abs(last[0] - pt[0]) < eps && Math.abs(last[1] - pt[1]) < eps) {
+ continue;
+ }
+ filteredPath.push(pt);
+ }
+ if (filteredPath.length < 3) return; // still need 3 unique vertices
+
+ // Compute centroid using projected coordinates (more accurate for tiny polygons)
+ const centroid = computePolygonCentroidProjected(filteredPath);
+
+ // Immediately set centroid and show marker so user sees instant feedback
+ // console.log('Setting polygon centroid (immediate):', { lat: centroid.lat, lng: centroid.lng });
+ setPolygonCentroid([centroid.lat, centroid.lng]);
+ setLocationLocked(true);
+ if (onLocationLockChange) {
+ try {
+ onLocationLockChange(true);
+ } catch (e) {
+ /* ignore */
+ }
+ }
+
+ // Force-add immediate marker to map (bypass state debounce)
+ try {
+ if (mapRef.current) {
+ // remove any existing markers to avoid duplicates
+ markersRef.current.forEach((m) => m.remove());
+ markersRef.current = [];
+ // addMarkerToMap expects [lng, lat]
+ addMarkerToMap(centroid.lng, centroid.lat, locatePropertyIcon);
+ }
+ } catch (e) {
+ console.warn('Immediate marker add failed', e);
+ }
+
+ // Create shape entry WITHOUT waiting for address (store address after geocode)
+ const tempId = `polygon_${Date.now()}`;
+ const shapeData: ShapeData = {
+ type: 'polygon',
+ // store as MapLibre [lng, lat] order
+ coordinates: filteredPath.map(([lat, lng]) => [lng, lat]),
+ id: tempId,
+ // address will be filled after reverse-geocode
+ addedAt: new Date().toISOString(),
+ };
+
+ // Calculate approximate area for polygon
+ const area = calculatePolygonArea(filteredPath);
+ shapeData.area = area;
+
+ // Replace any existing polygon with the new one
+ setDrawnShapes((prev) => {
+ const nonPolygonShapes = prev.filter((shape) => shape.type !== 'polygon');
+ return [...nonPolygonShapes, shapeData];
+ });
+
+ // remember that this shape id produced the current centroid so drawnShapes effect doesn't override marker
+ lastCentroidShapeIdRef.current = tempId;
+
+ // Perform reverse geocoding asynchronously and update the shape & parent afterwards
+ (async () => {
+ try {
+ const centroidAddress = await getReverseGeocodedAddress(
+ centroid.lat,
+ centroid.lng
+ );
+ // update the shape with the address
+ setDrawnShapes((prev) =>
+ prev.map((s) => (s.id === tempId ? { ...s, address: centroidAddress } : s))
+ );
+ // update parent location with nicer address
+ try {
+ onLocationUpdate(centroid.lat, centroid.lng, centroidAddress);
+ } catch (err) {
+ console.warn('onLocationUpdate failed after geocode', err);
+ }
+ } catch (err) {
+ console.warn('Reverse geocode failed (async), centroid already shown', err);
+ try {
+ onLocationUpdate(
+ centroid.lat,
+ centroid.lng,
+ `${centroid.lat.toFixed(6)}, ${centroid.lng.toFixed(6)}`
+ );
+ } catch {}
+ }
+ })();
+
+ // Also pan map directly to centroid now (avoid setView effect interfering with user panning)
+ try {
+ if (mapRef.current) {
+ programmaticPanRef.current = true;
+
+ // Compute zoom as before
+ const coords = filteredPath as [number, number][];
+ const lats = coords.map((c) => c[0]);
+ const lngs = coords.map((c) => c[1]);
+ const latDiff = Math.max(...lats) - Math.min(...lats);
+ const lngDiff = Math.max(...lngs) - Math.min(...lngs);
+ const maxDiff = Math.max(latDiff, lngDiff);
+
+ let targetZoom = 20;
+ if (maxDiff > 0.01) targetZoom = 15;
+ else if (maxDiff > 0.005) targetZoom = 16;
+ else if (maxDiff > 0.002) targetZoom = 17;
+ else if (maxDiff > 0.001) targetZoom = 18;
+ else if (maxDiff > 0.0005) targetZoom = 19;
+
+ mapRef.current.flyTo({
+ center: [centroid.lng, centroid.lat],
+ zoom: targetZoom,
+ essential: true,
+ duration: 1000,
+ });
+ }
+ } catch (e) {
+ /* ignore */
+ }
+
+ // finalize drawing state
+ setCurrentPath([]);
+ setActiveTool(null);
+ setIsDrawing(false);
+
+ // notify parent synchronously that a shape was started/drawn (address may be updated later)
+ if (onShapeDrawn) {
+ try {
+ onShapeDrawn(shapeData);
+ } catch (e) {
+ /* ignore */
+ }
+ }
+ };
+
+ // Expose control methods to parent via externalFinishRef
+ useEffect(() => {
+ if (externalFinishRef) {
+ try {
+ externalFinishRef.current = {
+ finish: () => {
+ finishShape();
+ },
+ start: () => {
+ setActiveTool('polygon');
+ setIsDrawing(true);
+ },
+ clear: () => {
+ clearAllShapes();
+ },
+ };
+ } catch (e) {
+ /* ignore */
+ }
+ }
+ return () => {
+ if (externalFinishRef) {
+ try {
+ externalFinishRef.current = null;
+ } catch (e) {
+ /* ignore */
+ }
+ }
+ };
+ }, [externalFinishRef, finishShape]);
+
+ // Handle map movement for location updates
+ const handleMapMoveEnd = () => {
+ if (!mapRef.current || readOnly) return;
+
+ // If location is locked (e.g. after finishing polygon) or a polygon centroid exists, do not update
+ if (locationLocked || polygonCentroid) return;
+
+ // Ignore moveend events which were caused by our own programmatic setView
+ if (programmaticPanRef.current) {
+ programmaticPanRef.current = false;
+ return;
+ }
+
+ try {
+ const mapCenter = mapRef.current.getCenter();
+ const lat = mapCenter.lat;
+ const lng = mapCenter.lng;
+
+ // Clear previous timeout
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+
+ // Debounce the location update
+ debounceTimeoutRef.current = window.setTimeout(async () => {
+ try {
+ const address = await getReverseGeocodedAddress(lat, lng);
+ onLocationUpdate(lat, lng, address);
+ } catch (error) {
+ console.error('Error getting address:', error);
+ onLocationUpdate(lat, lng, `${lat.toFixed(6)}, ${lng.toFixed(6)}`);
+ }
+ }, 1000); // Increased debounce time to reduce re-renders
+ } catch (error) {
+ console.error('Error in handleMapMoveEnd:', error);
+ }
+ };
+
+ // Initialize map
+ useEffect(() => {
+ if (!mapContainerRef.current || mapRef.current) return;
+
+ const map = new maplibregl.Map({
+ container: mapContainerRef.current,
+ style: 'https://api.maptiler.com/maps/base-v4/style.json?key=YguiTF06mLtcpSVKIQyc',
+ center: [center[1], center[0]], // MapLibre uses [lng, lat]
+ zoom: 16,
+ attributionControl: false,
+ interactive: !readOnly,
+ });
+
+ mapRef.current = map;
+
+ // Set external ref if provided
+ if (externalMapRef) {
+ try {
+ externalMapRef.current = map;
+ } catch (err) {
+ /* ignore */
+ }
+ }
+
+ // Add navigation controls if not readonly
+ try {
+ if (!readOnly) {
+ // show zoom control, hide compass
+ map.addControl(
+ new maplibregl.NavigationControl({ showCompass: false }),
+ 'bottom-left'
+ );
+ }
+ } catch (e) {}
+
+ // Setup event handlers
+ map.on('moveend', handleMapMoveEnd);
+
+ map.on('movestart', () => {
+ try {
+ // If the user starts moving the map after we locked the location (e.g. after finishing polygon),
+ // treat this as an explicit user interaction and unlock the location so subsequent moves update location again.
+ // Only unlock location if there is no polygon centroid (i.e. selection is not fixed by a marker)
+ if (locationLocked && !programmaticPanRef.current) {
+ if (!polygonCentroid) {
+ setLocationLocked(false);
+ }
+ // if polygonCentroid exists, keep locationLocked=true so marker/selection remains fixed
+ }
+ if (typeof onMapMoveStart === 'function') {
+ onMapMoveStart();
+ }
+ } catch (e) {
+ /* ignore */
+ }
+ });
+
+ // Handle map clicks for polygon drawing
+ map.on('click', (e) => {
+ try {
+ const { lat, lng } = e.lngLat;
+ if (activeToolRef.current === 'polygon' && isDrawingRef.current) {
+ handlePathClick(lat, lng);
+ }
+ } catch (err) {
+ /* ignore */
+ }
+ });
+
+ // Wait for map load and style readiness before marking ready
+ map.once('load', () => {
+ if (map.isStyleLoaded()) {
+ setMapReady(true);
+ } else {
+ // If style not ready yet, wait for styledata
+ map.once('styledata', () => {
+ setMapReady(true);
+ });
+ }
+ });
+
+ // Initial address load
+ if (!readOnly) {
+ setTimeout(() => handleMapMoveEnd(), 2000);
+ }
+
+ return () => {
+ map.remove();
+ mapRef.current = null;
+ };
+ }, []);
+
+ // Update map center when center prop changes (for polygon centroid)
+ useEffect(() => {
+ if (mapRef.current && polygonCentroid) {
+ // Only update if we have a polygon centroid
+ const [lat, lng] = polygonCentroid;
+ mapRef.current.setCenter([lng, lat]);
+ }
+ }, [polygonCentroid]);
+
+ // Render drawn shapes on map
+ useEffect(() => {
+ if (!mapRef.current || !shapesVisible || !mapReady) return;
+
+ // Guard: wait for style to be loaded before rendering shapes
+ if (!mapRef.current.isStyleLoaded()) {
+ // Wait for style to be ready, then clean up any old polygon layers/sources and add current shapes
+ mapRef.current.once('styledata', () => {
+ if (mapRef.current && mapRef.current.isStyleLoaded()) {
+ // First, remove all existing polygon layers/sources that we manage
+ polygonLayersRef.current.forEach((layerId) => {
+ try {
+ if (mapRef.current!.getLayer(layerId)) {
+ mapRef.current!.removeLayer(layerId);
+ }
+ if (mapRef.current!.getLayer(`${layerId}-outline`)) {
+ mapRef.current!.removeLayer(`${layerId}-outline`);
+ }
+ if (mapRef.current!.getSource(layerId)) {
+ mapRef.current!.removeSource(layerId);
+ }
+ } catch (e) {
+ // Ignore errors during cleanup
+ }
+ });
+ polygonLayersRef.current = [];
+
+ // Then add all current shapes
+ drawnShapes.forEach((shape) => {
+ if (shape.type === 'polygon') {
+ addPolygonToMap(shape);
+ }
+ });
+ }
+ });
+ return;
+ }
+
+ // Style already loaded: ensure old layers removed, then add current shapes
+ // (cleanup in case sources/layers exist from prior renders)
+ polygonLayersRef.current.forEach((layerId) => {
+ try {
+ if (!mapRef.current) return;
+ if (mapRef.current.getLayer(layerId)) {
+ mapRef.current.removeLayer(layerId);
+ }
+ if (mapRef.current.getLayer(`${layerId}-outline`)) {
+ mapRef.current.removeLayer(`${layerId}-outline`);
+ }
+ if (mapRef.current.getSource(layerId)) {
+ mapRef.current.removeSource(layerId);
+ }
+ } catch (e) {
+ /* ignore */
+ }
+ });
+ polygonLayersRef.current = [];
+
+ drawnShapes.forEach((shape) => {
+ if (readOnly) return;
+ if (shape.type === 'polygon') {
+ addPolygonToMap(shape);
+ }
+ });
+ }, [drawnShapes, shapesVisible, readOnly]);
+
+ // Render current drawing path
+ useEffect(() => {
+ if (!mapRef.current || !isDrawing || currentPath.length < 1) {
+ // Remove temporary drawing layer if exists
+ if (mapRef.current?.getLayer('temp-polygon-line')) {
+ mapRef.current.removeLayer('temp-polygon-line');
+ }
+ if (mapRef.current?.getSource('temp-polygon')) {
+ mapRef.current.removeSource('temp-polygon');
+ }
+ if (mapRef.current?.getLayer('temp-polygon-vertices')) {
+ mapRef.current.removeLayer('temp-polygon-vertices');
+ }
+ if (mapRef.current?.getSource('temp-polygon-vertices')) {
+ mapRef.current.removeSource('temp-polygon-vertices');
+ }
+ return;
+ }
+
+ const map = mapRef.current;
+ const pathCoords = currentPath.map(([lat, lng]) => [lng, lat]);
+ const coordinates =
+ pathCoords.length >= 2 ? [...pathCoords, pathCoords[0]] : pathCoords;
+
+ if (pathCoords.length >= 2) {
+ const geojson = {
+ type: 'Feature' as const,
+ geometry: {
+ type: 'LineString' as const,
+ coordinates,
+ },
+ properties: {},
+ };
+
+ if (!map.getSource('temp-polygon')) {
+ map.addSource('temp-polygon', {
+ type: 'geojson',
+ data: geojson as any,
+ });
+
+ map.addLayer({
+ id: 'temp-polygon-line',
+ type: 'line',
+ source: 'temp-polygon',
+ paint: {
+ 'line-color': '#f59e0b',
+ 'line-width': 3,
+ 'line-opacity': 0.7,
+ 'line-dasharray': [2, 2],
+ },
+ });
+ } else {
+ (map.getSource('temp-polygon') as maplibregl.GeoJSONSource).setData(
+ geojson as any
+ );
+ }
+ }
+ // Create/update a GeoJSON source for vertices
+ const vertexFeatures = pathCoords.map((c) => ({
+ type: 'Feature',
+ geometry: { type: 'Point', coordinates: c },
+ properties: {},
+ }));
+ const verticesGeoJSON = {
+ type: 'FeatureCollection',
+ features: vertexFeatures,
+ };
+
+ if (!map.getSource('temp-polygon-vertices')) {
+ map.addSource('temp-polygon-vertices', {
+ type: 'geojson',
+ data: verticesGeoJSON as any,
+ });
+
+ map.addLayer({
+ id: 'temp-polygon-vertices',
+ type: 'circle',
+ source: 'temp-polygon-vertices',
+ paint: {
+ 'circle-radius': 5,
+ 'circle-color': '#e86813ff',
+ 'circle-stroke-color': '#ffffff',
+ 'circle-stroke-width': 1,
+ },
+ });
+ } else {
+ (map.getSource('temp-polygon-vertices') as maplibregl.GeoJSONSource).setData(
+ verticesGeoJSON as any
+ );
+ }
+ }, [isDrawing, currentPath]);
+
+ // Render markers (readonly marker and polygon centroid marker)
+ useEffect(() => {
+ if (!mapRef.current) return;
+
+ const renderMarkers = () => {
+ if (!mapRef.current) return;
+
+ // console.log('Rendering markers - readOnly:', readOnly, 'polygonCentroid:', polygonCentroid);
+
+ // If we already added an immediate centroid marker and it's at the same position,
+ // keep it to avoid a visual jump. Compare approx equality within epsilon.
+ const eps = 1e-6;
+ if (polygonCentroid && markersRef.current.length > 0) {
+ try {
+ const existing = markersRef.current[0];
+ const pos = existing.getLngLat(); // {lng, lat}
+ const [plat, plng] = polygonCentroid;
+ if (Math.abs(pos.lat - plat) < eps && Math.abs(pos.lng - plng) < eps) {
+ // console.log('Existing marker matches centroid โ reusing to avoid jump');
+ return; // nothing to do
+ }
+ } catch (e) {
+ // fall through to re-create markers
+ }
+ }
+
+ // Clear existing markers
+ markersRef.current.forEach((marker) => marker.remove());
+ markersRef.current = [];
+
+ // If a polygon centroid exists, prefer showing a marker at the centroid
+ if (polygonCentroid) {
+ // console.log('Adding marker at polygon centroid:', polygonCentroid);
+ // polygonCentroid is [lat, lng], so we need [lng, lat] for MapLibre
+ addMarkerToMap(polygonCentroid[1], polygonCentroid[0], locatePropertyIcon);
+ } else if (readOnly) {
+ // No polygon centroid available, fall back to center for readonly view
+ // console.log('Adding readonly marker at center (fallback):', center);
+ addMarkerToMap(center[1], center[0], locatePropertyIcon);
+ }
+ };
+
+ // For MapLibre, we need to wait for both 'load' and 'style.load' events
+ if (mapRef.current.loaded() && mapRef.current.isStyleLoaded()) {
+ renderMarkers();
+ } else if (mapRef.current.loaded()) {
+ // Map is loaded but style might not be
+ if (mapRef.current.isStyleLoaded()) {
+ renderMarkers();
+ } else {
+ mapRef.current.once('style.load', renderMarkers);
+ }
+ } else {
+ // Map not loaded yet
+ mapRef.current.once('load', () => {
+ if (mapRef.current?.isStyleLoaded()) {
+ renderMarkers();
+ } else {
+ mapRef.current?.once('style.load', renderMarkers);
+ }
+ });
+ }
+ }, [readOnly, center, polygonCentroid, drawnShapes]);
+
+ // Cleanup
+ useEffect(() => {
+ // Reference onClearAll so TypeScript doesn't warn if it's unused
+ if (typeof onClearAll === 'undefined') {
+ // noop
+ }
+
+ return () => {
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ // Note: we avoid forcing setView on every center change to not fight user panning.
+ // finishShape will pan the map to centroid when needed.
+
+ // Note: movestart is wired in the MapContainer ref callback if onMapMoveStart is provided
+
+ return (
+
+
+
+ {/* Address overlay for readOnly view */}
+ {readOnly && addressLabel && (
+
+ {addressLabel}
+
+ )}
+
+ {/* Selection overlay: only when not readOnly and not actively drawing and no polygon centroid exists. This overlay is a DOM element
+ centered over the map and moves visually with the map (user pans map to change selected coordinates). */}
+ {!readOnly && !isDrawing && activeTool === null && !polygonCentroid && (
+
+
+
+ )}
+
+ );
+};
+
+export default LocationMapWithDrawing;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/LocationPage.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/LocationPage.tsx
new file mode 100644
index 0000000..c152b3f
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/LocationPage.tsx
@@ -0,0 +1,208 @@
+import React, { useEffect, useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+// import '../../../styles/Agent/LocationPage.css';
+import '../../../styles/LocationPage.css';
+import LocalPhoneOutlinedIcon from '@mui/icons-material/LocalPhoneOutlined';
+import { SiOpenstreetmap } from 'react-icons/si';
+import { BsPinMap } from 'react-icons/bs';
+import Layout from '../../features/Agent/components/Layout';
+import { fetchData } from '../../../services/dataService';
+import { useAppLocalization } from '../../../services/AgentLocalisation/localisation-search-property';
+import type { DatabaseData } from '../../../services/dataService';
+
+// Type for location data
+interface Location {
+ address: string;
+ coordinates?: {
+ lat: number;
+ lng: number;
+ };
+}
+
+// Props for LocationPage component
+interface LocationPageProps {
+ location?: Location;
+ phoneNumber?: string;
+ onBack?: () => void;
+}
+
+// Main component for displaying property location details
+const LocationPage: React.FC = ({
+ location: propLocation,
+ phoneNumber = '+91 39243 22342',
+ onBack,
+}) => {
+ const { propertyId } = useParams<{ propertyId: string }>();
+ const navigate = useNavigate();
+ const localization = useAppLocalization();
+ const [, setData] = useState(null);
+ const [location, setLocation] = useState(
+ propLocation || { address: 'Loading...', coordinates: { lat: 0, lng: 0 } }
+ );
+
+ // Fetch property data if propertyId is provided
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ const fetchedData = await fetchData();
+ setData(fetchedData);
+
+ if (propertyId && fetchedData?.properties) {
+ const selectedProperty = fetchedData.properties.find(
+ (p) => p.id === propertyId
+ );
+ if (selectedProperty) {
+ setLocation({
+ address: selectedProperty.address,
+ coordinates: { lat: 16.2973, lng: 80.4364 }, // Default coordinates
+ });
+ }
+ }
+ } catch (error) {
+ console.error('Error loading data:', error);
+ }
+ };
+
+ if (!propLocation || propertyId) {
+ loadData();
+ }
+ }, [propertyId, propLocation]);
+
+ // Handle back navigation
+ const handleBack = () => {
+ if (onBack) {
+ onBack();
+ } else {
+ navigate(-1); // Go back to previous page
+ }
+ };
+ // Handle map action button clicks (Google, Apple, OpenStreetMap)
+ const handleMapAction = (type: 'google' | 'apple' | 'street') => {
+ // Handle different map actions
+ console.log(`Opening ${type} maps for:`, location.address);
+ };
+
+ // Handle phone call button click
+ const handleCall = () => {
+ window.open(`tel:${phoneNumber}`);
+ };
+
+ // Main render: layout, map, address, map actions, and phone info
+ return (
+ {}} // No tab change needed on location page
+ showHeader={false}
+ showNavigation={false}
+ headerProps={{
+ title: localization.locationPageTitle,
+ showBackButton: true,
+ onBack: handleBack,
+ showHome: true,
+ onHome: () => navigate('/'),
+ showLanguage: false,
+ showProfile: false,
+ }}
+ >
+
+ {/* Map Container */}
+
+
+ {/* Map background with location pin */}
+
+
+ {/* Location Info Tooltip */}
+
+
+
Veroja QLD 4105, Australia
+
+ {localization.phonePrefixText}: 0540 792 235 756
+
+
{localization.directionsText}
+
+
+
+
+
+ {/* Address Section */}
+
+
+
{localization.addressText}
+
+
+
+
+
+
+
+
{location.address}
+
+ {/* Map Action Buttons */}
+
+
handleMapAction('google')}>
+
+ {localization.googleMapsText}
+
+
+
handleMapAction('apple')}>
+
+
+
+ {localization.appleMapsText}
+
+
+
handleMapAction('street')}>
+
+
+
+ {localization.openStreetMapsText}
+
+
+
+ {/* Phone Section */}
+
+
+
{localization.phoneNumberText}
+
+
+
+ {phoneNumber}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default LocationPage;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/LocationSelectionPage.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/LocationSelectionPage.tsx
new file mode 100644
index 0000000..3209730
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/LocationSelectionPage.tsx
@@ -0,0 +1,958 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import { IconButton } from '@mui/material';
+import LocationMapWithDrawing from './LocationMapWithDrawing';
+import undoIcon from '../../assets/Agent/undo.svg';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import { usePropertyInformationLocalization } from '../../../services/AgentLocalisation/localisation-propertyInformation';
+// Remove Leaflet import
+// import L from 'leaflet';
+import maplibregl from 'maplibre-gl';
+import 'maplibre-gl/dist/maplibre-gl.css';
+import '../../../styles/LocationSelection.css';
+
+// Helper function to get reverse geocoded address with fallback
+const getReverseGeocodedAddress = async (lat: number, lng: number): Promise => {
+ try {
+ // Use Photon API which is CORS-friendly
+ const response = await fetch(
+ `https://photon.komoot.io/reverse?lat=${lat}&lon=${lng}`,
+ {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ },
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.features && data.features.length > 0) {
+ const feature = data.features[0];
+ if (feature.properties) {
+ const props = feature.properties;
+ const parts = [];
+
+ // Build address from available components
+ if (props.housenumber) parts.push(props.housenumber);
+ if (props.street) parts.push(props.street);
+ if (props.district) parts.push(props.district);
+ if (props.city) parts.push(props.city);
+ if (props.state) parts.push(props.state);
+
+ if (parts.length > 0) {
+ return parts.join(', ');
+ }
+
+ // Fallback to name if available
+ if (props.name) {
+ return props.name;
+ }
+ }
+ }
+ }
+ } catch (error) {
+ console.warn('Photon geocoding failed, trying alternative:', error);
+ }
+
+ // Try alternative approach with a simple area lookup
+ try {
+ if (lat >= 8 && lat <= 37 && lng >= 68 && lng <= 97) {
+ const areas = {
+ bangalore: { lat: 12.9716, lng: 77.5946, name: 'Bangalore, Karnataka' },
+ delhi: { lat: 28.6139, lng: 77.209, name: 'Delhi' },
+ mumbai: { lat: 19.076, lng: 72.8777, name: 'Mumbai, Maharashtra' },
+ chennai: { lat: 13.0827, lng: 80.2707, name: 'Chennai, Tamil Nadu' },
+ hyderabad: { lat: 17.385, lng: 78.4867, name: 'Hyderabad, Telangana' },
+ pune: { lat: 18.5204, lng: 73.8567, name: 'Pune, Maharashtra' },
+ };
+ let closestArea = 'Unknown Area, India';
+ let minDistance = Infinity;
+ Object.values(areas).forEach((area) => {
+ const distance = Math.sqrt(
+ Math.pow(lat - area.lat, 2) + Math.pow(lng - area.lng, 2)
+ );
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestArea = area.name;
+ }
+ });
+ return `Near ${closestArea}`;
+ }
+ } catch (error) {
+ console.warn('Area lookup failed:', error);
+ }
+ return `Location: ${lat.toFixed(4)}ยฐN, ${lng.toFixed(4)}ยฐE`;
+};
+
+// Helper function to calculate centroid of a polygon (matching LocationMapWithDrawing implementation)
+const calculatePolygonCentroid = (
+ coordinates: number[][]
+): { lat: number; lng: number } => {
+ if (!coordinates || coordinates.length === 0) {
+ return { lat: 12.9716, lng: 77.5946 };
+ }
+ // Fixed: MapLibre coordinates are [lng, lat], so we need to swap
+ const pts = coordinates.map(([lng, lat]) => ({ x: lng, y: lat }));
+ let twiceArea = 0;
+ let xSum = 0;
+ let ySum = 0;
+ for (let i = 0; i < pts.length; i++) {
+ const j = (i + 1) % pts.length;
+ const cross = pts[i].x * pts[j].y - pts[j].x * pts[i].y;
+ twiceArea += cross;
+ xSum += (pts[i].x + pts[j].x) * cross;
+ ySum += (pts[i].y + pts[j].y) * cross;
+ }
+ const area = twiceArea / 2;
+ if (Math.abs(area) < 1e-9) {
+ const avg = pts.reduce((acc, p) => ({ x: acc.x + p.x, y: acc.y + p.y }), {
+ x: 0,
+ y: 0,
+ });
+ const result = { lat: avg.y / pts.length, lng: avg.x / pts.length };
+ return result;
+ }
+ const cx = xSum / (6 * area);
+ const cy = ySum / (6 * area);
+ const result = { lat: cy, lng: cx };
+ return result;
+};
+
+// Type for location data (coordinates and address)
+interface LocationData {
+ coordinates: [number, number]; // [lng, lat] for MapLibre
+ address: string;
+}
+
+// Type for drawn shape data (point, polygon, etc.)
+interface ShapeData {
+ type: 'point' | 'polyline' | 'rectangle' | 'polygon' | 'deleted';
+ coordinates?: number[] | number[][];
+ area?: number;
+ layer?: any;
+ address?: string;
+ addedAt?: string;
+}
+
+// Main component for selecting and confirming property location on a map
+const LocationSelectionPage: React.FC = () => {
+ const navigate = useNavigate();
+ const { updateForm, formData } = usePropertyForm();
+ const location = useLocation();
+ const urlParams = new URLSearchParams(location.search);
+ const mode = urlParams.get('mode');
+ const {
+ previousBtn,
+ newPropertyFormTitle,
+ addLocationTitle,
+ searchPlaceholder,
+ startDrawingBtn,
+ finishDrawingBtn,
+ selectedLocationTitle,
+ loadingAddressText,
+ multiplePropertiesTitle,
+ loadingPropertiesText,
+ noPropertiesText,
+ confirmLocationBtn,
+ doNotConsiderText,
+ } = usePropertyInformationLocalization();
+
+ // State for selected location (coordinates and address)
+ const [selectedLocation, setSelectedLocation] = useState(() => {
+ if (formData.locationData) {
+ if (mode === 'polygon' && formData.locationData.drawnShapes) {
+ const existingPolygon = formData.locationData.drawnShapes.find(
+ (shape) => shape.type === 'polygon'
+ );
+ if (existingPolygon && existingPolygon.coordinates) {
+ const centroid = calculatePolygonCentroid(
+ existingPolygon.coordinates as number[][]
+ );
+ return {
+ coordinates: [centroid.lng!, centroid.lat!],
+ address:
+ formData.locationData.address ||
+ `Centroid: ${centroid.lat.toFixed(6)}, ${centroid.lng.toFixed(6)}`,
+ };
+ }
+ }
+ return {
+ coordinates: [
+ formData.locationData.coordinates?.lng!,
+ formData.locationData.coordinates?.lat!,
+ ],
+ address: formData.locationData.address,
+ };
+ }
+ return {
+ coordinates: [77.5946, 12.9716], // [lng, lat]
+ address: 'Vittal Mallya Road, Richmond Town, Bengaluru, Karnataka',
+ };
+ });
+
+ // State for loading, search, polygon, and map refs
+ const [isLoading, setIsLoading] = useState(false);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [searchResults, setSearchResults] = useState([]);
+ const [showSearchResults, setShowSearchResults] = useState(false);
+ const [searchLoading, setSearchLoading] = useState(false);
+ const [polygonActive, setPolygonActive] = useState(false);
+ const polygonControlRef = useRef<{
+ finish?: () => void;
+ start?: () => void;
+ clear?: () => void;
+ } | null>(null);
+ const mapRef = useRef(null);
+ const searchTimeoutRef = useRef(null);
+
+ const [selectedPropertyIndex, setSelectedPropertyIndex] = useState(0);
+ const [propertyOptions, setPropertyOptions] = useState([]);
+ const [loadingPropertyOptions, setLoadingPropertyOptions] = useState(false);
+ const [locationLocked, setLocationLocked] = useState(false);
+
+ // State for drawn shapes on the map
+ const [drawnShapes, setDrawnShapes] = useState(() => {
+ if (formData.locationData?.drawnShapes) {
+ return formData.locationData.drawnShapes.map((shape, index) => ({
+ type: shape.type,
+ coordinates: shape.coordinates,
+ area: shape.area,
+ address: shape.address,
+ layer: `existing_${index}`,
+ }));
+ }
+ return [];
+ });
+
+ // Handle location update from map (pin or move)
+ const handleLocationUpdate = (lat: number, lng: number, address: string) => {
+ // If the map locked the location (after polygon finish), ignore further map move/zoom events
+ if (locationLocked) {
+ setIsLoading(false);
+ return;
+ }
+ setSelectedLocation({
+ coordinates: [lng, lat], // [lng, lat]
+ address: address,
+ });
+ setIsLoading(false);
+ };
+
+ // Handle shape drawn or deleted on the map
+ const handleShapeDrawn = async (shapeData: ShapeData) => {
+ if (shapeData.type === 'deleted') {
+ setDrawnShapes((prev) => prev.filter((shape) => shape.layer !== shapeData.layer));
+ const remainingPolygons = drawnShapes.filter(
+ (shape) => shape.type === 'polygon' && shape.layer !== shapeData.layer
+ );
+ if (remainingPolygons.length === 0) {
+ setPropertyOptions([]);
+ }
+ } else {
+ setDrawnShapes((prev) => {
+ const existingIndex = prev.findIndex((shape) => shape.layer === shapeData.layer);
+ if (existingIndex >= 0) {
+ const updated = [...prev];
+ updated[existingIndex] = shapeData;
+ return updated;
+ } else {
+ return [...prev, shapeData];
+ }
+ });
+
+ if (shapeData.type === 'polygon') {
+ setLoadingPropertyOptions(true);
+ try {
+ if (mode === 'polygon') {
+ setDrawnShapes((prev) => {
+ const nonPolygonShapes = prev.filter((shape) => shape.type !== 'polygon');
+ return [...nonPolygonShapes, shapeData];
+ });
+
+ // Note: selectedLocation is already updated by handleLocationUpdate with centroid address
+ // No need to update it again here to avoid overriding the correct address
+ }
+
+ let options = await generatePropertyOptions(shapeData);
+ // Deduplicate options by address (keep first occurrence). Keep the 'do_not_consider_key' item at the end.
+ const dedupePropertyOptions = (opts: any[]) => {
+ const seen = new Map();
+ let doNotConsider: any = null;
+ for (const o of opts) {
+ if (o.address === 'do_not_consider_key') {
+ doNotConsider = o;
+ continue;
+ }
+ const key = (o.address || '').trim();
+ if (!seen.has(key)) {
+ seen.set(key, o);
+ }
+ }
+ const result = Array.from(seen.values());
+ if (doNotConsider) result.push(doNotConsider);
+ return result;
+ };
+
+ options = dedupePropertyOptions(options);
+ setPropertyOptions(options);
+ // If only one real property (not counting do_not_consider), auto-select it and hide list
+ const realOptions = options.filter((o) => o.address !== 'do_not_consider_key');
+ if (realOptions.length === 1) {
+ const idx = options.findIndex((o) => o.id === realOptions[0].id);
+ if (idx >= 0) setSelectedPropertyIndex(idx);
+ } else {
+ // default selection to last (do_not_consider) if multiple
+ setSelectedPropertyIndex(options.length - 1);
+ }
+ } catch (error) {
+ setPropertyOptions([]);
+ } finally {
+ setLoadingPropertyOptions(false);
+ }
+ }
+ }
+ };
+
+ // Generate property options from polygon shape
+ const generatePropertyOptions = async (polygon: ShapeData) => {
+ if (!polygon.coordinates || polygon.type !== 'polygon') return [];
+ const coords = polygon.coordinates as number[][];
+ const properties = [];
+ if (coords.length > 3) {
+ for (let index = 0; index < coords.length && index < 5; index++) {
+ const coord = coords[index];
+ // MapLibre uses [lng, lat]
+ let lng = coord[0];
+ let lat = coord[1];
+
+ // Validate coordinates
+ if (Math.abs(lat) > 90 || Math.abs(lng) > 180) {
+ // Swap if needed
+ [lat, lng] = [lng, lat];
+ }
+ try {
+ const address = await getReverseGeocodedAddress(lat, lng);
+ if (
+ address &&
+ !address.startsWith('Location:') &&
+ !address.startsWith('Near')
+ ) {
+ properties.push({
+ address: address,
+ coordinates: { lat, lng },
+ id: `property_${index}`,
+ });
+ }
+ } catch (error) {
+ continue;
+ }
+ }
+ }
+ properties.push({
+ address: 'do_not_consider_key',
+ coordinates: null,
+ id: 'not_considered',
+ });
+ return properties;
+ };
+
+ // Handle search input for address/location
+ const handleSearchInput = (value: string) => {
+ setSearchQuery(value);
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+ if (value.trim().length > 2) {
+ searchTimeoutRef.current = setTimeout(() => {
+ performSearch(value, true);
+ }, 300);
+ } else {
+ setSearchResults([]);
+ setShowSearchResults(false);
+ }
+ };
+
+ // Perform address/location search (autocomplete or direct)
+ const performSearch = async (query: string, isAutocomplete: boolean = false) => {
+ if (!query.trim()) return;
+ try {
+ if (isAutocomplete) {
+ setSearchLoading(true);
+ } else {
+ setIsLoading(true);
+ }
+ const limit = isAutocomplete ? 5 : 1;
+ const response = await fetch(
+ `https://photon.komoot.io/api?q=${encodeURIComponent(query)}&limit=${limit}`,
+ {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ },
+ }
+ );
+ if (response.ok) {
+ const data = await response.json();
+ if (data.features && data.features.length > 0) {
+ if (isAutocomplete) {
+ const formattedResults = data.features.map((feature: any, index: number) => {
+ const coords = feature.geometry.coordinates;
+ const props = feature.properties;
+ const parts = [];
+ if (props.name) parts.push(props.name);
+ if (props.street) parts.push(props.street);
+ if (props.city) parts.push(props.city);
+ if (props.state) parts.push(props.state);
+ if (props.country) parts.push(props.country);
+
+ const displayName =
+ parts.length > 0
+ ? parts.join(', ')
+ : `Location: ${coords[1].toFixed(4)}ยฐ, ${coords[0].toFixed(4)}ยฐ`;
+ return {
+ id: index,
+ displayName,
+ coordinates: [coords[1], coords[0]], // [lat, lng] for display
+ type: props.type || 'location',
+ };
+ });
+ setSearchResults(formattedResults);
+ setShowSearchResults(true);
+ } else {
+ const feature = data.features[0];
+ const coords = feature.geometry.coordinates;
+ const lat = coords[1];
+ const lng = coords[0];
+ await selectLocation(lat, lng);
+ }
+ } else {
+ if (isAutocomplete) {
+ setSearchResults([]);
+ setShowSearchResults(false);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Search failed:', error);
+ } finally {
+ if (isAutocomplete) {
+ setSearchLoading(false);
+ } else {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ // Select a location from search or map
+ const selectLocation = async (lat: number, lng: number) => {
+ try {
+ setIsLoading(true);
+ const address = await getReverseGeocodedAddress(lat, lng);
+ setSelectedLocation({
+ coordinates: [lng, lat], // [lng, lat]
+ address: address,
+ });
+ if (mapRef.current) {
+ // MapLibre uses flyTo with center as [lng, lat]
+ mapRef.current.flyTo({
+ center: [lng, lat],
+ zoom: 16,
+ });
+ }
+ setShowSearchResults(false);
+ } catch (error) {
+ console.error('Failed to select location:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Handle click on a search result
+ const handleSearchResultClick = (result: any) => {
+ setSearchQuery(result.displayName);
+ selectLocation(result.coordinates[0], result.coordinates[1]);
+ };
+
+ // Handle search button click
+ const handleSearch = () => {
+ if (searchQuery.trim()) {
+ performSearch(searchQuery, false);
+ setShowSearchResults(false);
+ }
+ };
+
+ // Handle back navigation
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ // Handle confirm location button click (save to form)
+ const handleConfirmLocation = async () => {
+ const firstPolygon = drawnShapes.find((s) => s.type === 'polygon') as any | undefined;
+ let addressToSave = selectedLocation.address;
+ let coordsToSave = {
+ lat: selectedLocation.coordinates[1],
+ lng: selectedLocation.coordinates[0],
+ };
+
+ if (firstPolygon && propertyOptions.length > 0) {
+ if (selectedPropertyIndex < propertyOptions.length) {
+ const selectedProperty = propertyOptions[selectedPropertyIndex];
+ if (selectedProperty.coordinates) {
+ addressToSave = selectedProperty.address;
+ coordsToSave = selectedProperty.coordinates;
+ } else {
+ const coords = (firstPolygon.coordinates as number[][]) || [];
+ if (coords.length > 0) {
+ const firstCoord = coords[0];
+ // MapLibre coordinates are [lng, lat]
+ let lng = firstCoord[0];
+ let lat = firstCoord[1];
+
+ // Validate
+ if (Math.abs(lat) > 90 || Math.abs(lng) > 180) {
+ [lat, lng] = [lng, lat];
+ }
+ try {
+ setIsLoading(true);
+ addressToSave = await getReverseGeocodedAddress(lat, lng);
+ } catch (err) {
+ addressToSave = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
+ } finally {
+ setIsLoading(false);
+ }
+ coordsToSave = { lat, lng };
+ }
+ }
+ }
+ }
+
+ if (!firstPolygon) {
+ const pinned = drawnShapes.find((s) => s.type === 'point') as any | undefined;
+ if (pinned && pinned.coordinates && (pinned.coordinates as number[]).length >= 2) {
+ // MapLibre coordinates are [lng, lat]
+ let plng = (pinned.coordinates as number[])[0];
+ let plat = (pinned.coordinates as number[])[1];
+
+ if (Math.abs(plat) > 90 || Math.abs(plng) > 180) {
+ [plat, plng] = [plng, plat];
+ }
+ if (pinned.address) {
+ addressToSave = pinned.address;
+ } else {
+ try {
+ setIsLoading(true);
+ addressToSave = await getReverseGeocodedAddress(plat, plng);
+ } catch (err) {
+ addressToSave = `${plat.toFixed(6)}, ${plng.toFixed(6)}`;
+ } finally {
+ setIsLoading(false);
+ }
+ }
+ coordsToSave = { lat: plat, lng: plng };
+ }
+ }
+
+ const locationData = {
+ gisDataId: formData.locationData?.gisDataId,
+ address: addressToSave,
+ coordinates: coordsToSave,
+ timestamp: new Date().toISOString(),
+ drawnShapes: drawnShapes
+ .filter((shape) => shape.type !== 'deleted' && shape.type !== 'polyline')
+ .map((shape) => ({
+ type: shape.type as 'point' | 'rectangle' | 'polygon',
+ coordinates: shape.coordinates || [],
+ area: shape.area,
+ address: shape.address,
+ addedAt: (shape as any).addedAt,
+ })),
+ };
+ updateForm({ locationData });
+ navigate(-1);
+ };
+
+ // On mount: reverse geocode initial location
+ useEffect(() => {
+ const reverseGeocode = async () => {
+ try {
+ setIsLoading(true);
+ const address = await getReverseGeocodedAddress(
+ selectedLocation.coordinates[1], // lat
+ selectedLocation.coordinates[0] // lng
+ );
+ setSelectedLocation((prev) => ({
+ ...prev,
+ address: address,
+ }));
+ } catch (error) {
+ setSelectedLocation((prev) => ({
+ ...prev,
+ address: `${selectedLocation.coordinates[1].toFixed(
+ 6
+ )}, ${selectedLocation.coordinates[0].toFixed(6)}`,
+ }));
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ reverseGeocode();
+ }, []);
+
+ // On mount: center map on polygon centroid if present
+ useEffect(() => {
+ if (mode === 'polygon' && mapRef.current) {
+ const existingPolygon = drawnShapes.find((s) => s.type === 'polygon');
+ if (existingPolygon && existingPolygon.coordinates) {
+ const centroid = calculatePolygonCentroid(
+ existingPolygon.coordinates as number[][]
+ );
+ setTimeout(() => {
+ if (mapRef.current) {
+ // MapLibre uses flyTo with center as [lng, lat]
+ mapRef.current.flyTo({
+ center: [centroid.lng, centroid.lat],
+ zoom: 16,
+ });
+ }
+ }, 500);
+ }
+ }
+ }, [mode, drawnShapes]);
+
+ // On mount: load property options for existing polygon
+ useEffect(() => {
+ const loadExistingPolygonOptions = async () => {
+ const existingPolygon = drawnShapes.find((s) => s.type === 'polygon');
+ if (existingPolygon && mode !== 'polygon') {
+ setLoadingPropertyOptions(true);
+ try {
+ const options = await generatePropertyOptions(existingPolygon);
+ // Deduplicate options and keep do_not_consider at the end
+ const dedupePropertyOptions = (opts: any[]) => {
+ const seen = new Map();
+ let doNotConsider: any = null;
+ for (const o of opts) {
+ if (o.address === 'do_not_consider_key') {
+ doNotConsider = o;
+ continue;
+ }
+ const key = (o.address || '').trim();
+ if (!seen.has(key)) {
+ seen.set(key, o);
+ }
+ }
+ const result = Array.from(seen.values());
+ if (doNotConsider) result.push(doNotConsider);
+ return result;
+ };
+ const deduped = dedupePropertyOptions(options);
+ setPropertyOptions(deduped);
+ setSelectedPropertyIndex(deduped.length - 1);
+ } catch (error) {
+ setPropertyOptions([]);
+ } finally {
+ setLoadingPropertyOptions(false);
+ }
+ } else if (mode === 'polygon') {
+ setPropertyOptions([]);
+ setSelectedPropertyIndex(0);
+ }
+ };
+ loadExistingPolygonOptions();
+ }, []);
+
+ // Cleanup search timeout on unmount
+ useEffect(() => {
+ return () => {
+ if (searchTimeoutRef.current) {
+ clearTimeout(searchTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ // Hide search results dropdown when clicking outside
+ useEffect(() => {
+ const handleClickOutside = (event: MouseEvent) => {
+ const target = event.target as Element;
+ if (!target.closest('.search-bar-container')) {
+ setShowSearchResults(false);
+ }
+ };
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ // Main render: map, search, polygon controls, property options, and confirm button
+ return (
+
+
+
+
{newPropertyFormTitle}
+
{addLocationTitle}
+
+
+ {previousBtn}
+
+
+
+
+
+
+
handleSearchInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleSearch();
+ } else if (e.key === 'Escape') {
+ setShowSearchResults(false);
+ }
+ }}
+ onFocus={() => {
+ if (searchResults.length > 0) {
+ setShowSearchResults(true);
+ }
+ }}
+ className="map-search-input"
+ />
+
+ {searchLoading ? (
+
+ ) : (
+
+
+
+
+ )}
+
+
+ {showSearchResults && searchResults.length > 0 && (
+
+ {searchResults.map((result) => (
+
handleSearchResultClick(result)}
+ >
+
+
+
{result.displayName}
+
{result.type}
+
+
+ ))}
+
+ )}
+ {(mode === 'polygon' || polygonActive) && (
+
+
+
{
+ try {
+ if (polygonControlRef.current && polygonControlRef.current.clear)
+ polygonControlRef.current.clear();
+ } catch (e) {
+ /* ignore */
+ }
+ setDrawnShapes([]);
+ setPropertyOptions([]);
+ updateForm({
+ locationData: {
+ gisDataId: formData.locationData?.gisDataId!,
+ drawnShapes: [],
+ coordinates: {},
+ address: '',
+ },
+ });
+ }}
+ title="Clear drawings"
+ className="polygon-control-refresh"
+ >
+
+
+
+
{
+ setPolygonActive(true);
+ if (polygonControlRef.current?.start) polygonControlRef.current.start();
+ }}
+ title={startDrawingBtn}
+ >
+
+
+
+ {startDrawingBtn}
+
+
{
+ if (polygonControlRef.current?.finish)
+ polygonControlRef.current.finish();
+ setPolygonActive(false);
+ }}
+ title={finishDrawingBtn}
+ >
+
+
+
+ {finishDrawingBtn}
+
+
+ )}
+
+
+ {
+ setDrawnShapes([]);
+ updateForm({
+ locationData: {
+ gisDataId: formData.locationData?.gisDataId,
+ drawnShapes: [],
+ coordinates: {},
+ address: '',
+ },
+ });
+ }}
+ externalFinishRef={polygonControlRef}
+ startDrawing={polygonActive}
+ externalMapRef={mapRef}
+ onLocationLockChange={(locked: boolean) => setLocationLocked(locked)}
+ />
+
+
+
+
+
{selectedLocationTitle}
+
+ {isLoading ? (
+
{loadingAddressText}
+ ) : (
+
{selectedLocation.address}
+ )}
+
+
+ {drawnShapes.filter((shape) => shape.type === 'polygon').length > 0 && (
+
+
{multiplePropertiesTitle}
+ {loadingPropertyOptions ? (
+
+
{loadingPropertiesText}
+
+ ) : propertyOptions.length > 0 ? (
+
+ {/* If there's exactly one real property (excluding 'do_not_consider_key') we skip rendering the list and auto-use it */}
+ {propertyOptions.filter((p: any) => p.address !== 'do_not_consider_key')
+ .length === 1 ? (
+
+
+ {
+ propertyOptions.find(
+ (p: any) => p.address !== 'do_not_consider_key'
+ )!.address
+ }
+
+
+ ) : (
+ propertyOptions.map((property: any, index: number) => (
+
+ setSelectedPropertyIndex(index)}
+ className="property-radio"
+ />
+
+ {property.address === 'do_not_consider_key'
+ ? doNotConsiderText
+ : property.address}
+
+
+ ))
+ )}
+
+ ) : (
+
+ )}
+
+ )}
+
+ {confirmLocationBtn}
+
+
+
+ );
+};
+
+export default LocationSelectionPage;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/LocationViewPage.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/LocationViewPage.tsx
new file mode 100644
index 0000000..58bf706
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/LocationViewPage.tsx
@@ -0,0 +1,201 @@
+// LocationViewPage: Read-only view for displaying saved property location, map, and drawn shapes
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import LocationMapWithDrawing from './LocationMapWithDrawing';
+import '../../../styles/LocationSelection.css';
+import { useLocationViewLocalization } from '../../../services/AgentLocalisation/localisation-locationview';
+
+
+// Main component for viewing property location and drawn shapes in read-only mode
+const LocationViewPage: React.FC = () => {
+ const navigate = useNavigate();
+ const { formData } = usePropertyForm();
+
+ // Localization
+ const {
+ previousText,
+ newPropertyFormText,
+ addLocationText,
+ searchPlaceholderText,
+ mapInstructionText,
+ noShapesMessageText,
+ selectedLocationText,
+ mainLocationText,
+ additionalMarkedAreasText,
+ pinText,
+ rectangleText,
+ polygonText,
+ verticesText,
+ noAdditionalAreasText,
+ locationSavedOnText,
+ atText,
+ confirmLocationText
+ } = useLocationViewLocalization();
+
+
+ // Handle back navigation
+ const handleBack = () => {
+ navigate(-1);
+ };
+
+ // If no location data, redirect back
+ // If no location data, redirect back
+ if (!formData.locationData) {
+ navigate(-1);
+ return null;
+ }
+
+ // Extract location data from form
+ const { locationData } = formData;
+
+ // Main render: header, map, address, drawn shapes summary, and confirm button
+ return (
+
+ {/* Header */}
+
+
+
+
+ {previousText}
+
+
{newPropertyFormText}
+
{addLocationText}
+
+
+
+ {/* Map Container */}
+
+ {/* Search Bar */}
+
+
+ {/* Map Instruction */}
+
+
+
+ {}} // Read-only mode - no updates
+ initialShapes={locationData.drawnShapes || []}
+ readOnly={true}
+ />
+
+
+ {/* No shapes message */}
+ {(!locationData.drawnShapes || locationData.drawnShapes.length === 0) && (
+
+ {noShapesMessageText}
+
+ )}
+
+
+ {/* Bottom Panel */}
+
+
+
{selectedLocationText}
+
+
{locationData.address}
+
+
+
+ {mainLocationText}: {locationData.coordinates?.lat!.toFixed(6)}ยฐ, {locationData.coordinates?.lng!.toFixed(6)}ยฐ
+
+
+
+ {/* Drawn Shapes Summary */}
+ {locationData.drawnShapes && locationData.drawnShapes.length > 0 ? (
+
+
+ {additionalMarkedAreasText} ({locationData.drawnShapes.length})
+
+ {locationData.drawnShapes.map((shape, index) => (
+
+
+ {shape.type === 'point' ? (
+ `${pinText} ${index + 1}`
+ ) : shape.type === 'rectangle' ? (
+ `${rectangleText} ${index + 1}${shape.area ? ` (${(shape.area / 1000000).toFixed(3)} kmยฒ)` : ''}`
+ ) : (
+ `${polygonText} ${index + 1}${shape.area ? ` (${(shape.area / 1000000).toFixed(3)} kmยฒ)` : ''}`
+ )}
+
+ {shape.address && (
+
+ ๐ {shape.address}
+
+ )}
+ {shape.coordinates && (
+
+ ๐ {
+ shape.type === 'point'
+ ? `${(shape.coordinates as number[])[1].toFixed(6)}ยฐ, ${(shape.coordinates as number[])[0].toFixed(6)}ยฐ`
+ : `${(shape.coordinates as number[][]).length} ${verticesText}`
+ }
+
+ )}
+
+ ))}
+
+ ) : (
+
+ {noAdditionalAreasText}
+
+ )}
+
+ {locationData.timestamp && (
+
+ {locationSavedOnText} {new Date(locationData.timestamp).toLocaleDateString('en-GB', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric'
+ })} {atText} {new Date(locationData.timestamp).toLocaleTimeString('en-GB', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit'
+ })}
+
+ )}
+
+
+
+ {confirmLocationText}
+
+
+
+ );
+};
+
+export default LocationViewPage;
\ No newline at end of file
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/OwnerDetails.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/OwnerDetails.tsx
new file mode 100644
index 0000000..e549c9b
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/OwnerDetails.tsx
@@ -0,0 +1,724 @@
+import React, { useEffect, useState } from 'react';
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Button from '@mui/material/Button';
+import Typography from '@mui/material/Typography';
+import Checkbox from '@mui/material/Checkbox';
+import { useNavigate, useLocation } from 'react-router-dom';
+import OwnerCard from '../../features/PropertyForm/components/OwnerDetail/OwnerCard';
+import { useFormMode } from '../../../context/FormModeContext';
+import { useOwnerDetailsLocalization } from '../../../services/AgentLocalisation/localisation-owner-details';
+import {
+ validateAadhar,
+ validateMobile,
+ validateEmail,
+} from '../../../validations/formValidations';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useValidationLocalization } from '../../../services/AgentLocalisation/localisation-FormValidations';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import CustomDropdown from '../../features/PropertyForm/components/IGRSDetail/IGRSdropdown';
+import FormTextField from '../../features/PropertyForm/components/IGRSDetail/IGRSFormTextFiled';
+import { uniformInputSx, verifyButtonSx } from './styles/sharedStyles';
+import {
+ useAddOwnerMutation,
+ useDeleteOwnerMutation,
+ useGetOwnersByPropertyIdQuery,
+ useUpdateOwnerMutation,
+} from '../../../redux/apis/ownerApi';
+import type {
+ AddOwnerResponse,
+ Owner,
+ UpdateOwnerResponse,
+} from '../../../redux/apis/ownerApi';
+import type { AlertType } from '../../models/AlertType.model';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+// Allow only alphabetic input (for names, etc.)
+const onlyAlphabetInput = (value: string) => value.replace(/[^a-zA-Z\s]/g, '');
+
+// Sanitizer AND keyboard handler for mobile number input
+const onlyMobileInput = (value: string, maxLen: number = 10) => {
+ let digits = value.replace(/[^0-9]/g, '');
+ digits = digits.replace(/^0+/, '');
+ if (digits.length > maxLen) digits = digits.slice(0, maxLen);
+ if (digits.length > 0 && !/^[6-9]/.test(digits)) {
+ digits = '';
+ }
+ return digits;
+};
+
+// Block forbidden characters for number input (same as igrs-details-page)
+const handleNumberKeyDown = (e: React.KeyboardEvent) => {
+ const forbidden = ['-', 'e', 'E', '+', '.', ' '];
+ if (forbidden.includes(e.key)) {
+ e.preventDefault();
+ }
+};
+
+// Main component for entering and managing property owner details
+const OwnerDetails: React.FC = () => {
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const editingOwner = location.state?.editOwner as Owner | undefined;
+
+ const { formData, updateForm } = usePropertyForm();
+ const propertyId = formData.id;
+
+ const [addOwner] = useAddOwnerMutation();
+ const [updateOwner] = useUpdateOwnerMutation();
+ const { data: ownersData, refetch } = useGetOwnersByPropertyIdQuery(propertyId, {
+ skip: !propertyId,
+ });
+
+ const {
+ aadhaarLabel,
+ ownerNameLabel,
+ mobileNumberLabel,
+ genderLabel,
+ emailLabel,
+ guardianLabel,
+ guardianRelationshipLabel,
+ addOwnerValidationMsg,
+ ownerNameRequiredError,
+ genderRequiredError,
+ guardianRequiredError,
+ guardianRelationshipRequiredError,
+ primaryOwnerText,
+ ownerText,
+ nameText,
+ viewOwnersText,
+ genderOptions,
+ guardianRelationshipOptions,
+ propertyFormTitle,
+ newPropertyFormTitle,
+ ownerDetailsSubtitle,
+ previousText,
+ saveDraftText,
+ } = useOwnerDetailsLocalization();
+
+ const {
+ nextButtonText,
+ Onlynumbersareallowedupto12digitsMSG,
+ Aadhaarnumbermustbe12digitsMSG,
+ OnlyalphabetsareallowedMSG,
+ Mobilenumbermustbe10digitsMSG,
+ Onlynumbersareallowedupto10digitsMSG,
+ InvalidemailaddressMSG,
+ AddGuardianMSG,
+ ThisFieldIsRequiredMSG,
+ } = useLocalization();
+
+ // State for notification popup
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ // Show error popup with message
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ const { messages: validationMessages } = useValidationLocalization();
+
+ // List of owners fetched from API
+ const owners = ownersData?.data ?? [];
+
+ useEffect(()=>{
+ if(owners){
+ updateForm({owners: owners});
+ }
+ }, [owners]);
+
+
+ console.log(formData);
+
+ // State for editing, dropdowns, and guardian fields
+ const [isEditing, setIsEditing] = useState(false);
+ const [showGuardianFields, setShowGuardianFields] = useState(false);
+ const [showGuardianRelationshipDropdown, setShowGuardianRelationshipDropdown] =
+ useState(false);
+ const [showGenderDropdown, setShowGenderDropdown] = useState(false);
+ const [deleteOwner] = useDeleteOwnerMutation();
+
+ // Initial state for owner form
+ const initialOwnerForm: Owner = {
+ ID: '',
+ PropertyID: propertyId ?? '',
+ Name: '',
+ AdhaarNo: 0,
+ ContactNo: '',
+ Email: '',
+ Gender: '',
+ Guardian: '',
+ GuardianType: '',
+ RelationshipToProperty: '',
+ OwnershipShare: 100,
+ IsPrimaryOwner: false,
+ CreatedAt: '',
+ UpdatedAt: '',
+ };
+
+ // State for owner form fields
+ const [ownerForm, setOwnerForm] = useState(initialOwnerForm);
+
+ // State for form field errors
+ const [errors, setErrors] = useState>({
+ ownerName: '',
+ aadhaar: '',
+ mobile: '',
+ email: '',
+ gender: '',
+ guardian: '',
+ guardianRelationship: '',
+ });
+
+ // Handle guardian name input change
+ const handleGuardianChange = (val: string) => {
+ if (val && /[^a-zA-Z\s]/.test(val)) {
+ setErrors((e) => ({ ...e, guardian: OnlyalphabetsareallowedMSG }));
+ } else if (!val && showGuardianFields) {
+ setErrors((e) => ({ ...e, guardian: guardianRequiredError }));
+ } else {
+ setErrors((e) => ({ ...e, guardian: '' }));
+ }
+ setOwnerForm((f) => ({ ...f, Guardian: onlyAlphabetInput(val) }));
+ };
+
+ // Handle blur event for form fields (validation)
+ const handleBlur = (field: string, value: string) => {
+ const v = value ?? '';
+ let error: string = '';
+
+ if (!v.trim()) {
+ switch (field) {
+ case 'AdhaarNo':
+ error = ThisFieldIsRequiredMSG ?? '';
+ break;
+ case 'Name':
+ error = ownerNameRequiredError;
+ break;
+ case 'ContactNo':
+ error = ThisFieldIsRequiredMSG ?? '';
+ break;
+ case 'Email':
+ error = ThisFieldIsRequiredMSG ?? '';
+ break;
+ case 'Gender':
+ error = genderRequiredError;
+ break;
+ case 'Guardian':
+ if (showGuardianFields) error = guardianRequiredError;
+ break;
+ case 'GuardianType':
+ if (showGuardianFields) error = guardianRelationshipRequiredError;
+ break;
+ }
+ }
+
+ if (field === 'AdhaarNo') {
+ if (v && v.length !== 12) error = Aadhaarnumbermustbe12digitsMSG;
+ else if (v && /[^0-9]/.test(v)) error = Onlynumbersareallowedupto12digitsMSG;
+ else if (v && /^\d{12}$/.test(v)) {
+ const out = validateAadhar(v, validationMessages);
+ if (out) error = out;
+ }
+ }
+
+ if (field === 'ContactNo') {
+ if (v && v.length !== 10) error = Mobilenumbermustbe10digitsMSG;
+ else if (v && /[^0-9]/.test(v)) error = Onlynumbersareallowedupto10digitsMSG;
+ else if (v && !/^[6-9]/.test(v))
+ error = 'Mobile number should start with 6, 7, 8, or 9';
+ else if (v && /^\d{10}$/.test(v)) {
+ const out = validateMobile(v, validationMessages);
+ if (out) error = out;
+ }
+ }
+
+ if (field === 'Email') {
+ if (v && !/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(v))
+ error = InvalidemailaddressMSG;
+ else if (v) {
+ const out = validateEmail(v, validationMessages);
+ if (out) error = out;
+ }
+ }
+
+ if (field === 'Name' && v && /[^a-zA-Z\s]/.test(v))
+ error = OnlyalphabetsareallowedMSG;
+ if (field === 'Guardian' && v && /[^a-zA-Z\s]/.test(v))
+ error = OnlyalphabetsareallowedMSG;
+
+ setErrors((s) => ({
+ ...s,
+ [field === 'GuardianType'
+ ? 'guardianRelationship'
+ : field === 'Guardian'
+ ? 'guardian'
+ : field.toLowerCase()]: error,
+ }));
+ };
+
+ // Handle gender dropdown selection
+ const handleGenderSelect = (option: string) => {
+ setOwnerForm((prev) => ({ ...prev, Gender: option }));
+ setShowGenderDropdown(false);
+ setErrors((e) => ({
+ ...e,
+ gender: option ? '' : genderRequiredError,
+ }));
+ };
+
+ // Handle guardian relationship dropdown selection
+ const handleGuardianRelationshipSelect = (_f: string, option: string) => {
+ setOwnerForm((prev) => ({ ...prev, GuardianType: option }));
+ setShowGuardianRelationshipDropdown(false);
+ setErrors((e) => ({
+ ...e,
+ guardianRelationship: option ? '' : guardianRelationshipRequiredError,
+ }));
+ };
+
+ // Validate all owner fields and return error messages
+ function validateOwner(owner: Owner) {
+ return {
+ ownerName: owner.Name
+ ? /[^a-zA-Z\s]/.test(owner.Name)
+ ? OnlyalphabetsareallowedMSG
+ : ''
+ : ownerNameRequiredError,
+ aadhaar: owner.AdhaarNo
+ ? owner.AdhaarNo.toString().length === 12
+ ? /^\d{12}$/.test(owner.AdhaarNo.toString())
+ ? validateAadhar(owner.AdhaarNo.toString(), validationMessages) || ''
+ : Onlynumbersareallowedupto12digitsMSG
+ : Aadhaarnumbermustbe12digitsMSG
+ : Aadhaarnumbermustbe12digitsMSG,
+ mobile: owner.ContactNo
+ ? owner.ContactNo.length === 10
+ ? /^\d{10}$/.test(owner.ContactNo)
+ ? /^[6-9]/.test(owner.ContactNo)
+ ? validateMobile(owner.ContactNo, validationMessages) || ''
+ : 'Mobile number should start with 6, 7, 8, or 9'
+ : Onlynumbersareallowedupto10digitsMSG
+ : Mobilenumbermustbe10digitsMSG
+ : Mobilenumbermustbe10digitsMSG,
+ email: owner.Email
+ ? /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test(owner.Email)
+ ? validateEmail(owner.Email, validationMessages) || ''
+ : InvalidemailaddressMSG
+ : InvalidemailaddressMSG,
+ gender: owner.Gender ? '' : genderRequiredError,
+ guardian: showGuardianFields
+ ? owner.Guardian
+ ? /[^a-zA-Z\s]/.test(owner.Guardian)
+ ? OnlyalphabetsareallowedMSG
+ : ''
+ : guardianRequiredError
+ : '',
+ guardianRelationship: showGuardianFields
+ ? owner.GuardianType
+ ? ''
+ : guardianRelationshipRequiredError
+ : '',
+ };
+ }
+
+ // Handle add owner button click (save and go to next step)
+ const handleAddOwner = async () => {
+ try {
+ await handleSaveDraft();
+ navigate('/property-form/owner-details-two');
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ // Handle delete owner action
+ const handleDeleteOwner = async (id: string) => {
+ try {
+ await deleteOwner(id).unwrap();
+
+ // Wait for refetch to get fresh data
+ const { data: freshOwnersData } = await refetch();
+ const freshOwners = freshOwnersData?.data ?? [];
+
+ // Update form with fresh data from server
+ updateForm({ owners: freshOwners as Owner[] });
+ } catch (error) {
+ showErrorPopup('Failed to delete owner');
+ console.error('Delete owner error:', error);
+ }
+ };
+
+ // Handle back navigation
+ const handleGoBack = () => navigate(-1);
+
+ // Handle save draft action (validate and save owner)
+ const handleSaveDraft = async () => {
+ if (!formData.id) {
+ showErrorPopup('Property ID is missing. Please complete previous steps.');
+ return;
+ }
+
+ const validationResults = validateOwner(ownerForm);
+ setErrors(validationResults);
+
+ const hasError = Object.values(validationResults).some(Boolean);
+ if (hasError) {
+ showErrorPopup(addOwnerValidationMsg);
+ throw new Error(addOwnerValidationMsg);
+ }
+
+ const payload: Omit = {
+ PropertyID: formData.id,
+ Name: ownerForm.Name,
+ AdhaarNo: Number(ownerForm.AdhaarNo),
+ ContactNo: ownerForm.ContactNo,
+ Email: ownerForm.Email,
+ Gender: ownerForm.Gender.toUpperCase(),
+ Guardian: ownerForm.Guardian,
+ GuardianType: ownerForm.GuardianType?.toUpperCase() ?? '',
+ RelationshipToProperty: 'OWNER',
+ OwnershipShare: 100.0,
+ IsPrimaryOwner: owners.length === 0,
+ };
+
+ let response: UpdateOwnerResponse | AddOwnerResponse | undefined;
+
+ try {
+ if (ownerForm.ID) {
+ // Update owner (PUT)
+ response = await updateOwner({ id: ownerForm.ID, data: payload as any }).unwrap();
+ } else {
+ // Add owner (POST)
+ response = await addOwner(payload).unwrap();
+ }
+
+ console.log(response);
+
+ // Wait for refetch to complete and get fresh data
+ const { data: freshOwnersData } = await refetch();
+ const freshOwners = freshOwnersData?.data ?? [];
+
+ console.log('Fresh owners from refetch:', freshOwners);
+
+ // Update context with fresh data from server
+ await updateForm({
+ owners: freshOwners as Owner[],
+ });
+ // Reset form after successful operation
+ setOwnerForm(initialOwnerForm);
+ setErrors({
+ ownerName: '',
+ aadhaar: '',
+ mobile: '',
+ email: '',
+ gender: '',
+ guardian: '',
+ guardianRelationship: '',
+ });
+ setShowGuardianFields(false);
+ setIsEditing(false);
+ } catch (error) {
+ showErrorPopup('Failed to add owner');
+ throw error;
+ }
+ };
+
+ // On mount: if editing owner, populate form fields
+ useEffect(() => {
+ if (editingOwner) {
+ setOwnerForm(editingOwner);
+ setIsEditing(true);
+ setShowGuardianFields(!!editingOwner.Guardian || !!editingOwner.GuardianType);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [editingOwner]);
+
+ // Prepare gender dropdown options
+ const genderDropdownOptions = (genderOptions || []).map((g, i) => ({
+ id: i,
+ label: g ?? '',
+ }));
+ // Prepare guardian relationship dropdown options
+ const guardianRelOptions = (guardianRelationshipOptions || []).map((g, i) => ({
+ id: i,
+ label: g ?? '',
+ }));
+
+ // Styles for container and form layout
+ const containerSx = {
+ width: '100%',
+ margin: '0 auto',
+ minHeight: '100vh' as const,
+ bgcolor: '#fff',
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ display: 'flex' as const,
+ flexDirection: 'column' as const,
+ };
+ const headerSx = { backgroundColor: '#F9E6E0', padding: '16px' };
+ const formContentSx = {
+ flex: 1,
+ display: 'flex',
+ flexDirection: 'column',
+ px: '8%',
+ py: 3,
+ backgroundColor: '#FFFFFF',
+ };
+
+ // Main render: notification popup, step header, owner cards, and owner form
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+
+
+
+
+ {/* owner cards */}
+ {!isEditing && owners.length > 0 && (
+
+ {owners.map((owner) => (
+ handleDeleteOwner(owner.ID)}
+ onViewOwners={() => navigate('/property-form/owner-details-two')}
+ primaryOwnerText={primaryOwnerText}
+ ownerText={ownerText}
+ nameText={nameText}
+ viewOwnersText={viewOwnersText}
+ />
+ ))}
+
+ )}
+
+ {owners.length > 0 && (
+
+ Additional Owner Details:
+
+ )}
+
+
+ {
+ e.preventDefault();
+ handleAddOwner();
+ }}
+ >
+ {/* Aadhaar */}
+
+
+ setOwnerForm((f) => ({
+ ...f,
+ AdhaarNo: Number(val.replace(/[^0-9]/g, '').slice(0, 12)),
+ }))
+ }
+ onBlur={() => handleBlur('AdhaarNo', ownerForm.AdhaarNo.toString())}
+ placeholder=""
+ type="number"
+ required
+ error={errors.aadhaar}
+ touched={!!errors.aadhaar}
+ sx={{ width: '100%', ...uniformInputSx }}
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ />
+
+
+ {/* Owner name */}
+
+
+ setOwnerForm((f) => ({ ...f, Name: onlyAlphabetInput(val) }))
+ }
+ onBlur={() => handleBlur('Name', ownerForm.Name)}
+ placeholder=""
+ type="text"
+ required
+ error={errors.ownerName}
+ touched={!!errors.ownerName}
+ sx={{ width: '100%', ...uniformInputSx }}
+ />
+
+
+ {/* Mobile */}
+
+
+ setOwnerForm((f) => ({
+ ...f,
+ ContactNo: onlyMobileInput(val, 10),
+ }))
+ }
+ onBlur={() => handleBlur('ContactNo', ownerForm.ContactNo)}
+ placeholder=""
+ type="number"
+ required
+ error={errors.mobile}
+ touched={!!errors.mobile}
+ sx={{ width: '100%', ...uniformInputSx }}
+ inputProps={{ onKeyDown: handleNumberKeyDown }}
+ />
+
+
+ {/* Gender */}
+ handleGenderSelect(v)}
+ closeOtherDropdowns={() => setShowGenderDropdown(false)}
+ onBlur={() => handleBlur('Gender', ownerForm.Gender)}
+ required
+ error={errors.gender}
+ touched={!!errors.gender}
+ />
+
+ {/* Email */}
+
+ {
+ setOwnerForm((prev) => ({ ...prev, Email: val }));
+ setErrors((e) => ({ ...e, email: '' }));
+ }}
+ onBlur={() => handleBlur('Email', ownerForm.Email)}
+ placeholder=""
+ type="text"
+ required
+ error={errors.email}
+ touched={!!errors.email}
+ sx={{ width: '100%', ...uniformInputSx }}
+ />
+
+
+ {/* Guardian checkbox */}
+
+ {
+ const checked = e.target.checked;
+ setShowGuardianFields(checked);
+ if (!checked) {
+ setOwnerForm((prev) => ({
+ ...prev,
+ Guardian: '',
+ GuardianType: '',
+ }));
+ setErrors((prev) => ({
+ ...prev,
+ guardian: '',
+ guardianRelationship: '',
+ }));
+ }
+ }}
+ sx={{ mr: 1, color: '#8a4a20', '&.Mui-checked': { color: '#8a4a20' } }}
+ />
+ {AddGuardianMSG}
+
+
+ {showGuardianFields && (
+ <>
+
+ handleGuardianChange(val)}
+ onBlur={() => handleBlur('Guardian', ownerForm.Guardian ?? '')}
+ placeholder=""
+ type="text"
+ required
+ error={errors.guardian}
+ touched={!!errors.guardian}
+ sx={{ width: '100%', ...uniformInputSx }}
+ />
+
+
+ handleGuardianRelationshipSelect(_n, v)}
+ closeOtherDropdowns={() => setShowGuardianRelationshipDropdown(false)}
+ selectText=""
+ onBlur={() => handleBlur('GuardianType', ownerForm.GuardianType ?? '')}
+ required
+ error={errors.guardianRelationship}
+ touched={!!errors.guardianRelationship}
+ />
+ >
+ )}
+
+
+
+ {nextButtonText}
+
+
+
+
+
+ >
+ );
+};
+
+export default OwnerDetails;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/OwnerDetailsTwo.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/OwnerDetailsTwo.tsx
new file mode 100644
index 0000000..b88152c
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/OwnerDetailsTwo.tsx
@@ -0,0 +1,301 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import AddIcon from '@mui/icons-material/Add';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import OwnerCardDetail from '../../features/PropertyForm/components/OwnerDetail/OwnerCardDetail';
+import { useFormMode } from '../../../context/FormModeContext';
+import { usePropertyForm } from '../../../context/PropertyFormContext';
+// import type { Owner } from '../../../context/PropertyFormContext';
+import { useOwnerDetailsLocalization } from '../../../services/AgentLocalisation/localisation-OwnerDetailsTwo';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import { useLocalization } from '../../../services/AgentLocalisation/formLocalisation';
+import { verifyButtonSx } from './styles/sharedStyles';
+import Button from '@mui/material/Button';
+import type { AlertType } from '../../models/AlertType.model';
+import { useDeleteOwnerMutation, useGetOwnersByPropertyIdQuery, type Owner } from '../../../redux/apis/ownerApi';
+import { NotificationPopup } from '../../components/Popup/NotificationPopup';
+
+// Main component for displaying, adding, and editing multiple property owners
+const OwnerDetailsTwo: React.FC = () => {
+ const { mode } = useFormMode();
+ const navigate = useNavigate();
+
+ const { formData, updateForm } = usePropertyForm();
+ const [deleteOwner] = useDeleteOwnerMutation();
+
+ const propertyId = formData.id;
+
+ const {
+ data: ownersData,
+ isLoading,
+ refetch,
+ } = useGetOwnersByPropertyIdQuery(propertyId, {
+ skip: !propertyId,
+ });
+
+ // List of owners fetched from API
+ const owners = ownersData?.data ?? [];
+ const {
+ noOwnersFoundText,
+ addOwnerText,
+ addOwnerRequiredAlert,
+ // draftSavedAlert,
+ ownerNameLabel,
+ mobileNumberLabel,
+ aadhaarLabel,
+ emailLabel,
+ guardianLabel,
+ // guardianRelationshipLabel,
+ primaryOwnerText,
+ propertyFormTitle,
+ newPropertyFormTitle,
+ ownerDetailsSubtitle,
+ previousText,
+ saveDraftText,
+ } = useOwnerDetailsLocalization();
+
+ const { nextButtonText } = useLocalization();
+
+ // Inline styles derived from property-form.css (attached)
+ // Inline styles for layout and buttons
+ const styles = {
+ container: {
+ width: '100%',
+ // maxWidth: '100%',
+ margin: 0,
+ minHeight: '100vh',
+ display: 'flex',
+ flexDirection: 'column' as const,
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
+ padding: 0,
+ backgroundColor: '#FFFFFF',
+ },
+ headerWrapper: {
+ backgroundColor: '#F9E6E0',
+ padding: '4%',
+ // display: 'flex',
+ // alignItems: 'flex-start',
+ // justifyContent: 'flex-start',
+ },
+ addOwnerButton: {
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: 8,
+ backgroundColor: '#f7e4db',
+ borderRadius: 12,
+ color: '#333333',
+ fontSize: 16,
+ fontWeight: 600,
+ cursor: 'pointer',
+ transition: 'background-color 0.2s',
+ width: 'fit-content',
+ border: 'none',
+ padding: '10px 16px',
+ marginLeft: '7%',
+ marginTop: '4%',
+ },
+ formContent: {
+ width: '100%',
+ flex: 1,
+ padding: '24px 16px',
+ backgroundColor: '#FFFFFF',
+ alignItems: 'center',
+ },
+ noOwnersText: {
+ marginLeft: '4%',
+ color: '#333333',
+ },
+ formSubmitWrapper: {
+ backgroundColor: '#FFFFFF',
+ marginBottom: '8%',
+ minWidth: '40vw',
+ display: 'flex',
+ justifyContent: 'flex-end',
+ paddingRight: '4%',
+ },
+ primaryOwnerBadge: {
+ backgroundColor: '#C8504B',
+ color: '#FFFFFF',
+ padding: '4px 8px',
+ borderRadius: 8,
+ fontSize: 12,
+ fontWeight: 600,
+ },
+ };
+
+ // Handle edit owner action (navigate to owner details form)
+ const handleEditOwner = (owner: Owner) => {
+ navigate('/property-form/owner-details', { state: { editOwner: owner } });
+ };
+
+ // Handle delete owner action (remove from form context)
+ const handleDeleteOwner = async (id: string) => {
+ try {
+ await deleteOwner(id).unwrap();
+
+ // Wait for refetch to get fresh data
+ const { data: freshOwnersData } = await refetch();
+ const freshOwners = freshOwnersData?.data ?? [];
+
+ // Update form with fresh data from server
+ updateForm({ owners: freshOwners as Owner[] });
+ } catch (error) {
+ showErrorPopup('Failed to delete owner');
+ console.error('Delete owner error:', error);
+ }
+};
+
+ // Handle add owner button click (navigate to owner details form)
+ const handleAddOwner = () => {
+ navigate('/property-form/owner-details');
+ };
+
+ // Handle back navigation to property information
+ const handleGoBack = () => {
+ navigate(-1);
+ };
+
+ // State for notification popup
+ const [popup, setPopup] = useState<{
+ type: AlertType;
+ open: boolean;
+ title: string;
+ message: string;
+ duration: number;
+ }>({
+ type: 'warning',
+ open: false,
+ title: '',
+ message: '',
+ duration: 3000,
+ });
+
+ // Show error popup with message
+ function showErrorPopup(message: string, duration = 3000) {
+ setPopup((prev) => ({ ...prev, open: false }));
+ setTimeout(() => {
+ setPopup({
+ type: 'warning',
+ open: true,
+ title: 'Warning!',
+ message,
+ duration,
+ });
+ }, 10);
+ }
+
+ // Save current owners as draft in form context
+ const handleSaveDraft = () => {
+ updateForm({ owners });
+ };
+
+ // Handle submit button click (validate and go to next step)
+ const handleSubmit = () => {
+ if (owners.length === 0) {
+ showErrorPopup(addOwnerRequiredAlert);
+ return;
+ }
+ navigate('/property-form/property-address');
+ };
+
+ // Main render: notification popup, step header, owner cards, add/next buttons
+ return (
+ <>
+ setPopup((p) => ({ ...p, open: false }))}
+ />
+
+ {/* Header area (StepHeader receives props exactly as before) */}
+
+
+
+
+
+ {isLoading ? (
+
+ Loading owners...
+
+ ) : owners.length > 0 ? (
+ owners.map((owner, idx) => (
+ handleDeleteOwner(owner.ID)}
+ onViewOwners={() => {}}
+ isDetailed={true}
+ aadhar={owner.AdhaarNo.toString()}
+ mobile={owner.ContactNo}
+ email={owner.Email}
+ guardian={owner.Guardian}
+ guardianRelationship={owner.GuardianType}
+ nameText={ownerNameLabel}
+ mobileNumberLabel={mobileNumberLabel}
+ aadhaarLabel={aadhaarLabel}
+ emailLabel={emailLabel}
+ guardianLabel={guardianLabel}
+ primaryOwnerText={primaryOwnerText}
+ onEdit={() => handleEditOwner(owner)}
+ />
+ ))
+ ) : (
+
+ {noOwnersFoundText}
+
+ )}
+
+
+
+
+ }
+ variant="contained"
+ disableElevation
+ sx={{
+ textTransform: 'none',
+ backgroundColor: '#f7e4db',
+ color: '#333333',
+ fontWeight: 600,
+ borderRadius: '12px',
+ padding: '10px 16px',
+ width: '46%',
+ '&:hover': {
+ backgroundColor: '#f0dacd',
+ },
+ }}
+ >
+ {addOwnerText}
+
+
+ {mode === 'verify' ? 'Verify' :nextButtonText}
+
+
+
+ >
+ );
+};
+
+export default OwnerDetailsTwo;
diff --git a/frontend/mobile-ui/src/app/pages/PropertyForm/PreliminaryInfo.tsx b/frontend/mobile-ui/src/app/pages/PropertyForm/PreliminaryInfo.tsx
new file mode 100644
index 0000000..b6f7587
--- /dev/null
+++ b/frontend/mobile-ui/src/app/pages/PropertyForm/PreliminaryInfo.tsx
@@ -0,0 +1,283 @@
+// PreliminaryInfo page for collecting initial property details in the form flow
+import { type FC, Fragment, useState } from 'react';
+import {
+ Box,
+ Card,
+ CardActionArea,
+ CardContent,
+ Typography,
+ Radio,
+ Button,
+ Stack,
+ FormControl,
+ FormControlLabel,
+ RadioGroup,
+} from '@mui/material';
+import CottageOutlinedIcon from '@mui/icons-material/CottageOutlined';
+import DomainAddOutlinedIcon from '@mui/icons-material/DomainAddOutlined';
+import CasinoOutlinedIcon from '@mui/icons-material/CasinoOutlined';
+import WeekendOutlinedIcon from '@mui/icons-material/WeekendOutlined';
+import StepHeader from '../../features/Agent/components/StepHeader';
+import CountIncrementor from '../../features/PropertyForm/components/CountIncrementors';
+import { useNavigate } from 'react-router-dom';
+
+// Types for property card options
+type CardType = 'vacant' | 'structure' | 'multi' | 'unit';
+
+// Icons for each property card type
+const cardIcons: Record = {
+ vacant: ,
+ structure: ,
+ multi: ,
+ unit: ,
+};
+
+// Labels for each property card type
+const cardLabels: Record = {
+ vacant: 'Vacant land',
+ structure: 'Land with structure',
+ multi: 'Land with multiple structures',
+ unit: 'Building unit',
+};
+
+// Main component for preliminary property info step
+export const PreliminaryInfo: FC = () => {
+ // State for selected card, floor/basement counts, and mezzanine option
+ const [active, setActive] = useState(null);
+ const [floors, setFloors] = useState(0);
+ const [basements, setBasements] = useState(0);
+ const [mezzanine, setMezzanine] = useState('no');
+ const navigate = useNavigate();
+ // Key for saving form data in localStorage
+ const LOCAL_STORAGE_KEY = 'propertyFormData';
+
+ // List of selectable property cards (order of display)
+ const cardOrder: CardType[] = ['vacant', 'structure', 'multi', 'unit'];
+
+ // Helper function for rendering extra form fields for certain card types
+ const renderFormByType = (type: CardType) => {
+ if (type === 'structure' && active === 'structure') {
+ return (
+
+
+
+
+
+
+
+
+ Does this structure have a Mezzanine Floor?
+
+ setMezzanine(e.target.value)}
+ sx={{ gap: 2 }}
+ >
+ }
+ label="No"
+ />
+ }
+ label="Yes"
+ />
+
+
+
+
+ );
+ }
+ if (type === 'multi' && active === 'multi') {
+ return (
+
+
+
+
+
+
+ );
+ }
+ return null;
+ };
+
+ // Handler for Continue button: saves form data and navigates to next step
+ const handleContinue = () => {
+ const formData = {
+ propertyType: active,
+ floors,
+ basements,
+ mezzanine: active === 'structure' ? mezzanine : undefined,
+ };
+ // Save locally
+ try {
+ localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(formData));
+ navigate('/property-form/property-information');
+ } catch (e) {
+ // Handle localStorage error (optional)
+ }
+ // Print (console output for developer)
+ console.log('Saved Property Form:', formData);
+ // You can trigger next step navigation here if required
+ };
+
+ // Main UI rendering
+ return (
+
+ {/* Step header for navigation and progress */}
+ {
+ navigate(-1);
+ }}
+ onSaveDraft={() => {}}
+ previousText={'Previous'}
+ saveDraftText={'Save Draft'}
+ />
+
+ {/* Property type cards and dynamic form fields */}
+
+
+ {cardOrder.map((key) => (
+
+
+ setActive(key)}
+ >
+
+
+ {cardIcons[key]}
+
+
+