RoyAalekh commited on
Commit
afc0068
Β·
1 Parent(s): 2150b01

Major refactoring: Modular architecture implementation (v5.0.0)

Browse files

- Refactored monolithic 1,244-line JavaScript into 6 specialized modules
- Implemented professional modular architecture with clean separation of concerns
- Created modules: AuthManager, ApiClient, UIManager, FormManager, AutoCompleteManager, MediaManager
- Enhanced performance with debounced search, Promise.all(), and optimized DOM queries
- Removed all emojis for professional field research environment
- Added comprehensive error handling and validation
- Maintained backward compatibility - no breaking changes
- Updated documentation (README.md, llm.txt) to reflect new architecture
- Created backup of original app.js file

Benefits:
βœ“ Better maintainability and testability
βœ“ Improved performance and error handling
βœ“ Professional emoji-free interface
βœ“ Scalable modular design
βœ“ Enterprise-grade code organization

DOCUMENTATION.md ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TreeTrack Documentation
2
+
3
+ ## Overview
4
+
5
+ TreeTrack is a modern web application for documenting and managing tree data with geospatial mapping capabilities. Built with FastAPI backend and vanilla JavaScript frontend, it features user authentication, file uploads, interactive maps, and persistent cloud storage via Supabase.
6
+
7
+ ## Architecture
8
+
9
+ ### Technology Stack
10
+ - **Backend**: FastAPI (Python)
11
+ - **Frontend**: Vanilla JavaScript, HTML5, CSS3
12
+ - **Database**: Supabase PostgreSQL
13
+ - **Storage**: Supabase Storage (private buckets)
14
+ - **Authentication**: JWT tokens with cookie support
15
+ - **Deployment**: Hugging Face Spaces (Docker)
16
+
17
+ ### Key Features
18
+ - Tree data entry with photo/audio uploads
19
+ - Interactive map with tree markers
20
+ - User authentication and permissions
21
+ - Auto-complete for tree species
22
+ - Persistent cloud storage
23
+ - Mobile-responsive design
24
+ - PWA capabilities with service worker
25
+
26
+ ## API Endpoints
27
+
28
+ ### Authentication APIs
29
+
30
+ #### POST `/api/auth/login`
31
+ Login with username and password
32
+ ```json
33
+ Request:
34
+ {
35
+ "username": "aalekh",
36
+ "password": "aalekh@maptrees"
37
+ }
38
+
39
+ Response:
40
+ {
41
+ "access_token": "jwt_token_here",
42
+ "token_type": "bearer",
43
+ "user": {
44
+ "username": "aalekh",
45
+ "role": "admin"
46
+ }
47
+ }
48
+ ```
49
+
50
+ #### GET `/api/auth/validate`
51
+ Validate current authentication token
52
+ ```json
53
+ Response:
54
+ {
55
+ "username": "aalekh",
56
+ "role": "admin"
57
+ }
58
+ ```
59
+
60
+ ### Tree Management APIs
61
+
62
+ #### GET `/api/trees`
63
+ Get all trees with optional filtering
64
+ ```json
65
+ Query Parameters:
66
+ - user: filter by username
67
+ - limit: number of results (default: 100)
68
+
69
+ Response:
70
+ [
71
+ {
72
+ "id": 1,
73
+ "local_name": "Assamese Name",
74
+ "scientific_name": "Species scientificus",
75
+ "common_name": "Common Name",
76
+ "tree_code": "TC001",
77
+ "location": "POINT(91.7362 26.1724)",
78
+ "created_by": "aalekh",
79
+ "created_at": "2024-01-01T12:00:00Z",
80
+ "photos": {
81
+ "trunk_url": "https://signed-url...",
82
+ "leaves_url": "https://signed-url...",
83
+ "flowers_url": "https://signed-url...",
84
+ "fruits_url": "https://signed-url..."
85
+ },
86
+ "audio_url": "https://signed-url..."
87
+ }
88
+ ]
89
+ ```
90
+
91
+ #### POST `/api/trees`
92
+ Create a new tree record
93
+ ```json
94
+ Request (multipart/form-data):
95
+ - local_name: string
96
+ - scientific_name: string
97
+ - common_name: string
98
+ - tree_code: string
99
+ - latitude: float
100
+ - longitude: float
101
+ - height: float
102
+ - girth: float
103
+ - fruiting_season: string
104
+ - notes: string
105
+ - trunk_photo: file (optional)
106
+ - leaves_photo: file (optional)
107
+ - flowers_photo: file (optional)
108
+ - fruits_photo: file (optional)
109
+ - audio: file (optional)
110
+
111
+ Response:
112
+ {
113
+ "id": 1,
114
+ "message": "Tree created successfully"
115
+ }
116
+ ```
117
+
118
+ #### PUT `/api/trees/{tree_id}`
119
+ Update an existing tree record
120
+ ```json
121
+ Request: Same as POST /api/trees
122
+ Response: Same as POST /api/trees
123
+ ```
124
+
125
+ #### DELETE `/api/trees/{tree_id}`
126
+ Delete a tree record
127
+ ```json
128
+ Response:
129
+ {
130
+ "message": "Tree deleted successfully"
131
+ }
132
+ ```
133
+
134
+ ### File Management APIs
135
+
136
+ #### GET `/api/trees/{tree_id}/photo/{photo_type}`
137
+ Get signed URL for tree photo
138
+ - photo_type: trunk, leaves, flowers, fruits
139
+
140
+ #### GET `/api/trees/{tree_id}/audio`
141
+ Get signed URL for tree audio recording
142
+
143
+ ### Utility APIs
144
+
145
+ #### GET `/api/tree-suggestions`
146
+ Get autocomplete suggestions for tree species
147
+ ```json
148
+ Query Parameters:
149
+ - query: search term
150
+ - limit: number of results (default: 10)
151
+
152
+ Response:
153
+ [
154
+ {
155
+ "local_name": "Assamese Name",
156
+ "scientific_name": "Species scientificus",
157
+ "common_name": "Common Name",
158
+ "tree_code": "TC001",
159
+ "fruiting_season": "Winter"
160
+ }
161
+ ]
162
+ ```
163
+
164
+ #### GET `/api/statistics`
165
+ Get application statistics
166
+ ```json
167
+ Response:
168
+ {
169
+ "total_trees": 42,
170
+ "total_users": 4,
171
+ "trees_with_photos": 38,
172
+ "trees_with_audio": 15
173
+ }
174
+ ```
175
+
176
+ #### GET `/api/version`
177
+ Get current application version
178
+ ```json
179
+ Response:
180
+ {
181
+ "version": "1.0.0",
182
+ "timestamp": "2024-01-01T12:00:00Z"
183
+ }
184
+ ```
185
+
186
+ ## User Authentication
187
+
188
+ ### User Accounts
189
+ The system includes predefined user accounts:
190
+
191
+ - **aalekh** (Admin): Full permissions
192
+ - **admin** (System Admin): Full permissions
193
+ - **ishita** (Researcher): Create/edit own records
194
+ - **jeeb** (Researcher): Create/edit own records
195
+
196
+ ### Security
197
+ - Passwords stored as salted hashes using PBKDF2
198
+ - JWT tokens for API authentication
199
+ - HTTP-only cookies for web interface
200
+ - Environment variables for password configuration
201
+ - Role-based access control
202
+
203
+ ### Required Environment Variables
204
+ ```bash
205
+ AALEKH_PASSWORD=aalekh@maptrees
206
+ ADMIN_PASSWORD=admin@maptrees
207
+ ISHITA_PASSWORD=ishita@maptrees
208
+ JEEB_PASSWORD=jeeb@maptrees
209
+ SUPABASE_URL=your_supabase_url
210
+ SUPABASE_KEY=your_supabase_key
211
+ SUPABASE_SERVICE_KEY=your_service_key
212
+ ```
213
+
214
+ ## Database Schema
215
+
216
+ ### Trees Table
217
+ ```sql
218
+ CREATE TABLE trees (
219
+ id SERIAL PRIMARY KEY,
220
+ local_name VARCHAR(255),
221
+ scientific_name VARCHAR(255),
222
+ common_name VARCHAR(255),
223
+ tree_code VARCHAR(50),
224
+ location GEOMETRY(POINT, 4326),
225
+ latitude DECIMAL(10, 8),
226
+ longitude DECIMAL(11, 8),
227
+ height DECIMAL(5, 2),
228
+ girth DECIMAL(5, 2),
229
+ fruiting_season VARCHAR(100),
230
+ notes TEXT,
231
+ trunk_photo VARCHAR(255),
232
+ leaves_photo VARCHAR(255),
233
+ flowers_photo VARCHAR(255),
234
+ fruits_photo VARCHAR(255),
235
+ audio_file VARCHAR(255),
236
+ created_by VARCHAR(100),
237
+ created_at TIMESTAMP DEFAULT NOW(),
238
+ updated_at TIMESTAMP DEFAULT NOW()
239
+ );
240
+ ```
241
+
242
+ ## File Storage
243
+
244
+ ### Supabase Storage Structure
245
+ ```
246
+ Buckets:
247
+ β”œβ”€β”€ tree-photos/
248
+ β”‚ β”œβ”€β”€ {tree_id}/
249
+ β”‚ β”‚ β”œβ”€β”€ trunk.jpg
250
+ β”‚ β”‚ β”œβ”€β”€ leaves.jpg
251
+ β”‚ β”‚ β”œβ”€β”€ flowers.jpg
252
+ β”‚ β”‚ └── fruits.jpg
253
+ └── tree-audio/
254
+ └── {tree_id}/
255
+ └── recording.webm
256
+ ```
257
+
258
+ ### File Access
259
+ - All files stored in private buckets
260
+ - Access via signed URLs (24-hour expiry)
261
+ - Automatic cleanup of expired URLs
262
+ - Support for images (JPEG, PNG, WebP) and audio (WebM, MP3, WAV)
263
+
264
+ ## Frontend Components
265
+
266
+ ### Main Form (`/`)
267
+ - Tree data entry form
268
+ - Photo capture/upload (4 categories)
269
+ - Audio recording
270
+ - Location selection via map
271
+ - Auto-complete for species
272
+ - Mobile-responsive design
273
+
274
+ ### Map View (`/map`)
275
+ - Interactive Leaflet map
276
+ - Tree markers with popups
277
+ - Filter by user
278
+ - Edit/delete functionality
279
+ - Location-based clustering
280
+
281
+ ### Login Page (`/login`)
282
+ - User authentication
283
+ - Quick-select user accounts
284
+ - Password security
285
+ - Session management
286
+
287
+ ## Mobile Support
288
+
289
+ ### Progressive Web App (PWA)
290
+ - Service worker for caching
291
+ - Offline capability
292
+ - App-like experience
293
+ - Push notification support
294
+
295
+ ### Responsive Design
296
+ - Mobile-first approach
297
+ - Touch-friendly controls
298
+ - Optimized for various screen sizes
299
+ - Camera integration for photos
300
+
301
+ ## Deployment
302
+
303
+ ### Hugging Face Spaces
304
+ The application is deployed on Hugging Face Spaces using Docker.
305
+
306
+ #### Configuration Files
307
+ - `Dockerfile`: Container setup
308
+ - `requirements.txt`: Python dependencies
309
+ - `.env`: Environment variables (not in repo)
310
+ - `app.py`: Main application entry point
311
+
312
+ #### Deployment Process
313
+ 1. Push changes to HF Spaces repository
314
+ 2. Docker container rebuilds automatically
315
+ 3. Environment variables configured in Spaces secrets
316
+ 4. Application starts on container startup
317
+
318
+ ### Local Development
319
+ ```bash
320
+ # Clone repository
321
+ git clone https://huggingface.co/spaces/your-username/TreeTrack
322
+ cd TreeTrack
323
+
324
+ # Install dependencies
325
+ pip install -r requirements.txt
326
+
327
+ # Set environment variables
328
+ cp .env.example .env
329
+ # Edit .env with your credentials
330
+
331
+ # Run application
332
+ uvicorn app:app --reload --host 0.0.0.0 --port 7860
333
+ ```
334
+
335
+ ## Cache Management
336
+
337
+ ### Service Worker Strategy
338
+ - Network-first for API calls
339
+ - Cache-first for static assets
340
+ - Version-based cache invalidation
341
+ - Automatic cache updates
342
+
343
+ ### Version Control
344
+ - Timestamp-based versioning
345
+ - Automatic cache busting
346
+ - Development environment detection
347
+ - Manual cache refresh endpoints
348
+
349
+ ## Security Considerations
350
+
351
+ ### Data Protection
352
+ - HTTPS enforced
353
+ - Private file storage
354
+ - Signed URLs with expiry
355
+ - User-based access control
356
+
357
+ ### Authentication Security
358
+ - Password hashing with salt
359
+ - JWT token expiration
360
+ - HTTP-only cookies
361
+ - CSRF protection
362
+
363
+ ## Troubleshooting
364
+
365
+ ### Common Issues
366
+
367
+ #### Cache Problems
368
+ ```bash
369
+ # Clear browser cache
370
+ # Unregister service workers
371
+ # Check version endpoints
372
+ GET /api/version
373
+ ```
374
+
375
+ #### Authentication Issues
376
+ ```bash
377
+ # Verify environment variables
378
+ # Check user credentials
379
+ # Clear cookies and localStorage
380
+ ```
381
+
382
+ #### File Upload Problems
383
+ ```bash
384
+ # Check Supabase storage configuration
385
+ # Verify bucket permissions
386
+ # Monitor file size limits
387
+ ```
388
+
389
+ ### Debug Endpoints
390
+ - `/api/version`: Check app version
391
+ - `/api/statistics`: Verify data counts
392
+ - Browser DevTools: Network and Console tabs
393
+
394
+ ## Contributing
395
+
396
+ ### Development Workflow
397
+ 1. Fork the repository
398
+ 2. Create feature branch
399
+ 3. Make changes with tests
400
+ 4. Submit pull request
401
+ 5. Code review and merge
402
+
403
+ ### Code Standards
404
+ - Python: PEP 8 compliance
405
+ - JavaScript: ESLint configuration
406
+ - Documentation: Markdown format
407
+ - Commits: Conventional commit messages
408
+
409
+ ## Resources
410
+
411
+ ### External Links
412
+ - [FastAPI Documentation](https://fastapi.tiangolo.com/)
413
+ - [Supabase Documentation](https://supabase.com/docs)
414
+ - [Leaflet Maps](https://leafletjs.com/)
415
+ - [Hugging Face Spaces](https://huggingface.co/spaces)
416
+
417
+ ### Internal Files
418
+ - `llm.txt`: LLM knowledge base
419
+ - `SECURITY.md`: Security documentation
420
+ - `requirements.txt`: Dependencies
421
+ - `static/`: Frontend assets
422
+
423
+ ---
424
+
425
+ ## Support
426
+
427
+ For issues, questions, or contributions, please contact the development team or create an issue in the repository.
428
+
429
+ **Last Updated**: January 2024
430
+ **Version**: 1.0.0
README.md CHANGED
@@ -1,29 +1,30 @@
1
  ---
2
  title: TreeTrack
3
- emoji: 🌳
4
  colorFrom: green
5
  colorTo: blue
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
9
  license: mit
10
- short_description: Enhanced tree mapping and urban forestry management
11
  ---
12
 
13
- # 🌳 TreeTrack - Enhanced Tree Mapping Application
14
 
15
- A **secure, robust, and high-performance** web application for mapping, tracking, and managing urban forest data using FastAPI with comprehensive security implementations and best practices.
16
 
17
- ## ✨ NEW: Intelligent Auto-Suggestion System
18
 
 
19
  - **146 Pre-loaded Tree Species**: Comprehensive database from Tezpur research team
20
- - **Smart Auto-completion**: Real-time species suggestions as you type
21
  - **Multi-language Support**: Local Assamese names, scientific names, common names
22
  - **Tree Code Validation**: Reference codes (AA, AP, AC, etc.) with instant lookup
23
  - **Auto-fill Fields**: Selecting one suggestion populates related fields automatically
24
- - **Seasonal Data**: Fruiting seasons and ecological information
 
25
 
26
- ## 🎯 Core Features
27
 
28
  - **Interactive Tree Mapping**: Add, edit, and visualize trees on an interactive map
29
  - **Intelligent Species Identification**: Auto-suggestions from master species database
@@ -33,7 +34,7 @@ A **secure, robust, and high-performance** web application for mapping, tracking
33
  - **RESTful API**: Full API documentation with FastAPI's automatic OpenAPI docs
34
  - **Responsive Design**: Works on desktop and mobile devices
35
 
36
- ## πŸš€ Usage
37
 
38
  1. **View Trees**: Browse the interactive map to see all registered trees
39
  2. **Add New Trees**: Click on the map to add new tree entries with detailed information
@@ -41,7 +42,7 @@ A **secure, robust, and high-performance** web application for mapping, tracking
41
  4. **API Access**: Use the `/docs` endpoint for full API documentation
42
  5. **Statistics**: View comprehensive statistics about your urban forest
43
 
44
- ## πŸ“Š API Endpoints
45
 
46
  ### Core Tree Management
47
  - `GET /` - Main application interface
@@ -64,7 +65,7 @@ A **secure, robust, and high-performance** web application for mapping, tracking
64
  - `GET /download/csv` - Download CSV export of tree data
65
  - `GET /download/status` - Download database status report
66
 
67
- ## πŸ›‘οΈ Security Features
68
 
69
  - Input validation and sanitization
70
  - SQL injection prevention
@@ -73,14 +74,16 @@ A **secure, robust, and high-performance** web application for mapping, tracking
73
  - Secure file path validation
74
  - Comprehensive error handling
75
 
76
- ## πŸ”§ Technical Stack
77
 
78
  - **Backend**: FastAPI (Python 3.11+)
79
  - **Database**: SQLite with WAL mode
80
- - **Frontend**: Vanilla JavaScript with modern practices
 
81
  - **Mapping**: Interactive maps with marker clustering
82
  - **Validation**: Pydantic models with custom validators
83
  - **Security**: Multiple layers of protection
 
84
 
85
  ---
86
 
 
1
  ---
2
  title: TreeTrack
 
3
  colorFrom: green
4
  colorTo: blue
5
  sdk: docker
6
  app_port: 7860
7
  pinned: false
8
  license: mit
9
+ short_description: Professional field research platform for tree mapping and urban forestry management
10
  ---
11
 
12
+ # TreeTrack - Professional Field Research Platform
13
 
14
+ A **secure, robust, and high-performance** web application for mapping, tracking, and managing urban forest data using FastAPI with comprehensive security implementations and modern modular architecture.
15
 
16
+ ## NEW: Modular Architecture & Enhanced Features
17
 
18
+ - **Modular JavaScript Architecture**: Clean separation of concerns with 6 specialized modules
19
  - **146 Pre-loaded Tree Species**: Comprehensive database from Tezpur research team
20
+ - **Smart Auto-completion**: Real-time species suggestions with debounced search
21
  - **Multi-language Support**: Local Assamese names, scientific names, common names
22
  - **Tree Code Validation**: Reference codes (AA, AP, AC, etc.) with instant lookup
23
  - **Auto-fill Fields**: Selecting one suggestion populates related fields automatically
24
+ - **Enhanced Performance**: Optimized API calls and improved error handling
25
+ - **Professional UI**: Emoji-free interface suitable for research environments
26
 
27
+ ## Core Features
28
 
29
  - **Interactive Tree Mapping**: Add, edit, and visualize trees on an interactive map
30
  - **Intelligent Species Identification**: Auto-suggestions from master species database
 
34
  - **RESTful API**: Full API documentation with FastAPI's automatic OpenAPI docs
35
  - **Responsive Design**: Works on desktop and mobile devices
36
 
37
+ ## Usage
38
 
39
  1. **View Trees**: Browse the interactive map to see all registered trees
40
  2. **Add New Trees**: Click on the map to add new tree entries with detailed information
 
42
  4. **API Access**: Use the `/docs` endpoint for full API documentation
43
  5. **Statistics**: View comprehensive statistics about your urban forest
44
 
45
+ ## API Endpoints
46
 
47
  ### Core Tree Management
48
  - `GET /` - Main application interface
 
65
  - `GET /download/csv` - Download CSV export of tree data
66
  - `GET /download/status` - Download database status report
67
 
68
+ ## Security Features
69
 
70
  - Input validation and sanitization
71
  - SQL injection prevention
 
74
  - Secure file path validation
75
  - Comprehensive error handling
76
 
77
+ ## Technical Stack
78
 
79
  - **Backend**: FastAPI (Python 3.11+)
80
  - **Database**: SQLite with WAL mode
81
+ - **Frontend**: Modular JavaScript with ES6 modules
82
+ - **Architecture**: 6 specialized modules (Auth, API, UI, Form, AutoComplete, Media)
83
  - **Mapping**: Interactive maps with marker clustering
84
  - **Validation**: Pydantic models with custom validators
85
  - **Security**: Multiple layers of protection
86
+ - **Performance**: Debounced search, Promise.all(), optimized DOM queries
87
 
88
  ---
89
 
SPECTACULAR_VISUALIZATION_ROADMAP.md ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TreeTrack Spectacular Visualization Roadmap
2
+
3
+ ## Vision Statement
4
+ Transform TreeTrack into a spectacular, immersive tree exploration experience with interactive storytelling and thematic navigation, while maintaining data collection as the primary objective.
5
+
6
+ ---
7
+
8
+ ## Core Philosophy
9
+ **"Data Collection First, Spectacular Visualization Second"**
10
+
11
+ - Preserve and enhance existing robust data collection system
12
+ - Add spectacular visualization as progressive enhancement
13
+ - Maintain mobile-first approach for field researchers
14
+ - Ensure backward compatibility and performance
15
+
16
+ ---
17
+
18
+ ## Current State Assessment
19
+
20
+ ### What We Have (Strong Foundation)
21
+ - **Comprehensive Data Model**: 12 detailed tree fields including geolocation, measurements, phenology, utilities
22
+ - **Rich Media Support**: Photo categories (Leaf, Bark, Fruit, Seed, Flower, Full tree), audio storytelling
23
+ - **Professional Map Interface**: Leaflet-based with modern design system
24
+ - **Authentication System**: Multi-user with role-based permissions
25
+ - **Cloud Infrastructure**: Supabase backend with file storage
26
+ - **Master Species Database**: 146 tree species with autocomplete
27
+ - **Mobile PWA**: Service worker, offline capability, responsive design
28
+
29
+ ### What Needs Fixing First
30
+ 1. **Cache Management Issues**: Service worker causing development problems
31
+ 2. **Map Performance**: Optimize marker clustering and rendering
32
+ 3. **Mobile UX**: Enhance touch interactions and form usability
33
+ 4. **Audio Integration**: Improve recording and playback experience
34
+ 5. **Photo Management**: Better categorization and thumbnail generation
35
+ 6. **Search & Filter**: More intuitive tree discovery
36
+
37
+ ---
38
+
39
+ ## Implementation Phases
40
+
41
+ ### **Phase 0: Foundation Fixes** (Priority 1 - Immediate)
42
+ *Focus: Fix current issues and strengthen the foundation*
43
+
44
+ #### Week 1-2: Critical Fixes
45
+ - [ ] **Cache Management Overhaul**
46
+ - Fix service worker caching issues
47
+ - Implement proper versioning strategy
48
+ - Add development mode detection
49
+
50
+ - [ ] **Mobile Experience Enhancement**
51
+ - Optimize form layout for mobile data entry
52
+ - Improve camera integration
53
+ - Better audio recording interface
54
+ - Touch-friendly map interactions
55
+
56
+ - [ ] **Performance Optimization**
57
+ - Implement efficient marker clustering
58
+ - Add progressive loading for large datasets
59
+ - Optimize image loading and thumbnails
60
+
61
+ #### Week 3-4: Core Improvements
62
+ - [ ] **Enhanced Search & Discovery**
63
+ - Advanced filtering by species, utilities, phenology
64
+ - Improved autocomplete with fuzzy matching
65
+ - Location-based tree discovery
66
+
67
+ - [ ] **Data Quality Tools**
68
+ - Validation improvements
69
+ - Data completeness indicators
70
+ - Bulk edit capabilities for researchers
71
+
72
+ ---
73
+
74
+ ### **Phase 1: Spectacular Story Mode** (Priority 2 - Q2 2025)
75
+ *Focus: Add immersive exploration mode with dark theme and interactive storytelling*
76
+
77
+ #### Core Features
78
+ - [ ] **Story Mode Toggle**
79
+ - Switch between "Data Collection Mode" and "Story Mode"
80
+ - Dark, immersive theme with geographic aesthetics
81
+ - Smooth transitions and animations
82
+
83
+ - [ ] **Interactive Tree Stories**
84
+ - Click trees to reveal rich story popups
85
+ - Integration with existing storytelling text and audio
86
+ - Photo galleries with smooth lightbox experience
87
+ - Cultural and ecological significance highlighting
88
+
89
+ - [ ] **Thematic Navigation** ("Travel by Themes")
90
+ - **Religious Trees**: Filter trees with religious significance
91
+ - **Medicine & Healing**: Traditional medicinal uses
92
+ - **Food & Sustenance**: Edible fruits, leaves, bark
93
+ - **Biodiversity Hotspots**: Ecological importance
94
+ - **Cultural Heritage**: Local traditions and stories
95
+ - **Seasonal Journeys**: Phenology-based exploration
96
+
97
+ #### Visual Enhancements
98
+ - [ ] **Immersive Dark Theme**
99
+ - Deep background gradients with atmospheric effects
100
+ - Subtle geographic pattern overlays
101
+ - Dynamic lighting based on tree themes
102
+
103
+ - [ ] **Animated Tree Markers**
104
+ - Pulsing markers that respond to themes
105
+ - Color-coded by utilities or phenology stages
106
+ - Smooth zoom and transition animations
107
+
108
+ - [ ] **Atmospheric UI Elements**
109
+ - Overlay text: "CLICK ON TREE TO DISCOVER ITS STORY"
110
+ - Elegant typography with multiple language support
111
+ - Floating navigation elements
112
+
113
+ #### Technical Implementation
114
+ - [ ] **Enhanced Leaflet Integration**
115
+ - Custom marker animations with CSS transforms
116
+ - Advanced popup system with rich media
117
+ - Smooth map transitions and effects
118
+
119
+ - [ ] **Audio Storytelling System**
120
+ - Background audio playback
121
+ - Spatial audio effects (trees closer = louder)
122
+ - Audio waveform visualization
123
+
124
+ ---
125
+
126
+ ### **Phase 2: Advanced Exploration Features** (Priority 3 - Q3 2025)
127
+ *Focus: Next-level interactive experiences*
128
+
129
+ #### Advanced Storytelling
130
+ - [ ] **Multi-Language Support**
131
+ - Assamese, Hindi, English story versions
132
+ - Voice narration in local languages
133
+ - Cultural context for each region
134
+
135
+ - [ ] **Temporal Exploration**
136
+ - "Time Travel" through seasons
137
+ - Historical tree data visualization
138
+ - Growth progression animations
139
+
140
+ - [ ] **Community Stories**
141
+ - User-contributed stories and folklore
142
+ - Community voting on favorite trees
143
+ - Collaborative storytelling features
144
+
145
+ #### Enhanced Visualization
146
+ - [ ] **3D Tree Visualization**
147
+ - Three.js integration for 3D tree models
148
+ - Realistic tree rendering based on measurements
149
+ - Seasonal appearance changes
150
+
151
+ - [ ] **Augmented Reality (AR)**
152
+ - Mobile AR for tree identification
153
+ - Overlay information in real-world view
154
+ - Virtual tree placement and measurement
155
+
156
+ #### Data Analytics Dashboard
157
+ - [ ] **Ecological Insights**
158
+ - Biodiversity heat maps
159
+ - Phenology pattern analysis
160
+ - Climate impact visualization
161
+
162
+ - [ ] **Research Tools**
163
+ - Export capabilities for researchers
164
+ - Statistical analysis integration
165
+ - Collaborative research features
166
+
167
+ ---
168
+
169
+ ### **Phase 3: Next-Generation Features** (Priority 4 - Q4 2025)
170
+ *Focus: Cutting-edge technology integration*
171
+
172
+ #### AI & Machine Learning
173
+ - [ ] **Automated Tree Identification**
174
+ - AI-powered species identification from photos
175
+ - Automated measurement estimation
176
+ - Quality validation of data entries
177
+
178
+ - [ ] **Predictive Analytics**
179
+ - Tree health prediction models
180
+ - Optimal planting location suggestions
181
+ - Climate change impact forecasting
182
+
183
+ #### Advanced Interactions
184
+ - [ ] **Voice Interface**
185
+ - Voice-controlled data entry
186
+ - Audio-guided tree exploration
187
+ - Hands-free operation for field work
188
+
189
+ - [ ] **IoT Integration**
190
+ - Sensor data integration (soil, weather)
191
+ - Real-time environmental monitoring
192
+ - Automated data collection
193
+
194
+ #### Community Platform
195
+ - [ ] **Social Features**
196
+ - Tree adoption programs
197
+ - Community challenges and gamification
198
+ - Educational content and quizzes
199
+
200
+ ---
201
+
202
+ ## Technical Architecture Plan
203
+
204
+ ### Frontend Enhancements
205
+ ```
206
+ TreeTrack/static/
207
+ β”œβ”€β”€ core/
208
+ β”‚ β”œβ”€β”€ app.js # Core application logic (existing)
209
+ β”‚ β”œβ”€β”€ map.js # Enhanced map functionality
210
+ β”‚ └── data-entry.js # Optimized forms
211
+ β”œβ”€β”€ story-mode/
212
+ β”‚ β”œβ”€β”€ story-controller.js # Story mode orchestration
213
+ β”‚ β”œβ”€β”€ theme-navigation.js # Thematic filtering
214
+ β”‚ β”œβ”€β”€ audio-player.js # Enhanced audio system
215
+ β”‚ └── story-popup.js # Rich story popups
216
+ β”œβ”€β”€ themes/
217
+ β”‚ β”œβ”€β”€ default.css # Current professional theme
218
+ β”‚ β”œβ”€β”€ story-dark.css # Dark immersive theme
219
+ β”‚ └── mobile.css # Mobile optimizations
220
+ └── assets/
221
+ β”œβ”€β”€ audio/ # Story narrations
222
+ β”œβ”€β”€ textures/ # Geographic patterns
223
+ └── icons/ # Themed markers
224
+ ```
225
+
226
+ ### Backend API Extensions
227
+ ```python
228
+ # New API endpoints for spectacular features
229
+ /api/stories/ # Rich story content
230
+ /api/themes/ # Thematic filtering
231
+ /api/audio/streaming/ # Audio streaming
232
+ /api/analytics/ # Usage analytics
233
+ /api/community/ # Community features
234
+ ```
235
+
236
+ ### Database Schema Extensions
237
+ ```sql
238
+ -- Story mode enhancements
239
+ ALTER TABLE trees ADD COLUMN story_priority INTEGER DEFAULT 0;
240
+ ALTER TABLE trees ADD COLUMN cultural_significance TEXT;
241
+ ALTER TABLE trees ADD COLUMN audio_duration DECIMAL(5,2);
242
+ ALTER TABLE trees ADD COLUMN theme_tags JSON;
243
+
244
+ -- Community features
245
+ CREATE TABLE tree_stories (
246
+ id SERIAL PRIMARY KEY,
247
+ tree_id INTEGER REFERENCES trees(id),
248
+ contributor_name VARCHAR(255),
249
+ story_text TEXT,
250
+ audio_file VARCHAR(255),
251
+ language VARCHAR(10),
252
+ created_at TIMESTAMP DEFAULT NOW()
253
+ );
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Success Metrics
259
+
260
+ ### Data Collection (Primary)
261
+ - **Efficiency**: 50% reduction in data entry time
262
+ - **Accuracy**: 95% data validation success rate
263
+ - **Adoption**: 100% of field teams using mobile app
264
+ - **Coverage**: 1000+ trees documented per quarter
265
+
266
+ ### Spectacular Visualization (Secondary)
267
+ - **Engagement**: 80% of users try Story Mode
268
+ - **Discovery**: Average 5+ trees explored per session
269
+ - **Community**: 50+ community-contributed stories
270
+ - **Education**: 90% of users learn something new about trees
271
+
272
+ ### Technical Performance
273
+ - **Speed**: <2s load time on mobile
274
+ - **Reliability**: 99.9% uptime
275
+ - **Accessibility**: WCAG 2.1 AA compliance
276
+ - **Cross-platform**: iOS, Android, Desktop compatibility
277
+
278
+ ---
279
+
280
+ ## Community Impact Goals
281
+
282
+ ### Research Excellence
283
+ - Support 10+ research publications using TreeTrack data
284
+ - Enable collaboration between 5+ research institutions
285
+ - Provide open data for conservation initiatives
286
+
287
+ ### Educational Outreach
288
+ - Create educational content for schools
289
+ - Support environmental awareness campaigns
290
+ - Enable citizen science participation
291
+
292
+ ### Cultural Preservation
293
+ - Document traditional ecological knowledge
294
+ - Preserve local tree-related folklore
295
+ - Support indigenous community narratives
296
+
297
+ ---
298
+
299
+ ## Implementation Priority Matrix
300
+
301
+ ### **Immediate (Next 2 Months)**
302
+ 1. Fix cache management issues
303
+ 2. Optimize mobile data entry experience
304
+ 3. Improve search and filtering
305
+ 4. Enhance photo management
306
+
307
+ ### **Short Term (3-6 Months)**
308
+ 1. Implement Story Mode toggle
309
+ 2. Create thematic navigation
310
+ 3. Build interactive story popups
311
+ 4. Add dark immersive theme
312
+
313
+ ### **Medium Term (6-12 Months)**
314
+ 1. Advanced audio storytelling
315
+ 2. 3D visualization capabilities
316
+ 3. Multi-language support
317
+ 4. Community features
318
+
319
+ ### **Long Term (12+ Months)**
320
+ 1. AR integration
321
+ 2. AI-powered features
322
+ 3. IoT sensor integration
323
+ 4. Advanced analytics platform
324
+
325
+ ---
326
+
327
+ ## Innovation Opportunities
328
+
329
+ ### Unique Differentiators
330
+ - **Cultural Integration**: Deep integration of local folklore and traditional knowledge
331
+ - **Multi-Sensory**: Audio, visual, and tactile experiences
332
+ - **Field-First Design**: Built specifically for field researchers
333
+ - **Community-Driven**: Stories and knowledge from local communities
334
+
335
+ ### Potential Partnerships
336
+ - **Research Institutions**: University collaborations
337
+ - **Conservation Organizations**: NGO partnerships
338
+ - **Technology Companies**: AR/VR technology integration
339
+ - **Cultural Organizations**: Local folklore documentation
340
+
341
+ ---
342
+
343
+ ## Vision Realization
344
+
345
+ **The Ultimate Goal**: Transform TreeTrack into the world's most engaging and comprehensive tree documentation platform - where rigorous scientific data collection meets spectacular storytelling, creating an immersive experience that educates, inspires, and preserves the cultural and ecological heritage of trees.
346
+
347
+ **Impact Statement**: "Every tree has a story. TreeTrack makes those stories discoverable, preservable, and unforgettable."
348
+
349
+ ---
350
+
351
+ *This roadmap is a living document that evolves with user feedback, technological advances, and community needs. The foundation is strong - now we build something spectacular on top of it.*
352
+
353
+ **Next Step**: Focus on Phase 0 critical fixes to ensure the foundation is solid before adding spectacular features.
llm.txt CHANGED
@@ -1,13 +1,15 @@
1
- # TreeTrack Project - LLM Knowledge Base
2
 
3
  ## PROJECT OVERVIEW
4
- TreeTrack is a comprehensive tree mapping and urban forestry management web application deployed on **Hugging Face Spaces** using **Docker**.
5
 
6
  ### Key Information:
7
  - **Platform**: Hugging Face Spaces (NOT local server)
8
  - **Deployment**: Docker container with port 7860
9
- - **Technology Stack**: FastAPI + Vanilla JavaScript + SQLite
10
- - **Purpose**: Field research tool for tree documentation with 12 comprehensive data fields
 
 
11
 
12
  ## DEPLOYMENT ARCHITECTURE
13
 
@@ -28,6 +30,50 @@ EXPOSE 7860
28
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
29
  ```
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  ## TECHNOLOGY STACK
32
 
33
  ### Backend
@@ -38,9 +84,11 @@ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
38
  - **Validation**: Pydantic 2.10.0+ with custom validators
39
  - **Configuration**: pydantic-settings for environment-based config
40
 
41
- ### Frontend
42
- - **JavaScript**: Vanilla ES6+ (no frameworks)
43
- - **CSS**: Custom responsive design with Inter font
 
 
44
  - **PWA**: Service Worker for offline capabilities
45
  - **Maps**: Interactive mapping with marker clustering
46
  - **Media**: Camera integration and audio recording
@@ -163,13 +211,14 @@ async def add_cache_headers(request: Request, call_next):
163
  response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
164
  ```
165
 
166
- ## FILE STRUCTURE
167
 
168
  ```
169
  TreeTrack/
170
  README.md # HF Spaces config + documentation
171
  Dockerfile # HF Spaces Docker configuration
172
  requirements.txt # Python dependencies
 
173
  app.py # Main FastAPI application
174
  config.py # Comprehensive configuration system
175
  version.json # Version tracking for cache busting
@@ -179,11 +228,20 @@ TreeTrack/
179
  .dockerignore-cachebust # Docker ignore with cache busting
180
  .gitattributes # Git LFS configuration
181
  static/
182
- index.html # Main application interface
183
  map.html # Interactive map interface
184
- app.js # Frontend application logic
185
  map.js # Map functionality
186
  sw.js # Service Worker for PWA/offline
 
 
 
 
 
 
 
 
 
187
  ```
188
 
189
  ## API ENDPOINTS
 
1
+ # TreeTrack - Professional Field Research Platform - LLM Knowledge Base
2
 
3
  ## PROJECT OVERVIEW
4
+ TreeTrack is a comprehensive tree mapping and urban forestry management web application with a modern modular architecture, deployed on **Hugging Face Spaces** using **Docker**.
5
 
6
  ### Key Information:
7
  - **Platform**: Hugging Face Spaces (NOT local server)
8
  - **Deployment**: Docker container with port 7860
9
+ - **Technology Stack**: FastAPI + Modular JavaScript (ES6) + SQLite
10
+ - **Purpose**: Professional field research tool for tree documentation with 12 comprehensive data fields
11
+ - **Architecture**: Refactored modular design with 6 specialized JavaScript modules
12
+ - **Version**: 5.0.0 (Major refactoring completed)
13
 
14
  ## DEPLOYMENT ARCHITECTURE
15
 
 
30
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
31
  ```
32
 
33
+ ## MAJOR REFACTORING (Version 5.0.0)
34
+
35
+ ### Transformation Overview
36
+ - **From**: Monolithic 1,244-line JavaScript file
37
+ - **To**: Modular architecture with 6 specialized modules
38
+ - **Benefits**: Better maintainability, testability, and performance
39
+ - **No Breaking Changes**: All functionality preserved
40
+ - **Professional UI**: Completely emoji-free interface
41
+
42
+ ### Modular Architecture
43
+
44
+ #### Module Structure
45
+ ```
46
+ static/js/
47
+ β”œβ”€β”€ modules/
48
+ β”‚ β”œβ”€β”€ auth-manager.js - Authentication & permissions (95 lines)
49
+ β”‚ β”œβ”€β”€ api-client.js - API communication (148 lines)
50
+ β”‚ β”œβ”€β”€ ui-manager.js - User interface & messaging (192 lines)
51
+ β”‚ β”œβ”€β”€ form-manager.js - Form handling & validation (285 lines)
52
+ β”‚ β”œβ”€β”€ autocomplete-manager.js - Smart suggestions (322 lines)
53
+ β”‚ └── media-manager.js - File uploads & recording (268 lines)
54
+ └── tree-track-app.js - Main orchestrator (206 lines)
55
+ ```
56
+
57
+ #### Module Dependencies
58
+ ```
59
+ TreeTrackApp
60
+ β”œβ”€β”€ AuthManager (independent)
61
+ β”œβ”€β”€ ApiClient (depends on AuthManager)
62
+ β”œβ”€β”€ UIManager (depends on AuthManager)
63
+ β”œβ”€β”€ FormManager (depends on ApiClient, UIManager)
64
+ β”œβ”€β”€ AutoCompleteManager (depends on ApiClient, UIManager)
65
+ └── MediaManager (depends on FormManager, UIManager)
66
+ ```
67
+
68
+ #### Key Improvements
69
+ - **Separation of Concerns**: Each module has single responsibility
70
+ - **Dependency Injection**: Clean dependencies between modules
71
+ - **ES6 Modules**: Modern import/export structure
72
+ - **Event Delegation**: Replaced global functions with proper event handling
73
+ - **Performance**: Debounced search, Promise.all(), DOM caching
74
+ - **Error Handling**: Centralized error management
75
+ - **Code Reuse**: Shared utilities across modules
76
+
77
  ## TECHNOLOGY STACK
78
 
79
  ### Backend
 
84
  - **Validation**: Pydantic 2.10.0+ with custom validators
85
  - **Configuration**: pydantic-settings for environment-based config
86
 
87
+ ### Frontend (Refactored)
88
+ - **JavaScript**: Modular ES6+ with 6 specialized modules
89
+ - **Architecture**: Dependency injection pattern
90
+ - **Performance**: Debounced search (300ms), parallel API calls
91
+ - **CSS**: Custom responsive design with Inter font (emoji-free)
92
  - **PWA**: Service Worker for offline capabilities
93
  - **Maps**: Interactive mapping with marker clustering
94
  - **Media**: Camera integration and audio recording
 
211
  response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
212
  ```
213
 
214
+ ## FILE STRUCTURE (Updated)
215
 
216
  ```
217
  TreeTrack/
218
  README.md # HF Spaces config + documentation
219
  Dockerfile # HF Spaces Docker configuration
220
  requirements.txt # Python dependencies
221
+ llm.txt # LLM knowledge base (updated)
222
  app.py # Main FastAPI application
223
  config.py # Comprehensive configuration system
224
  version.json # Version tracking for cache busting
 
228
  .dockerignore-cachebust # Docker ignore with cache busting
229
  .gitattributes # Git LFS configuration
230
  static/
231
+ index.html # Main application interface (updated)
232
  map.html # Interactive map interface
233
+ app.js.backup # Original monolithic file (backup)
234
  map.js # Map functionality
235
  sw.js # Service Worker for PWA/offline
236
+ js/
237
+ modules/
238
+ auth-manager.js # Authentication module
239
+ api-client.js # API communication module
240
+ ui-manager.js # UI management module
241
+ form-manager.js # Form handling module
242
+ autocomplete-manager.js # Smart suggestions module
243
+ media-manager.js # Media handling module
244
+ tree-track-app.js # Main orchestrator
245
  ```
246
 
247
  ## API ENDPOINTS
static/app.js CHANGED
@@ -294,14 +294,22 @@ class TreeTrackApp {
294
  const categoryDiv = document.createElement('div');
295
  categoryDiv.className = 'photo-category';
296
  categoryDiv.innerHTML = `
297
- <div>
298
- <label>${category}</label>
299
- <div class="file-upload photo-upload" data-category="${category}">
300
- πŸ“· Click to upload ${category} photo or use camera
301
  </div>
302
- <div class="uploaded-file" id="photo-${category}" style="display: none;"></div>
303
  </div>
304
- <button type="button" class="btn btn-small" onclick="app.capturePhoto('${category}')">πŸ“Έ Camera</button>
 
 
 
 
 
 
 
 
 
305
  `;
306
  container.appendChild(categoryDiv);
307
  });
@@ -438,7 +446,7 @@ class TreeTrackApp {
438
  // Update UI
439
  const resultDiv = document.getElementById(`photo-${category}`);
440
  resultDiv.style.display = 'block';
441
- resultDiv.innerHTML = `βœ… ${file.name} uploaded successfully`;
442
  } else {
443
  throw new Error('Upload failed');
444
  }
@@ -482,7 +490,7 @@ class TreeTrackApp {
482
 
483
  // Update UI
484
  const resultDiv = document.getElementById('audioUploadResult');
485
- resultDiv.innerHTML = `<div class="uploaded-file">🎡 ${file.name} uploaded successfully</div>`;
486
  } else {
487
  throw new Error('Upload failed');
488
  }
@@ -529,7 +537,7 @@ class TreeTrackApp {
529
  const status = document.getElementById('recordingStatus');
530
  if (recordBtn) {
531
  recordBtn.classList.add('recording');
532
- recordBtn.innerHTML = '⏹';
533
  }
534
  if (status) {
535
  status.textContent = 'Recording... Click to stop';
@@ -552,7 +560,7 @@ class TreeTrackApp {
552
  const status = document.getElementById('recordingStatus');
553
  if (recordBtn) {
554
  recordBtn.classList.remove('recording');
555
- recordBtn.innerHTML = '●';
556
  }
557
  if (status) {
558
  status.textContent = 'Recording saved!';
@@ -562,17 +570,17 @@ class TreeTrackApp {
562
 
563
  getCurrentLocation() {
564
  if (navigator.geolocation) {
565
- document.getElementById('getLocation').textContent = 'πŸ“ Getting...';
566
 
567
  navigator.geolocation.getCurrentPosition(
568
  (position) => {
569
  document.getElementById('latitude').value = position.coords.latitude.toFixed(7);
570
  document.getElementById('longitude').value = position.coords.longitude.toFixed(7);
571
- document.getElementById('getLocation').textContent = 'πŸ“ GPS';
572
  this.showMessage('Location retrieved successfully!', 'success');
573
  },
574
  (error) => {
575
- document.getElementById('getLocation').textContent = 'πŸ“ GPS';
576
  this.showMessage('Error getting location: ' + error.message, 'error');
577
  }
578
  );
@@ -625,7 +633,7 @@ class TreeTrackApp {
625
 
626
  if (response.ok) {
627
  const result = await response.json();
628
- this.showMessage(`🌳 Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`, 'success');
629
  this.resetFormSilently();
630
  this.loadTrees(); // Refresh the tree list
631
  } else {
@@ -695,16 +703,16 @@ class TreeTrackApp {
695
  <div class="tree-header">
696
  <div class="tree-id">Tree #${tree.id}</div>
697
  <div class="tree-actions">
698
- ${canEdit ? `<button class="btn-icon edit-tree" onclick="app.editTree(${tree.id})" title="Edit Tree">✏️</button>` : ''}
699
- ${canDelete ? `<button class="btn-icon delete-tree" onclick="app.deleteTree(${tree.id})" title="Delete Tree">πŸ—‘οΈ</button>` : ''}
700
  </div>
701
  </div>
702
  <div class="tree-info">
703
  ${tree.scientific_name || tree.common_name || tree.local_name || 'Unnamed'}
704
- <br>πŸ“ ${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)}
705
- ${tree.tree_code ? `<br>🏷️ ${tree.tree_code}` : ''}
706
- <br>πŸ“… ${new Date(tree.created_at).toLocaleDateString()}
707
- <br>πŸ‘€ ${tree.created_by || 'Unknown'}
708
  </div>
709
  </div>
710
  `;
@@ -849,7 +857,7 @@ class TreeTrackApp {
849
 
850
  if (response.ok) {
851
  const result = await response.json();
852
- this.showMessage(`🌳 Tree #${result.id} updated successfully!`, 'success');
853
  this.cancelEdit(); // Exit edit mode
854
  this.loadTrees(); // Refresh the tree list
855
  } else {
@@ -875,7 +883,7 @@ class TreeTrackApp {
875
  if (!response) return;
876
 
877
  if (response.ok) {
878
- this.showMessage(`πŸ—‘οΈ Tree #${treeId} deleted successfully.`, 'success');
879
  this.loadTrees(); // Refresh the tree list
880
  } else {
881
  const error = await response.json();
 
294
  const categoryDiv = document.createElement('div');
295
  categoryDiv.className = 'photo-category';
296
  categoryDiv.innerHTML = `
297
+ <div class="photo-category-header">
298
+ <div class="photo-category-title">
299
+ <div class="photo-category-icon">IMG</div>
300
+ ${category}
301
  </div>
 
302
  </div>
303
+ <div class="photo-upload-area">
304
+ <div class="photo-upload" data-category="${category}">
305
+ <div class="photo-upload-icon">+</div>
306
+ <div>Click to select ${category} photo</div>
307
+ </div>
308
+ <button type="button" class="camera-btn" onclick="app.capturePhoto('${category}')">
309
+ Camera
310
+ </button>
311
+ </div>
312
+ <div class="uploaded-file" id="photo-${category}" style="display: none;"></div>
313
  `;
314
  container.appendChild(categoryDiv);
315
  });
 
446
  // Update UI
447
  const resultDiv = document.getElementById(`photo-${category}`);
448
  resultDiv.style.display = 'block';
449
+ resultDiv.innerHTML = `${file.name} uploaded successfully`;
450
  } else {
451
  throw new Error('Upload failed');
452
  }
 
490
 
491
  // Update UI
492
  const resultDiv = document.getElementById('audioUploadResult');
493
+ resultDiv.innerHTML = `<div class="uploaded-file">${file.name} uploaded successfully</div>`;
494
  } else {
495
  throw new Error('Upload failed');
496
  }
 
537
  const status = document.getElementById('recordingStatus');
538
  if (recordBtn) {
539
  recordBtn.classList.add('recording');
540
+ recordBtn.innerHTML = 'Stop';
541
  }
542
  if (status) {
543
  status.textContent = 'Recording... Click to stop';
 
560
  const status = document.getElementById('recordingStatus');
561
  if (recordBtn) {
562
  recordBtn.classList.remove('recording');
563
+ recordBtn.innerHTML = 'Record';
564
  }
565
  if (status) {
566
  status.textContent = 'Recording saved!';
 
570
 
571
  getCurrentLocation() {
572
  if (navigator.geolocation) {
573
+ document.getElementById('getLocation').textContent = 'Getting...';
574
 
575
  navigator.geolocation.getCurrentPosition(
576
  (position) => {
577
  document.getElementById('latitude').value = position.coords.latitude.toFixed(7);
578
  document.getElementById('longitude').value = position.coords.longitude.toFixed(7);
579
+ document.getElementById('getLocation').textContent = 'Get GPS Location';
580
  this.showMessage('Location retrieved successfully!', 'success');
581
  },
582
  (error) => {
583
+ document.getElementById('getLocation').textContent = 'Get GPS Location';
584
  this.showMessage('Error getting location: ' + error.message, 'error');
585
  }
586
  );
 
633
 
634
  if (response.ok) {
635
  const result = await response.json();
636
+ this.showMessage(`Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`, 'success');
637
  this.resetFormSilently();
638
  this.loadTrees(); // Refresh the tree list
639
  } else {
 
703
  <div class="tree-header">
704
  <div class="tree-id">Tree #${tree.id}</div>
705
  <div class="tree-actions">
706
+ ${canEdit ? `<button class="btn-icon edit-tree" onclick="app.editTree(${tree.id})" title="Edit Tree">Edit</button>` : ''}
707
+ ${canDelete ? `<button class="btn-icon delete-tree" onclick="app.deleteTree(${tree.id})" title="Delete Tree">Delete</button>` : ''}
708
  </div>
709
  </div>
710
  <div class="tree-info">
711
  ${tree.scientific_name || tree.common_name || tree.local_name || 'Unnamed'}
712
+ <br>${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)}
713
+ ${tree.tree_code ? `<br>Code: ${tree.tree_code}` : ''}
714
+ <br>${new Date(tree.created_at).toLocaleDateString()}
715
+ <br>By: ${tree.created_by || 'Unknown'}
716
  </div>
717
  </div>
718
  `;
 
857
 
858
  if (response.ok) {
859
  const result = await response.json();
860
+ this.showMessage(`Tree #${result.id} updated successfully!`, 'success');
861
  this.cancelEdit(); // Exit edit mode
862
  this.loadTrees(); // Refresh the tree list
863
  } else {
 
883
  if (!response) return;
884
 
885
  if (response.ok) {
886
+ this.showMessage(`Tree #${treeId} deleted successfully.`, 'success');
887
  this.loadTrees(); // Refresh the tree list
888
  } else {
889
  const error = await response.json();
static/app.js.backup ADDED
@@ -0,0 +1,1243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // TreeTrack Enhanced JavaScript - Comprehensive Field Research Tool with Authentication
2
+ class TreeTrackApp {
3
+ constructor() {
4
+ this.uploadedPhotos = {};
5
+ this.audioFile = null;
6
+ this.mediaRecorder = null;
7
+ this.audioChunks = [];
8
+ this.isRecording = false;
9
+
10
+ // Auto-suggestion properties
11
+ this.searchTimeouts = {};
12
+ this.activeDropdowns = new Set();
13
+ this.selectedIndex = -1;
14
+ this.availableTreeCodes = [];
15
+
16
+ // Authentication properties
17
+ this.currentUser = null;
18
+ this.authToken = null;
19
+
20
+ this.init();
21
+ }
22
+
23
+ async init() {
24
+ // Check authentication first
25
+ if (!await this.checkAuthentication()) {
26
+ window.location.href = '/login';
27
+ return;
28
+ }
29
+
30
+ await this.loadFormOptions();
31
+ this.setupEventListeners();
32
+ this.setupUserInterface();
33
+ this.loadTrees();
34
+ this.loadSelectedLocation();
35
+
36
+ // Initialize auto-suggestions after a brief delay to ensure DOM is ready
37
+ setTimeout(() => {
38
+ this.initializeAutoSuggestions();
39
+ }, 100);
40
+ }
41
+
42
+ // Authentication methods
43
+ async checkAuthentication() {
44
+ const token = localStorage.getItem('auth_token');
45
+ if (!token) {
46
+ return false;
47
+ }
48
+
49
+ try {
50
+ const response = await fetch('/api/auth/validate', {
51
+ headers: {
52
+ 'Authorization': `Bearer ${token}`
53
+ }
54
+ });
55
+
56
+ if (response.ok) {
57
+ const data = await response.json();
58
+ this.currentUser = data.user;
59
+ this.authToken = token;
60
+ return true;
61
+ } else {
62
+ // Token invalid, remove it
63
+ localStorage.removeItem('auth_token');
64
+ localStorage.removeItem('user_info');
65
+ return false;
66
+ }
67
+ } catch (error) {
68
+ console.error('Auth validation error:', error);
69
+ return false;
70
+ }
71
+ }
72
+
73
+ setupUserInterface() {
74
+ // Update existing user info elements
75
+ this.displayUserInfo();
76
+
77
+ // Add logout functionality
78
+ this.addLogoutButton();
79
+ }
80
+
81
+ displayUserInfo() {
82
+ if (!this.currentUser) return;
83
+
84
+ // Update existing user info elements in the new HTML structure
85
+ const userNameEl = document.getElementById('userName');
86
+ const userRoleEl = document.getElementById('userRole');
87
+ const userAvatarEl = document.getElementById('userAvatar');
88
+
89
+ if (userNameEl) {
90
+ userNameEl.textContent = this.currentUser.full_name;
91
+ }
92
+
93
+ if (userRoleEl) {
94
+ userRoleEl.textContent = this.currentUser.role;
95
+ }
96
+
97
+ if (userAvatarEl) {
98
+ userAvatarEl.textContent = this.currentUser.full_name.charAt(0).toUpperCase();
99
+ }
100
+ }
101
+
102
+ addLogoutButton() {
103
+ const logoutBtn = document.getElementById('logoutBtn');
104
+ if (logoutBtn) {
105
+ logoutBtn.addEventListener('click', () => this.logout());
106
+ }
107
+ }
108
+
109
+ addCustomStyles() {
110
+ const style = document.createElement('style');
111
+ style.textContent = `
112
+ .user-info {
113
+ color: white;
114
+ text-align: center;
115
+ margin: 0 1rem;
116
+ }
117
+ .user-greeting {
118
+ font-size: 0.875rem;
119
+ font-weight: 500;
120
+ }
121
+ .user-role {
122
+ font-size: 0.75rem;
123
+ opacity: 0.8;
124
+ text-transform: capitalize;
125
+ }
126
+ .tree-header {
127
+ display: flex;
128
+ justify-content: space-between;
129
+ align-items: center;
130
+ margin-bottom: 0.5rem;
131
+ }
132
+ .tree-actions {
133
+ display: flex;
134
+ gap: 0.25rem;
135
+ }
136
+ .btn-icon {
137
+ background: none;
138
+ border: none;
139
+ cursor: pointer;
140
+ padding: 0.25rem;
141
+ border-radius: 4px;
142
+ font-size: 0.875rem;
143
+ transition: background-color 0.2s;
144
+ }
145
+ .btn-icon:hover {
146
+ background-color: rgba(0,0,0,0.1);
147
+ }
148
+ .edit-tree:hover {
149
+ background-color: rgba(59, 130, 246, 0.1);
150
+ }
151
+ .delete-tree:hover {
152
+ background-color: rgba(239, 68, 68, 0.1);
153
+ }
154
+ .logout-btn {
155
+ margin-left: 1rem;
156
+ }
157
+ @media (max-width: 768px) {
158
+ .user-info {
159
+ margin: 0 0.5rem;
160
+ }
161
+ .user-greeting {
162
+ font-size: 0.75rem;
163
+ }
164
+ .user-role {
165
+ font-size: 0.625rem;
166
+ }
167
+ }
168
+ `;
169
+ document.head.appendChild(style);
170
+ }
171
+
172
+ async logout() {
173
+ try {
174
+ await fetch('/api/auth/logout', {
175
+ method: 'POST',
176
+ headers: {
177
+ 'Authorization': `Bearer ${this.authToken}`
178
+ }
179
+ });
180
+ } catch (error) {
181
+ console.error('Logout error:', error);
182
+ } finally {
183
+ localStorage.removeItem('auth_token');
184
+ localStorage.removeItem('user_info');
185
+ window.location.href = '/login';
186
+ }
187
+ }
188
+
189
+ // Enhanced API calls with authentication
190
+ async authenticatedFetch(url, options = {}) {
191
+ const headers = {
192
+ 'Content-Type': 'application/json',
193
+ 'Authorization': `Bearer ${this.authToken}`,
194
+ ...options.headers
195
+ };
196
+
197
+ const response = await fetch(url, {
198
+ ...options,
199
+ headers
200
+ });
201
+
202
+ if (response.status === 401) {
203
+ // Token expired or invalid
204
+ localStorage.removeItem('auth_token');
205
+ localStorage.removeItem('user_info');
206
+ window.location.href = '/login';
207
+ return null;
208
+ }
209
+
210
+ return response;
211
+ }
212
+
213
+ // Permission checking methods
214
+ canEditTree(createdBy) {
215
+ if (!this.currentUser) return false;
216
+
217
+ // Admin and system can edit any tree
218
+ if (this.currentUser.permissions.includes('admin') || this.currentUser.permissions.includes('system')) {
219
+ return true;
220
+ }
221
+
222
+ // Users can edit trees they created
223
+ if (this.currentUser.permissions.includes('edit_own') && createdBy === this.currentUser.username) {
224
+ return true;
225
+ }
226
+
227
+ // Users with delete permission can edit any tree
228
+ if (this.currentUser.permissions.includes('delete')) {
229
+ return true;
230
+ }
231
+
232
+ return false;
233
+ }
234
+
235
+ canDeleteTree(createdBy) {
236
+ if (!this.currentUser) return false;
237
+
238
+ // Only admin and system can delete trees
239
+ if (this.currentUser.permissions.includes('admin') || this.currentUser.permissions.includes('system')) {
240
+ return true;
241
+ }
242
+
243
+ // Users with explicit delete permission
244
+ if (this.currentUser.permissions.includes('delete')) {
245
+ return true;
246
+ }
247
+
248
+ return false;
249
+ }
250
+
251
+ async loadFormOptions() {
252
+ try {
253
+ // Load utility options
254
+ const utilityResponse = await this.authenticatedFetch('/api/utilities');
255
+ if (!utilityResponse) return;
256
+ const utilityData = await utilityResponse.json();
257
+ this.renderMultiSelect('utilityOptions', utilityData.utilities);
258
+
259
+ // Load phenology stages
260
+ const phenologyResponse = await this.authenticatedFetch('/api/phenology-stages');
261
+ if (!phenologyResponse) return;
262
+ const phenologyData = await phenologyResponse.json();
263
+ this.renderMultiSelect('phenologyOptions', phenologyData.stages);
264
+
265
+ // Load photo categories
266
+ const categoriesResponse = await this.authenticatedFetch('/api/photo-categories');
267
+ if (!categoriesResponse) return;
268
+ const categoriesData = await categoriesResponse.json();
269
+ this.renderPhotoCategories(categoriesData.categories);
270
+
271
+ } catch (error) {
272
+ console.error('Error loading form options:', error);
273
+ }
274
+ }
275
+
276
+ renderMultiSelect(containerId, options) {
277
+ const container = document.getElementById(containerId);
278
+ container.innerHTML = '';
279
+
280
+ options.forEach(option => {
281
+ const label = document.createElement('label');
282
+ label.innerHTML = `
283
+ <input type="checkbox" value="${option}"> ${option}
284
+ `;
285
+ container.appendChild(label);
286
+ });
287
+ }
288
+
289
+ renderPhotoCategories(categories) {
290
+ const container = document.getElementById('photoCategories');
291
+ container.innerHTML = '';
292
+
293
+ categories.forEach(category => {
294
+ const categoryDiv = document.createElement('div');
295
+ categoryDiv.className = 'photo-category';
296
+ categoryDiv.innerHTML = `
297
+ <div class="photo-category-header">
298
+ <div class="photo-category-title">
299
+ <div class="photo-category-icon">IMG</div>
300
+ ${category}
301
+ </div>
302
+ </div>
303
+ <div class="photo-upload-area">
304
+ <div class="photo-upload" data-category="${category}">
305
+ <div class="photo-upload-icon">+</div>
306
+ <div>Click to select ${category} photo</div>
307
+ </div>
308
+ <button type="button" class="camera-btn" onclick="app.capturePhoto('${category}')">
309
+ Camera
310
+ </button>
311
+ </div>
312
+ <div class="uploaded-file" id="photo-${category}" style="display: none;"></div>
313
+ `;
314
+ container.appendChild(categoryDiv);
315
+ });
316
+
317
+ this.setupPhotoUploads();
318
+ }
319
+
320
+ setupEventListeners() {
321
+ // Form submission
322
+ document.getElementById('treeForm').addEventListener('submit', (e) => this.handleSubmit(e));
323
+
324
+ // Reset form
325
+ document.getElementById('resetForm').addEventListener('click', () => this.resetForm());
326
+
327
+ // GPS location
328
+ document.getElementById('getLocation').addEventListener('click', () => this.getCurrentLocation());
329
+
330
+ // Audio recording - check if element exists
331
+ const recordBtn = document.getElementById('recordBtn');
332
+ if (recordBtn) {
333
+ recordBtn.addEventListener('click', () => this.toggleRecording());
334
+ }
335
+
336
+ // Audio file upload
337
+ document.getElementById('audioUpload').addEventListener('click', () => this.selectAudioFile());
338
+
339
+ // Drag and drop for audio
340
+ this.setupDragAndDrop();
341
+ }
342
+
343
+ loadSelectedLocation() {
344
+ // Load location from map selection
345
+ const selectedLocation = localStorage.getItem('selectedLocation');
346
+ if (selectedLocation) {
347
+ try {
348
+ const location = JSON.parse(selectedLocation);
349
+ document.getElementById('latitude').value = location.lat.toFixed(6);
350
+ document.getElementById('longitude').value = location.lng.toFixed(6);
351
+
352
+ // Clear the stored location
353
+ localStorage.removeItem('selectedLocation');
354
+
355
+ // Show success message
356
+ this.showMessage('Location loaded from map!', 'success');
357
+ } catch (error) {
358
+ console.error('Error loading selected location:', error);
359
+ }
360
+ }
361
+ }
362
+
363
+ setupPhotoUploads() {
364
+ document.querySelectorAll('.photo-upload').forEach(upload => {
365
+ upload.addEventListener('click', (e) => {
366
+ const category = e.target.getAttribute('data-category');
367
+ this.selectPhotoFile(category);
368
+ });
369
+ });
370
+ }
371
+
372
+ setupDragAndDrop() {
373
+ const audioUpload = document.getElementById('audioUpload');
374
+
375
+ audioUpload.addEventListener('dragover', (e) => {
376
+ e.preventDefault();
377
+ audioUpload.classList.add('dragover');
378
+ });
379
+
380
+ audioUpload.addEventListener('dragleave', () => {
381
+ audioUpload.classList.remove('dragover');
382
+ });
383
+
384
+ audioUpload.addEventListener('drop', (e) => {
385
+ e.preventDefault();
386
+ audioUpload.classList.remove('dragover');
387
+
388
+ const files = e.dataTransfer.files;
389
+ if (files.length > 0 && files[0].type.startsWith('audio/')) {
390
+ this.uploadAudioFile(files[0]);
391
+ }
392
+ });
393
+ }
394
+
395
+ async selectPhotoFile(category) {
396
+ const input = document.createElement('input');
397
+ input.type = 'file';
398
+ input.accept = 'image/*';
399
+ input.capture = 'environment'; // Use rear camera if available
400
+
401
+ input.onchange = (e) => {
402
+ const file = e.target.files[0];
403
+ if (file) {
404
+ this.uploadPhotoFile(file, category);
405
+ }
406
+ };
407
+
408
+ input.click();
409
+ }
410
+
411
+ async capturePhoto(category) {
412
+ // For mobile devices, this will trigger the camera
413
+ const input = document.createElement('input');
414
+ input.type = 'file';
415
+ input.accept = 'image/*';
416
+ input.capture = 'environment';
417
+
418
+ input.onchange = (e) => {
419
+ const file = e.target.files[0];
420
+ if (file) {
421
+ this.uploadPhotoFile(file, category);
422
+ }
423
+ };
424
+
425
+ input.click();
426
+ }
427
+
428
+ async uploadPhotoFile(file, category) {
429
+ const formData = new FormData();
430
+ formData.append('file', file);
431
+ formData.append('category', category);
432
+
433
+ try {
434
+ const response = await fetch('/api/upload/image', {
435
+ method: 'POST',
436
+ headers: {
437
+ 'Authorization': `Bearer ${this.authToken}`
438
+ },
439
+ body: formData
440
+ });
441
+
442
+ if (response.ok) {
443
+ const result = await response.json();
444
+ this.uploadedPhotos[category] = result.filename;
445
+
446
+ // Update UI
447
+ const resultDiv = document.getElementById(`photo-${category}`);
448
+ resultDiv.style.display = 'block';
449
+ resultDiv.innerHTML = `${file.name} uploaded successfully`;
450
+ } else {
451
+ throw new Error('Upload failed');
452
+ }
453
+ } catch (error) {
454
+ console.error('Error uploading photo:', error);
455
+ this.showMessage('Error uploading photo: ' + error.message, 'error');
456
+ }
457
+ }
458
+
459
+ async selectAudioFile() {
460
+ const input = document.createElement('input');
461
+ input.type = 'file';
462
+ input.accept = 'audio/*';
463
+
464
+ input.onchange = (e) => {
465
+ const file = e.target.files[0];
466
+ if (file) {
467
+ this.uploadAudioFile(file);
468
+ }
469
+ };
470
+
471
+ input.click();
472
+ }
473
+
474
+ async uploadAudioFile(file) {
475
+ const formData = new FormData();
476
+ formData.append('file', file);
477
+
478
+ try {
479
+ const response = await fetch('/api/upload/audio', {
480
+ method: 'POST',
481
+ headers: {
482
+ 'Authorization': `Bearer ${this.authToken}`
483
+ },
484
+ body: formData
485
+ });
486
+
487
+ if (response.ok) {
488
+ const result = await response.json();
489
+ this.audioFile = result.filename;
490
+
491
+ // Update UI
492
+ const resultDiv = document.getElementById('audioUploadResult');
493
+ resultDiv.innerHTML = `<div class="uploaded-file">${file.name} uploaded successfully</div>`;
494
+ } else {
495
+ throw new Error('Upload failed');
496
+ }
497
+ } catch (error) {
498
+ console.error('Error uploading audio:', error);
499
+ this.showMessage('Error uploading audio: ' + error.message, 'error');
500
+ }
501
+ }
502
+
503
+ async toggleRecording() {
504
+ if (!this.isRecording) {
505
+ await this.startRecording();
506
+ } else {
507
+ this.stopRecording();
508
+ }
509
+ }
510
+
511
+ async startRecording() {
512
+ try {
513
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
514
+ this.mediaRecorder = new MediaRecorder(stream);
515
+ this.audioChunks = [];
516
+
517
+ this.mediaRecorder.ondataavailable = (event) => {
518
+ this.audioChunks.push(event.data);
519
+ };
520
+
521
+ this.mediaRecorder.onstop = async () => {
522
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
523
+ const audioFile = new File([audioBlob], 'recording.wav', { type: 'audio/wav' });
524
+ await this.uploadAudioFile(audioFile);
525
+
526
+ // Show playback
527
+ const audioElement = document.getElementById('audioPlayback');
528
+ audioElement.src = URL.createObjectURL(audioBlob);
529
+ audioElement.classList.remove('hidden');
530
+ };
531
+
532
+ this.mediaRecorder.start();
533
+ this.isRecording = true;
534
+
535
+ // Update UI - check if elements exist
536
+ const recordBtn = document.getElementById('recordBtn');
537
+ const status = document.getElementById('recordingStatus');
538
+ if (recordBtn) {
539
+ recordBtn.classList.add('recording');
540
+ recordBtn.innerHTML = 'Stop';
541
+ }
542
+ if (status) {
543
+ status.textContent = 'Recording... Click to stop';
544
+ }
545
+
546
+ } catch (error) {
547
+ console.error('Error starting recording:', error);
548
+ this.showMessage('Error accessing microphone: ' + error.message, 'error');
549
+ }
550
+ }
551
+
552
+ stopRecording() {
553
+ if (this.mediaRecorder && this.isRecording) {
554
+ this.mediaRecorder.stop();
555
+ this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
556
+ this.isRecording = false;
557
+
558
+ // Update UI - check if elements exist
559
+ const recordBtn = document.getElementById('recordBtn');
560
+ const status = document.getElementById('recordingStatus');
561
+ if (recordBtn) {
562
+ recordBtn.classList.remove('recording');
563
+ recordBtn.innerHTML = 'Record';
564
+ }
565
+ if (status) {
566
+ status.textContent = 'Recording saved!';
567
+ }
568
+ }
569
+ }
570
+
571
+ getCurrentLocation() {
572
+ if (navigator.geolocation) {
573
+ document.getElementById('getLocation').textContent = 'Getting...';
574
+
575
+ navigator.geolocation.getCurrentPosition(
576
+ (position) => {
577
+ document.getElementById('latitude').value = position.coords.latitude.toFixed(7);
578
+ document.getElementById('longitude').value = position.coords.longitude.toFixed(7);
579
+ document.getElementById('getLocation').textContent = 'Get GPS Location';
580
+ this.showMessage('Location retrieved successfully!', 'success');
581
+ },
582
+ (error) => {
583
+ document.getElementById('getLocation').textContent = 'Get GPS Location';
584
+ this.showMessage('Error getting location: ' + error.message, 'error');
585
+ }
586
+ );
587
+ } else {
588
+ this.showMessage('Geolocation is not supported by this browser.', 'error');
589
+ }
590
+ }
591
+
592
+ getSelectedValues(containerId) {
593
+ const container = document.getElementById(containerId);
594
+ const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked');
595
+ return Array.from(checkboxes).map(cb => cb.value);
596
+ }
597
+
598
+ async handleSubmit(e) {
599
+ e.preventDefault();
600
+
601
+ const utilityValues = this.getSelectedValues('utilityOptions');
602
+ const phenologyValues = this.getSelectedValues('phenologyOptions');
603
+
604
+ const treeData = {
605
+ latitude: parseFloat(document.getElementById('latitude').value),
606
+ longitude: parseFloat(document.getElementById('longitude').value),
607
+ local_name: document.getElementById('localName').value || null,
608
+ scientific_name: document.getElementById('scientificName').value || null,
609
+ common_name: document.getElementById('commonName').value || null,
610
+ tree_code: document.getElementById('treeCode').value || null,
611
+ height: document.getElementById('height').value ? parseFloat(document.getElementById('height').value) : null,
612
+ width: document.getElementById('width').value ? parseFloat(document.getElementById('width').value) : null,
613
+ utility: utilityValues.length > 0 ? utilityValues : [],
614
+ phenology_stages: phenologyValues.length > 0 ? phenologyValues : [],
615
+ storytelling_text: document.getElementById('storytellingText').value || null,
616
+ storytelling_audio: this.audioFile,
617
+ photographs: Object.keys(this.uploadedPhotos).length > 0 ? this.uploadedPhotos : null,
618
+ notes: document.getElementById('notes').value || null
619
+ };
620
+
621
+ // Debug log to check the data structure
622
+ console.log('Tree data being sent:', treeData);
623
+ console.log('Utility type:', typeof treeData.utility, treeData.utility);
624
+ console.log('Phenology type:', typeof treeData.phenology_stages, treeData.phenology_stages);
625
+
626
+ try {
627
+ const response = await this.authenticatedFetch('/api/trees', {
628
+ method: 'POST',
629
+ body: JSON.stringify(treeData)
630
+ });
631
+
632
+ if (!response) return;
633
+
634
+ if (response.ok) {
635
+ const result = await response.json();
636
+ this.showMessage(`Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`, 'success');
637
+ this.resetFormSilently();
638
+ this.loadTrees(); // Refresh the tree list
639
+ } else {
640
+ const error = await response.json();
641
+ this.showMessage('Error saving tree: ' + (error.detail || 'Unknown error'), 'error');
642
+ }
643
+ } catch (error) {
644
+ console.error('Error submitting form:', error);
645
+ this.showMessage('Network error: ' + error.message, 'error');
646
+ }
647
+ }
648
+
649
+ resetForm() {
650
+ this.resetFormSilently();
651
+ this.showMessage('Form has been reset.', 'success');
652
+ }
653
+
654
+ resetFormSilently() {
655
+ document.getElementById('treeForm').reset();
656
+ this.uploadedPhotos = {};
657
+ this.audioFile = null;
658
+
659
+ // Clear uploaded file indicators
660
+ document.querySelectorAll('.uploaded-file').forEach(el => {
661
+ el.style.display = 'none';
662
+ el.innerHTML = '';
663
+ });
664
+
665
+ // Reset audio controls - check if elements exist
666
+ const audioElement = document.getElementById('audioPlayback');
667
+ if (audioElement) {
668
+ audioElement.classList.add('hidden');
669
+ audioElement.src = '';
670
+ }
671
+
672
+ const recordingStatus = document.getElementById('recordingStatus');
673
+ if (recordingStatus) {
674
+ recordingStatus.textContent = 'Click to start recording';
675
+ }
676
+
677
+ const audioUploadResult = document.getElementById('audioUploadResult');
678
+ if (audioUploadResult) {
679
+ audioUploadResult.innerHTML = '';
680
+ }
681
+ }
682
+
683
+ async loadTrees() {
684
+ try {
685
+ const response = await this.authenticatedFetch('/api/trees?limit=20');
686
+ if (!response) return;
687
+
688
+ const trees = await response.json();
689
+
690
+ const treeList = document.getElementById('treeList');
691
+
692
+ if (trees.length === 0) {
693
+ treeList.innerHTML = '<div class="loading">No trees recorded yet</div>';
694
+ return;
695
+ }
696
+
697
+ treeList.innerHTML = trees.map(tree => {
698
+ const canEdit = this.canEditTree(tree.created_by);
699
+ const canDelete = this.canDeleteTree(tree.created_by);
700
+
701
+ return `
702
+ <div class="tree-item" data-tree-id="${tree.id}">
703
+ <div class="tree-header">
704
+ <div class="tree-id">Tree #${tree.id}</div>
705
+ <div class="tree-actions">
706
+ ${canEdit ? `<button class="btn-icon edit-tree" onclick="app.editTree(${tree.id})" title="Edit Tree">Edit</button>` : ''}
707
+ ${canDelete ? `<button class="btn-icon delete-tree" onclick="app.deleteTree(${tree.id})" title="Delete Tree">Delete</button>` : ''}
708
+ </div>
709
+ </div>
710
+ <div class="tree-info">
711
+ ${tree.scientific_name || tree.common_name || tree.local_name || 'Unnamed'}
712
+ <br>${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)}
713
+ ${tree.tree_code ? `<br>Code: ${tree.tree_code}` : ''}
714
+ <br>${new Date(tree.created_at).toLocaleDateString()}
715
+ <br>By: ${tree.created_by || 'Unknown'}
716
+ </div>
717
+ </div>
718
+ `;
719
+ }).join('');
720
+
721
+ } catch (error) {
722
+ console.error('Error loading trees:', error);
723
+ document.getElementById('treeList').innerHTML = '<div class="loading">Error loading trees</div>';
724
+ }
725
+ }
726
+
727
+ async editTree(treeId) {
728
+ try {
729
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`);
730
+ if (!response) return;
731
+
732
+ if (!response.ok) {
733
+ throw new Error('Failed to fetch tree data');
734
+ }
735
+
736
+ const tree = await response.json();
737
+
738
+ // Populate form with tree data
739
+ document.getElementById('latitude').value = tree.latitude;
740
+ document.getElementById('longitude').value = tree.longitude;
741
+ document.getElementById('localName').value = tree.local_name || '';
742
+ document.getElementById('scientificName').value = tree.scientific_name || '';
743
+ document.getElementById('commonName').value = tree.common_name || '';
744
+ document.getElementById('treeCode').value = tree.tree_code || '';
745
+ document.getElementById('height').value = tree.height || '';
746
+ document.getElementById('width').value = tree.width || '';
747
+ document.getElementById('storytellingText').value = tree.storytelling_text || '';
748
+ document.getElementById('notes').value = tree.notes || '';
749
+
750
+ // Handle utility checkboxes
751
+ if (tree.utility && Array.isArray(tree.utility)) {
752
+ document.querySelectorAll('#utilityOptions input[type="checkbox"]').forEach(checkbox => {
753
+ checkbox.checked = tree.utility.includes(checkbox.value);
754
+ });
755
+ }
756
+
757
+ // Handle phenology checkboxes
758
+ if (tree.phenology_stages && Array.isArray(tree.phenology_stages)) {
759
+ document.querySelectorAll('#phenologyOptions input[type="checkbox"]').forEach(checkbox => {
760
+ checkbox.checked = tree.phenology_stages.includes(checkbox.value);
761
+ });
762
+ }
763
+
764
+ // Update form to edit mode
765
+ this.setEditMode(treeId);
766
+
767
+ this.showMessage(`Loaded tree #${treeId} for editing. Make changes and save.`, 'success');
768
+
769
+ } catch (error) {
770
+ console.error('Error loading tree for edit:', error);
771
+ this.showMessage('Error loading tree data: ' + error.message, 'error');
772
+ }
773
+ }
774
+
775
+ setEditMode(treeId) {
776
+ // Change form submit behavior
777
+ const form = document.getElementById('treeForm');
778
+ form.dataset.editId = treeId;
779
+
780
+ // Update submit button
781
+ const submitBtn = document.querySelector('button[type="submit"]');
782
+ submitBtn.textContent = 'Update Tree Record';
783
+
784
+ // Add cancel edit button
785
+ if (!document.getElementById('cancelEdit')) {
786
+ const cancelBtn = document.createElement('button');
787
+ cancelBtn.type = 'button';
788
+ cancelBtn.id = 'cancelEdit';
789
+ cancelBtn.className = 'btn btn-outline';
790
+ cancelBtn.textContent = 'Cancel Edit';
791
+ cancelBtn.addEventListener('click', () => this.cancelEdit());
792
+
793
+ const formActions = document.querySelector('.form-actions');
794
+ formActions.insertBefore(cancelBtn, submitBtn);
795
+ }
796
+
797
+ // Update form submit handler for edit mode
798
+ form.removeEventListener('submit', this.handleSubmit);
799
+ form.addEventListener('submit', (e) => this.handleEditSubmit(e, treeId));
800
+ }
801
+
802
+ cancelEdit() {
803
+ // Reset form
804
+ this.resetFormSilently();
805
+
806
+ // Remove edit mode
807
+ const form = document.getElementById('treeForm');
808
+ delete form.dataset.editId;
809
+
810
+ // Restore original submit button
811
+ const submitBtn = document.querySelector('button[type="submit"]');
812
+ submitBtn.textContent = 'Save Tree Record';
813
+
814
+ // Remove cancel button
815
+ const cancelBtn = document.getElementById('cancelEdit');
816
+ if (cancelBtn) {
817
+ cancelBtn.remove();
818
+ }
819
+
820
+ // Restore original form handler
821
+ form.removeEventListener('submit', this.handleEditSubmit);
822
+ form.addEventListener('submit', (e) => this.handleSubmit(e));
823
+
824
+ this.showMessage('Edit cancelled. Form cleared.', 'success');
825
+ }
826
+
827
+ async handleEditSubmit(e, treeId) {
828
+ e.preventDefault();
829
+
830
+ const utilityValues = this.getSelectedValues('utilityOptions');
831
+ const phenologyValues = this.getSelectedValues('phenologyOptions');
832
+
833
+ const treeData = {
834
+ latitude: parseFloat(document.getElementById('latitude').value),
835
+ longitude: parseFloat(document.getElementById('longitude').value),
836
+ local_name: document.getElementById('localName').value || null,
837
+ scientific_name: document.getElementById('scientificName').value || null,
838
+ common_name: document.getElementById('commonName').value || null,
839
+ tree_code: document.getElementById('treeCode').value || null,
840
+ height: document.getElementById('height').value ? parseFloat(document.getElementById('height').value) : null,
841
+ width: document.getElementById('width').value ? parseFloat(document.getElementById('width').value) : null,
842
+ utility: utilityValues.length > 0 ? utilityValues : [],
843
+ phenology_stages: phenologyValues.length > 0 ? phenologyValues : [],
844
+ storytelling_text: document.getElementById('storytellingText').value || null,
845
+ storytelling_audio: this.audioFile,
846
+ photographs: Object.keys(this.uploadedPhotos).length > 0 ? this.uploadedPhotos : null,
847
+ notes: document.getElementById('notes').value || null
848
+ };
849
+
850
+ try {
851
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`, {
852
+ method: 'PUT',
853
+ body: JSON.stringify(treeData)
854
+ });
855
+
856
+ if (!response) return;
857
+
858
+ if (response.ok) {
859
+ const result = await response.json();
860
+ this.showMessage(`Tree #${result.id} updated successfully!`, 'success');
861
+ this.cancelEdit(); // Exit edit mode
862
+ this.loadTrees(); // Refresh the tree list
863
+ } else {
864
+ const error = await response.json();
865
+ this.showMessage('Error updating tree: ' + (error.detail || 'Unknown error'), 'error');
866
+ }
867
+ } catch (error) {
868
+ console.error('Error updating tree:', error);
869
+ this.showMessage('Network error: ' + error.message, 'error');
870
+ }
871
+ }
872
+
873
+ async deleteTree(treeId) {
874
+ if (!confirm(`Are you sure you want to delete Tree #${treeId}? This action cannot be undone.`)) {
875
+ return;
876
+ }
877
+
878
+ try {
879
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`, {
880
+ method: 'DELETE'
881
+ });
882
+
883
+ if (!response) return;
884
+
885
+ if (response.ok) {
886
+ this.showMessage(`Tree #${treeId} deleted successfully.`, 'success');
887
+ this.loadTrees(); // Refresh the tree list
888
+ } else {
889
+ const error = await response.json();
890
+ this.showMessage('Error deleting tree: ' + (error.detail || 'Unknown error'), 'error');
891
+ }
892
+ } catch (error) {
893
+ console.error('Error deleting tree:', error);
894
+ this.showMessage('Network error: ' + error.message, 'error');
895
+ }
896
+ }
897
+
898
+ showMessage(message, type) {
899
+ const messageDiv = document.getElementById('message');
900
+ messageDiv.className = `message ${type === 'error' ? 'error' : 'success'}`;
901
+ messageDiv.textContent = message;
902
+
903
+ // Auto-hide after 5 seconds
904
+ setTimeout(() => {
905
+ messageDiv.textContent = '';
906
+ messageDiv.className = '';
907
+ }, 5000);
908
+ }
909
+
910
+ // Auto-suggestion functionality
911
+ async initializeAutoSuggestions() {
912
+ try {
913
+ // Load available tree codes for validation
914
+ const codesResponse = await this.authenticatedFetch('/api/tree-codes');
915
+ if (!codesResponse) return;
916
+
917
+ const codesData = await codesResponse.json();
918
+ this.availableTreeCodes = codesData.tree_codes || [];
919
+
920
+ // Setup autocomplete for tree identification fields
921
+ this.setupAutocomplete('localName', 'tree-suggestions');
922
+ this.setupAutocomplete('scientificName', 'tree-suggestions');
923
+ this.setupAutocomplete('commonName', 'tree-suggestions');
924
+ this.setupAutocomplete('treeCode', 'tree-codes');
925
+
926
+ } catch (error) {
927
+ console.error('Error initializing auto-suggestions:', error);
928
+ }
929
+ }
930
+
931
+ setupAutocomplete(fieldId, apiType) {
932
+ const input = document.getElementById(fieldId);
933
+ if (!input) return;
934
+
935
+ // Wrap input in container for dropdown positioning
936
+ if (!input.parentElement.classList.contains('autocomplete-container')) {
937
+ const container = document.createElement('div');
938
+ container.className = 'autocomplete-container';
939
+ input.parentNode.insertBefore(container, input);
940
+ container.appendChild(input);
941
+
942
+ // Create dropdown element
943
+ const dropdown = document.createElement('div');
944
+ dropdown.className = 'autocomplete-dropdown';
945
+ dropdown.id = `${fieldId}-dropdown`;
946
+ container.appendChild(dropdown);
947
+ }
948
+
949
+ // Add event listeners
950
+ input.addEventListener('input', (e) => this.handleInputChange(e, apiType));
951
+ input.addEventListener('keydown', (e) => this.handleKeyDown(e, fieldId));
952
+ input.addEventListener('blur', (e) => this.handleInputBlur(e, fieldId));
953
+ input.addEventListener('focus', (e) => this.handleInputFocus(e, fieldId));
954
+ }
955
+
956
+ async handleInputChange(event, apiType) {
957
+ const input = event.target;
958
+ const query = input.value.trim();
959
+ const fieldId = input.id;
960
+
961
+ // Clear previous timeout
962
+ if (this.searchTimeouts[fieldId]) {
963
+ clearTimeout(this.searchTimeouts[fieldId]);
964
+ }
965
+
966
+ if (query.length < 2) {
967
+ this.hideDropdown(fieldId);
968
+ return;
969
+ }
970
+
971
+ // Show loading state
972
+ this.showLoadingState(fieldId);
973
+
974
+ // Debounce search requests
975
+ this.searchTimeouts[fieldId] = setTimeout(async () => {
976
+ try {
977
+ let suggestions = [];
978
+
979
+ if (apiType === 'tree-codes') {
980
+ // Filter tree codes locally
981
+ suggestions = this.availableTreeCodes
982
+ .filter(code => code.toLowerCase().includes(query.toLowerCase()))
983
+ .slice(0, 10)
984
+ .map(code => ({
985
+ primary: code,
986
+ secondary: 'Tree Reference Code',
987
+ type: 'code'
988
+ }));
989
+ } else {
990
+ // Search tree suggestions from API
991
+ const response = await this.authenticatedFetch(`/api/tree-suggestions?query=${encodeURIComponent(query)}&limit=10`);
992
+ if (!response) return;
993
+
994
+ const data = await response.json();
995
+
996
+ if (data.suggestions) {
997
+ suggestions = data.suggestions.map(suggestion => ({
998
+ primary: this.getPrimaryText(suggestion, fieldId),
999
+ secondary: this.getSecondaryText(suggestion, fieldId),
1000
+ badges: this.getBadges(suggestion),
1001
+ data: suggestion
1002
+ }));
1003
+ }
1004
+ }
1005
+
1006
+ this.showSuggestions(fieldId, suggestions, query);
1007
+
1008
+ } catch (error) {
1009
+ console.error('Error fetching suggestions:', error);
1010
+ this.hideDropdown(fieldId);
1011
+ }
1012
+ }, 300); // 300ms debounce
1013
+ }
1014
+
1015
+ getPrimaryText(suggestion, fieldId) {
1016
+ switch (fieldId) {
1017
+ case 'localName':
1018
+ return suggestion.local_name || suggestion.scientific_name || suggestion.common_name;
1019
+ case 'scientificName':
1020
+ return suggestion.scientific_name || suggestion.local_name || suggestion.common_name;
1021
+ case 'commonName':
1022
+ return suggestion.common_name || suggestion.local_name || suggestion.scientific_name;
1023
+ default:
1024
+ return suggestion.local_name || suggestion.scientific_name || suggestion.common_name;
1025
+ }
1026
+ }
1027
+
1028
+ getSecondaryText(suggestion, fieldId) {
1029
+ const parts = [];
1030
+
1031
+ if (fieldId !== 'localName' && suggestion.local_name) {
1032
+ parts.push(`Local: ${suggestion.local_name}`);
1033
+ }
1034
+ if (fieldId !== 'scientificName' && suggestion.scientific_name) {
1035
+ parts.push(`Scientific: ${suggestion.scientific_name}`);
1036
+ }
1037
+ if (fieldId !== 'commonName' && suggestion.common_name) {
1038
+ parts.push(`Common: ${suggestion.common_name}`);
1039
+ }
1040
+ if (suggestion.tree_code) {
1041
+ parts.push(`Code: ${suggestion.tree_code}`);
1042
+ }
1043
+
1044
+ return parts.join(' β€’ ');
1045
+ }
1046
+
1047
+ getBadges(suggestion) {
1048
+ const badges = [];
1049
+ if (suggestion.tree_code) {
1050
+ badges.push(suggestion.tree_code);
1051
+ }
1052
+ if (suggestion.fruiting_season) {
1053
+ badges.push(`Season: ${suggestion.fruiting_season}`);
1054
+ }
1055
+ return badges;
1056
+ }
1057
+
1058
+ showLoadingState(fieldId) {
1059
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
1060
+ if (dropdown) {
1061
+ dropdown.innerHTML = '<div class="autocomplete-loading">Searching...</div>';
1062
+ dropdown.style.display = 'block';
1063
+ this.activeDropdowns.add(fieldId);
1064
+ }
1065
+ }
1066
+
1067
+ showSuggestions(fieldId, suggestions, query) {
1068
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
1069
+ if (!dropdown) return;
1070
+
1071
+ if (suggestions.length === 0) {
1072
+ dropdown.innerHTML = '<div class="autocomplete-no-results">No matching suggestions found</div>';
1073
+ dropdown.style.display = 'block';
1074
+ this.activeDropdowns.add(fieldId);
1075
+ return;
1076
+ }
1077
+
1078
+ const html = suggestions.map((suggestion, index) => `
1079
+ <div class="autocomplete-item" data-index="${index}" data-field="${fieldId}">
1080
+ <div class="autocomplete-primary">${this.highlightMatch(suggestion.primary, query)}</div>
1081
+ ${suggestion.secondary ? `<div class="autocomplete-secondary">${suggestion.secondary}</div>` : ''}
1082
+ ${suggestion.badges && suggestion.badges.length > 0 ?
1083
+ `<div>${suggestion.badges.map(badge => `<span class="autocomplete-badge">${badge}</span>`).join('')}</div>` : ''}
1084
+ </div>
1085
+ `).join('');
1086
+
1087
+ dropdown.innerHTML = html;
1088
+ dropdown.style.display = 'block';
1089
+ this.activeDropdowns.add(fieldId);
1090
+ this.selectedIndex = -1;
1091
+
1092
+ // Add click listeners to suggestion items
1093
+ dropdown.querySelectorAll('.autocomplete-item').forEach(item => {
1094
+ item.addEventListener('mousedown', (e) => this.handleSuggestionClick(e, suggestions));
1095
+ });
1096
+ }
1097
+
1098
+ highlightMatch(text, query) {
1099
+ if (!query || !text) return text;
1100
+
1101
+ const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
1102
+ return text.replace(regex, '<strong>$1</strong>');
1103
+ }
1104
+
1105
+ handleSuggestionClick(event, suggestions) {
1106
+ event.preventDefault();
1107
+ const item = event.target.closest('.autocomplete-item');
1108
+ const index = parseInt(item.dataset.index);
1109
+ const fieldId = item.dataset.field;
1110
+ const suggestion = suggestions[index];
1111
+
1112
+ this.applySuggestion(fieldId, suggestion);
1113
+ this.hideDropdown(fieldId);
1114
+ }
1115
+
1116
+ applySuggestion(fieldId, suggestion) {
1117
+ const input = document.getElementById(fieldId);
1118
+
1119
+ if (suggestion.type === 'code') {
1120
+ // Tree code suggestion
1121
+ input.value = suggestion.primary;
1122
+ } else {
1123
+ // Tree species suggestion - fill multiple fields
1124
+ const data = suggestion.data;
1125
+
1126
+ if (fieldId === 'localName' && data.local_name) {
1127
+ input.value = data.local_name;
1128
+ } else if (fieldId === 'scientificName' && data.scientific_name) {
1129
+ input.value = data.scientific_name;
1130
+ } else if (fieldId === 'commonName' && data.common_name) {
1131
+ input.value = data.common_name;
1132
+ } else {
1133
+ input.value = suggestion.primary;
1134
+ }
1135
+
1136
+ // Auto-fill other related fields if they're empty
1137
+ this.autoFillRelatedFields(data, fieldId);
1138
+ }
1139
+
1140
+ // Trigger input event for any validation
1141
+ input.dispatchEvent(new Event('input', { bubbles: true }));
1142
+ }
1143
+
1144
+ autoFillRelatedFields(data, excludeFieldId) {
1145
+ const fields = {
1146
+ 'localName': data.local_name,
1147
+ 'scientificName': data.scientific_name,
1148
+ 'commonName': data.common_name,
1149
+ 'treeCode': data.tree_code
1150
+ };
1151
+
1152
+ Object.entries(fields).forEach(([fieldId, value]) => {
1153
+ if (fieldId !== excludeFieldId && value) {
1154
+ const input = document.getElementById(fieldId);
1155
+ if (input && !input.value.trim()) {
1156
+ input.value = value;
1157
+ // Add visual indication that field was auto-filled
1158
+ input.style.backgroundColor = '#f0f9ff';
1159
+ setTimeout(() => {
1160
+ input.style.backgroundColor = '';
1161
+ }, 2000);
1162
+ }
1163
+ }
1164
+ });
1165
+ }
1166
+
1167
+ handleKeyDown(event, fieldId) {
1168
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
1169
+ if (!dropdown || dropdown.style.display === 'none') return;
1170
+
1171
+ const items = dropdown.querySelectorAll('.autocomplete-item');
1172
+ if (items.length === 0) return;
1173
+
1174
+ switch (event.key) {
1175
+ case 'ArrowDown':
1176
+ event.preventDefault();
1177
+ this.selectedIndex = Math.min(this.selectedIndex + 1, items.length - 1);
1178
+ this.updateHighlight(items);
1179
+ break;
1180
+
1181
+ case 'ArrowUp':
1182
+ event.preventDefault();
1183
+ this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
1184
+ this.updateHighlight(items);
1185
+ break;
1186
+
1187
+ case 'Enter':
1188
+ event.preventDefault();
1189
+ if (this.selectedIndex >= 0 && items[this.selectedIndex]) {
1190
+ items[this.selectedIndex].click();
1191
+ }
1192
+ break;
1193
+
1194
+ case 'Escape':
1195
+ event.preventDefault();
1196
+ this.hideDropdown(fieldId);
1197
+ break;
1198
+ }
1199
+ }
1200
+
1201
+ updateHighlight(items) {
1202
+ items.forEach((item, index) => {
1203
+ item.classList.toggle('highlighted', index === this.selectedIndex);
1204
+ });
1205
+ }
1206
+
1207
+ handleInputBlur(event, fieldId) {
1208
+ // Delay hiding to allow for click events on suggestions
1209
+ setTimeout(() => {
1210
+ this.hideDropdown(fieldId);
1211
+ }, 150);
1212
+ }
1213
+
1214
+ handleInputFocus(event, fieldId) {
1215
+ const input = event.target;
1216
+ if (input.value.length >= 2) {
1217
+ // Re-trigger search on focus if there's already content
1218
+ this.handleInputChange(event, fieldId === 'treeCode' ? 'tree-codes' : 'tree-suggestions');
1219
+ }
1220
+ }
1221
+
1222
+ hideDropdown(fieldId) {
1223
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
1224
+ if (dropdown) {
1225
+ dropdown.style.display = 'none';
1226
+ dropdown.innerHTML = '';
1227
+ this.activeDropdowns.delete(fieldId);
1228
+ this.selectedIndex = -1;
1229
+ }
1230
+ }
1231
+
1232
+ hideAllDropdowns() {
1233
+ this.activeDropdowns.forEach(fieldId => {
1234
+ this.hideDropdown(fieldId);
1235
+ });
1236
+ }
1237
+ }
1238
+
1239
+ // Initialize the app when the page loads
1240
+ let app;
1241
+ document.addEventListener('DOMContentLoaded', () => {
1242
+ app = new TreeTrackApp();
1243
+ });
static/index.html CHANGED
@@ -80,26 +80,26 @@
80
  -moz-osx-font-smoothing: grayscale;
81
  }
82
 
83
- /* Header */
84
  .header {
85
- background: linear-gradient(135deg, var(--primary-600) 0%, var(--primary-700) 100%);
86
- color: white;
87
  position: sticky;
88
  top: 0;
89
  z-index: 100;
90
- backdrop-filter: blur(12px);
91
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
92
  }
93
 
94
  .header-content {
95
  max-width: 1400px;
96
  margin: 0 auto;
97
- padding: var(--space-4) var(--space-6);
98
  display: flex;
99
  justify-content: space-between;
100
  align-items: center;
101
  flex-wrap: wrap;
102
- gap: var(--space-4);
103
  }
104
 
105
  .header-brand {
@@ -109,15 +109,20 @@
109
  }
110
 
111
  .header h1 {
112
- font-size: 1.75rem;
113
- font-weight: 700;
114
  margin: 0;
115
  letter-spacing: -0.025em;
 
 
 
 
116
  }
 
117
 
118
  .header-subtitle {
119
- font-size: 0.875rem;
120
- opacity: 0.9;
121
  font-weight: 400;
122
  }
123
 
@@ -130,24 +135,24 @@
130
  .user-info {
131
  display: flex;
132
  align-items: center;
133
- gap: var(--space-3);
134
- padding: var(--space-2) var(--space-4);
135
- background: rgba(255, 255, 255, 0.1);
136
- border-radius: var(--radius-xl);
137
- border: 1px solid rgba(255, 255, 255, 0.2);
138
- backdrop-filter: blur(8px);
139
  }
140
 
141
  .user-avatar {
142
- width: 32px;
143
- height: 32px;
144
  border-radius: 50%;
145
  background: var(--primary-500);
146
  display: flex;
147
  align-items: center;
148
  justify-content: center;
149
  font-weight: 600;
150
- font-size: 0.875rem;
 
151
  }
152
 
153
  .user-details {
@@ -157,13 +162,14 @@
157
  }
158
 
159
  .user-name {
160
- font-size: 0.875rem;
161
- font-weight: 600;
 
162
  }
163
 
164
  .user-role {
165
- font-size: 0.75rem;
166
- opacity: 0.8;
167
  text-transform: capitalize;
168
  }
169
 
@@ -198,13 +204,16 @@
198
  }
199
 
200
  .btn-secondary {
201
- background: rgba(255, 255, 255, 0.1);
202
- color: white;
203
- border: 1px solid rgba(255, 255, 255, 0.2);
 
 
204
  }
205
 
206
  .btn-secondary:hover {
207
- background: rgba(255, 255, 255, 0.2);
 
208
  transform: translateY(-1px);
209
  }
210
 
@@ -688,32 +697,105 @@
688
  display: none;
689
  }
690
 
691
- /* Mobile Optimizations */
692
  @media (max-width: 768px) {
693
  .header-content {
694
  padding: var(--space-3) var(--space-4);
 
 
 
695
  }
696
 
697
  .header h1 {
698
  font-size: 1.5rem;
699
  }
700
 
 
 
 
 
 
 
701
  .main-container {
702
- padding: var(--space-6) var(--space-4);
703
  }
704
 
705
  .card-header,
706
  .card-content {
707
- padding: var(--space-4);
 
 
 
 
 
 
 
 
708
  }
709
 
710
  .user-info {
711
  padding: var(--space-2) var(--space-3);
 
712
  }
713
 
714
  .user-details {
715
  display: none;
716
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  }
718
 
719
  /* Focus visible for accessibility */
@@ -723,6 +805,254 @@
723
  outline: 2px solid var(--primary-500);
724
  outline-offset: 2px;
725
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  </style>
727
  <script>
728
  // Force refresh if we detect cached version
@@ -743,7 +1073,7 @@
743
  <div class="header">
744
  <div class="header-content">
745
  <div class="header-brand">
746
- <h1>🌳 TreeTrack</h1>
747
  <div class="header-subtitle">Professional Field Research Platform</div>
748
  </div>
749
  <div class="header-actions">
@@ -754,7 +1084,7 @@
754
  <div class="user-role" id="userRole">User</div>
755
  </div>
756
  </div>
757
- <a href="/static/map.html" class="btn btn-secondary">πŸ“ View Map</a>
758
  <button id="logoutBtn" class="btn btn-secondary">Logout</button>
759
  </div>
760
  </div>
@@ -772,7 +1102,7 @@
772
  <!-- Location Section -->
773
  <div class="form-section">
774
  <div class="section-header">
775
- <h3 class="section-title">πŸ“ Geographic Location</h3>
776
  <p class="section-description">Precise coordinates are essential for mapping and field research</p>
777
  </div>
778
 
@@ -790,10 +1120,10 @@
790
  <div class="form-group">
791
  <div class="location-buttons">
792
  <button type="button" id="getLocation" class="btn btn-outline location-btn-gps">
793
- πŸ“ <span class="btn-text">Get GPS Location</span>
794
  </button>
795
  <a href="/static/map.html" class="btn btn-primary location-btn-map">
796
- πŸ—ΊοΈ <span class="btn-text">Select from Map</span>
797
  </a>
798
  </div>
799
  </div>
@@ -802,7 +1132,7 @@
802
  <!-- Identification Section -->
803
  <div class="form-section">
804
  <div class="section-header">
805
- <h3 class="section-title">🌿 Tree Identification</h3>
806
  <p class="section-description">Botanical and local identification details</p>
807
  </div>
808
 
@@ -830,7 +1160,7 @@
830
  <!-- Measurements Section -->
831
  <div class="form-section">
832
  <div class="section-header">
833
- <h3 class="section-title">πŸ“ Physical Measurements</h3>
834
  <p class="section-description">Quantitative assessment of tree dimensions</p>
835
  </div>
836
 
@@ -849,7 +1179,7 @@
849
  <!-- Utility Section -->
850
  <div class="form-section">
851
  <div class="section-header">
852
- <h3 class="section-title">🌱 Ecological & Cultural Utility</h3>
853
  <p class="section-description">Select all applicable ecological and cultural uses</p>
854
  </div>
855
 
@@ -863,7 +1193,7 @@
863
  <!-- Phenology Section -->
864
  <div class="form-section">
865
  <div class="section-header">
866
- <h3 class="section-title">πŸƒ Phenology Assessment</h3>
867
  <p class="section-description">Current developmental stages observed</p>
868
  </div>
869
 
@@ -877,7 +1207,7 @@
877
  <!-- Photography Section -->
878
  <div class="form-section">
879
  <div class="section-header">
880
- <h3 class="section-title">πŸ“· Photographic Documentation</h3>
881
  <p class="section-description">Upload photographs for different tree components</p>
882
  </div>
883
 
@@ -889,7 +1219,7 @@
889
  <!-- Cultural Documentation Section -->
890
  <div class="form-section">
891
  <div class="section-header">
892
- <h3 class="section-title">πŸ“– Cultural Documentation</h3>
893
  <p class="section-description">Stories, histories, and cultural significance</p>
894
  </div>
895
 
@@ -901,7 +1231,7 @@
901
  <div class="form-group">
902
  <label class="form-label">Audio Recording</label>
903
  <div class="file-upload-area" id="audioUpload">
904
- <div class="file-upload-icon">πŸŽ™οΈ</div>
905
  <div class="file-upload-text">Click to upload audio file</div>
906
  <div class="file-upload-hint">Or drag and drop (MP3, WAV, M4A)</div>
907
  </div>
@@ -912,7 +1242,7 @@
912
  <!-- Field Notes Section -->
913
  <div class="form-section">
914
  <div class="section-header">
915
- <h3 class="section-title">πŸ“ Field Notes</h3>
916
  <p class="section-description">Additional observations and remarks</p>
917
  </div>
918
 
@@ -924,7 +1254,7 @@
924
 
925
  <div class="form-actions">
926
  <button type="button" id="resetForm" class="btn btn-outline">Reset Form</button>
927
- <button type="submit" class="btn btn-primary btn-lg">πŸ’Ύ Save Tree Record</button>
928
  </div>
929
  </form>
930
 
@@ -949,6 +1279,6 @@
949
  </div>
950
  </div>
951
 
952
- <script src="/static/app.js?v=4.0.0&t=1754657582"></script>
953
  </body>
954
  </html>
 
80
  -moz-osx-font-smoothing: grayscale;
81
  }
82
 
83
+ /* Modern Header */
84
  .header {
85
+ background: white;
86
+ color: var(--gray-900);
87
  position: sticky;
88
  top: 0;
89
  z-index: 100;
90
+ border-bottom: 1px solid var(--gray-200);
91
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
92
  }
93
 
94
  .header-content {
95
  max-width: 1400px;
96
  margin: 0 auto;
97
+ padding: var(--space-3) var(--space-4);
98
  display: flex;
99
  justify-content: space-between;
100
  align-items: center;
101
  flex-wrap: wrap;
102
+ gap: var(--space-3);
103
  }
104
 
105
  .header-brand {
 
109
  }
110
 
111
  .header h1 {
112
+ font-size: 1.5rem;
113
+ font-weight: 600;
114
  margin: 0;
115
  letter-spacing: -0.025em;
116
+ color: var(--gray-900);
117
+ display: flex;
118
+ align-items: center;
119
+ gap: var(--space-2);
120
  }
121
+
122
 
123
  .header-subtitle {
124
+ font-size: 0.75rem;
125
+ color: var(--gray-600);
126
  font-weight: 400;
127
  }
128
 
 
135
  .user-info {
136
  display: flex;
137
  align-items: center;
138
+ gap: var(--space-2);
139
+ padding: var(--space-2) var(--space-3);
140
+ background: var(--gray-50);
141
+ border-radius: var(--radius-lg);
142
+ border: 1px solid var(--gray-200);
 
143
  }
144
 
145
  .user-avatar {
146
+ width: 28px;
147
+ height: 28px;
148
  border-radius: 50%;
149
  background: var(--primary-500);
150
  display: flex;
151
  align-items: center;
152
  justify-content: center;
153
  font-weight: 600;
154
+ font-size: 0.75rem;
155
+ color: white;
156
  }
157
 
158
  .user-details {
 
162
  }
163
 
164
  .user-name {
165
+ font-size: 0.8125rem;
166
+ font-weight: 500;
167
+ color: var(--gray-900);
168
  }
169
 
170
  .user-role {
171
+ font-size: 0.6875rem;
172
+ color: var(--gray-600);
173
  text-transform: capitalize;
174
  }
175
 
 
204
  }
205
 
206
  .btn-secondary {
207
+ background: var(--gray-100);
208
+ color: var(--gray-700);
209
+ border: 1px solid var(--gray-200);
210
+ font-size: 0.8125rem;
211
+ padding: var(--space-2) var(--space-3);
212
  }
213
 
214
  .btn-secondary:hover {
215
+ background: var(--gray-200);
216
+ color: var(--gray-800);
217
  transform: translateY(-1px);
218
  }
219
 
 
697
  display: none;
698
  }
699
 
700
+ /* Improved Mobile Responsiveness */
701
  @media (max-width: 768px) {
702
  .header-content {
703
  padding: var(--space-3) var(--space-4);
704
+ flex-direction: column;
705
+ align-items: stretch;
706
+ gap: var(--space-3);
707
  }
708
 
709
  .header h1 {
710
  font-size: 1.5rem;
711
  }
712
 
713
+ .header-actions {
714
+ flex-wrap: wrap;
715
+ justify-content: space-between;
716
+ width: 100%;
717
+ }
718
+
719
  .main-container {
720
+ padding: var(--space-4) var(--space-3);
721
  }
722
 
723
  .card-header,
724
  .card-content {
725
+ padding: var(--space-4) var(--space-3);
726
+ }
727
+
728
+ .card-title {
729
+ font-size: 1.125rem;
730
+ }
731
+
732
+ .section-title {
733
+ font-size: 1rem;
734
  }
735
 
736
  .user-info {
737
  padding: var(--space-2) var(--space-3);
738
+ flex-shrink: 0;
739
  }
740
 
741
  .user-details {
742
  display: none;
743
  }
744
+
745
+ /* Form improvements for mobile */
746
+ .form-input,
747
+ .form-textarea {
748
+ font-size: 16px; /* Prevent zoom on iOS */
749
+ padding: var(--space-3) var(--space-3);
750
+ }
751
+
752
+ .form-label {
753
+ font-size: 0.8125rem;
754
+ margin-bottom: var(--space-1);
755
+ }
756
+
757
+ .multi-select {
758
+ max-height: 150px;
759
+ }
760
+
761
+ .multi-select label {
762
+ padding: var(--space-2) var(--space-3);
763
+ margin: var(--space-1);
764
+ font-size: 0.8125rem;
765
+ }
766
+ }
767
+
768
+ @media (max-width: 480px) {
769
+ .header h1 {
770
+ font-size: 1.25rem;
771
+ }
772
+
773
+ .header-subtitle {
774
+ font-size: 0.75rem;
775
+ }
776
+
777
+ .main-container {
778
+ padding: var(--space-3) var(--space-2);
779
+ }
780
+
781
+ .card {
782
+ border-radius: var(--radius-lg);
783
+ }
784
+
785
+ .card-header,
786
+ .card-content {
787
+ padding: var(--space-3) var(--space-2);
788
+ }
789
+
790
+ .btn {
791
+ padding: var(--space-2) var(--space-3);
792
+ font-size: 0.8125rem;
793
+ }
794
+
795
+ .btn-lg {
796
+ padding: var(--space-3) var(--space-4);
797
+ font-size: 0.875rem;
798
+ }
799
  }
800
 
801
  /* Focus visible for accessibility */
 
805
  outline: 2px solid var(--primary-500);
806
  outline-offset: 2px;
807
  }
808
+
809
+ /* Autocomplete Dropdown Styles */
810
+ .autocomplete-container {
811
+ position: relative;
812
+ }
813
+
814
+ .autocomplete-dropdown {
815
+ position: absolute;
816
+ top: 100%;
817
+ left: 0;
818
+ right: 0;
819
+ z-index: 1000;
820
+ background: white;
821
+ border: 1px solid var(--gray-300);
822
+ border-top: none;
823
+ border-radius: 0 0 var(--radius-md) var(--radius-md);
824
+ max-height: 200px;
825
+ overflow-y: auto;
826
+ box-shadow: var(--shadow-lg);
827
+ display: none;
828
+ }
829
+
830
+ .autocomplete-item {
831
+ padding: var(--space-3) var(--space-4);
832
+ cursor: pointer;
833
+ border-bottom: 1px solid var(--gray-100);
834
+ transition: all 0.15s ease;
835
+ }
836
+
837
+ .autocomplete-item:hover,
838
+ .autocomplete-item.highlighted {
839
+ background: var(--primary-50);
840
+ }
841
+
842
+ .autocomplete-item:last-child {
843
+ border-bottom: none;
844
+ }
845
+
846
+ .autocomplete-primary {
847
+ font-size: 0.875rem;
848
+ font-weight: 500;
849
+ color: var(--gray-900);
850
+ margin-bottom: var(--space-1);
851
+ }
852
+
853
+ .autocomplete-secondary {
854
+ font-size: 0.75rem;
855
+ color: var(--gray-600);
856
+ margin-bottom: var(--space-2);
857
+ }
858
+
859
+ .autocomplete-badge {
860
+ display: inline-block;
861
+ background: var(--primary-100);
862
+ color: var(--primary-700);
863
+ padding: var(--space-1) var(--space-2);
864
+ border-radius: var(--radius-sm);
865
+ font-size: 0.75rem;
866
+ font-weight: 500;
867
+ margin-right: var(--space-1);
868
+ margin-bottom: var(--space-1);
869
+ }
870
+
871
+ .autocomplete-loading,
872
+ .autocomplete-no-results {
873
+ padding: var(--space-3) var(--space-4);
874
+ text-align: center;
875
+ color: var(--gray-500);
876
+ font-size: 0.875rem;
877
+ }
878
+
879
+ /* Improved Tree Item Actions */
880
+ .tree-actions {
881
+ display: flex;
882
+ gap: var(--space-1);
883
+ flex-shrink: 0;
884
+ }
885
+
886
+ .btn-icon {
887
+ padding: var(--space-1) var(--space-2);
888
+ font-size: 0.75rem;
889
+ font-weight: 500;
890
+ min-width: 44px; /* Ensure touch-friendly minimum */
891
+ height: 32px;
892
+ border: 1px solid var(--gray-300);
893
+ background: white;
894
+ border-radius: var(--radius-sm);
895
+ text-align: center;
896
+ transition: all 0.15s ease;
897
+ }
898
+
899
+ .btn-icon.edit-tree {
900
+ color: var(--primary-600);
901
+ border-color: var(--primary-200);
902
+ }
903
+
904
+ .btn-icon.edit-tree:hover {
905
+ background: var(--primary-50);
906
+ border-color: var(--primary-300);
907
+ color: var(--primary-700);
908
+ }
909
+
910
+ .btn-icon.delete-tree {
911
+ color: var(--red-600);
912
+ border-color: var(--red-200);
913
+ }
914
+
915
+ .btn-icon.delete-tree:hover {
916
+ background: var(--red-50);
917
+ border-color: var(--red-300);
918
+ color: var(--red-700);
919
+ }
920
+
921
+ /* Mobile Improvements for Tree Items */
922
+ @media (max-width: 768px) {
923
+ .tree-header {
924
+ flex-direction: column;
925
+ align-items: flex-start;
926
+ gap: var(--space-2);
927
+ }
928
+
929
+ .tree-actions {
930
+ align-self: flex-end;
931
+ gap: var(--space-2);
932
+ }
933
+
934
+ .btn-icon {
935
+ min-width: 48px;
936
+ height: 36px;
937
+ font-size: 0.8125rem;
938
+ }
939
+ }
940
+
941
+ /* Enhanced Photo Category Styling */
942
+ .photo-category {
943
+ margin-bottom: var(--space-6);
944
+ padding: var(--space-5);
945
+ border: 2px solid var(--gray-200);
946
+ border-radius: var(--radius-xl);
947
+ background: white;
948
+ box-shadow: var(--shadow-sm);
949
+ transition: all 0.2s ease;
950
+ }
951
+
952
+ .photo-category:hover {
953
+ border-color: var(--primary-200);
954
+ box-shadow: var(--shadow-md);
955
+ }
956
+
957
+ .photo-category-header {
958
+ display: flex;
959
+ justify-content: space-between;
960
+ align-items: center;
961
+ margin-bottom: var(--space-4);
962
+ padding-bottom: var(--space-3);
963
+ border-bottom: 1px solid var(--gray-100);
964
+ }
965
+
966
+ .photo-category-title {
967
+ font-size: 1rem;
968
+ font-weight: 600;
969
+ color: var(--gray-800);
970
+ display: flex;
971
+ align-items: center;
972
+ gap: var(--space-2);
973
+ }
974
+
975
+ .photo-category-icon {
976
+ width: 20px;
977
+ height: 20px;
978
+ background: var(--primary-100);
979
+ border-radius: var(--radius-sm);
980
+ display: flex;
981
+ align-items: center;
982
+ justify-content: center;
983
+ font-size: 0.75rem;
984
+ }
985
+
986
+ .photo-upload-area {
987
+ display: flex;
988
+ gap: var(--space-3);
989
+ }
990
+
991
+ .photo-upload {
992
+ flex: 1;
993
+ background: var(--gray-50);
994
+ border: 2px dashed var(--gray-300);
995
+ border-radius: var(--radius-lg);
996
+ padding: var(--space-4);
997
+ text-align: center;
998
+ cursor: pointer;
999
+ color: var(--gray-600);
1000
+ font-size: 0.875rem;
1001
+ transition: all 0.2s ease;
1002
+ min-height: 80px;
1003
+ display: flex;
1004
+ flex-direction: column;
1005
+ justify-content: center;
1006
+ align-items: center;
1007
+ gap: var(--space-2);
1008
+ }
1009
+
1010
+ .photo-upload:hover {
1011
+ border-color: var(--primary-400);
1012
+ background: var(--primary-50);
1013
+ color: var(--primary-600);
1014
+ }
1015
+
1016
+ .photo-upload-icon {
1017
+ font-size: 1.5rem;
1018
+ opacity: 0.6;
1019
+ }
1020
+
1021
+ .camera-btn {
1022
+ padding: var(--space-2) var(--space-4);
1023
+ background: var(--primary-500);
1024
+ color: white;
1025
+ border: none;
1026
+ border-radius: var(--radius-md);
1027
+ font-size: 0.8125rem;
1028
+ font-weight: 500;
1029
+ cursor: pointer;
1030
+ transition: all 0.2s ease;
1031
+ min-width: 80px;
1032
+ }
1033
+
1034
+ .camera-btn:hover {
1035
+ background: var(--primary-600);
1036
+ transform: translateY(-1px);
1037
+ }
1038
+
1039
+ .uploaded-file {
1040
+ background: var(--green-50);
1041
+ color: var(--green-700);
1042
+ padding: var(--space-2) var(--space-3);
1043
+ border-radius: var(--radius-md);
1044
+ font-size: 0.8125rem;
1045
+ margin-top: var(--space-3);
1046
+ border: 1px solid var(--green-200);
1047
+ display: flex;
1048
+ align-items: center;
1049
+ gap: var(--space-2);
1050
+ }
1051
+
1052
+ .uploaded-file::before {
1053
+ content: 'βœ“';
1054
+ font-weight: bold;
1055
+ }
1056
  </style>
1057
  <script>
1058
  // Force refresh if we detect cached version
 
1073
  <div class="header">
1074
  <div class="header-content">
1075
  <div class="header-brand">
1076
+ <h1>TreeTrack</h1>
1077
  <div class="header-subtitle">Professional Field Research Platform</div>
1078
  </div>
1079
  <div class="header-actions">
 
1084
  <div class="user-role" id="userRole">User</div>
1085
  </div>
1086
  </div>
1087
+ <a href="/static/map.html" class="btn btn-secondary">View Map</a>
1088
  <button id="logoutBtn" class="btn btn-secondary">Logout</button>
1089
  </div>
1090
  </div>
 
1102
  <!-- Location Section -->
1103
  <div class="form-section">
1104
  <div class="section-header">
1105
+ <h3 class="section-title">Geographic Location</h3>
1106
  <p class="section-description">Precise coordinates are essential for mapping and field research</p>
1107
  </div>
1108
 
 
1120
  <div class="form-group">
1121
  <div class="location-buttons">
1122
  <button type="button" id="getLocation" class="btn btn-outline location-btn-gps">
1123
+ <span class="btn-text">Get GPS Location</span>
1124
  </button>
1125
  <a href="/static/map.html" class="btn btn-primary location-btn-map">
1126
+ <span class="btn-text">Select from Map</span>
1127
  </a>
1128
  </div>
1129
  </div>
 
1132
  <!-- Identification Section -->
1133
  <div class="form-section">
1134
  <div class="section-header">
1135
+ <h3 class="section-title">Tree Identification</h3>
1136
  <p class="section-description">Botanical and local identification details</p>
1137
  </div>
1138
 
 
1160
  <!-- Measurements Section -->
1161
  <div class="form-section">
1162
  <div class="section-header">
1163
+ <h3 class="section-title">Physical Measurements</h3>
1164
  <p class="section-description">Quantitative assessment of tree dimensions</p>
1165
  </div>
1166
 
 
1179
  <!-- Utility Section -->
1180
  <div class="form-section">
1181
  <div class="section-header">
1182
+ <h3 class="section-title">Ecological & Cultural Utility</h3>
1183
  <p class="section-description">Select all applicable ecological and cultural uses</p>
1184
  </div>
1185
 
 
1193
  <!-- Phenology Section -->
1194
  <div class="form-section">
1195
  <div class="section-header">
1196
+ <h3 class="section-title">Phenology Assessment</h3>
1197
  <p class="section-description">Current developmental stages observed</p>
1198
  </div>
1199
 
 
1207
  <!-- Photography Section -->
1208
  <div class="form-section">
1209
  <div class="section-header">
1210
+ <h3 class="section-title">Photographic Documentation</h3>
1211
  <p class="section-description">Upload photographs for different tree components</p>
1212
  </div>
1213
 
 
1219
  <!-- Cultural Documentation Section -->
1220
  <div class="form-section">
1221
  <div class="section-header">
1222
+ <h3 class="section-title">Cultural Documentation</h3>
1223
  <p class="section-description">Stories, histories, and cultural significance</p>
1224
  </div>
1225
 
 
1231
  <div class="form-group">
1232
  <label class="form-label">Audio Recording</label>
1233
  <div class="file-upload-area" id="audioUpload">
1234
+ <div class="file-upload-icon">MIC</div>
1235
  <div class="file-upload-text">Click to upload audio file</div>
1236
  <div class="file-upload-hint">Or drag and drop (MP3, WAV, M4A)</div>
1237
  </div>
 
1242
  <!-- Field Notes Section -->
1243
  <div class="form-section">
1244
  <div class="section-header">
1245
+ <h3 class="section-title">Field Notes</h3>
1246
  <p class="section-description">Additional observations and remarks</p>
1247
  </div>
1248
 
 
1254
 
1255
  <div class="form-actions">
1256
  <button type="button" id="resetForm" class="btn btn-outline">Reset Form</button>
1257
+ <button type="submit" class="btn btn-primary btn-lg">Save Tree Record</button>
1258
  </div>
1259
  </form>
1260
 
 
1279
  </div>
1280
  </div>
1281
 
1282
+ <script type="module" src="/static/js/tree-track-app.js?v=5.0.0"></script>
1283
  </body>
1284
  </html>
static/js/modules/api-client.js ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * API Client Module
3
+ * Handles all API communication with authentication
4
+ */
5
+ export class ApiClient {
6
+ constructor(authManager) {
7
+ this.authManager = authManager;
8
+ }
9
+
10
+ async authenticatedFetch(url, options = {}) {
11
+ const headers = {
12
+ ...this.authManager.getAuthHeaders(),
13
+ ...options.headers
14
+ };
15
+
16
+ const response = await fetch(url, {
17
+ ...options,
18
+ headers
19
+ });
20
+
21
+ if (response.status === 401) {
22
+ // Token expired or invalid
23
+ this.authManager.clearAuthData();
24
+ window.location.href = '/login';
25
+ return null;
26
+ }
27
+
28
+ return response;
29
+ }
30
+
31
+ async loadFormOptions() {
32
+ try {
33
+ const [utilityResponse, phenologyResponse, categoriesResponse] = await Promise.all([
34
+ this.authenticatedFetch('/api/utilities'),
35
+ this.authenticatedFetch('/api/phenology-stages'),
36
+ this.authenticatedFetch('/api/photo-categories')
37
+ ]);
38
+
39
+ if (!utilityResponse || !phenologyResponse || !categoriesResponse) {
40
+ throw new Error('Failed to load form options');
41
+ }
42
+
43
+ const [utilityData, phenologyData, categoriesData] = await Promise.all([
44
+ utilityResponse.json(),
45
+ phenologyResponse.json(),
46
+ categoriesResponse.json()
47
+ ]);
48
+
49
+ return {
50
+ utilities: utilityData.utilities,
51
+ phenologyStages: phenologyData.stages,
52
+ photoCategories: categoriesData.categories
53
+ };
54
+ } catch (error) {
55
+ console.error('Error loading form options:', error);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ async saveTree(treeData) {
61
+ const response = await this.authenticatedFetch('/api/trees', {
62
+ method: 'POST',
63
+ body: JSON.stringify(treeData)
64
+ });
65
+
66
+ if (!response) return null;
67
+
68
+ if (!response.ok) {
69
+ const error = await response.json();
70
+ throw new Error(error.detail || 'Unknown error');
71
+ }
72
+
73
+ return await response.json();
74
+ }
75
+
76
+ async updateTree(treeId, treeData) {
77
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`, {
78
+ method: 'PUT',
79
+ body: JSON.stringify(treeData)
80
+ });
81
+
82
+ if (!response) return null;
83
+
84
+ if (!response.ok) {
85
+ const error = await response.json();
86
+ throw new Error(error.detail || 'Unknown error');
87
+ }
88
+
89
+ return await response.json();
90
+ }
91
+
92
+ async deleteTree(treeId) {
93
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`, {
94
+ method: 'DELETE'
95
+ });
96
+
97
+ if (!response) return null;
98
+
99
+ if (!response.ok) {
100
+ const error = await response.json();
101
+ throw new Error(error.detail || 'Unknown error');
102
+ }
103
+
104
+ return true;
105
+ }
106
+
107
+ async loadTrees(limit = 20) {
108
+ const response = await this.authenticatedFetch(`/api/trees?limit=${limit}`);
109
+ if (!response) return [];
110
+
111
+ if (!response.ok) {
112
+ throw new Error('Failed to load trees');
113
+ }
114
+
115
+ return await response.json();
116
+ }
117
+
118
+ async loadTree(treeId) {
119
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`);
120
+ if (!response) return null;
121
+
122
+ if (!response.ok) {
123
+ throw new Error('Failed to fetch tree data');
124
+ }
125
+
126
+ return await response.json();
127
+ }
128
+
129
+ async loadTreeCodes() {
130
+ const response = await this.authenticatedFetch('/api/tree-codes');
131
+ if (!response) return [];
132
+
133
+ if (!response.ok) {
134
+ throw new Error('Failed to load tree codes');
135
+ }
136
+
137
+ const data = await response.json();
138
+ return data.tree_codes || [];
139
+ }
140
+
141
+ async searchTreeSuggestions(query, limit = 10) {
142
+ const response = await this.authenticatedFetch(
143
+ `/api/tree-suggestions?query=${encodeURIComponent(query)}&limit=${limit}`
144
+ );
145
+
146
+ if (!response) return [];
147
+
148
+ if (!response.ok) {
149
+ throw new Error('Failed to search tree suggestions');
150
+ }
151
+
152
+ const data = await response.json();
153
+ return data.suggestions || [];
154
+ }
155
+
156
+ async uploadFile(file, type, category = null) {
157
+ const formData = new FormData();
158
+ formData.append('file', file);
159
+ if (category) {
160
+ formData.append('category', category);
161
+ }
162
+
163
+ const endpoint = type === 'image' ? '/api/upload/image' : '/api/upload/audio';
164
+
165
+ const response = await fetch(endpoint, {
166
+ method: 'POST',
167
+ headers: {
168
+ 'Authorization': `Bearer ${this.authManager.authToken}`
169
+ },
170
+ body: formData
171
+ });
172
+
173
+ if (!response.ok) {
174
+ throw new Error('Upload failed');
175
+ }
176
+
177
+ return await response.json();
178
+ }
179
+ }
static/js/modules/auth-manager.js ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Authentication Manager Module
3
+ * Handles user authentication, session management, and permissions
4
+ */
5
+ export class AuthManager {
6
+ constructor() {
7
+ this.currentUser = null;
8
+ this.authToken = null;
9
+ }
10
+
11
+ async checkAuthentication() {
12
+ const token = localStorage.getItem('auth_token');
13
+ if (!token) {
14
+ return false;
15
+ }
16
+
17
+ try {
18
+ const response = await fetch('/api/auth/validate', {
19
+ headers: {
20
+ 'Authorization': `Bearer ${token}`
21
+ }
22
+ });
23
+
24
+ if (response.ok) {
25
+ const data = await response.json();
26
+ this.currentUser = data.user;
27
+ this.authToken = token;
28
+ return true;
29
+ } else {
30
+ this.clearAuthData();
31
+ return false;
32
+ }
33
+ } catch (error) {
34
+ console.error('Auth validation error:', error);
35
+ return false;
36
+ }
37
+ }
38
+
39
+ async logout() {
40
+ try {
41
+ await fetch('/api/auth/logout', {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Authorization': `Bearer ${this.authToken}`
45
+ }
46
+ });
47
+ } catch (error) {
48
+ console.error('Logout error:', error);
49
+ } finally {
50
+ this.clearAuthData();
51
+ window.location.href = '/login';
52
+ }
53
+ }
54
+
55
+ clearAuthData() {
56
+ localStorage.removeItem('auth_token');
57
+ localStorage.removeItem('user_info');
58
+ this.currentUser = null;
59
+ this.authToken = null;
60
+ }
61
+
62
+ canEditTree(createdBy) {
63
+ if (!this.currentUser) return false;
64
+
65
+ const permissions = this.currentUser.permissions || [];
66
+
67
+ // Admin and system can edit any tree
68
+ if (permissions.includes('admin') || permissions.includes('system')) {
69
+ return true;
70
+ }
71
+
72
+ // Users can edit trees they created
73
+ if (permissions.includes('edit_own') && createdBy === this.currentUser.username) {
74
+ return true;
75
+ }
76
+
77
+ // Users with delete permission can edit any tree
78
+ if (permissions.includes('delete')) {
79
+ return true;
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ canDeleteTree(createdBy) {
86
+ if (!this.currentUser) return false;
87
+
88
+ const permissions = this.currentUser.permissions || [];
89
+
90
+ // Only admin and system can delete trees
91
+ if (permissions.includes('admin') || permissions.includes('system')) {
92
+ return true;
93
+ }
94
+
95
+ // Users with explicit delete permission
96
+ if (permissions.includes('delete')) {
97
+ return true;
98
+ }
99
+
100
+ return false;
101
+ }
102
+
103
+ getAuthHeaders() {
104
+ return {
105
+ 'Content-Type': 'application/json',
106
+ 'Authorization': `Bearer ${this.authToken}`
107
+ };
108
+ }
109
+ }
static/js/modules/autocomplete-manager.js ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AutoComplete Manager Module
3
+ * Handles intelligent auto-suggestions and field auto-completion
4
+ */
5
+ export class AutoCompleteManager {
6
+ constructor(apiClient, uiManager) {
7
+ this.apiClient = apiClient;
8
+ this.uiManager = uiManager;
9
+ this.searchTimeouts = {};
10
+ this.activeDropdowns = new Set();
11
+ this.selectedIndex = -1;
12
+ this.availableTreeCodes = [];
13
+ }
14
+
15
+ async initialize() {
16
+ try {
17
+ this.availableTreeCodes = await this.apiClient.loadTreeCodes();
18
+ this.setupAutocomplete('localName', 'tree-suggestions');
19
+ this.setupAutocomplete('scientificName', 'tree-suggestions');
20
+ this.setupAutocomplete('commonName', 'tree-suggestions');
21
+ this.setupAutocomplete('treeCode', 'tree-codes');
22
+ } catch (error) {
23
+ console.error('Error initializing auto-suggestions:', error);
24
+ }
25
+ }
26
+
27
+ setupAutocomplete(fieldId, apiType) {
28
+ const input = document.getElementById(fieldId);
29
+ if (!input) return;
30
+
31
+ this.createAutocompleteContainer(input, fieldId);
32
+ this.attachEventListeners(input, fieldId, apiType);
33
+ }
34
+
35
+ createAutocompleteContainer(input, fieldId) {
36
+ if (input.parentElement.classList.contains('autocomplete-container')) return;
37
+
38
+ const container = document.createElement('div');
39
+ container.className = 'autocomplete-container';
40
+ input.parentNode.insertBefore(container, input);
41
+ container.appendChild(input);
42
+
43
+ const dropdown = document.createElement('div');
44
+ dropdown.className = 'autocomplete-dropdown';
45
+ dropdown.id = `${fieldId}-dropdown`;
46
+ container.appendChild(dropdown);
47
+ }
48
+
49
+ attachEventListeners(input, fieldId, apiType) {
50
+ input.addEventListener('input', (e) => this.handleInputChange(e, apiType));
51
+ input.addEventListener('keydown', (e) => this.handleKeyDown(e, fieldId));
52
+ input.addEventListener('blur', (e) => this.handleInputBlur(e, fieldId));
53
+ input.addEventListener('focus', (e) => this.handleInputFocus(e, fieldId, apiType));
54
+ }
55
+
56
+ async handleInputChange(event, apiType) {
57
+ const input = event.target;
58
+ const query = input.value.trim();
59
+ const fieldId = input.id;
60
+
61
+ this.clearSearchTimeout(fieldId);
62
+
63
+ if (query.length < 2) {
64
+ this.hideDropdown(fieldId);
65
+ return;
66
+ }
67
+
68
+ this.showLoadingState(fieldId);
69
+ this.debounceSearch(fieldId, query, apiType);
70
+ }
71
+
72
+ clearSearchTimeout(fieldId) {
73
+ if (this.searchTimeouts[fieldId]) {
74
+ clearTimeout(this.searchTimeouts[fieldId]);
75
+ }
76
+ }
77
+
78
+ debounceSearch(fieldId, query, apiType) {
79
+ this.searchTimeouts[fieldId] = setTimeout(async () => {
80
+ try {
81
+ const suggestions = await this.fetchSuggestions(query, apiType, fieldId);
82
+ this.showSuggestions(fieldId, suggestions, query);
83
+ } catch (error) {
84
+ console.error('Error fetching suggestions:', error);
85
+ this.hideDropdown(fieldId);
86
+ }
87
+ }, 300);
88
+ }
89
+
90
+ async fetchSuggestions(query, apiType, fieldId) {
91
+ if (apiType === 'tree-codes') {
92
+ return this.filterTreeCodes(query);
93
+ } else {
94
+ return this.searchTreeSuggestions(query, fieldId);
95
+ }
96
+ }
97
+
98
+ filterTreeCodes(query) {
99
+ return this.availableTreeCodes
100
+ .filter(code => code.toLowerCase().includes(query.toLowerCase()))
101
+ .slice(0, 10)
102
+ .map(code => ({
103
+ primary: code,
104
+ secondary: 'Tree Reference Code',
105
+ type: 'code'
106
+ }));
107
+ }
108
+
109
+ async searchTreeSuggestions(query, fieldId) {
110
+ const suggestions = await this.apiClient.searchTreeSuggestions(query, 10);
111
+ return suggestions.map(suggestion => ({
112
+ primary: this.getPrimaryText(suggestion, fieldId),
113
+ secondary: this.getSecondaryText(suggestion, fieldId),
114
+ badges: this.getBadges(suggestion),
115
+ data: suggestion
116
+ }));
117
+ }
118
+
119
+ getPrimaryText(suggestion, fieldId) {
120
+ const fieldMap = {
121
+ 'localName': suggestion.local_name,
122
+ 'scientificName': suggestion.scientific_name,
123
+ 'commonName': suggestion.common_name
124
+ };
125
+
126
+ return fieldMap[fieldId] ||
127
+ suggestion.local_name ||
128
+ suggestion.scientific_name ||
129
+ suggestion.common_name;
130
+ }
131
+
132
+ getSecondaryText(suggestion, fieldId) {
133
+ const parts = [];
134
+
135
+ if (fieldId !== 'localName' && suggestion.local_name) {
136
+ parts.push(`Local: ${suggestion.local_name}`);
137
+ }
138
+ if (fieldId !== 'scientificName' && suggestion.scientific_name) {
139
+ parts.push(`Scientific: ${suggestion.scientific_name}`);
140
+ }
141
+ if (fieldId !== 'commonName' && suggestion.common_name) {
142
+ parts.push(`Common: ${suggestion.common_name}`);
143
+ }
144
+ if (suggestion.tree_code) {
145
+ parts.push(`Code: ${suggestion.tree_code}`);
146
+ }
147
+
148
+ return parts.join(' β€’ ');
149
+ }
150
+
151
+ getBadges(suggestion) {
152
+ const badges = [];
153
+ if (suggestion.tree_code) {
154
+ badges.push(suggestion.tree_code);
155
+ }
156
+ if (suggestion.fruiting_season) {
157
+ badges.push(`Season: ${suggestion.fruiting_season}`);
158
+ }
159
+ return badges;
160
+ }
161
+
162
+ showLoadingState(fieldId) {
163
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
164
+ if (!dropdown) return;
165
+
166
+ dropdown.innerHTML = '<div class="autocomplete-loading">Searching...</div>';
167
+ dropdown.style.display = 'block';
168
+ this.activeDropdowns.add(fieldId);
169
+ }
170
+
171
+ showSuggestions(fieldId, suggestions, query) {
172
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
173
+ if (!dropdown) return;
174
+
175
+ if (suggestions.length === 0) {
176
+ dropdown.innerHTML = '<div class="autocomplete-no-results">No matching suggestions found</div>';
177
+ dropdown.style.display = 'block';
178
+ this.activeDropdowns.add(fieldId);
179
+ return;
180
+ }
181
+
182
+ const html = this.buildSuggestionsHTML(suggestions, query, fieldId);
183
+ dropdown.innerHTML = html;
184
+ dropdown.style.display = 'block';
185
+ this.activeDropdowns.add(fieldId);
186
+ this.selectedIndex = -1;
187
+
188
+ this.attachSuggestionClickHandlers(dropdown, suggestions);
189
+ }
190
+
191
+ buildSuggestionsHTML(suggestions, query, fieldId) {
192
+ return suggestions.map((suggestion, index) => `
193
+ <div class="autocomplete-item" data-index="${index}" data-field="${fieldId}">
194
+ <div class="autocomplete-primary">${this.highlightMatch(suggestion.primary, query)}</div>
195
+ ${suggestion.secondary ? `<div class="autocomplete-secondary">${suggestion.secondary}</div>` : ''}
196
+ ${this.buildBadgesHTML(suggestion.badges)}
197
+ </div>
198
+ `).join('');
199
+ }
200
+
201
+ buildBadgesHTML(badges) {
202
+ if (!badges || badges.length === 0) return '';
203
+
204
+ const badgeElements = badges.map(badge => `<span class="autocomplete-badge">${badge}</span>`).join('');
205
+ return `<div>${badgeElements}</div>`;
206
+ }
207
+
208
+ highlightMatch(text, query) {
209
+ if (!query || !text) return text;
210
+
211
+ const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
212
+ return text.replace(regex, '<strong>$1</strong>');
213
+ }
214
+
215
+ attachSuggestionClickHandlers(dropdown, suggestions) {
216
+ dropdown.querySelectorAll('.autocomplete-item').forEach(item => {
217
+ item.addEventListener('mousedown', (e) => this.handleSuggestionClick(e, suggestions));
218
+ });
219
+ }
220
+
221
+ handleSuggestionClick(event, suggestions) {
222
+ event.preventDefault();
223
+ const item = event.target.closest('.autocomplete-item');
224
+ const index = parseInt(item.dataset.index);
225
+ const fieldId = item.dataset.field;
226
+ const suggestion = suggestions[index];
227
+
228
+ this.applySuggestion(fieldId, suggestion);
229
+ this.hideDropdown(fieldId);
230
+ }
231
+
232
+ applySuggestion(fieldId, suggestion) {
233
+ const input = document.getElementById(fieldId);
234
+
235
+ if (suggestion.type === 'code') {
236
+ input.value = suggestion.primary;
237
+ } else {
238
+ this.applySuggestionData(input, fieldId, suggestion);
239
+ this.autoFillRelatedFields(suggestion.data, fieldId);
240
+ }
241
+
242
+ input.dispatchEvent(new Event('input', { bubbles: true }));
243
+ }
244
+
245
+ applySuggestionData(input, fieldId, suggestion) {
246
+ const data = suggestion.data;
247
+ const fieldValueMap = {
248
+ 'localName': data.local_name,
249
+ 'scientificName': data.scientific_name,
250
+ 'commonName': data.common_name
251
+ };
252
+
253
+ input.value = fieldValueMap[fieldId] || suggestion.primary;
254
+ }
255
+
256
+ autoFillRelatedFields(data, excludeFieldId) {
257
+ const fields = {
258
+ 'localName': data.local_name,
259
+ 'scientificName': data.scientific_name,
260
+ 'commonName': data.common_name,
261
+ 'treeCode': data.tree_code
262
+ };
263
+
264
+ Object.entries(fields).forEach(([fieldId, value]) => {
265
+ if (fieldId !== excludeFieldId && value) {
266
+ this.fillEmptyField(fieldId, value);
267
+ }
268
+ });
269
+ }
270
+
271
+ fillEmptyField(fieldId, value) {
272
+ const input = document.getElementById(fieldId);
273
+ if (input && !input.value.trim()) {
274
+ input.value = value;
275
+ this.uiManager.highlightAutoFilledField(fieldId);
276
+ }
277
+ }
278
+
279
+ handleKeyDown(event, fieldId) {
280
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
281
+ if (!dropdown || dropdown.style.display === 'none') return;
282
+
283
+ const items = dropdown.querySelectorAll('.autocomplete-item');
284
+ if (items.length === 0) return;
285
+
286
+ switch (event.key) {
287
+ case 'ArrowDown':
288
+ event.preventDefault();
289
+ this.selectedIndex = Math.min(this.selectedIndex + 1, items.length - 1);
290
+ this.updateHighlight(items);
291
+ break;
292
+
293
+ case 'ArrowUp':
294
+ event.preventDefault();
295
+ this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
296
+ this.updateHighlight(items);
297
+ break;
298
+
299
+ case 'Enter':
300
+ event.preventDefault();
301
+ if (this.selectedIndex >= 0 && items[this.selectedIndex]) {
302
+ items[this.selectedIndex].click();
303
+ }
304
+ break;
305
+
306
+ case 'Escape':
307
+ event.preventDefault();
308
+ this.hideDropdown(fieldId);
309
+ break;
310
+ }
311
+ }
312
+
313
+ updateHighlight(items) {
314
+ items.forEach((item, index) => {
315
+ item.classList.toggle('highlighted', index === this.selectedIndex);
316
+ });
317
+ }
318
+
319
+ handleInputBlur(event, fieldId) {
320
+ setTimeout(() => this.hideDropdown(fieldId), 150);
321
+ }
322
+
323
+ handleInputFocus(event, fieldId, apiType) {
324
+ const input = event.target;
325
+ if (input.value.length >= 2) {
326
+ this.handleInputChange(event, apiType);
327
+ }
328
+ }
329
+
330
+ hideDropdown(fieldId) {
331
+ const dropdown = document.getElementById(`${fieldId}-dropdown`);
332
+ if (dropdown) {
333
+ dropdown.style.display = 'none';
334
+ dropdown.innerHTML = '';
335
+ this.activeDropdowns.delete(fieldId);
336
+ this.selectedIndex = -1;
337
+ }
338
+ }
339
+
340
+ hideAllDropdowns() {
341
+ this.activeDropdowns.forEach(fieldId => {
342
+ this.hideDropdown(fieldId);
343
+ });
344
+ }
345
+ }
static/js/modules/form-manager.js ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Form Manager Module
3
+ * Handles form operations, validation, and data management
4
+ */
5
+ export class FormManager {
6
+ constructor(apiClient, uiManager) {
7
+ this.apiClient = apiClient;
8
+ this.uiManager = uiManager;
9
+ this.uploadedPhotos = {};
10
+ this.audioFile = null;
11
+ this.currentEditId = null;
12
+ }
13
+
14
+ async initialize() {
15
+ try {
16
+ const formOptions = await this.apiClient.loadFormOptions();
17
+ this.renderMultiSelect('utilityOptions', formOptions.utilities);
18
+ this.renderMultiSelect('phenologyOptions', formOptions.phenologyStages);
19
+ this.renderPhotoCategories(formOptions.photoCategories);
20
+ } catch (error) {
21
+ console.error('Error initializing form:', error);
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ renderMultiSelect(containerId, options) {
27
+ const container = document.getElementById(containerId);
28
+ if (!container) return;
29
+
30
+ container.innerHTML = '';
31
+
32
+ options.forEach(option => {
33
+ const label = document.createElement('label');
34
+ label.innerHTML = `
35
+ <input type="checkbox" value="${option}"> ${option}
36
+ `;
37
+ container.appendChild(label);
38
+ });
39
+ }
40
+
41
+ renderPhotoCategories(categories) {
42
+ const container = document.getElementById('photoCategories');
43
+ if (!container) return;
44
+
45
+ container.innerHTML = '';
46
+
47
+ categories.forEach(category => {
48
+ const categoryDiv = document.createElement('div');
49
+ categoryDiv.className = 'photo-category';
50
+ categoryDiv.innerHTML = `
51
+ <div class="photo-category-header">
52
+ <div class="photo-category-title">
53
+ <div class="photo-category-icon">IMG</div>
54
+ ${category}
55
+ </div>
56
+ </div>
57
+ <div class="photo-upload-area">
58
+ <div class="photo-upload" data-category="${category}">
59
+ <div class="photo-upload-icon">+</div>
60
+ <div>Click to select ${category} photo</div>
61
+ </div>
62
+ <button type="button" class="camera-btn" data-category="${category}">
63
+ Camera
64
+ </button>
65
+ </div>
66
+ <div class="uploaded-file" id="photo-${category}" style="display: none;"></div>
67
+ `;
68
+ container.appendChild(categoryDiv);
69
+ });
70
+ }
71
+
72
+ getSelectedValues(containerId) {
73
+ const container = document.getElementById(containerId);
74
+ if (!container) return [];
75
+
76
+ const checkboxes = container.querySelectorAll('input[type="checkbox"]:checked');
77
+ return Array.from(checkboxes).map(cb => cb.value);
78
+ }
79
+
80
+ getFormData() {
81
+ const utilityValues = this.getSelectedValues('utilityOptions');
82
+ const phenologyValues = this.getSelectedValues('phenologyOptions');
83
+
84
+ return {
85
+ latitude: this.getNumericValue('latitude'),
86
+ longitude: this.getNumericValue('longitude'),
87
+ local_name: this.getStringValue('localName'),
88
+ scientific_name: this.getStringValue('scientificName'),
89
+ common_name: this.getStringValue('commonName'),
90
+ tree_code: this.getStringValue('treeCode'),
91
+ height: this.getNumericValue('height'),
92
+ width: this.getNumericValue('width'),
93
+ utility: utilityValues.length > 0 ? utilityValues : [],
94
+ phenology_stages: phenologyValues.length > 0 ? phenologyValues : [],
95
+ storytelling_text: this.getStringValue('storytellingText'),
96
+ storytelling_audio: this.audioFile,
97
+ photographs: Object.keys(this.uploadedPhotos).length > 0 ? this.uploadedPhotos : null,
98
+ notes: this.getStringValue('notes')
99
+ };
100
+ }
101
+
102
+ getNumericValue(fieldId) {
103
+ const element = document.getElementById(fieldId);
104
+ if (!element || !element.value) return null;
105
+ const value = parseFloat(element.value);
106
+ return isNaN(value) ? null : value;
107
+ }
108
+
109
+ getStringValue(fieldId) {
110
+ const element = document.getElementById(fieldId);
111
+ return element && element.value ? element.value : null;
112
+ }
113
+
114
+ populateForm(treeData) {
115
+ this.setFieldValue('latitude', treeData.latitude);
116
+ this.setFieldValue('longitude', treeData.longitude);
117
+ this.setFieldValue('localName', treeData.local_name || '');
118
+ this.setFieldValue('scientificName', treeData.scientific_name || '');
119
+ this.setFieldValue('commonName', treeData.common_name || '');
120
+ this.setFieldValue('treeCode', treeData.tree_code || '');
121
+ this.setFieldValue('height', treeData.height || '');
122
+ this.setFieldValue('width', treeData.width || '');
123
+ this.setFieldValue('storytellingText', treeData.storytelling_text || '');
124
+ this.setFieldValue('notes', treeData.notes || '');
125
+
126
+ // Handle utility checkboxes
127
+ if (treeData.utility && Array.isArray(treeData.utility)) {
128
+ this.setCheckboxValues('utilityOptions', treeData.utility);
129
+ }
130
+
131
+ // Handle phenology checkboxes
132
+ if (treeData.phenology_stages && Array.isArray(treeData.phenology_stages)) {
133
+ this.setCheckboxValues('phenologyOptions', treeData.phenology_stages);
134
+ }
135
+ }
136
+
137
+ setFieldValue(fieldId, value) {
138
+ const element = document.getElementById(fieldId);
139
+ if (element) {
140
+ element.value = value;
141
+ }
142
+ }
143
+
144
+ setCheckboxValues(containerId, values) {
145
+ const container = document.getElementById(containerId);
146
+ if (!container) return;
147
+
148
+ container.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
149
+ checkbox.checked = values.includes(checkbox.value);
150
+ });
151
+ }
152
+
153
+ resetForm(silent = false) {
154
+ const form = document.getElementById('treeForm');
155
+ if (form) {
156
+ form.reset();
157
+ }
158
+
159
+ this.uploadedPhotos = {};
160
+ this.audioFile = null;
161
+ this.currentEditId = null;
162
+
163
+ // Clear uploaded file indicators
164
+ document.querySelectorAll('.uploaded-file').forEach(el => {
165
+ el.style.display = 'none';
166
+ el.innerHTML = '';
167
+ });
168
+
169
+ // Reset audio controls
170
+ this.resetAudioControls();
171
+
172
+ if (!silent) {
173
+ this.uiManager.showMessage('Form has been reset.', 'success');
174
+ }
175
+ }
176
+
177
+ resetAudioControls() {
178
+ const audioElement = document.getElementById('audioPlayback');
179
+ if (audioElement) {
180
+ audioElement.classList.add('hidden');
181
+ audioElement.src = '';
182
+ }
183
+
184
+ const recordingStatus = document.getElementById('recordingStatus');
185
+ if (recordingStatus) {
186
+ recordingStatus.textContent = 'Click to start recording';
187
+ }
188
+
189
+ const audioUploadResult = document.getElementById('audioUploadResult');
190
+ if (audioUploadResult) {
191
+ audioUploadResult.innerHTML = '';
192
+ }
193
+ }
194
+
195
+ async handleFileUpload(file, type, category = null) {
196
+ try {
197
+ const result = await this.apiClient.uploadFile(file, type, category);
198
+
199
+ if (type === 'image' && category) {
200
+ this.uploadedPhotos[category] = result.filename;
201
+ this.showUploadSuccess(category, file.name, 'photo');
202
+ } else if (type === 'audio') {
203
+ this.audioFile = result.filename;
204
+ this.showUploadSuccess(null, file.name, 'audio');
205
+ }
206
+
207
+ return result;
208
+ } catch (error) {
209
+ console.error(`Error uploading ${type}:`, error);
210
+ this.uiManager.showMessage(`Error uploading ${type}: ${error.message}`, 'error');
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ showUploadSuccess(category, filename, type) {
216
+ if (type === 'photo' && category) {
217
+ const resultDiv = document.getElementById(`photo-${category}`);
218
+ if (resultDiv) {
219
+ resultDiv.style.display = 'block';
220
+ resultDiv.innerHTML = `${filename} uploaded successfully`;
221
+ }
222
+ } else if (type === 'audio') {
223
+ const resultDiv = document.getElementById('audioUploadResult');
224
+ if (resultDiv) {
225
+ resultDiv.innerHTML = `<div class="uploaded-file">${filename} uploaded successfully</div>`;
226
+ }
227
+ }
228
+ }
229
+
230
+ setEditMode(treeId) {
231
+ this.currentEditId = treeId;
232
+
233
+ // Update submit button
234
+ const submitBtn = document.querySelector('button[type="submit"]');
235
+ if (submitBtn) {
236
+ submitBtn.textContent = 'Update Tree Record';
237
+ }
238
+
239
+ // Add cancel edit button if it doesn't exist
240
+ this.addCancelButton();
241
+ }
242
+
243
+ addCancelButton() {
244
+ if (document.getElementById('cancelEdit')) return;
245
+
246
+ const cancelBtn = document.createElement('button');
247
+ cancelBtn.type = 'button';
248
+ cancelBtn.id = 'cancelEdit';
249
+ cancelBtn.className = 'btn btn-outline';
250
+ cancelBtn.textContent = 'Cancel Edit';
251
+
252
+ const formActions = document.querySelector('.form-actions');
253
+ const submitBtn = document.querySelector('button[type="submit"]');
254
+ if (formActions && submitBtn) {
255
+ formActions.insertBefore(cancelBtn, submitBtn);
256
+ }
257
+ }
258
+
259
+ exitEditMode() {
260
+ this.currentEditId = null;
261
+
262
+ // Restore original submit button text
263
+ const submitBtn = document.querySelector('button[type="submit"]');
264
+ if (submitBtn) {
265
+ submitBtn.textContent = 'Save Tree Record';
266
+ }
267
+
268
+ // Remove cancel button
269
+ const cancelBtn = document.getElementById('cancelEdit');
270
+ if (cancelBtn) {
271
+ cancelBtn.remove();
272
+ }
273
+ }
274
+
275
+ isInEditMode() {
276
+ return this.currentEditId !== null;
277
+ }
278
+
279
+ getCurrentEditId() {
280
+ return this.currentEditId;
281
+ }
282
+
283
+ validateForm() {
284
+ const latitude = this.getNumericValue('latitude');
285
+ const longitude = this.getNumericValue('longitude');
286
+
287
+ if (latitude === null || longitude === null) {
288
+ throw new Error('Latitude and longitude are required');
289
+ }
290
+
291
+ if (latitude < -90 || latitude > 90) {
292
+ throw new Error('Latitude must be between -90 and 90 degrees');
293
+ }
294
+
295
+ if (longitude < -180 || longitude > 180) {
296
+ throw new Error('Longitude must be between -180 and 180 degrees');
297
+ }
298
+
299
+ return true;
300
+ }
301
+ }
static/js/modules/media-manager.js ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Media Manager Module
3
+ * Handles audio recording, photo capture, and file uploads
4
+ */
5
+ export class MediaManager {
6
+ constructor(formManager, uiManager) {
7
+ this.formManager = formManager;
8
+ this.uiManager = uiManager;
9
+ this.mediaRecorder = null;
10
+ this.audioChunks = [];
11
+ this.isRecording = false;
12
+ }
13
+
14
+ initialize() {
15
+ this.setupDragAndDrop();
16
+ this.attachEventListeners();
17
+ }
18
+
19
+ attachEventListeners() {
20
+ // Photo upload handlers
21
+ this.setupPhotoUploadHandlers();
22
+
23
+ // Audio upload handler
24
+ const audioUpload = document.getElementById('audioUpload');
25
+ if (audioUpload) {
26
+ audioUpload.addEventListener('click', () => this.selectAudioFile());
27
+ }
28
+
29
+ // Recording button (if exists)
30
+ const recordBtn = document.getElementById('recordBtn');
31
+ if (recordBtn) {
32
+ recordBtn.addEventListener('click', () => this.toggleRecording());
33
+ }
34
+ }
35
+
36
+ setupPhotoUploadHandlers() {
37
+ document.addEventListener('click', (e) => {
38
+ if (e.target.matches('.photo-upload, .photo-upload *')) {
39
+ const uploadArea = e.target.closest('.photo-upload');
40
+ const category = uploadArea?.dataset.category;
41
+ if (category) {
42
+ this.selectPhotoFile(category);
43
+ }
44
+ }
45
+
46
+ if (e.target.matches('.camera-btn')) {
47
+ const category = e.target.dataset.category;
48
+ if (category) {
49
+ this.capturePhoto(category);
50
+ }
51
+ }
52
+ });
53
+ }
54
+
55
+ setupDragAndDrop() {
56
+ const audioUpload = document.getElementById('audioUpload');
57
+ if (!audioUpload) return;
58
+
59
+ audioUpload.addEventListener('dragover', (e) => {
60
+ e.preventDefault();
61
+ audioUpload.classList.add('dragover');
62
+ });
63
+
64
+ audioUpload.addEventListener('dragleave', () => {
65
+ audioUpload.classList.remove('dragover');
66
+ });
67
+
68
+ audioUpload.addEventListener('drop', (e) => {
69
+ e.preventDefault();
70
+ audioUpload.classList.remove('dragover');
71
+
72
+ const files = e.dataTransfer.files;
73
+ if (files.length > 0 && files[0].type.startsWith('audio/')) {
74
+ this.handleAudioFile(files[0]);
75
+ }
76
+ });
77
+ }
78
+
79
+ async selectPhotoFile(category) {
80
+ const input = this.uiManager.createFileInput('image/*', true);
81
+
82
+ input.onchange = (e) => {
83
+ const file = e.target.files[0];
84
+ if (file) {
85
+ this.handlePhotoFile(file, category);
86
+ }
87
+ };
88
+
89
+ input.click();
90
+ }
91
+
92
+ async capturePhoto(category) {
93
+ const input = this.uiManager.createFileInput('image/*', true);
94
+
95
+ input.onchange = (e) => {
96
+ const file = e.target.files[0];
97
+ if (file) {
98
+ this.handlePhotoFile(file, category);
99
+ }
100
+ };
101
+
102
+ input.click();
103
+ }
104
+
105
+ async selectAudioFile() {
106
+ const input = this.uiManager.createFileInput('audio/*');
107
+
108
+ input.onchange = (e) => {
109
+ const file = e.target.files[0];
110
+ if (file) {
111
+ this.handleAudioFile(file);
112
+ }
113
+ };
114
+
115
+ input.click();
116
+ }
117
+
118
+ async handlePhotoFile(file, category) {
119
+ try {
120
+ this.validateImageFile(file);
121
+ await this.formManager.handleFileUpload(file, 'image', category);
122
+ } catch (error) {
123
+ this.uiManager.showMessage(`Error with photo file: ${error.message}`, 'error');
124
+ }
125
+ }
126
+
127
+ async handleAudioFile(file) {
128
+ try {
129
+ this.validateAudioFile(file);
130
+ await this.formManager.handleFileUpload(file, 'audio');
131
+ } catch (error) {
132
+ this.uiManager.showMessage(`Error with audio file: ${error.message}`, 'error');
133
+ }
134
+ }
135
+
136
+ validateImageFile(file) {
137
+ const maxSize = 10 * 1024 * 1024; // 10MB
138
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
139
+
140
+ if (file.size > maxSize) {
141
+ throw new Error('Image file size must be less than 10MB');
142
+ }
143
+
144
+ if (!allowedTypes.includes(file.type)) {
145
+ throw new Error('Only JPEG, PNG, GIF, and WebP images are allowed');
146
+ }
147
+ }
148
+
149
+ validateAudioFile(file) {
150
+ const maxSize = 50 * 1024 * 1024; // 50MB
151
+ const allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/mp4', 'audio/aac', 'audio/ogg'];
152
+
153
+ if (file.size > maxSize) {
154
+ throw new Error('Audio file size must be less than 50MB');
155
+ }
156
+
157
+ if (!allowedTypes.includes(file.type)) {
158
+ throw new Error('Only MP3, WAV, M4A, AAC, and OGG audio files are allowed');
159
+ }
160
+ }
161
+
162
+ async toggleRecording() {
163
+ if (!this.isRecording) {
164
+ await this.startRecording();
165
+ } else {
166
+ this.stopRecording();
167
+ }
168
+ }
169
+
170
+ async startRecording() {
171
+ try {
172
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
173
+ this.mediaRecorder = new MediaRecorder(stream);
174
+ this.audioChunks = [];
175
+
176
+ this.mediaRecorder.ondataavailable = (event) => {
177
+ this.audioChunks.push(event.data);
178
+ };
179
+
180
+ this.mediaRecorder.onstop = async () => {
181
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
182
+ const audioFile = new File([audioBlob], 'recording.wav', { type: 'audio/wav' });
183
+ await this.handleAudioFile(audioFile);
184
+
185
+ this.showAudioPlayback(audioBlob);
186
+ };
187
+
188
+ this.mediaRecorder.start();
189
+ this.isRecording = true;
190
+ this.updateRecordingUI(true);
191
+
192
+ } catch (error) {
193
+ console.error('Error starting recording:', error);
194
+ this.uiManager.showMessage('Error accessing microphone: ' + error.message, 'error');
195
+ }
196
+ }
197
+
198
+ stopRecording() {
199
+ if (this.mediaRecorder && this.isRecording) {
200
+ this.mediaRecorder.stop();
201
+ this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
202
+ this.isRecording = false;
203
+ this.updateRecordingUI(false);
204
+ }
205
+ }
206
+
207
+ updateRecordingUI(isRecording) {
208
+ const recordBtn = document.getElementById('recordBtn');
209
+ const status = document.getElementById('recordingStatus');
210
+
211
+ if (recordBtn) {
212
+ recordBtn.classList.toggle('recording', isRecording);
213
+ recordBtn.innerHTML = isRecording ? 'Stop' : 'Record';
214
+ }
215
+
216
+ if (status) {
217
+ status.textContent = isRecording
218
+ ? 'Recording... Click to stop'
219
+ : 'Recording saved!';
220
+ }
221
+ }
222
+
223
+ showAudioPlayback(audioBlob) {
224
+ const audioElement = document.getElementById('audioPlayback');
225
+ if (audioElement) {
226
+ audioElement.src = URL.createObjectURL(audioBlob);
227
+ audioElement.classList.remove('hidden');
228
+ }
229
+ }
230
+
231
+ getCurrentLocation() {
232
+ if (!navigator.geolocation) {
233
+ this.uiManager.showMessage('Geolocation is not supported by this browser.', 'error');
234
+ return;
235
+ }
236
+
237
+ this.uiManager.updateLocationButtonState(true);
238
+
239
+ navigator.geolocation.getCurrentPosition(
240
+ (position) => {
241
+ const latitude = position.coords.latitude.toFixed(7);
242
+ const longitude = position.coords.longitude.toFixed(7);
243
+
244
+ this.setLocationFields(latitude, longitude);
245
+ this.uiManager.updateLocationButtonState(false);
246
+ this.uiManager.showMessage('Location retrieved successfully!', 'success');
247
+ },
248
+ (error) => {
249
+ this.uiManager.updateLocationButtonState(false);
250
+ this.uiManager.showMessage('Error getting location: ' + error.message, 'error');
251
+ },
252
+ {
253
+ enableHighAccuracy: true,
254
+ timeout: 10000,
255
+ maximumAge: 300000 // 5 minutes
256
+ }
257
+ );
258
+ }
259
+
260
+ setLocationFields(latitude, longitude) {
261
+ const latElement = document.getElementById('latitude');
262
+ const lngElement = document.getElementById('longitude');
263
+
264
+ if (latElement && lngElement) {
265
+ latElement.value = latitude;
266
+ lngElement.value = longitude;
267
+ }
268
+ }
269
+
270
+ // Helper methods for file handling
271
+ getFileExtension(filename) {
272
+ return filename.split('.').pop().toLowerCase();
273
+ }
274
+
275
+ formatFileSize(bytes) {
276
+ if (bytes === 0) return '0 Bytes';
277
+
278
+ const k = 1024;
279
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
280
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
281
+
282
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
283
+ }
284
+
285
+ isImageFile(file) {
286
+ return file.type.startsWith('image/');
287
+ }
288
+
289
+ isAudioFile(file) {
290
+ return file.type.startsWith('audio/');
291
+ }
292
+
293
+ createImagePreview(file, callback) {
294
+ const reader = new FileReader();
295
+ reader.onload = (e) => callback(e.target.result);
296
+ reader.readAsDataURL(file);
297
+ }
298
+ }
static/js/modules/ui-manager.js ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * UI Manager Module
3
+ * Handles user interface updates, messages, and tree list display
4
+ */
5
+ export class UIManager {
6
+ constructor(authManager) {
7
+ this.authManager = authManager;
8
+ }
9
+
10
+ initialize() {
11
+ this.displayUserInfo();
12
+ this.loadSelectedLocation();
13
+ }
14
+
15
+ displayUserInfo() {
16
+ if (!this.authManager.currentUser) return;
17
+
18
+ const userNameEl = document.getElementById('userName');
19
+ const userRoleEl = document.getElementById('userRole');
20
+ const userAvatarEl = document.getElementById('userAvatar');
21
+
22
+ if (userNameEl) {
23
+ userNameEl.textContent = this.authManager.currentUser.full_name;
24
+ }
25
+
26
+ if (userRoleEl) {
27
+ userRoleEl.textContent = this.authManager.currentUser.role;
28
+ }
29
+
30
+ if (userAvatarEl) {
31
+ userAvatarEl.textContent = this.authManager.currentUser.full_name.charAt(0).toUpperCase();
32
+ }
33
+ }
34
+
35
+ loadSelectedLocation() {
36
+ const selectedLocation = localStorage.getItem('selectedLocation');
37
+ if (selectedLocation) {
38
+ try {
39
+ const location = JSON.parse(selectedLocation);
40
+ const latElement = document.getElementById('latitude');
41
+ const lngElement = document.getElementById('longitude');
42
+
43
+ if (latElement && lngElement) {
44
+ latElement.value = location.lat.toFixed(6);
45
+ lngElement.value = location.lng.toFixed(6);
46
+
47
+ // Clear the stored location
48
+ localStorage.removeItem('selectedLocation');
49
+
50
+ this.showMessage('Location loaded from map!', 'success');
51
+ }
52
+ } catch (error) {
53
+ console.error('Error loading selected location:', error);
54
+ }
55
+ }
56
+ }
57
+
58
+ showMessage(message, type) {
59
+ const messageDiv = document.getElementById('message');
60
+ if (!messageDiv) return;
61
+
62
+ messageDiv.className = `message ${type === 'error' ? 'error' : 'success'}`;
63
+ messageDiv.textContent = message;
64
+
65
+ // Auto-hide after 5 seconds
66
+ setTimeout(() => {
67
+ messageDiv.textContent = '';
68
+ messageDiv.className = '';
69
+ }, 5000);
70
+ }
71
+
72
+ renderTreeList(trees) {
73
+ const treeList = document.getElementById('treeList');
74
+ if (!treeList) return;
75
+
76
+ if (trees.length === 0) {
77
+ treeList.innerHTML = '<div class="loading">No trees recorded yet</div>';
78
+ return;
79
+ }
80
+
81
+ treeList.innerHTML = trees.map(tree => {
82
+ const canEdit = this.authManager.canEditTree(tree.created_by);
83
+ const canDelete = this.authManager.canDeleteTree(tree.created_by);
84
+
85
+ return `
86
+ <div class="tree-item" data-tree-id="${tree.id}">
87
+ <div class="tree-header">
88
+ <div class="tree-id">Tree #${tree.id}</div>
89
+ <div class="tree-actions">
90
+ ${canEdit ? `<button class="btn-icon edit-tree" data-tree-id="${tree.id}" title="Edit Tree">Edit</button>` : ''}
91
+ ${canDelete ? `<button class="btn-icon delete-tree" data-tree-id="${tree.id}" title="Delete Tree">Delete</button>` : ''}
92
+ </div>
93
+ </div>
94
+ <div class="tree-info">
95
+ ${tree.scientific_name || tree.common_name || tree.local_name || 'Unnamed'}
96
+ <br>${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)}
97
+ ${tree.tree_code ? `<br>Code: ${tree.tree_code}` : ''}
98
+ <br>${new Date(tree.created_at).toLocaleDateString()}
99
+ <br>By: ${tree.created_by || 'Unknown'}
100
+ </div>
101
+ </div>
102
+ `;
103
+ }).join('');
104
+ }
105
+
106
+ showLoadingState(containerId, message = 'Loading...') {
107
+ const container = document.getElementById(containerId);
108
+ if (!container) return;
109
+
110
+ container.innerHTML = `
111
+ <div class="loading">
112
+ <div class="spinner"></div>
113
+ ${message}
114
+ </div>
115
+ `;
116
+ }
117
+
118
+ showErrorState(containerId, message = 'Error loading content') {
119
+ const container = document.getElementById(containerId);
120
+ if (!container) return;
121
+
122
+ container.innerHTML = `<div class="loading">${message}</div>`;
123
+ }
124
+
125
+ updateLocationButtonState(isGetting) {
126
+ const locationBtn = document.getElementById('getLocation');
127
+ if (!locationBtn) return;
128
+
129
+ locationBtn.textContent = isGetting ? 'Getting...' : 'Get GPS Location';
130
+ locationBtn.disabled = isGetting;
131
+ }
132
+
133
+ highlightAutoFilledField(fieldId) {
134
+ const input = document.getElementById(fieldId);
135
+ if (input && !input.value.trim()) {
136
+ input.style.backgroundColor = '#f0f9ff';
137
+ setTimeout(() => {
138
+ input.style.backgroundColor = '';
139
+ }, 2000);
140
+ }
141
+ }
142
+
143
+ createFileInput(accept, capture = false) {
144
+ const input = document.createElement('input');
145
+ input.type = 'file';
146
+ input.accept = accept;
147
+ if (capture) {
148
+ input.capture = 'environment';
149
+ }
150
+ return input;
151
+ }
152
+
153
+ confirmDeletion(treeId) {
154
+ return confirm(`Are you sure you want to delete Tree #${treeId}? This action cannot be undone.`);
155
+ }
156
+
157
+ scrollToTop() {
158
+ window.scrollTo({ top: 0, behavior: 'smooth' });
159
+ }
160
+
161
+ focusFirstError() {
162
+ const firstError = document.querySelector('.form-input.error, .message.error');
163
+ if (firstError) {
164
+ firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
165
+ if (firstError.focus) {
166
+ firstError.focus();
167
+ }
168
+ }
169
+ }
170
+
171
+ addFieldError(fieldId, message) {
172
+ const field = document.getElementById(fieldId);
173
+ if (!field) return;
174
+
175
+ field.classList.add('error');
176
+
177
+ // Remove existing error message
178
+ const existingError = field.parentNode.querySelector('.field-error');
179
+ if (existingError) {
180
+ existingError.remove();
181
+ }
182
+
183
+ // Add error message
184
+ const errorEl = document.createElement('div');
185
+ errorEl.className = 'field-error';
186
+ errorEl.textContent = message;
187
+ field.parentNode.appendChild(errorEl);
188
+ }
189
+
190
+ clearFieldErrors() {
191
+ document.querySelectorAll('.form-input.error').forEach(field => {
192
+ field.classList.remove('error');
193
+ });
194
+
195
+ document.querySelectorAll('.field-error').forEach(error => {
196
+ error.remove();
197
+ });
198
+ }
199
+
200
+ showUploadProgress(filename, progress) {
201
+ // This could be expanded for actual progress tracking
202
+ console.log(`Uploading ${filename}: ${progress}%`);
203
+ }
204
+ }
static/js/tree-track-app.js ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * TreeTrack Enhanced Application - Modular Architecture
3
+ * Professional Field Research Tool with Authentication
4
+ */
5
+
6
+ // Import all modules
7
+ import { AuthManager } from './modules/auth-manager.js';
8
+ import { ApiClient } from './modules/api-client.js';
9
+ import { UIManager } from './modules/ui-manager.js';
10
+ import { FormManager } from './modules/form-manager.js';
11
+ import { AutoCompleteManager } from './modules/autocomplete-manager.js';
12
+ import { MediaManager } from './modules/media-manager.js';
13
+
14
+ export class TreeTrackApp {
15
+ constructor() {
16
+ // Initialize core managers
17
+ this.authManager = new AuthManager();
18
+ this.apiClient = new ApiClient(this.authManager);
19
+ this.uiManager = new UIManager(this.authManager);
20
+ this.formManager = new FormManager(this.apiClient, this.uiManager);
21
+ this.autoCompleteManager = new AutoCompleteManager(this.apiClient, this.uiManager);
22
+ this.mediaManager = new MediaManager(this.formManager, this.uiManager);
23
+
24
+ this.init();
25
+ }
26
+
27
+ async init() {
28
+ try {
29
+ // Check authentication first
30
+ if (!await this.authManager.checkAuthentication()) {
31
+ window.location.href = '/login';
32
+ return;
33
+ }
34
+
35
+ // Initialize all managers
36
+ await this.initializeManagers();
37
+
38
+ // Setup event listeners
39
+ this.setupEventListeners();
40
+
41
+ // Load initial data
42
+ await this.loadInitialData();
43
+
44
+ } catch (error) {
45
+ console.error('Error initializing TreeTrack app:', error);
46
+ this.uiManager.showMessage('Error initializing application: ' + error.message, 'error');
47
+ }
48
+ }
49
+
50
+ async initializeManagers() {
51
+ // Initialize UI first
52
+ this.uiManager.initialize();
53
+
54
+ // Initialize form components
55
+ await this.formManager.initialize();
56
+
57
+ // Initialize media handling
58
+ this.mediaManager.initialize();
59
+
60
+ // Initialize autocomplete (with delay to ensure DOM is ready)
61
+ setTimeout(async () => {
62
+ await this.autoCompleteManager.initialize();
63
+ }, 100);
64
+ }
65
+
66
+ setupEventListeners() {
67
+ // Form submission
68
+ const form = document.getElementById('treeForm');
69
+ if (form) {
70
+ form.addEventListener('submit', (e) => this.handleFormSubmit(e));
71
+ }
72
+
73
+ // Reset form
74
+ const resetBtn = document.getElementById('resetForm');
75
+ if (resetBtn) {
76
+ resetBtn.addEventListener('click', () => this.handleFormReset());
77
+ }
78
+
79
+ // GPS location
80
+ const locationBtn = document.getElementById('getLocation');
81
+ if (locationBtn) {
82
+ locationBtn.addEventListener('click', () => this.mediaManager.getCurrentLocation());
83
+ }
84
+
85
+ // Logout functionality
86
+ const logoutBtn = document.getElementById('logoutBtn');
87
+ if (logoutBtn) {
88
+ logoutBtn.addEventListener('click', () => this.authManager.logout());
89
+ }
90
+
91
+ // Tree actions (edit/delete)
92
+ document.addEventListener('click', (e) => {
93
+ if (e.target.matches('.edit-tree')) {
94
+ const treeId = e.target.dataset.treeId;
95
+ if (treeId) {
96
+ this.handleEditTree(parseInt(treeId));
97
+ }
98
+ }
99
+
100
+ if (e.target.matches('.delete-tree')) {
101
+ const treeId = e.target.dataset.treeId;
102
+ if (treeId) {
103
+ this.handleDeleteTree(parseInt(treeId));
104
+ }
105
+ }
106
+
107
+ if (e.target.matches('#cancelEdit')) {
108
+ this.handleCancelEdit();
109
+ }
110
+ });
111
+ }
112
+
113
+ async loadInitialData() {
114
+ try {
115
+ await this.loadTrees();
116
+ } catch (error) {
117
+ console.error('Error loading initial data:', error);
118
+ }
119
+ }
120
+
121
+ async handleFormSubmit(e) {
122
+ e.preventDefault();
123
+
124
+ try {
125
+ // Clear any previous field errors
126
+ this.uiManager.clearFieldErrors();
127
+
128
+ // Validate form
129
+ this.formManager.validateForm();
130
+
131
+ // Get form data
132
+ const treeData = this.formManager.getFormData();
133
+
134
+ // Save or update tree
135
+ let result;
136
+ if (this.formManager.isInEditMode()) {
137
+ result = await this.apiClient.updateTree(this.formManager.getCurrentEditId(), treeData);
138
+ this.uiManager.showMessage(`Tree #${result.id} updated successfully!`, 'success');
139
+ this.handleCancelEdit();
140
+ } else {
141
+ result = await this.apiClient.saveTree(treeData);
142
+ this.uiManager.showMessage(
143
+ `Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`,
144
+ 'success'
145
+ );
146
+ this.formManager.resetForm(true);
147
+ }
148
+
149
+ // Refresh tree list
150
+ await this.loadTrees();
151
+ this.uiManager.scrollToTop();
152
+
153
+ } catch (error) {
154
+ console.error('Error submitting form:', error);
155
+ this.uiManager.showMessage('Error saving tree: ' + error.message, 'error');
156
+ this.uiManager.focusFirstError();
157
+ }
158
+ }
159
+
160
+ handleFormReset() {
161
+ this.formManager.resetForm();
162
+ this.uiManager.clearFieldErrors();
163
+ }
164
+
165
+ async handleEditTree(treeId) {
166
+ try {
167
+ this.uiManager.showLoadingState('message', 'Loading tree data...');
168
+
169
+ const tree = await this.apiClient.loadTree(treeId);
170
+ if (!tree) return;
171
+
172
+ this.formManager.populateForm(tree);
173
+ this.formManager.setEditMode(treeId);
174
+
175
+ this.uiManager.showMessage(`Loaded tree #${treeId} for editing. Make changes and save.`, 'success');
176
+ this.uiManager.scrollToTop();
177
+
178
+ } catch (error) {
179
+ console.error('Error loading tree for edit:', error);
180
+ this.uiManager.showMessage('Error loading tree data: ' + error.message, 'error');
181
+ }
182
+ }
183
+
184
+ async handleDeleteTree(treeId) {
185
+ if (!this.uiManager.confirmDeletion(treeId)) {
186
+ return;
187
+ }
188
+
189
+ try {
190
+ await this.apiClient.deleteTree(treeId);
191
+ this.uiManager.showMessage(`Tree #${treeId} deleted successfully.`, 'success');
192
+ await this.loadTrees();
193
+ } catch (error) {
194
+ console.error('Error deleting tree:', error);
195
+ this.uiManager.showMessage('Error deleting tree: ' + error.message, 'error');
196
+ }
197
+ }
198
+
199
+ handleCancelEdit() {
200
+ this.formManager.resetForm(true);
201
+ this.formManager.exitEditMode();
202
+ this.uiManager.clearFieldErrors();
203
+ this.uiManager.showMessage('Edit cancelled. Form cleared.', 'success');
204
+ }
205
+
206
+ async loadTrees() {
207
+ try {
208
+ this.uiManager.showLoadingState('treeList', 'Loading trees...');
209
+ const trees = await this.apiClient.loadTrees();
210
+ this.uiManager.renderTreeList(trees);
211
+ } catch (error) {
212
+ console.error('Error loading trees:', error);
213
+ this.uiManager.showErrorState('treeList', 'Error loading trees');
214
+ }
215
+ }
216
+
217
+ // Utility methods for external access (maintaining compatibility)
218
+ showMessage(message, type) {
219
+ this.uiManager.showMessage(message, type);
220
+ }
221
+
222
+ // Global functions for backward compatibility (deprecated - use event delegation instead)
223
+ editTree(treeId) {
224
+ console.warn('Global editTree function is deprecated. Use data attributes instead.');
225
+ this.handleEditTree(treeId);
226
+ }
227
+
228
+ deleteTree(treeId) {
229
+ console.warn('Global deleteTree function is deprecated. Use data attributes instead.');
230
+ this.handleDeleteTree(treeId);
231
+ }
232
+
233
+ capturePhoto(category) {
234
+ console.warn('Global capturePhoto function is deprecated. Use data attributes instead.');
235
+ this.mediaManager.capturePhoto(category);
236
+ }
237
+ }
238
+
239
+ // Initialize the app when the page loads
240
+ let app;
241
+ document.addEventListener('DOMContentLoaded', () => {
242
+ app = new TreeTrackApp();
243
+ });
244
+
245
+ // Expose app globally for debugging and backward compatibility
246
+ window.TreeTrackApp = TreeTrackApp;
247
+ window.app = app;