RoyAalekh commited on
Commit
ca371b2
·
1 Parent(s): a84323e

feat: Add comprehensive authentication system with user management

Browse files

- Add 4 user accounts (admin, system, researcher1, researcher2) with role-based permissions
- Implement session-based authentication with 8-hour timeout
- Add login page with elegant design and account selection
- Integrate authentication middleware into all API endpoints
- Add user info display and logout functionality in headers
- Implement tree ownership tracking with created_by field
- Add edit/delete permissions based on user roles and tree ownership
- Enhance tree list with user info and action buttons
- Update map view with authentication and elegant tree tooltips
- Add comprehensive edit/delete functionality with permission checks
- Implement tree management in both form and map interfaces
- Add enhanced popup details with action buttons in map view
- Include user greeting and role display in both interfaces
- Add form edit mode with cancel functionality
- Secure all file uploads and API endpoints with authentication
- Maintain backward compatibility while adding new features

Files changed (6) hide show
  1. app.py +159 -11
  2. auth.py +193 -0
  3. static/app.js +453 -29
  4. static/login.html +412 -0
  5. static/map.html +1 -1
  6. static/map.js +635 -437
app.py CHANGED
@@ -10,7 +10,7 @@ from datetime import datetime
10
  from typing import Any, Optional, List, Dict
11
 
12
  import uvicorn
13
- from fastapi import FastAPI, HTTPException, Request, status, File, UploadFile, Form
14
  from fastapi.middleware.cors import CORSMiddleware
15
  from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
16
  from fastapi.staticfiles import StaticFiles
@@ -23,6 +23,7 @@ from supabase_database import SupabaseDatabase
23
  from supabase_storage import SupabaseFileStorage
24
  from config import get_settings
25
  from master_tree_database import create_master_tree_database, get_tree_suggestions, get_all_tree_codes
 
26
 
27
  # Configure logging
28
  logging.basicConfig(
@@ -64,6 +65,54 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
64
  db = SupabaseDatabase()
65
  storage = SupabaseFileStorage()
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  # Pydantic models (same as before)
68
  class Tree(BaseModel):
69
  """Complete tree model with all 12 fields"""
@@ -245,10 +294,65 @@ async def health_check():
245
  }
246
 
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  # Frontend routes
 
 
 
 
 
 
 
 
 
 
 
249
  @app.get("/", response_class=HTMLResponse, tags=["Frontend"])
250
- async def read_root():
251
- """Serve the main application page"""
 
 
 
 
 
252
  try:
253
  with open("static/index.html", encoding="utf-8") as f:
254
  content = f.read()
@@ -259,8 +363,13 @@ async def read_root():
259
 
260
 
261
  @app.get("/map", response_class=HTMLResponse, tags=["Frontend"])
262
- async def serve_map():
263
- """Serve the map page"""
 
 
 
 
 
264
  return RedirectResponse(url="/static/map.html")
265
 
266
 
@@ -271,6 +380,7 @@ async def get_trees(
271
  offset: int = 0,
272
  species: str = None,
273
  health_status: str = None,
 
274
  ):
275
  """Get trees with pagination and filters"""
276
  if limit < 1 or limit > settings.server.max_trees_per_request:
@@ -301,12 +411,15 @@ async def get_trees(
301
 
302
 
303
  @app.post("/api/trees", response_model=Tree, status_code=status.HTTP_201_CREATED, tags=["Trees"])
304
- async def create_tree(tree: TreeCreate):
305
  """Create a new tree record"""
306
  try:
307
  # Convert to dict for database insertion
308
  tree_data = tree.model_dump(exclude_unset=True)
309
 
 
 
 
310
  # Create tree in database
311
  created_tree = db.create_tree(tree_data)
312
 
@@ -322,7 +435,7 @@ async def create_tree(tree: TreeCreate):
322
 
323
 
324
  @app.get("/api/trees/{tree_id}", response_model=Tree, tags=["Trees"])
325
- async def get_tree(tree_id: int):
326
  """Get a specific tree by ID"""
327
  try:
328
  tree = db.get_tree(tree_id)
@@ -345,9 +458,30 @@ async def get_tree(tree_id: int):
345
 
346
 
347
  @app.put("/api/trees/{tree_id}", response_model=Tree, tags=["Trees"])
348
- async def update_tree(tree_id: int, tree_update: TreeUpdate):
349
  """Update a tree record"""
350
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  # Convert to dict for database update
352
  update_data = tree_update.model_dump(exclude_unset=True)
353
 
@@ -374,9 +508,12 @@ async def update_tree(tree_id: int, tree_update: TreeUpdate):
374
 
375
 
376
  @app.delete("/api/trees/{tree_id}", tags=["Trees"])
377
- async def delete_tree(tree_id: int):
378
  """Delete a tree record"""
379
  try:
 
 
 
380
  # Get tree data first to clean up files
381
  tree = db.get_tree(tree_id)
382
 
@@ -386,6 +523,16 @@ async def delete_tree(tree_id: int):
386
  detail=f"Tree with ID {tree_id} not found",
387
  )
388
 
 
 
 
 
 
 
 
 
 
 
389
  # Delete tree from database
390
  db.delete_tree(tree_id)
391
 
@@ -415,7 +562,8 @@ async def delete_tree(tree_id: int):
415
  @app.post("/api/upload/image", tags=["Files"])
416
  async def upload_image(
417
  file: UploadFile = File(...),
418
- category: str = Form(...)
 
419
  ):
420
  """Upload an image file with cloud persistence"""
421
  # Validate file type
@@ -454,7 +602,7 @@ async def upload_image(
454
 
455
 
456
  @app.post("/api/upload/audio", tags=["Files"])
457
- async def upload_audio(file: UploadFile = File(...)):
458
  """Upload an audio file with cloud persistence"""
459
  # Validate file type
460
  if not file.content_type or not file.content_type.startswith('audio/'):
 
10
  from typing import Any, Optional, List, Dict
11
 
12
  import uvicorn
13
+ from fastapi import FastAPI, HTTPException, Request, status, File, UploadFile, Form, Depends, Cookie
14
  from fastapi.middleware.cors import CORSMiddleware
15
  from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
16
  from fastapi.staticfiles import StaticFiles
 
23
  from supabase_storage import SupabaseFileStorage
24
  from config import get_settings
25
  from master_tree_database import create_master_tree_database, get_tree_suggestions, get_all_tree_codes
26
+ from auth import auth_manager
27
 
28
  # Configure logging
29
  logging.basicConfig(
 
65
  db = SupabaseDatabase()
66
  storage = SupabaseFileStorage()
67
 
68
+ # Authentication models
69
+ class LoginRequest(BaseModel):
70
+ username: str
71
+ password: str
72
+
73
+ class LoginResponse(BaseModel):
74
+ token: str
75
+ user: Dict[str, Any]
76
+
77
+ class UserInfo(BaseModel):
78
+ username: str
79
+ role: str
80
+ full_name: str
81
+ permissions: List[str]
82
+
83
+ # Helper function for authentication
84
+ def get_current_user(request: Request) -> Optional[Dict[str, Any]]:
85
+ """Extract user info from request headers"""
86
+ auth_header = request.headers.get('Authorization')
87
+ if not auth_header or not auth_header.startswith('Bearer '):
88
+ return None
89
+
90
+ token = auth_header.split(' ')[1]
91
+ return auth_manager.validate_session(token)
92
+
93
+ def require_auth(request: Request) -> Dict[str, Any]:
94
+ """Dependency that requires authentication"""
95
+ user = get_current_user(request)
96
+ if not user:
97
+ raise HTTPException(
98
+ status_code=status.HTTP_401_UNAUTHORIZED,
99
+ detail="Authentication required",
100
+ headers={"WWW-Authenticate": "Bearer"},
101
+ )
102
+ return user
103
+
104
+ def require_permission(permission: str):
105
+ """Dependency factory for specific permissions"""
106
+ def check_permission(request: Request) -> Dict[str, Any]:
107
+ user = require_auth(request)
108
+ if permission not in user.get('permissions', []):
109
+ raise HTTPException(
110
+ status_code=status.HTTP_403_FORBIDDEN,
111
+ detail=f"Permission '{permission}' required"
112
+ )
113
+ return user
114
+ return check_permission
115
+
116
  # Pydantic models (same as before)
117
  class Tree(BaseModel):
118
  """Complete tree model with all 12 fields"""
 
294
  }
295
 
296
 
297
+ # Authentication routes
298
+ @app.post("/api/auth/login", response_model=LoginResponse, tags=["Authentication"])
299
+ async def login(login_data: LoginRequest):
300
+ """Authenticate user and create session"""
301
+ result = auth_manager.authenticate(login_data.username, login_data.password)
302
+ if not result:
303
+ raise HTTPException(
304
+ status_code=status.HTTP_401_UNAUTHORIZED,
305
+ detail="Invalid username or password"
306
+ )
307
+ return result
308
+
309
+ @app.get("/api/auth/validate", tags=["Authentication"])
310
+ async def validate_session(user: Dict[str, Any] = Depends(require_auth)):
311
+ """Validate current session"""
312
+ return {
313
+ "valid": True,
314
+ "user": user
315
+ }
316
+
317
+ @app.post("/api/auth/logout", tags=["Authentication"])
318
+ async def logout(request: Request):
319
+ """Logout user and invalidate session"""
320
+ auth_header = request.headers.get('Authorization')
321
+ if auth_header and auth_header.startswith('Bearer '):
322
+ token = auth_header.split(' ')[1]
323
+ auth_manager.logout(token)
324
+ return {"message": "Logged out successfully"}
325
+
326
+ @app.get("/api/auth/user", response_model=UserInfo, tags=["Authentication"])
327
+ async def get_user_info(user: Dict[str, Any] = Depends(require_auth)):
328
+ """Get current user information"""
329
+ return UserInfo(
330
+ username=user["username"],
331
+ role=user["role"],
332
+ full_name=user["full_name"],
333
+ permissions=user["permissions"]
334
+ )
335
+
336
  # Frontend routes
337
+ @app.get("/login", response_class=HTMLResponse, tags=["Frontend"])
338
+ async def serve_login():
339
+ """Serve the login page"""
340
+ try:
341
+ with open("static/login.html", encoding="utf-8") as f:
342
+ content = f.read()
343
+ return HTMLResponse(content=content)
344
+ except FileNotFoundError:
345
+ logger.error("login.html not found")
346
+ raise HTTPException(status_code=404, detail="Login page not found")
347
+
348
  @app.get("/", response_class=HTMLResponse, tags=["Frontend"])
349
+ async def read_root(request: Request):
350
+ """Serve the main application page with auth check"""
351
+ # Check if user is authenticated
352
+ user = get_current_user(request)
353
+ if not user:
354
+ return RedirectResponse(url="/login")
355
+
356
  try:
357
  with open("static/index.html", encoding="utf-8") as f:
358
  content = f.read()
 
363
 
364
 
365
  @app.get("/map", response_class=HTMLResponse, tags=["Frontend"])
366
+ async def serve_map(request: Request):
367
+ """Serve the map page with auth check"""
368
+ # Check if user is authenticated
369
+ user = get_current_user(request)
370
+ if not user:
371
+ return RedirectResponse(url="/login")
372
+
373
  return RedirectResponse(url="/static/map.html")
374
 
375
 
 
380
  offset: int = 0,
381
  species: str = None,
382
  health_status: str = None,
383
+ user: Dict[str, Any] = Depends(require_auth)
384
  ):
385
  """Get trees with pagination and filters"""
386
  if limit < 1 or limit > settings.server.max_trees_per_request:
 
411
 
412
 
413
  @app.post("/api/trees", response_model=Tree, status_code=status.HTTP_201_CREATED, tags=["Trees"])
414
+ async def create_tree(tree: TreeCreate, user: Dict[str, Any] = Depends(require_permission("write"))):
415
  """Create a new tree record"""
416
  try:
417
  # Convert to dict for database insertion
418
  tree_data = tree.model_dump(exclude_unset=True)
419
 
420
+ # Add created_by field
421
+ tree_data['created_by'] = user['username']
422
+
423
  # Create tree in database
424
  created_tree = db.create_tree(tree_data)
425
 
 
435
 
436
 
437
  @app.get("/api/trees/{tree_id}", response_model=Tree, tags=["Trees"])
438
+ async def get_tree(tree_id: int, user: Dict[str, Any] = Depends(require_auth)):
439
  """Get a specific tree by ID"""
440
  try:
441
  tree = db.get_tree(tree_id)
 
458
 
459
 
460
  @app.put("/api/trees/{tree_id}", response_model=Tree, tags=["Trees"])
461
+ async def update_tree(tree_id: int, tree_update: TreeUpdate, request: Request):
462
  """Update a tree record"""
463
  try:
464
+ # Get current user
465
+ user = require_auth(request)
466
+
467
+ # Get existing tree to check permissions
468
+ existing_tree = db.get_tree(tree_id)
469
+ if not existing_tree:
470
+ raise HTTPException(
471
+ status_code=status.HTTP_404_NOT_FOUND,
472
+ detail=f"Tree with ID {tree_id} not found",
473
+ )
474
+
475
+ # Check if user can edit this tree
476
+ auth_header = request.headers.get('Authorization', '')
477
+ token = auth_header.split(' ')[1] if auth_header.startswith('Bearer ') else ''
478
+
479
+ if not auth_manager.can_edit_tree(token, existing_tree.get('created_by', '')):
480
+ raise HTTPException(
481
+ status_code=status.HTTP_403_FORBIDDEN,
482
+ detail="You don't have permission to edit this tree",
483
+ )
484
+
485
  # Convert to dict for database update
486
  update_data = tree_update.model_dump(exclude_unset=True)
487
 
 
508
 
509
 
510
  @app.delete("/api/trees/{tree_id}", tags=["Trees"])
511
+ async def delete_tree(tree_id: int, request: Request):
512
  """Delete a tree record"""
513
  try:
514
+ # Get current user
515
+ user = require_auth(request)
516
+
517
  # Get tree data first to clean up files
518
  tree = db.get_tree(tree_id)
519
 
 
523
  detail=f"Tree with ID {tree_id} not found",
524
  )
525
 
526
+ # Check if user can delete this tree
527
+ auth_header = request.headers.get('Authorization', '')
528
+ token = auth_header.split(' ')[1] if auth_header.startswith('Bearer ') else ''
529
+
530
+ if not auth_manager.can_delete_tree(token, tree.get('created_by', '')):
531
+ raise HTTPException(
532
+ status_code=status.HTTP_403_FORBIDDEN,
533
+ detail="You don't have permission to delete this tree",
534
+ )
535
+
536
  # Delete tree from database
537
  db.delete_tree(tree_id)
538
 
 
562
  @app.post("/api/upload/image", tags=["Files"])
563
  async def upload_image(
564
  file: UploadFile = File(...),
565
+ category: str = Form(...),
566
+ user: Dict[str, Any] = Depends(require_permission("write"))
567
  ):
568
  """Upload an image file with cloud persistence"""
569
  # Validate file type
 
602
 
603
 
604
  @app.post("/api/upload/audio", tags=["Files"])
605
+ async def upload_audio(file: UploadFile = File(...), user: Dict[str, Any] = Depends(require_permission("write"))):
606
  """Upload an audio file with cloud persistence"""
607
  # Validate file type
608
  if not file.content_type or not file.content_type.startswith('audio/'):
auth.py ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TreeTrack Authentication Module
3
+ Simple session-based authentication with predefined users
4
+ """
5
+
6
+ import hashlib
7
+ import secrets
8
+ from typing import Dict, Optional, Any
9
+ from datetime import datetime, timedelta
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class AuthManager:
15
+ def __init__(self):
16
+ self.sessions: Dict[str, Dict[str, Any]] = {}
17
+ self.session_timeout = timedelta(hours=8) # 8-hour session timeout
18
+
19
+ # Predefined user accounts (in production, use a database)
20
+ self.users = {
21
+ # Owner account
22
+ "admin": {
23
+ "password_hash": self._hash_password("treetrack2025!"),
24
+ "role": "admin",
25
+ "full_name": "TreeTrack Administrator",
26
+ "permissions": ["read", "write", "delete", "admin"]
27
+ },
28
+
29
+ # System account
30
+ "system": {
31
+ "password_hash": self._hash_password("system@tree2025"),
32
+ "role": "system",
33
+ "full_name": "System User",
34
+ "permissions": ["read", "write", "delete", "system"]
35
+ },
36
+
37
+ # User accounts (you can share these credentials)
38
+ "researcher1": {
39
+ "password_hash": self._hash_password("field@research2025"),
40
+ "role": "researcher",
41
+ "full_name": "Field Researcher 1",
42
+ "permissions": ["read", "write", "edit_own"]
43
+ },
44
+
45
+ "researcher2": {
46
+ "password_hash": self._hash_password("tree@study2025"),
47
+ "role": "researcher",
48
+ "full_name": "Field Researcher 2",
49
+ "permissions": ["read", "write", "edit_own"]
50
+ }
51
+ }
52
+
53
+ logger.info(f"AuthManager initialized with {len(self.users)} user accounts")
54
+
55
+ def _hash_password(self, password: str) -> str:
56
+ """Hash password with salt"""
57
+ salt = "treetrack_salt_2025" # In production, use unique salts per user
58
+ return hashlib.pbkdf2_hex(password.encode(), salt.encode(), 100000)
59
+
60
+ def authenticate(self, username: str, password: str) -> Optional[Dict[str, Any]]:
61
+ """Authenticate user credentials"""
62
+ try:
63
+ if username not in self.users:
64
+ logger.warning(f"Authentication attempt with unknown username: {username}")
65
+ return None
66
+
67
+ user = self.users[username]
68
+ password_hash = self._hash_password(password)
69
+
70
+ if password_hash == user["password_hash"]:
71
+ # Create session
72
+ session_token = secrets.token_urlsafe(32)
73
+ session_data = {
74
+ "username": username,
75
+ "role": user["role"],
76
+ "full_name": user["full_name"],
77
+ "permissions": user["permissions"],
78
+ "created_at": datetime.now(),
79
+ "last_activity": datetime.now()
80
+ }
81
+
82
+ self.sessions[session_token] = session_data
83
+ logger.info(f"User {username} authenticated successfully")
84
+
85
+ return {
86
+ "token": session_token,
87
+ "user": session_data
88
+ }
89
+ else:
90
+ logger.warning(f"Invalid password for user: {username}")
91
+ return None
92
+
93
+ except Exception as e:
94
+ logger.error(f"Authentication error for {username}: {e}")
95
+ return None
96
+
97
+ def validate_session(self, token: str) -> Optional[Dict[str, Any]]:
98
+ """Validate session token and return user data"""
99
+ try:
100
+ if not token or token not in self.sessions:
101
+ return None
102
+
103
+ session = self.sessions[token]
104
+ now = datetime.now()
105
+
106
+ # Check if session has expired
107
+ if now - session["last_activity"] > self.session_timeout:
108
+ del self.sessions[token]
109
+ logger.info(f"Session expired for user: {session['username']}")
110
+ return None
111
+
112
+ # Update last activity
113
+ session["last_activity"] = now
114
+ return session
115
+
116
+ except Exception as e:
117
+ logger.error(f"Session validation error: {e}")
118
+ return None
119
+
120
+ def logout(self, token: str) -> bool:
121
+ """Logout user and invalidate session"""
122
+ try:
123
+ if token in self.sessions:
124
+ username = self.sessions[token]["username"]
125
+ del self.sessions[token]
126
+ logger.info(f"User {username} logged out")
127
+ return True
128
+ return False
129
+ except Exception as e:
130
+ logger.error(f"Logout error: {e}")
131
+ return False
132
+
133
+ def has_permission(self, token: str, permission: str) -> bool:
134
+ """Check if user has specific permission"""
135
+ session = self.validate_session(token)
136
+ if not session:
137
+ return False
138
+ return permission in session.get("permissions", [])
139
+
140
+ def can_edit_tree(self, token: str, tree_created_by: str) -> bool:
141
+ """Check if user can edit a specific tree"""
142
+ session = self.validate_session(token)
143
+ if not session:
144
+ return False
145
+
146
+ # Admin and system can edit any tree
147
+ if "admin" in session["permissions"] or "system" in session["permissions"]:
148
+ return True
149
+
150
+ # Users can edit trees they created
151
+ if "edit_own" in session["permissions"] and tree_created_by == session["username"]:
152
+ return True
153
+
154
+ # Users with delete permission can edit any tree
155
+ if "delete" in session["permissions"]:
156
+ return True
157
+
158
+ return False
159
+
160
+ def can_delete_tree(self, token: str, tree_created_by: str) -> bool:
161
+ """Check if user can delete a specific tree"""
162
+ session = self.validate_session(token)
163
+ if not session:
164
+ return False
165
+
166
+ # Only admin and system can delete trees
167
+ if "admin" in session["permissions"] or "system" in session["permissions"]:
168
+ return True
169
+
170
+ # Users with explicit delete permission
171
+ if "delete" in session["permissions"]:
172
+ return True
173
+
174
+ return False
175
+
176
+ def cleanup_expired_sessions(self):
177
+ """Remove expired sessions (can be called periodically)"""
178
+ now = datetime.now()
179
+ expired_tokens = []
180
+
181
+ for token, session in self.sessions.items():
182
+ if now - session["last_activity"] > self.session_timeout:
183
+ expired_tokens.append(token)
184
+
185
+ for token in expired_tokens:
186
+ username = self.sessions[token]["username"]
187
+ del self.sessions[token]
188
+ logger.info(f"Cleaned up expired session for user: {username}")
189
+
190
+ return len(expired_tokens)
191
+
192
+ # Global auth manager instance
193
+ auth_manager = AuthManager()
static/app.js CHANGED
@@ -1,4 +1,4 @@
1
- // TreeTrack Enhanced JavaScript - Comprehensive Field Research Tool
2
  class TreeTrackApp {
3
  constructor() {
4
  this.uploadedPhotos = {};
@@ -13,12 +13,23 @@ class TreeTrackApp {
13
  this.selectedIndex = -1;
14
  this.availableTreeCodes = [];
15
 
 
 
 
 
16
  this.init();
17
  }
18
 
19
  async init() {
 
 
 
 
 
 
20
  await this.loadFormOptions();
21
  this.setupEventListeners();
 
22
  this.loadTrees();
23
  this.loadSelectedLocation();
24
 
@@ -28,20 +39,239 @@ class TreeTrackApp {
28
  }, 100);
29
  }
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  async loadFormOptions() {
32
  try {
33
  // Load utility options
34
- const utilityResponse = await fetch('/api/utilities');
 
35
  const utilityData = await utilityResponse.json();
36
  this.renderMultiSelect('utilityOptions', utilityData.utilities);
37
 
38
  // Load phenology stages
39
- const phenologyResponse = await fetch('/api/phenology-stages');
 
40
  const phenologyData = await phenologyResponse.json();
41
  this.renderMultiSelect('phenologyOptions', phenologyData.stages);
42
 
43
  // Load photo categories
44
- const categoriesResponse = await fetch('/api/photo-categories');
 
45
  const categoriesData = await categoriesResponse.json();
46
  this.renderPhotoCategories(categoriesData.categories);
47
 
@@ -74,11 +304,11 @@ class TreeTrackApp {
74
  <div>
75
  <label>${category}</label>
76
  <div class="file-upload photo-upload" data-category="${category}">
77
- Camera Click to upload ${category} photo or use camera
78
  </div>
79
  <div class="uploaded-file" id="photo-${category}" style="display: none;"></div>
80
  </div>
81
- <button type="button" class="btn btn-small" onclick="app.capturePhoto('${category}')">Photo Camera</button>
82
  `;
83
  container.appendChild(categoryDiv);
84
  });
@@ -199,6 +429,9 @@ class TreeTrackApp {
199
  try {
200
  const response = await fetch('/api/upload/image', {
201
  method: 'POST',
 
 
 
202
  body: formData
203
  });
204
 
@@ -209,7 +442,7 @@ class TreeTrackApp {
209
  // Update UI
210
  const resultDiv = document.getElementById(`photo-${category}`);
211
  resultDiv.style.display = 'block';
212
- resultDiv.innerHTML = ` ${file.name} uploaded successfully`;
213
  } else {
214
  throw new Error('Upload failed');
215
  }
@@ -241,6 +474,9 @@ class TreeTrackApp {
241
  try {
242
  const response = await fetch('/api/upload/audio', {
243
  method: 'POST',
 
 
 
244
  body: formData
245
  });
246
 
@@ -250,7 +486,7 @@ class TreeTrackApp {
250
 
251
  // Update UI
252
  const resultDiv = document.getElementById('audioUploadResult');
253
- resultDiv.innerHTML = `<div class="uploaded-file"> ${file.name} uploaded successfully</div>`;
254
  } else {
255
  throw new Error('Upload failed');
256
  }
@@ -315,24 +551,24 @@ class TreeTrackApp {
315
  const recordBtn = document.getElementById('recordBtn');
316
  const status = document.getElementById('recordingStatus');
317
  recordBtn.classList.remove('recording');
318
- recordBtn.innerHTML = '';
319
  status.textContent = 'Recording saved!';
320
  }
321
  }
322
 
323
  getCurrentLocation() {
324
  if (navigator.geolocation) {
325
- document.getElementById('getLocation').textContent = ' Getting...';
326
 
327
  navigator.geolocation.getCurrentPosition(
328
  (position) => {
329
  document.getElementById('latitude').value = position.coords.latitude.toFixed(7);
330
  document.getElementById('longitude').value = position.coords.longitude.toFixed(7);
331
- document.getElementById('getLocation').textContent = ' GPS';
332
  this.showMessage('Location retrieved successfully!', 'success');
333
  },
334
  (error) => {
335
- document.getElementById('getLocation').textContent = ' GPS';
336
  this.showMessage('Error getting location: ' + error.message, 'error');
337
  }
338
  );
@@ -376,14 +612,13 @@ class TreeTrackApp {
376
  console.log('Phenology type:', typeof treeData.phenology_stages, treeData.phenology_stages);
377
 
378
  try {
379
- const response = await fetch('/api/trees', {
380
  method: 'POST',
381
- headers: {
382
- 'Content-Type': 'application/json',
383
- },
384
  body: JSON.stringify(treeData)
385
  });
386
 
 
 
387
  if (response.ok) {
388
  const result = await response.json();
389
  this.showMessage(`🌳 Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`, 'success');
@@ -426,7 +661,9 @@ class TreeTrackApp {
426
 
427
  async loadTrees() {
428
  try {
429
- const response = await fetch('/api/trees?limit=20');
 
 
430
  const trees = await response.json();
431
 
432
  const treeList = document.getElementById('treeList');
@@ -436,17 +673,29 @@ class TreeTrackApp {
436
  return;
437
  }
438
 
439
- treeList.innerHTML = trees.map(tree => `
440
- <div class="tree-item">
441
- <div class="tree-id">Tree #${tree.id}</div>
442
- <div class="tree-info">
443
- ${tree.scientific_name || tree.common_name || tree.local_name || 'Unnamed'}
444
- <br> ${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)}
445
- ${tree.tree_code ? '<br> ' + tree.tree_code : ''}
446
- <br> ${new Date(tree.created_at).toLocaleDateString()}
 
 
 
 
 
 
 
 
 
 
 
 
447
  </div>
448
- </div>
449
- `).join('');
450
 
451
  } catch (error) {
452
  console.error('Error loading trees:', error);
@@ -454,6 +703,177 @@ class TreeTrackApp {
454
  }
455
  }
456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  showMessage(message, type) {
458
  const messageDiv = document.getElementById('message');
459
  messageDiv.className = type === 'error' ? 'error-message' : 'success-message';
@@ -470,7 +890,9 @@ class TreeTrackApp {
470
  async initializeAutoSuggestions() {
471
  try {
472
  // Load available tree codes for validation
473
- const codesResponse = await fetch('/api/tree-codes');
 
 
474
  const codesData = await codesResponse.json();
475
  this.availableTreeCodes = codesData.tree_codes || [];
476
 
@@ -545,7 +967,9 @@ class TreeTrackApp {
545
  }));
546
  } else {
547
  // Search tree suggestions from API
548
- const response = await fetch(`/api/tree-suggestions?query=${encodeURIComponent(query)}&limit=10`);
 
 
549
  const data = await response.json();
550
 
551
  if (data.suggestions) {
 
1
+ // TreeTrack Enhanced JavaScript - Comprehensive Field Research Tool with Authentication
2
  class TreeTrackApp {
3
  constructor() {
4
  this.uploadedPhotos = {};
 
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
 
 
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
+ // Add user info to header
75
+ this.displayUserInfo();
76
+
77
+ // Add logout functionality
78
+ this.addLogoutButton();
79
+
80
+ // Add custom styles for new elements
81
+ this.addCustomStyles();
82
+ }
83
+
84
+ displayUserInfo() {
85
+ if (!this.currentUser) return;
86
+
87
+ const headerContent = document.querySelector('.header-content');
88
+ if (headerContent) {
89
+ // Create user info display
90
+ const userInfo = document.createElement('div');
91
+ userInfo.className = 'user-info';
92
+ userInfo.innerHTML = `
93
+ <div class="user-greeting">Welcome, ${this.currentUser.full_name}</div>
94
+ <div class="user-role">${this.currentUser.role}</div>
95
+ `;
96
+
97
+ // Insert before the map link
98
+ const mapLink = headerContent.querySelector('.map-link');
99
+ if (mapLink) {
100
+ headerContent.insertBefore(userInfo, mapLink);
101
+ }
102
+ }
103
+ }
104
+
105
+ addLogoutButton() {
106
+ const headerContent = document.querySelector('.header-content');
107
+ if (headerContent) {
108
+ const logoutBtn = document.createElement('button');
109
+ logoutBtn.className = 'btn btn-outline btn-small logout-btn';
110
+ logoutBtn.innerHTML = '🚪 Logout';
111
+ logoutBtn.addEventListener('click', () => this.logout());
112
+ headerContent.appendChild(logoutBtn);
113
+ }
114
+ }
115
+
116
+ addCustomStyles() {
117
+ const style = document.createElement('style');
118
+ style.textContent = `
119
+ .user-info {
120
+ color: white;
121
+ text-align: center;
122
+ margin: 0 1rem;
123
+ }
124
+ .user-greeting {
125
+ font-size: 0.875rem;
126
+ font-weight: 500;
127
+ }
128
+ .user-role {
129
+ font-size: 0.75rem;
130
+ opacity: 0.8;
131
+ text-transform: capitalize;
132
+ }
133
+ .tree-header {
134
+ display: flex;
135
+ justify-content: space-between;
136
+ align-items: center;
137
+ margin-bottom: 0.5rem;
138
+ }
139
+ .tree-actions {
140
+ display: flex;
141
+ gap: 0.25rem;
142
+ }
143
+ .btn-icon {
144
+ background: none;
145
+ border: none;
146
+ cursor: pointer;
147
+ padding: 0.25rem;
148
+ border-radius: 4px;
149
+ font-size: 0.875rem;
150
+ transition: background-color 0.2s;
151
+ }
152
+ .btn-icon:hover {
153
+ background-color: rgba(0,0,0,0.1);
154
+ }
155
+ .edit-tree:hover {
156
+ background-color: rgba(59, 130, 246, 0.1);
157
+ }
158
+ .delete-tree:hover {
159
+ background-color: rgba(239, 68, 68, 0.1);
160
+ }
161
+ .logout-btn {
162
+ margin-left: 1rem;
163
+ }
164
+ @media (max-width: 768px) {
165
+ .user-info {
166
+ margin: 0 0.5rem;
167
+ }
168
+ .user-greeting {
169
+ font-size: 0.75rem;
170
+ }
171
+ .user-role {
172
+ font-size: 0.625rem;
173
+ }
174
+ }
175
+ `;
176
+ document.head.appendChild(style);
177
+ }
178
+
179
+ async logout() {
180
+ try {
181
+ await fetch('/api/auth/logout', {
182
+ method: 'POST',
183
+ headers: {
184
+ 'Authorization': `Bearer ${this.authToken}`
185
+ }
186
+ });
187
+ } catch (error) {
188
+ console.error('Logout error:', error);
189
+ } finally {
190
+ localStorage.removeItem('auth_token');
191
+ localStorage.removeItem('user_info');
192
+ window.location.href = '/login';
193
+ }
194
+ }
195
+
196
+ // Enhanced API calls with authentication
197
+ async authenticatedFetch(url, options = {}) {
198
+ const headers = {
199
+ 'Content-Type': 'application/json',
200
+ 'Authorization': `Bearer ${this.authToken}`,
201
+ ...options.headers
202
+ };
203
+
204
+ const response = await fetch(url, {
205
+ ...options,
206
+ headers
207
+ });
208
+
209
+ if (response.status === 401) {
210
+ // Token expired or invalid
211
+ localStorage.removeItem('auth_token');
212
+ localStorage.removeItem('user_info');
213
+ window.location.href = '/login';
214
+ return null;
215
+ }
216
+
217
+ return response;
218
+ }
219
+
220
+ // Permission checking methods
221
+ canEditTree(createdBy) {
222
+ if (!this.currentUser) return false;
223
+
224
+ // Admin and system can edit any tree
225
+ if (this.currentUser.permissions.includes('admin') || this.currentUser.permissions.includes('system')) {
226
+ return true;
227
+ }
228
+
229
+ // Users can edit trees they created
230
+ if (this.currentUser.permissions.includes('edit_own') && createdBy === this.currentUser.username) {
231
+ return true;
232
+ }
233
+
234
+ // Users with delete permission can edit any tree
235
+ if (this.currentUser.permissions.includes('delete')) {
236
+ return true;
237
+ }
238
+
239
+ return false;
240
+ }
241
+
242
+ canDeleteTree(createdBy) {
243
+ if (!this.currentUser) return false;
244
+
245
+ // Only admin and system can delete trees
246
+ if (this.currentUser.permissions.includes('admin') || this.currentUser.permissions.includes('system')) {
247
+ return true;
248
+ }
249
+
250
+ // Users with explicit delete permission
251
+ if (this.currentUser.permissions.includes('delete')) {
252
+ return true;
253
+ }
254
+
255
+ return false;
256
+ }
257
+
258
  async loadFormOptions() {
259
  try {
260
  // Load utility options
261
+ const utilityResponse = await this.authenticatedFetch('/api/utilities');
262
+ if (!utilityResponse) return;
263
  const utilityData = await utilityResponse.json();
264
  this.renderMultiSelect('utilityOptions', utilityData.utilities);
265
 
266
  // Load phenology stages
267
+ const phenologyResponse = await this.authenticatedFetch('/api/phenology-stages');
268
+ if (!phenologyResponse) return;
269
  const phenologyData = await phenologyResponse.json();
270
  this.renderMultiSelect('phenologyOptions', phenologyData.stages);
271
 
272
  // Load photo categories
273
+ const categoriesResponse = await this.authenticatedFetch('/api/photo-categories');
274
+ if (!categoriesResponse) return;
275
  const categoriesData = await categoriesResponse.json();
276
  this.renderPhotoCategories(categoriesData.categories);
277
 
 
304
  <div>
305
  <label>${category}</label>
306
  <div class="file-upload photo-upload" data-category="${category}">
307
+ 📷 Click to upload ${category} photo or use camera
308
  </div>
309
  <div class="uploaded-file" id="photo-${category}" style="display: none;"></div>
310
  </div>
311
+ <button type="button" class="btn btn-small" onclick="app.capturePhoto('${category}')">📸 Camera</button>
312
  `;
313
  container.appendChild(categoryDiv);
314
  });
 
429
  try {
430
  const response = await fetch('/api/upload/image', {
431
  method: 'POST',
432
+ headers: {
433
+ 'Authorization': `Bearer ${this.authToken}`
434
+ },
435
  body: formData
436
  });
437
 
 
442
  // Update UI
443
  const resultDiv = document.getElementById(`photo-${category}`);
444
  resultDiv.style.display = 'block';
445
+ resultDiv.innerHTML = `✅ ${file.name} uploaded successfully`;
446
  } else {
447
  throw new Error('Upload failed');
448
  }
 
474
  try {
475
  const response = await fetch('/api/upload/audio', {
476
  method: 'POST',
477
+ headers: {
478
+ 'Authorization': `Bearer ${this.authToken}`
479
+ },
480
  body: formData
481
  });
482
 
 
486
 
487
  // Update UI
488
  const resultDiv = document.getElementById('audioUploadResult');
489
+ resultDiv.innerHTML = `<div class="uploaded-file">🎵 ${file.name} uploaded successfully</div>`;
490
  } else {
491
  throw new Error('Upload failed');
492
  }
 
551
  const recordBtn = document.getElementById('recordBtn');
552
  const status = document.getElementById('recordingStatus');
553
  recordBtn.classList.remove('recording');
554
+ recordBtn.innerHTML = '';
555
  status.textContent = 'Recording saved!';
556
  }
557
  }
558
 
559
  getCurrentLocation() {
560
  if (navigator.geolocation) {
561
+ document.getElementById('getLocation').textContent = '📍 Getting...';
562
 
563
  navigator.geolocation.getCurrentPosition(
564
  (position) => {
565
  document.getElementById('latitude').value = position.coords.latitude.toFixed(7);
566
  document.getElementById('longitude').value = position.coords.longitude.toFixed(7);
567
+ document.getElementById('getLocation').textContent = '📍 GPS';
568
  this.showMessage('Location retrieved successfully!', 'success');
569
  },
570
  (error) => {
571
+ document.getElementById('getLocation').textContent = '📍 GPS';
572
  this.showMessage('Error getting location: ' + error.message, 'error');
573
  }
574
  );
 
612
  console.log('Phenology type:', typeof treeData.phenology_stages, treeData.phenology_stages);
613
 
614
  try {
615
+ const response = await this.authenticatedFetch('/api/trees', {
616
  method: 'POST',
 
 
 
617
  body: JSON.stringify(treeData)
618
  });
619
 
620
+ if (!response) return;
621
+
622
  if (response.ok) {
623
  const result = await response.json();
624
  this.showMessage(`🌳 Tree successfully added! Tree ID: ${result.id}. The form has been cleared for your next entry.`, 'success');
 
661
 
662
  async loadTrees() {
663
  try {
664
+ const response = await this.authenticatedFetch('/api/trees?limit=20');
665
+ if (!response) return;
666
+
667
  const trees = await response.json();
668
 
669
  const treeList = document.getElementById('treeList');
 
673
  return;
674
  }
675
 
676
+ treeList.innerHTML = trees.map(tree => {
677
+ const canEdit = this.canEditTree(tree.created_by);
678
+ const canDelete = this.canDeleteTree(tree.created_by);
679
+
680
+ return `
681
+ <div class="tree-item" data-tree-id="${tree.id}">
682
+ <div class="tree-header">
683
+ <div class="tree-id">Tree #${tree.id}</div>
684
+ <div class="tree-actions">
685
+ ${canEdit ? `<button class="btn-icon edit-tree" onclick="app.editTree(${tree.id})" title="Edit Tree">✏️</button>` : ''}
686
+ ${canDelete ? `<button class="btn-icon delete-tree" onclick="app.deleteTree(${tree.id})" title="Delete Tree">🗑️</button>` : ''}
687
+ </div>
688
+ </div>
689
+ <div class="tree-info">
690
+ ${tree.scientific_name || tree.common_name || tree.local_name || 'Unnamed'}
691
+ <br>📍 ${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)}
692
+ ${tree.tree_code ? `<br>🏷️ ${tree.tree_code}` : ''}
693
+ <br>📅 ${new Date(tree.created_at).toLocaleDateString()}
694
+ <br>👤 ${tree.created_by || 'Unknown'}
695
+ </div>
696
  </div>
697
+ `;
698
+ }).join('');
699
 
700
  } catch (error) {
701
  console.error('Error loading trees:', error);
 
703
  }
704
  }
705
 
706
+ async editTree(treeId) {
707
+ try {
708
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`);
709
+ if (!response) return;
710
+
711
+ if (!response.ok) {
712
+ throw new Error('Failed to fetch tree data');
713
+ }
714
+
715
+ const tree = await response.json();
716
+
717
+ // Populate form with tree data
718
+ document.getElementById('latitude').value = tree.latitude;
719
+ document.getElementById('longitude').value = tree.longitude;
720
+ document.getElementById('localName').value = tree.local_name || '';
721
+ document.getElementById('scientificName').value = tree.scientific_name || '';
722
+ document.getElementById('commonName').value = tree.common_name || '';
723
+ document.getElementById('treeCode').value = tree.tree_code || '';
724
+ document.getElementById('height').value = tree.height || '';
725
+ document.getElementById('width').value = tree.width || '';
726
+ document.getElementById('storytellingText').value = tree.storytelling_text || '';
727
+ document.getElementById('notes').value = tree.notes || '';
728
+
729
+ // Handle utility checkboxes
730
+ if (tree.utility && Array.isArray(tree.utility)) {
731
+ document.querySelectorAll('#utilityOptions input[type="checkbox"]').forEach(checkbox => {
732
+ checkbox.checked = tree.utility.includes(checkbox.value);
733
+ });
734
+ }
735
+
736
+ // Handle phenology checkboxes
737
+ if (tree.phenology_stages && Array.isArray(tree.phenology_stages)) {
738
+ document.querySelectorAll('#phenologyOptions input[type="checkbox"]').forEach(checkbox => {
739
+ checkbox.checked = tree.phenology_stages.includes(checkbox.value);
740
+ });
741
+ }
742
+
743
+ // Update form to edit mode
744
+ this.setEditMode(treeId);
745
+
746
+ this.showMessage(`Loaded tree #${treeId} for editing. Make changes and save.`, 'success');
747
+
748
+ } catch (error) {
749
+ console.error('Error loading tree for edit:', error);
750
+ this.showMessage('Error loading tree data: ' + error.message, 'error');
751
+ }
752
+ }
753
+
754
+ setEditMode(treeId) {
755
+ // Change form submit behavior
756
+ const form = document.getElementById('treeForm');
757
+ form.dataset.editId = treeId;
758
+
759
+ // Update submit button
760
+ const submitBtn = document.querySelector('button[type="submit"]');
761
+ submitBtn.textContent = 'Update Tree Record';
762
+
763
+ // Add cancel edit button
764
+ if (!document.getElementById('cancelEdit')) {
765
+ const cancelBtn = document.createElement('button');
766
+ cancelBtn.type = 'button';
767
+ cancelBtn.id = 'cancelEdit';
768
+ cancelBtn.className = 'btn btn-outline';
769
+ cancelBtn.textContent = 'Cancel Edit';
770
+ cancelBtn.addEventListener('click', () => this.cancelEdit());
771
+
772
+ const formActions = document.querySelector('.form-actions');
773
+ formActions.insertBefore(cancelBtn, submitBtn);
774
+ }
775
+
776
+ // Update form submit handler for edit mode
777
+ form.removeEventListener('submit', this.handleSubmit);
778
+ form.addEventListener('submit', (e) => this.handleEditSubmit(e, treeId));
779
+ }
780
+
781
+ cancelEdit() {
782
+ // Reset form
783
+ this.resetFormSilently();
784
+
785
+ // Remove edit mode
786
+ const form = document.getElementById('treeForm');
787
+ delete form.dataset.editId;
788
+
789
+ // Restore original submit button
790
+ const submitBtn = document.querySelector('button[type="submit"]');
791
+ submitBtn.textContent = 'Save Tree Record';
792
+
793
+ // Remove cancel button
794
+ const cancelBtn = document.getElementById('cancelEdit');
795
+ if (cancelBtn) {
796
+ cancelBtn.remove();
797
+ }
798
+
799
+ // Restore original form handler
800
+ form.removeEventListener('submit', this.handleEditSubmit);
801
+ form.addEventListener('submit', (e) => this.handleSubmit(e));
802
+
803
+ this.showMessage('Edit cancelled. Form cleared.', 'success');
804
+ }
805
+
806
+ async handleEditSubmit(e, treeId) {
807
+ e.preventDefault();
808
+
809
+ const utilityValues = this.getSelectedValues('utilityOptions');
810
+ const phenologyValues = this.getSelectedValues('phenologyOptions');
811
+
812
+ const treeData = {
813
+ latitude: parseFloat(document.getElementById('latitude').value),
814
+ longitude: parseFloat(document.getElementById('longitude').value),
815
+ local_name: document.getElementById('localName').value || null,
816
+ scientific_name: document.getElementById('scientificName').value || null,
817
+ common_name: document.getElementById('commonName').value || null,
818
+ tree_code: document.getElementById('treeCode').value || null,
819
+ height: document.getElementById('height').value ? parseFloat(document.getElementById('height').value) : null,
820
+ width: document.getElementById('width').value ? parseFloat(document.getElementById('width').value) : null,
821
+ utility: utilityValues.length > 0 ? utilityValues : [],
822
+ phenology_stages: phenologyValues.length > 0 ? phenologyValues : [],
823
+ storytelling_text: document.getElementById('storytellingText').value || null,
824
+ storytelling_audio: this.audioFile,
825
+ photographs: Object.keys(this.uploadedPhotos).length > 0 ? this.uploadedPhotos : null,
826
+ notes: document.getElementById('notes').value || null
827
+ };
828
+
829
+ try {
830
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`, {
831
+ method: 'PUT',
832
+ body: JSON.stringify(treeData)
833
+ });
834
+
835
+ if (!response) return;
836
+
837
+ if (response.ok) {
838
+ const result = await response.json();
839
+ this.showMessage(`🌳 Tree #${result.id} updated successfully!`, 'success');
840
+ this.cancelEdit(); // Exit edit mode
841
+ this.loadTrees(); // Refresh the tree list
842
+ } else {
843
+ const error = await response.json();
844
+ this.showMessage('Error updating tree: ' + (error.detail || 'Unknown error'), 'error');
845
+ }
846
+ } catch (error) {
847
+ console.error('Error updating tree:', error);
848
+ this.showMessage('Network error: ' + error.message, 'error');
849
+ }
850
+ }
851
+
852
+ async deleteTree(treeId) {
853
+ if (!confirm(`Are you sure you want to delete Tree #${treeId}? This action cannot be undone.`)) {
854
+ return;
855
+ }
856
+
857
+ try {
858
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`, {
859
+ method: 'DELETE'
860
+ });
861
+
862
+ if (!response) return;
863
+
864
+ if (response.ok) {
865
+ this.showMessage(`🗑️ Tree #${treeId} deleted successfully.`, 'success');
866
+ this.loadTrees(); // Refresh the tree list
867
+ } else {
868
+ const error = await response.json();
869
+ this.showMessage('Error deleting tree: ' + (error.detail || 'Unknown error'), 'error');
870
+ }
871
+ } catch (error) {
872
+ console.error('Error deleting tree:', error);
873
+ this.showMessage('Network error: ' + error.message, 'error');
874
+ }
875
+ }
876
+
877
  showMessage(message, type) {
878
  const messageDiv = document.getElementById('message');
879
  messageDiv.className = type === 'error' ? 'error-message' : 'success-message';
 
890
  async initializeAutoSuggestions() {
891
  try {
892
  // Load available tree codes for validation
893
+ const codesResponse = await this.authenticatedFetch('/api/tree-codes');
894
+ if (!codesResponse) return;
895
+
896
  const codesData = await codesResponse.json();
897
  this.availableTreeCodes = codesData.tree_codes || [];
898
 
 
967
  }));
968
  } else {
969
  // Search tree suggestions from API
970
+ const response = await this.authenticatedFetch(`/api/tree-suggestions?query=${encodeURIComponent(query)}&limit=10`);
971
+ if (!response) return;
972
+
973
  const data = await response.json();
974
 
975
  if (data.suggestions) {
static/login.html ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TreeTrack Login - Field Research Access</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
9
+
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ body {
17
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
18
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
+ min-height: 100vh;
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ padding: 1rem;
24
+ }
25
+
26
+ .login-container {
27
+ background: rgba(255, 255, 255, 0.95);
28
+ backdrop-filter: blur(20px);
29
+ border-radius: 24px;
30
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
31
+ border: 1px solid rgba(255, 255, 255, 0.2);
32
+ width: 100%;
33
+ max-width: 400px;
34
+ padding: 2.5rem;
35
+ animation: slideIn 0.6s ease-out;
36
+ }
37
+
38
+ @keyframes slideIn {
39
+ from {
40
+ opacity: 0;
41
+ transform: translateY(30px) scale(0.95);
42
+ }
43
+ to {
44
+ opacity: 1;
45
+ transform: translateY(0) scale(1);
46
+ }
47
+ }
48
+
49
+ .logo-section {
50
+ text-align: center;
51
+ margin-bottom: 2rem;
52
+ }
53
+
54
+ .logo {
55
+ font-size: 2.5rem;
56
+ font-weight: 700;
57
+ background: linear-gradient(135deg, #1e40af, #3b82f6);
58
+ -webkit-background-clip: text;
59
+ -webkit-text-fill-color: transparent;
60
+ background-clip: text;
61
+ margin-bottom: 0.5rem;
62
+ letter-spacing: -0.02em;
63
+ }
64
+
65
+ .logo-subtitle {
66
+ color: #6b7280;
67
+ font-size: 0.875rem;
68
+ font-weight: 500;
69
+ }
70
+
71
+ .login-form {
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: 1.5rem;
75
+ }
76
+
77
+ .form-group {
78
+ display: flex;
79
+ flex-direction: column;
80
+ gap: 0.5rem;
81
+ }
82
+
83
+ .form-label {
84
+ font-weight: 600;
85
+ color: #374151;
86
+ font-size: 0.875rem;
87
+ }
88
+
89
+ .form-input {
90
+ padding: 0.875rem 1rem;
91
+ border: 2px solid #e5e7eb;
92
+ border-radius: 12px;
93
+ font-size: 1rem;
94
+ transition: all 0.2s ease;
95
+ background: #ffffff;
96
+ outline: none;
97
+ }
98
+
99
+ .form-input:focus {
100
+ border-color: #3b82f6;
101
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
102
+ transform: scale(1.02);
103
+ }
104
+
105
+ .login-button {
106
+ background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%);
107
+ color: white;
108
+ border: none;
109
+ padding: 1rem;
110
+ border-radius: 12px;
111
+ font-size: 1rem;
112
+ font-weight: 600;
113
+ cursor: pointer;
114
+ transition: all 0.3s ease;
115
+ margin-top: 1rem;
116
+ position: relative;
117
+ overflow: hidden;
118
+ }
119
+
120
+ .login-button:hover {
121
+ transform: translateY(-2px);
122
+ box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.4);
123
+ }
124
+
125
+ .login-button:active {
126
+ transform: translateY(0);
127
+ }
128
+
129
+ .login-button:disabled {
130
+ opacity: 0.7;
131
+ cursor: not-allowed;
132
+ transform: none;
133
+ }
134
+
135
+ .login-button.loading::after {
136
+ content: '';
137
+ position: absolute;
138
+ top: 50%;
139
+ left: 50%;
140
+ width: 20px;
141
+ height: 20px;
142
+ margin: -10px 0 0 -10px;
143
+ border: 2px solid transparent;
144
+ border-top: 2px solid #ffffff;
145
+ border-radius: 50%;
146
+ animation: spin 1s linear infinite;
147
+ }
148
+
149
+ @keyframes spin {
150
+ 0% { transform: rotate(0deg); }
151
+ 100% { transform: rotate(360deg); }
152
+ }
153
+
154
+ .message {
155
+ padding: 1rem;
156
+ border-radius: 12px;
157
+ margin-bottom: 1.5rem;
158
+ font-size: 0.875rem;
159
+ font-weight: 500;
160
+ text-align: center;
161
+ animation: fadeIn 0.3s ease;
162
+ }
163
+
164
+ @keyframes fadeIn {
165
+ from { opacity: 0; transform: translateY(-10px); }
166
+ to { opacity: 1; transform: translateY(0); }
167
+ }
168
+
169
+ .message.error {
170
+ background: #fef2f2;
171
+ color: #dc2626;
172
+ border: 1px solid #fecaca;
173
+ }
174
+
175
+ .message.success {
176
+ background: #f0fdf4;
177
+ color: #16a34a;
178
+ border: 1px solid #bbf7d0;
179
+ }
180
+
181
+ .demo-accounts {
182
+ margin-top: 2rem;
183
+ padding: 1.5rem;
184
+ background: rgba(59, 130, 246, 0.05);
185
+ border: 1px solid rgba(59, 130, 246, 0.1);
186
+ border-radius: 16px;
187
+ }
188
+
189
+ .demo-title {
190
+ font-size: 0.875rem;
191
+ font-weight: 600;
192
+ color: #1e40af;
193
+ margin-bottom: 1rem;
194
+ text-align: center;
195
+ }
196
+
197
+ .account-list {
198
+ display: flex;
199
+ flex-direction: column;
200
+ gap: 0.75rem;
201
+ }
202
+
203
+ .account-item {
204
+ background: rgba(255, 255, 255, 0.7);
205
+ padding: 0.75rem;
206
+ border-radius: 8px;
207
+ font-size: 0.75rem;
208
+ cursor: pointer;
209
+ transition: all 0.2s ease;
210
+ border: 1px solid rgba(59, 130, 246, 0.1);
211
+ }
212
+
213
+ .account-item:hover {
214
+ background: rgba(255, 255, 255, 0.9);
215
+ transform: translateY(-1px);
216
+ }
217
+
218
+ .account-role {
219
+ font-weight: 600;
220
+ color: #1e40af;
221
+ }
222
+
223
+ .account-username {
224
+ color: #6b7280;
225
+ margin-top: 0.25rem;
226
+ }
227
+
228
+ .footer {
229
+ text-align: center;
230
+ margin-top: 2rem;
231
+ color: #9ca3af;
232
+ font-size: 0.75rem;
233
+ }
234
+
235
+ @media (max-width: 480px) {
236
+ .login-container {
237
+ padding: 2rem 1.5rem;
238
+ margin: 0 1rem;
239
+ }
240
+
241
+ .logo {
242
+ font-size: 2rem;
243
+ }
244
+ }
245
+ </style>
246
+ </head>
247
+ <body>
248
+ <div class="login-container">
249
+ <div class="logo-section">
250
+ <div class="logo">🌳 TreeTrack</div>
251
+ <div class="logo-subtitle">Secure Field Research Access</div>
252
+ </div>
253
+
254
+ <form class="login-form" id="loginForm">
255
+ <div id="message" class="message" style="display: none;"></div>
256
+
257
+ <div class="form-group">
258
+ <label class="form-label" for="username">Username</label>
259
+ <input class="form-input" type="text" id="username" name="username" required autocomplete="username">
260
+ </div>
261
+
262
+ <div class="form-group">
263
+ <label class="form-label" for="password">Password</label>
264
+ <input class="form-input" type="password" id="password" name="password" required autocomplete="current-password">
265
+ </div>
266
+
267
+ <button class="login-button" type="submit" id="loginButton">
268
+ <span id="buttonText">Sign In to TreeTrack</span>
269
+ </button>
270
+ </form>
271
+
272
+ <div class="demo-accounts">
273
+ <div class="demo-title">🔐 Available Accounts</div>
274
+ <div class="account-list">
275
+ <div class="account-item" onclick="fillCredentials('admin', 'treetrack2025!')">
276
+ <div class="account-role">Administrator</div>
277
+ <div class="account-username">Full system access</div>
278
+ </div>
279
+ <div class="account-item" onclick="fillCredentials('researcher1', 'field@research2025')">
280
+ <div class="account-role">Field Researcher 1</div>
281
+ <div class="account-username">Research & documentation</div>
282
+ </div>
283
+ <div class="account-item" onclick="fillCredentials('researcher2', 'tree@study2025')">
284
+ <div class="account-role">Field Researcher 2</div>
285
+ <div class="account-username">Research & documentation</div>
286
+ </div>
287
+ <div class="account-item" onclick="fillCredentials('system', 'system@tree2025')">
288
+ <div class="account-role">System Account</div>
289
+ <div class="account-username">System operations</div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ <div class="footer">
295
+ © 2025 TreeTrack - Secure Field Research Platform
296
+ </div>
297
+ </div>
298
+
299
+ <script>
300
+ function fillCredentials(username, password) {
301
+ document.getElementById('username').value = username;
302
+ document.getElementById('password').value = password;
303
+
304
+ // Add visual feedback
305
+ const accountItems = document.querySelectorAll('.account-item');
306
+ accountItems.forEach(item => item.style.background = 'rgba(255, 255, 255, 0.7)');
307
+ event.target.closest('.account-item').style.background = 'rgba(59, 130, 246, 0.1)';
308
+ }
309
+
310
+ function showMessage(message, type = 'error') {
311
+ const messageEl = document.getElementById('message');
312
+ messageEl.textContent = message;
313
+ messageEl.className = `message ${type}`;
314
+ messageEl.style.display = 'block';
315
+
316
+ if (type === 'success') {
317
+ setTimeout(() => {
318
+ messageEl.style.display = 'none';
319
+ }, 3000);
320
+ }
321
+ }
322
+
323
+ function setLoading(loading) {
324
+ const button = document.getElementById('loginButton');
325
+ const buttonText = document.getElementById('buttonText');
326
+
327
+ if (loading) {
328
+ button.disabled = true;
329
+ button.classList.add('loading');
330
+ buttonText.textContent = 'Signing In...';
331
+ } else {
332
+ button.disabled = false;
333
+ button.classList.remove('loading');
334
+ buttonText.textContent = 'Sign In to TreeTrack';
335
+ }
336
+ }
337
+
338
+ document.getElementById('loginForm').addEventListener('submit', async (e) => {
339
+ e.preventDefault();
340
+
341
+ const username = document.getElementById('username').value.trim();
342
+ const password = document.getElementById('password').value;
343
+
344
+ if (!username || !password) {
345
+ showMessage('Please enter both username and password');
346
+ return;
347
+ }
348
+
349
+ setLoading(true);
350
+
351
+ try {
352
+ const response = await fetch('/api/auth/login', {
353
+ method: 'POST',
354
+ headers: {
355
+ 'Content-Type': 'application/json',
356
+ },
357
+ body: JSON.stringify({ username, password })
358
+ });
359
+
360
+ const result = await response.json();
361
+
362
+ if (response.ok) {
363
+ // Store authentication token
364
+ localStorage.setItem('auth_token', result.token);
365
+ localStorage.setItem('user_info', JSON.stringify(result.user));
366
+
367
+ showMessage('Login successful! Redirecting...', 'success');
368
+
369
+ // Redirect to main application
370
+ setTimeout(() => {
371
+ window.location.href = '/';
372
+ }, 1500);
373
+ } else {
374
+ showMessage(result.detail || 'Login failed. Please check your credentials.');
375
+ }
376
+ } catch (error) {
377
+ console.error('Login error:', error);
378
+ showMessage('Network error. Please try again.');
379
+ } finally {
380
+ setLoading(false);
381
+ }
382
+ });
383
+
384
+ // Check if already logged in
385
+ document.addEventListener('DOMContentLoaded', () => {
386
+ const token = localStorage.getItem('auth_token');
387
+ if (token) {
388
+ // Validate token
389
+ fetch('/api/auth/validate', {
390
+ headers: {
391
+ 'Authorization': `Bearer ${token}`
392
+ }
393
+ })
394
+ .then(response => response.ok ? window.location.href = '/' : null)
395
+ .catch(() => {
396
+ // Token invalid, remove it
397
+ localStorage.removeItem('auth_token');
398
+ localStorage.removeItem('user_info');
399
+ });
400
+ }
401
+ });
402
+
403
+ // Auto-fill demo credentials on page load for development
404
+ document.addEventListener('DOMContentLoaded', () => {
405
+ // Auto-select researcher1 account for easy testing
406
+ setTimeout(() => {
407
+ fillCredentials('researcher1', 'field@research2025');
408
+ }, 1000);
409
+ });
410
+ </script>
411
+ </body>
412
+ </html>
static/map.html CHANGED
@@ -41,7 +41,7 @@
41
  align-items: center;
42
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
43
  z-index: 1000;
44
- border-bottom: 5px solid #ff0000 !important; /* DEBUG: Red border to confirm new CSS */
45
  }
46
 
47
  .logo {
 
41
  align-items: center;
42
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
43
  z-index: 1000;
44
+ /* Red border removed after debugging */
45
  }
46
 
47
  .logo {
static/map.js CHANGED
@@ -1,549 +1,747 @@
1
- // TreeTrack Map - Interactive Map with Pin Management
2
  class TreeTrackMap {
3
  constructor() {
4
  this.map = null;
5
- this.currentMarker = null;
 
6
  this.treeMarkers = [];
7
  this.userLocation = null;
8
- this.selectedLocation = null;
 
 
 
 
9
 
10
  this.init();
11
  }
12
 
13
- init() {
14
- this.showLoading(true);
15
- this.initializeMap();
16
- this.setupEventListeners();
17
- this.loadExistingTrees();
18
- this.attemptGeolocation();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
21
- initializeMap() {
22
- // Initialize Leaflet map
 
 
 
 
 
23
  this.map = L.map('map', {
24
- zoomControl: false,
25
- attributionControl: false,
26
- tap: true,
27
- touchZoom: true,
28
- doubleClickZoom: false // Disable double click to prevent conflicts
29
- }).setView([26.2006, 92.9376], 13); // Default to Guwahati, Assam
30
-
31
- // Add tile layer - Using OpenStreetMap with satellite option
32
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
33
- attribution: '',
34
- maxZoom: 19
35
- }).addTo(this.map);
36
 
37
- // Add custom zoom controls
38
- L.control.zoom({
39
- position: 'bottomleft'
 
40
  }).addTo(this.map);
41
 
42
- // Add scale
43
- L.control.scale({
44
- position: 'bottomleft',
45
- metric: true,
46
- imperial: false
47
- }).addTo(this.map);
48
 
49
- this.showLoading(false);
50
  }
51
 
52
  setupEventListeners() {
53
- // Map click event for dropping pins
54
- this.map.on('click', (e) => {
55
- this.handleMapClick(e);
56
- });
57
 
58
- // Button event listeners
59
  document.getElementById('myLocationBtn').addEventListener('click', () => {
60
- this.goToMyLocation();
61
  });
62
 
 
63
  document.getElementById('clearPinsBtn').addEventListener('click', () => {
64
- this.clearTemporaryPin();
65
  });
66
 
 
67
  document.getElementById('useLocationBtn').addEventListener('click', () => {
68
  this.useSelectedLocation();
69
  });
70
 
 
71
  document.getElementById('cancelBtn').addEventListener('click', () => {
72
  this.cancelLocationSelection();
73
  });
74
 
75
- // Handle browser back/forward
76
- window.addEventListener('popstate', (e) => {
77
- if (e.state && e.state.location) {
78
- this.selectedLocation = e.state.location;
79
- this.showLocationPanel(true);
80
- }
81
- });
82
  }
83
 
84
- handleMapClick(e) {
85
- const lat = e.latlng.lat;
86
- const lng = e.latlng.lng;
87
-
88
- // Clear any existing temporary marker
89
- this.clearTemporaryPin();
90
-
91
- // Create new temporary marker
92
- this.currentMarker = L.circleMarker([lat, lng], {
93
- radius: 15,
94
- fillColor: '#ff6b35',
95
- color: 'white',
96
- weight: 3,
97
- opacity: 1,
98
- fillOpacity: 0.8,
99
- className: 'temp-pin'
100
- }).addTo(this.map);
101
-
102
- // Store selected location
103
- this.selectedLocation = { lat, lng };
 
 
 
 
104
 
105
- // Update info panel
106
- this.updateLocationDisplay(lat, lng);
107
- this.showLocationPanel(true);
108
 
109
- // Add to browser history
110
- history.pushState(
111
- { location: this.selectedLocation },
112
- '',
113
- `#lat=${lat.toFixed(6)}&lng=${lng.toFixed(6)}`
114
- );
115
 
116
- // Haptic feedback on mobile
117
- this.vibrate(50);
118
  }
119
 
120
- clearTemporaryPin() {
121
- if (this.currentMarker) {
122
- this.map.removeLayer(this.currentMarker);
123
- this.currentMarker = null;
124
  }
125
  this.selectedLocation = null;
126
- this.showLocationPanel(false);
127
-
128
- // Clear URL hash
129
- history.pushState('', document.title, window.location.pathname);
130
  }
131
 
132
- async loadExistingTrees() {
133
- try {
134
- const response = await fetch('/api/trees');
135
- if (response.ok) {
136
- const trees = await response.json();
137
- console.log('Loaded trees:', trees); // Debug log
138
- this.displayTreeMarkers(trees);
139
- this.updateTreeCounter(trees.length);
140
- } else {
141
- console.error('Failed to load trees:', response.status, response.statusText);
142
- }
143
- } catch (error) {
144
- console.error('Error loading trees:', error);
 
145
  }
 
 
 
 
 
 
 
 
 
 
 
 
146
  }
147
 
148
- displayTreeMarkers(trees) {
149
- // Clear existing tree markers
150
- this.treeMarkers.forEach(marker => {
151
- this.map.removeLayer(marker);
152
- });
153
- this.treeMarkers = [];
 
 
 
 
 
 
154
 
155
- // Add tree markers with custom icons
156
- trees.forEach(tree => {
157
- if (tree.latitude && tree.longitude) {
158
- // Create custom tree icon
159
- const treeIcon = this.createCustomTreeIcon(tree);
 
 
 
 
 
160
 
161
- const marker = L.marker([tree.latitude, tree.longitude], {
162
- icon: treeIcon
163
- }).addTo(this.map);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
- // Create hover tooltip
166
- const tooltipContent = this.createTreeTooltip(tree);
167
- marker.bindTooltip(tooltipContent, {
168
  permanent: false,
169
  direction: 'top',
170
  offset: [0, -10],
171
  className: 'tree-tooltip'
172
  });
173
 
174
- // Create popup content for clicks
175
- const popupContent = this.createTreePopup(tree);
176
- marker.bindPopup(popupContent, {
177
- maxWidth: 300,
178
- className: 'tree-popup'
179
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- this.treeMarkers.push(marker);
 
 
 
182
  }
183
- });
 
 
 
 
184
  }
185
 
186
- createCustomTreeIcon(tree) {
187
- // Create a beautiful custom tree icon
188
- const iconHtml = `
189
- <div class="custom-tree-marker">
190
- <div class="tree-icon-container">
191
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
192
- <path d="M12 2C10.9 2 10 2.9 10 4C10 5.1 10.9 6 12 6C13.1 6 14 5.1 14 4C14 2.9 13.1 2 12 2Z" fill="#1d4ed8"/>
193
- <path d="M12 6C9.8 6 8 7.8 8 10C8 12.2 9.8 14 12 14C14.2 14 16 12.2 16 10C16 7.8 14.2 6 12 6Z" fill="#3b82f6"/>
194
- <path d="M12 10C10.3 10 9 11.3 9 13C9 14.7 10.3 16 12 16C13.7 16 15 14.7 15 13C15 11.3 13.7 10 12 10Z" fill="#60a5fa"/>
195
- <rect x="11" y="16" width="2" height="6" fill="#8D6E63"/>
196
- <path d="M10 22H14V20H10V22Z" fill="#5D4037"/>
197
- </svg>
198
  </div>
199
- <div class="tree-marker-shadow"></div>
200
- </div>
201
- `;
202
-
203
- return L.divIcon({
204
- html: iconHtml,
205
  className: 'custom-tree-icon',
206
- iconSize: [32, 40],
207
- iconAnchor: [16, 40],
208
- popupAnchor: [0, -35]
209
  });
210
- }
211
 
212
- createTreeTooltip(tree) {
213
- const name = tree.local_name || tree.common_name || tree.scientific_name || `Tree #${tree.id}`;
214
- const height = tree.height ? `${tree.height}m` : '';
215
- const code = tree.tree_code ? `[${tree.tree_code}]` : '';
216
-
217
- return `
218
  <div class="tree-tooltip-content">
219
- <div class="tree-name">${name}</div>
220
- ${height || code ? `<div class="tree-details">${code} ${height}</div>` : ''}
221
  </div>
222
  `;
223
- }
224
 
225
- createTreePopup(tree) {
226
- return `
227
- <div style="padding: 10px; font-family: 'Segoe UI', sans-serif;">
228
- <h3 style="margin: 0 0 10px 0; color: #1e40af; font-size: 1.1rem;">
229
- ${tree.local_name || tree.common_name || 'Tree #' + tree.id}
230
- </h3>
231
- ${tree.scientific_name ? `<p style="margin: 0 0 8px 0; font-style: italic; color: #666;">${tree.scientific_name}</p>` : ''}
232
- ${tree.tree_code ? `<p style="margin: 0 0 8px 0;"><strong>Code:</strong> ${tree.tree_code}</p>` : ''}
233
- ${tree.height ? `<p style="margin: 0 0 5px 0;"><strong>Height:</strong> ${tree.height}m</p>` : ''}
234
- ${tree.width ? `<p style="margin: 0 0 5px 0;"><strong>Girth:</strong> ${tree.width}cm</p>` : ''}
235
- <div style="margin-top: 10px; font-size: 0.9rem; color: #888;">
236
- ${tree.latitude.toFixed(6)}, ${tree.longitude.toFixed(6)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  </div>
238
  </div>
239
  `;
240
- }
241
 
242
- updateTreeCounter(count) {
243
- document.getElementById('treeCount').textContent = count;
244
- }
 
245
 
246
- updateLocationDisplay(lat, lng) {
247
- document.getElementById('latValue').textContent = lat.toFixed(6);
248
- document.getElementById('lngValue').textContent = lng.toFixed(6);
249
  }
250
 
251
- showLocationPanel(show) {
252
- const panel = document.getElementById('infoPanel');
253
- if (show) {
254
- panel.classList.add('active');
255
- } else {
256
- panel.classList.remove('active');
257
- }
258
  }
259
 
260
- useSelectedLocation() {
261
- if (this.selectedLocation) {
262
- // Store in localStorage for form access
263
- localStorage.setItem('selectedLocation', JSON.stringify(this.selectedLocation));
 
 
 
 
 
 
 
 
264
 
265
- this.showMessage('Location saved! Redirecting to form...', 'success');
266
 
267
- // Redirect to form page after a short delay
268
  setTimeout(() => {
269
- window.location.href = '/static/index.html';
270
- }, 1500);
 
 
 
 
271
  }
272
  }
273
-
274
- cancelLocationSelection() {
275
- this.clearTemporaryPin();
276
- }
277
-
278
- async attemptGeolocation() {
279
- if ('geolocation' in navigator) {
280
- try {
281
- const position = await this.getCurrentPosition();
282
- this.userLocation = {
283
- lat: position.coords.latitude,
284
- lng: position.coords.longitude
285
- };
 
 
286
 
287
- // Add user location marker
288
- this.addUserLocationMarker();
 
 
 
 
 
289
 
290
- } catch (error) {
291
- console.log('Geolocation not available or denied');
 
292
  }
 
 
 
293
  }
294
  }
295
-
296
- getCurrentPosition() {
297
- return new Promise((resolve, reject) => {
298
- navigator.geolocation.getCurrentPosition(resolve, reject, {
299
- enableHighAccuracy: true,
300
- timeout: 10000,
301
- maximumAge: 300000 // 5 minutes
302
- });
303
- });
304
- }
305
-
306
- addUserLocationMarker() {
307
- if (this.userLocation) {
308
- // Add pulsing blue dot for user location
309
- const userMarker = L.circleMarker([this.userLocation.lat, this.userLocation.lng], {
310
- radius: 8,
311
- fillColor: '#2196F3',
312
- color: 'white',
313
- weight: 2,
314
- opacity: 1,
315
- fillOpacity: 1
316
- }).addTo(this.map);
317
-
318
- userMarker.bindPopup(' Your Location', {
319
- className: 'user-location-popup'
320
- });
321
- }
322
- }
323
-
324
- async goToMyLocation() {
325
- this.showLoading(true);
326
-
327
  try {
328
- const position = await this.getCurrentPosition();
329
- const lat = position.coords.latitude;
330
- const lng = position.coords.longitude;
331
 
332
- this.userLocation = { lat, lng };
 
 
333
 
334
- // Animate to user location
335
- this.map.flyTo([lat, lng], 16, {
336
- animate: true,
337
- duration: 1.5
338
- });
339
 
340
- // Update user location marker
341
- this.addUserLocationMarker();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
- this.showMessage('Found your location!', 'success');
 
 
 
 
 
 
 
 
 
 
344
 
345
  } catch (error) {
346
- this.showMessage('Could not get your location. Please check permissions.', 'error');
347
- } finally {
348
- this.showLoading(false);
349
  }
350
  }
351
 
352
- showMessage(text, type = 'success') {
353
- const messageEl = document.getElementById('message');
354
- messageEl.textContent = text;
355
- messageEl.className = `message ${type} show`;
356
-
357
- setTimeout(() => {
358
- messageEl.classList.remove('show');
359
- }, 3000);
360
  }
361
 
362
- showLoading(show) {
363
- const loadingEl = document.getElementById('loading');
364
- loadingEl.style.display = show ? 'block' : 'none';
365
  }
366
 
367
- vibrate(duration) {
368
- if ('vibrate' in navigator) {
369
- navigator.vibrate(duration);
370
- }
371
- }
372
- }
373
 
374
- // Enhanced Mobile Gestures and Touch Handling
375
- class MobileEnhancements {
376
- constructor(mapInstance) {
377
- this.map = mapInstance;
378
- this.initMobileFeatures();
379
  }
380
 
381
- initMobileFeatures() {
382
- // Prevent iOS bounce/overscroll
383
- document.addEventListener('touchmove', (e) => {
384
- if (e.target === document.getElementById('map')) {
385
- e.preventDefault();
386
- }
387
- }, { passive: false });
388
-
389
- // Handle orientation change
390
- window.addEventListener('orientationchange', () => {
391
  setTimeout(() => {
392
- this.map.map.invalidateSize();
393
- }, 500);
394
- });
395
-
396
- // Long press for pin dropping (alternative to tap)
397
- this.setupLongPressHandler();
398
-
399
- // Swipe gestures for panels
400
- this.setupSwipeGestures();
401
- }
402
-
403
- setupLongPressHandler() {
404
- let longPressTimer;
405
- let isLongPress = false;
406
-
407
- this.map.map.on('mousedown touchstart', (e) => {
408
- isLongPress = false;
409
- longPressTimer = setTimeout(() => {
410
- isLongPress = true;
411
- this.handleLongPress(e);
412
- }, 500); // 500ms long press
413
- });
414
-
415
- this.map.map.on('mouseup touchend mousemove touchmove', () => {
416
- clearTimeout(longPressTimer);
417
- });
418
- }
419
-
420
- handleLongPress(e) {
421
- // Vibrate to indicate long press detected
422
- this.map.vibrate([50, 50, 50]);
423
-
424
- // Handle the long press as a pin drop
425
- this.map.handleMapClick(e);
426
-
427
- // Show different message for long press
428
- this.map.showMessage('Pin dropped via long press!', 'success');
429
- }
430
-
431
- setupSwipeGestures() {
432
- let startY = 0;
433
- let startX = 0;
434
-
435
- document.addEventListener('touchstart', (e) => {
436
- startY = e.touches[0].clientY;
437
- startX = e.touches[0].clientX;
438
- });
439
-
440
- document.addEventListener('touchend', (e) => {
441
- if (!e.changedTouches[0]) return;
442
-
443
- const endY = e.changedTouches[0].clientY;
444
- const endX = e.changedTouches[0].clientX;
445
- const diffY = startY - endY;
446
- const diffX = startX - endX;
447
-
448
- // Detect swipe up on info panel to dismiss
449
- if (Math.abs(diffY) > Math.abs(diffX) && diffY > 50) {
450
- const infoPanel = document.getElementById('infoPanel');
451
- if (infoPanel.classList.contains('active')) {
452
- this.map.cancelLocationSelection();
453
- }
454
- }
455
- });
456
- }
457
- }
458
-
459
- // Progressive Web App features
460
- class PWAFeatures {
461
- constructor() {
462
- this.initPWA();
463
- }
464
-
465
- initPWA() {
466
- // Service worker registration
467
- if ('serviceWorker' in navigator) {
468
- navigator.serviceWorker.register('/static/sw.js')
469
- .then(registration => {
470
- console.log('SW registered: ', registration);
471
- })
472
- .catch(registrationError => {
473
- console.log('SW registration failed: ', registrationError);
474
- });
475
  }
476
-
477
- // Install prompt
478
- this.setupInstallPrompt();
479
- }
480
-
481
- setupInstallPrompt() {
482
- let deferredPrompt;
483
-
484
- window.addEventListener('beforeinstallprompt', (e) => {
485
- e.preventDefault();
486
- deferredPrompt = e;
487
-
488
- // Show custom install button (you can add this to UI)
489
- console.log('PWA install prompt available');
490
- });
491
-
492
- window.addEventListener('appinstalled', () => {
493
- console.log('PWA was installed');
494
- deferredPrompt = null;
495
- });
496
  }
497
  }
498
 
499
- // URL handling for deep linking
500
- class URLHandler {
501
- constructor(mapInstance) {
502
- this.map = mapInstance;
503
- this.handleInitialURL();
504
- }
505
-
506
- handleInitialURL() {
507
- const hash = window.location.hash;
508
- if (hash) {
509
- const params = new URLSearchParams(hash.substring(1));
510
- const lat = parseFloat(params.get('lat'));
511
- const lng = parseFloat(params.get('lng'));
512
-
513
- if (!isNaN(lat) && !isNaN(lng)) {
514
- // Restore location from URL
515
- setTimeout(() => {
516
- this.map.handleMapClick({
517
- latlng: { lat, lng }
518
- });
519
- this.map.map.setView([lat, lng], 16);
520
- }, 1000);
521
- }
522
- }
523
- }
524
- }
525
-
526
- // Initialize the application
527
  document.addEventListener('DOMContentLoaded', () => {
528
- // Main map instance
529
- const treeMap = new TreeTrackMap();
530
-
531
- // Mobile enhancements
532
- const mobileEnhancements = new MobileEnhancements(treeMap);
533
-
534
- // PWA features
535
- const pwaFeatures = new PWAFeatures();
536
-
537
- // URL handling
538
- const urlHandler = new URLHandler(treeMap);
539
-
540
- // Auto-refresh tree data every 30 seconds
541
- setInterval(() => {
542
- treeMap.loadExistingTrees();
543
- }, 30000);
544
  });
545
-
546
- // Export for use in other modules
547
- if (typeof module !== 'undefined' && module.exports) {
548
- module.exports = { TreeTrackMap, MobileEnhancements, PWAFeatures };
549
- }
 
1
+ // TreeTrack Enhanced Map with Authentication and Tree Management
2
  class TreeTrackMap {
3
  constructor() {
4
  this.map = null;
5
+ this.tempMarker = null;
6
+ this.selectedLocation = null;
7
  this.treeMarkers = [];
8
  this.userLocation = null;
9
+ this.isLocationSelected = false;
10
+
11
+ // Authentication properties
12
+ this.currentUser = null;
13
+ this.authToken = null;
14
 
15
  this.init();
16
  }
17
 
18
+ async init() {
19
+ // Check authentication first
20
+ if (!await this.checkAuthentication()) {
21
+ window.location.href = '/login';
22
+ return;
23
+ }
24
+
25
+ this.showLoading();
26
+
27
+ try {
28
+ await this.initializeMap();
29
+ this.setupEventListeners();
30
+ await this.loadTrees();
31
+ this.setupUserInterface();
32
+
33
+ setTimeout(() => {
34
+ this.hideLoading();
35
+ this.showGestureHint();
36
+ }, 1000);
37
+
38
+ } catch (error) {
39
+ console.error('Map initialization failed:', error);
40
+ this.showMessage('Failed to initialize map. Please refresh the page.', 'error');
41
+ this.hideLoading();
42
+ }
43
+ }
44
+
45
+ // Authentication methods
46
+ async checkAuthentication() {
47
+ const token = localStorage.getItem('auth_token');
48
+ if (!token) {
49
+ return false;
50
+ }
51
+
52
+ try {
53
+ const response = await fetch('/api/auth/validate', {
54
+ headers: {
55
+ 'Authorization': `Bearer ${token}`
56
+ }
57
+ });
58
+
59
+ if (response.ok) {
60
+ const data = await response.json();
61
+ this.currentUser = data.user;
62
+ this.authToken = token;
63
+ return true;
64
+ } else {
65
+ // Token invalid, remove it
66
+ localStorage.removeItem('auth_token');
67
+ localStorage.removeItem('user_info');
68
+ return false;
69
+ }
70
+ } catch (error) {
71
+ console.error('Auth validation error:', error);
72
+ return false;
73
+ }
74
+ }
75
+
76
+ setupUserInterface() {
77
+ // Add user info to header
78
+ this.displayUserInfo();
79
+
80
+ // Add logout functionality
81
+ this.addLogoutButton();
82
+ }
83
+
84
+ displayUserInfo() {
85
+ if (!this.currentUser) return;
86
+
87
+ const headerActions = document.querySelector('.header-actions');
88
+ if (headerActions) {
89
+ // Create user info display
90
+ const userInfo = document.createElement('div');
91
+ userInfo.className = 'user-info-map';
92
+ userInfo.innerHTML = `
93
+ <div style="color: white; text-align: center; margin-right: 1rem; font-size: 0.875rem;">
94
+ <div>${this.currentUser.full_name}</div>
95
+ <div style="opacity: 0.8; font-size: 0.75rem;">${this.currentUser.role}</div>
96
+ </div>
97
+ `;
98
+
99
+ // Insert before the tree counter
100
+ const treeCounter = headerActions.querySelector('.tree-counter');
101
+ if (treeCounter) {
102
+ headerActions.insertBefore(userInfo, treeCounter);
103
+ }
104
+ }
105
+ }
106
+
107
+ addLogoutButton() {
108
+ const headerActions = document.querySelector('.header-actions');
109
+ if (headerActions) {
110
+ const logoutBtn = document.createElement('button');
111
+ logoutBtn.className = 'btn btn-secondary';
112
+ logoutBtn.innerHTML = '🚪 Logout';
113
+ logoutBtn.style.marginLeft = '0.5rem';
114
+ logoutBtn.addEventListener('click', () => this.logout());
115
+ headerActions.appendChild(logoutBtn);
116
+ }
117
+ }
118
+
119
+ async logout() {
120
+ try {
121
+ await fetch('/api/auth/logout', {
122
+ method: 'POST',
123
+ headers: {
124
+ 'Authorization': `Bearer ${this.authToken}`
125
+ }
126
+ });
127
+ } catch (error) {
128
+ console.error('Logout error:', error);
129
+ } finally {
130
+ localStorage.removeItem('auth_token');
131
+ localStorage.removeItem('user_info');
132
+ window.location.href = '/login';
133
+ }
134
+ }
135
+
136
+ // Enhanced API calls with authentication
137
+ async authenticatedFetch(url, options = {}) {
138
+ const headers = {
139
+ 'Content-Type': 'application/json',
140
+ 'Authorization': `Bearer ${this.authToken}`,
141
+ ...options.headers
142
+ };
143
+
144
+ const response = await fetch(url, {
145
+ ...options,
146
+ headers
147
+ });
148
+
149
+ if (response.status === 401) {
150
+ // Token expired or invalid
151
+ localStorage.removeItem('auth_token');
152
+ localStorage.removeItem('user_info');
153
+ window.location.href = '/login';
154
+ return null;
155
+ }
156
+
157
+ return response;
158
+ }
159
+
160
+ // Permission checking methods
161
+ canEditTree(createdBy) {
162
+ if (!this.currentUser) return false;
163
+
164
+ // Admin and system can edit any tree
165
+ if (this.currentUser.permissions.includes('admin') || this.currentUser.permissions.includes('system')) {
166
+ return true;
167
+ }
168
+
169
+ // Users can edit trees they created
170
+ if (this.currentUser.permissions.includes('edit_own') && createdBy === this.currentUser.username) {
171
+ return true;
172
+ }
173
+
174
+ // Users with delete permission can edit any tree
175
+ if (this.currentUser.permissions.includes('delete')) {
176
+ return true;
177
+ }
178
+
179
+ return false;
180
+ }
181
+
182
+ canDeleteTree(createdBy) {
183
+ if (!this.currentUser) return false;
184
+
185
+ // Only admin and system can delete trees
186
+ if (this.currentUser.permissions.includes('admin') || this.currentUser.permissions.includes('system')) {
187
+ return true;
188
+ }
189
+
190
+ // Users with explicit delete permission
191
+ if (this.currentUser.permissions.includes('delete')) {
192
+ return true;
193
+ }
194
+
195
+ return false;
196
  }
197
 
198
+ async initializeMap() {
199
+ console.log('Initializing map...');
200
+
201
+ // Default location (you can change this to your preferred location)
202
+ const defaultLocation = [26.2006, 92.9376]; // Guwahati, Assam
203
+
204
+ // Initialize map
205
  this.map = L.map('map', {
206
+ center: defaultLocation,
207
+ zoom: 13,
208
+ zoomControl: true,
209
+ attributionControl: true
210
+ });
 
 
 
 
 
 
 
211
 
212
+ // Add tile layer
213
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
214
+ attribution: '© OpenStreetMap contributors',
215
+ maxZoom: 18
216
  }).addTo(this.map);
217
 
218
+ // Map click handler for pin dropping
219
+ this.map.on('click', (e) => {
220
+ this.onMapClick(e);
221
+ });
 
 
222
 
223
+ console.log('Map initialized successfully');
224
  }
225
 
226
  setupEventListeners() {
227
+ console.log('Setting up event listeners...');
 
 
 
228
 
229
+ // My Location button
230
  document.getElementById('myLocationBtn').addEventListener('click', () => {
231
+ this.getCurrentLocation();
232
  });
233
 
234
+ // Clear Pins button
235
  document.getElementById('clearPinsBtn').addEventListener('click', () => {
236
+ this.clearTempMarker();
237
  });
238
 
239
+ // Use Location button
240
  document.getElementById('useLocationBtn').addEventListener('click', () => {
241
  this.useSelectedLocation();
242
  });
243
 
244
+ // Cancel button
245
  document.getElementById('cancelBtn').addEventListener('click', () => {
246
  this.cancelLocationSelection();
247
  });
248
 
249
+ console.log('Event listeners setup complete');
 
 
 
 
 
 
250
  }
251
 
252
+ onMapClick(e) {
253
+ console.log('Map clicked at:', e.latlng);
254
+
255
+ this.selectedLocation = e.latlng;
256
+ this.isLocationSelected = true;
257
+
258
+ // Remove existing temp marker
259
+ this.clearTempMarker();
260
+
261
+ // Create new temp marker with glow effect
262
+ const tempIcon = L.divIcon({
263
+ html: `
264
+ <div class="custom-tree-marker">
265
+ <div class="tree-icon-container temp-pin">
266
+ 📍
267
+ </div>
268
+ <div class="tree-marker-shadow"></div>
269
+ </div>
270
+ `,
271
+ className: 'custom-tree-icon',
272
+ iconSize: [32, 42],
273
+ iconAnchor: [16, 42],
274
+ popupAnchor: [0, -42]
275
+ });
276
 
277
+ this.tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { icon: tempIcon }).addTo(this.map);
 
 
278
 
279
+ // Update coordinates display
280
+ document.getElementById('latValue').textContent = e.latlng.lat.toFixed(6);
281
+ document.getElementById('lngValue').textContent = e.latlng.lng.toFixed(6);
 
 
 
282
 
283
+ // Show info panel
284
+ this.showInfoPanel();
285
  }
286
 
287
+ clearTempMarker() {
288
+ if (this.tempMarker) {
289
+ this.map.removeLayer(this.tempMarker);
290
+ this.tempMarker = null;
291
  }
292
  this.selectedLocation = null;
293
+ this.isLocationSelected = false;
294
+ this.hideInfoPanel();
 
 
295
  }
296
 
297
+ showInfoPanel() {
298
+ const panel = document.getElementById('infoPanel');
299
+ panel.classList.add('active');
300
+ }
301
+
302
+ hideInfoPanel() {
303
+ const panel = document.getElementById('infoPanel');
304
+ panel.classList.remove('active');
305
+ }
306
+
307
+ useSelectedLocation() {
308
+ if (!this.selectedLocation) {
309
+ this.showMessage('No location selected', 'error');
310
+ return;
311
  }
312
+
313
+ // Store location for the form page
314
+ localStorage.setItem('selectedLocation', JSON.stringify({
315
+ lat: this.selectedLocation.lat,
316
+ lng: this.selectedLocation.lng
317
+ }));
318
+
319
+ this.showMessage('Location saved! Redirecting to form...', 'success');
320
+
321
+ setTimeout(() => {
322
+ window.location.href = '/static/index.html';
323
+ }, 1500);
324
  }
325
 
326
+ cancelLocationSelection() {
327
+ this.clearTempMarker();
328
+ this.showMessage('Selection cancelled', 'success');
329
+ }
330
+
331
+ getCurrentLocation() {
332
+ console.log('Getting current location...');
333
+
334
+ if (!navigator.geolocation) {
335
+ this.showMessage('Geolocation not supported by this browser', 'error');
336
+ return;
337
+ }
338
 
339
+ const myLocationBtn = document.getElementById('myLocationBtn');
340
+ myLocationBtn.textContent = 'Getting...';
341
+ myLocationBtn.disabled = true;
342
+
343
+ navigator.geolocation.getCurrentPosition(
344
+ (position) => {
345
+ console.log('Location found:', position.coords);
346
+
347
+ const lat = position.coords.latitude;
348
+ const lng = position.coords.longitude;
349
 
350
+ this.userLocation = { lat, lng };
351
+
352
+ // Center map on user location
353
+ this.map.setView([lat, lng], 16);
354
+
355
+ // Add user location marker
356
+ if (this.userLocationMarker) {
357
+ this.map.removeLayer(this.userLocationMarker);
358
+ }
359
+
360
+ const userIcon = L.divIcon({
361
+ html: `
362
+ <div class="custom-tree-marker">
363
+ <div class="tree-icon-container" style="background: linear-gradient(145deg, #3b82f6, #1d4ed8);">
364
+ 🫵
365
+ </div>
366
+ <div class="tree-marker-shadow"></div>
367
+ </div>
368
+ `,
369
+ className: 'custom-tree-icon',
370
+ iconSize: [32, 42],
371
+ iconAnchor: [16, 42],
372
+ popupAnchor: [0, -42]
373
+ });
374
+
375
+ this.userLocationMarker = L.marker([lat, lng], { icon: userIcon }).addTo(this.map);
376
 
377
+ // Add tooltip
378
+ this.userLocationMarker.bindTooltip('Your Location', {
 
379
  permanent: false,
380
  direction: 'top',
381
  offset: [0, -10],
382
  className: 'tree-tooltip'
383
  });
384
 
385
+ this.showMessage('📍 Location found!', 'success');
386
+
387
+ myLocationBtn.textContent = 'My Location';
388
+ myLocationBtn.disabled = false;
389
+ },
390
+ (error) => {
391
+ console.error('Geolocation error:', error);
392
+ let errorMessage = 'Failed to get location';
393
+
394
+ switch (error.code) {
395
+ case error.PERMISSION_DENIED:
396
+ errorMessage = 'Location access denied by user';
397
+ break;
398
+ case error.POSITION_UNAVAILABLE:
399
+ errorMessage = 'Location information unavailable';
400
+ break;
401
+ case error.TIMEOUT:
402
+ errorMessage = 'Location request timed out';
403
+ break;
404
+ }
405
+
406
+ this.showMessage(errorMessage, 'error');
407
+ myLocationBtn.textContent = 'My Location';
408
+ myLocationBtn.disabled = false;
409
+ },
410
+ {
411
+ enableHighAccuracy: true,
412
+ timeout: 10000,
413
+ maximumAge: 60000
414
+ }
415
+ );
416
+ }
417
+
418
+ async loadTrees() {
419
+ console.log('Loading trees...');
420
+
421
+ try {
422
+ const response = await this.authenticatedFetch('/api/trees?limit=1000');
423
+ if (!response) return;
424
+
425
+ const trees = await response.json();
426
+ console.log(`Loaded ${trees.length} trees`);
427
+
428
+ // Clear existing tree markers
429
+ this.clearTreeMarkers();
430
+
431
+ // Add tree markers
432
+ trees.forEach(tree => {
433
+ this.addTreeMarker(tree);
434
+ });
435
+
436
+ // Update tree count
437
+ document.getElementById('treeCount').textContent = trees.length;
438
 
439
+ if (trees.length > 0) {
440
+ // Fit map to show all trees
441
+ const group = new L.featureGroup(this.treeMarkers);
442
+ this.map.fitBounds(group.getBounds().pad(0.1));
443
  }
444
+
445
+ } catch (error) {
446
+ console.error('Error loading trees:', error);
447
+ this.showMessage('Failed to load trees', 'error');
448
+ }
449
  }
450
 
451
+ addTreeMarker(tree) {
452
+ const treeIcon = L.divIcon({
453
+ html: `
454
+ <div class="custom-tree-marker">
455
+ <div class="tree-icon-container tree-pin">
456
+ 🌳
457
+ </div>
458
+ <div class="tree-marker-shadow"></div>
 
 
 
 
459
  </div>
460
+ `,
 
 
 
 
 
461
  className: 'custom-tree-icon',
462
+ iconSize: [32, 42],
463
+ iconAnchor: [16, 42],
464
+ popupAnchor: [0, -42]
465
  });
 
466
 
467
+ const marker = L.marker([tree.latitude, tree.longitude], { icon: treeIcon }).addTo(this.map);
468
+
469
+ // Enhanced tooltip
470
+ const treeName = tree.scientific_name || tree.common_name || tree.local_name || 'Unknown Tree';
471
+ const tooltipContent = `
 
472
  <div class="tree-tooltip-content">
473
+ <div class="tree-name">${treeName}</div>
474
+ <div class="tree-details">ID: ${tree.id}${tree.tree_code ? ' • ' + tree.tree_code : ''}</div>
475
  </div>
476
  `;
 
477
 
478
+ marker.bindTooltip(tooltipContent, {
479
+ permanent: false,
480
+ direction: 'top',
481
+ offset: [0, -10],
482
+ className: 'tree-tooltip'
483
+ });
484
+
485
+ // Enhanced popup with action buttons
486
+ const canEdit = this.canEditTree(tree.created_by);
487
+ const canDelete = this.canDeleteTree(tree.created_by);
488
+
489
+ const popupContent = `
490
+ <div style="min-width: 280px; font-family: 'Segoe UI', sans-serif;">
491
+ <div style="border-bottom: 1px solid #e5e7eb; padding-bottom: 12px; margin-bottom: 12px;">
492
+ <h3 style="margin: 0 0 8px 0; color: #1e40af; font-size: 16px; font-weight: 600;">
493
+ ${treeName}
494
+ </h3>
495
+ <div style="color: #6b7280; font-size: 13px;">
496
+ <strong>Tree ID:</strong> #${tree.id}${tree.tree_code ? ' (' + tree.tree_code + ')' : ''}
497
+ </div>
498
+ </div>
499
+
500
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px; font-size: 13px;">
501
+ <div><strong>📍 Location:</strong></div>
502
+ <div>${tree.latitude.toFixed(4)}, ${tree.longitude.toFixed(4)}</div>
503
+
504
+ ${tree.height ? `<div><strong>📏 Height:</strong></div><div>${tree.height}m</div>` : ''}
505
+ ${tree.width ? `<div><strong>📐 Girth:</strong></div><div>${tree.width}cm</div>` : ''}
506
+
507
+ <div><strong>👤 Added by:</strong></div>
508
+ <div>${tree.created_by || 'Unknown'}</div>
509
+
510
+ <div><strong>📅 Date:</strong></div>
511
+ <div>${new Date(tree.created_at).toLocaleDateString()}</div>
512
+ </div>
513
+
514
+ ${tree.notes ? `
515
+ <div style="margin-bottom: 12px;">
516
+ <strong style="color: #374151; font-size: 13px;">📝 Notes:</strong>
517
+ <div style="color: #6b7280; font-size: 12px; margin-top: 4px; line-height: 1.4;">
518
+ ${tree.notes.substring(0, 120)}${tree.notes.length > 120 ? '...' : ''}
519
+ </div>
520
+ </div>
521
+ ` : ''}
522
+
523
+ <div style="display: flex; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #e5e7eb;">
524
+ ${canEdit ? `
525
+ <button onclick="mapApp.editTree(${tree.id})"
526
+ style="flex: 1; background: linear-gradient(145deg, #3b82f6, #2563eb); color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s;">
527
+ ✏️ Edit
528
+ </button>
529
+ ` : ''}
530
+ ${canDelete ? `
531
+ <button onclick="mapApp.deleteTree(${tree.id})"
532
+ style="flex: 1; background: linear-gradient(145deg, #ef4444, #dc2626); color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s;">
533
+ 🗑️ Delete
534
+ </button>
535
+ ` : ''}
536
+ <button onclick="mapApp.viewTreeDetails(${tree.id})"
537
+ style="flex: 1; background: linear-gradient(145deg, #059669, #047857); color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s;">
538
+ 👁️ View
539
+ </button>
540
  </div>
541
  </div>
542
  `;
 
543
 
544
+ marker.bindPopup(popupContent, {
545
+ maxWidth: 300,
546
+ className: 'tree-popup'
547
+ });
548
 
549
+ this.treeMarkers.push(marker);
 
 
550
  }
551
 
552
+ clearTreeMarkers() {
553
+ this.treeMarkers.forEach(marker => {
554
+ this.map.removeLayer(marker);
555
+ });
556
+ this.treeMarkers = [];
 
 
557
  }
558
 
559
+ // Tree management methods
560
+ async editTree(treeId) {
561
+ try {
562
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`);
563
+ if (!response) return;
564
+
565
+ if (!response.ok) {
566
+ throw new Error('Failed to fetch tree data');
567
+ }
568
+
569
+ // Store tree ID in localStorage for the form
570
+ localStorage.setItem('editTreeId', treeId);
571
 
572
+ this.showMessage('Loading tree for editing...', 'success');
573
 
574
+ // Redirect to form page
575
  setTimeout(() => {
576
+ window.location.href = '/';
577
+ }, 1000);
578
+
579
+ } catch (error) {
580
+ console.error('Error loading tree for edit:', error);
581
+ this.showMessage('Error loading tree data: ' + error.message, 'error');
582
  }
583
  }
584
+
585
+ async deleteTree(treeId) {
586
+ if (!confirm(`Are you sure you want to delete Tree #${treeId}? This action cannot be undone.`)) {
587
+ return;
588
+ }
589
+
590
+ try {
591
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`, {
592
+ method: 'DELETE'
593
+ });
594
+
595
+ if (!response) return;
596
+
597
+ if (response.ok) {
598
+ this.showMessage(`🗑️ Tree #${treeId} deleted successfully.`, 'success');
599
 
600
+ // Reload trees to update the map
601
+ setTimeout(() => {
602
+ this.loadTrees();
603
+ }, 1000);
604
+
605
+ // Close any open popups
606
+ this.map.closePopup();
607
 
608
+ } else {
609
+ const error = await response.json();
610
+ this.showMessage('Error deleting tree: ' + (error.detail || 'Unknown error'), 'error');
611
  }
612
+ } catch (error) {
613
+ console.error('Error deleting tree:', error);
614
+ this.showMessage('Network error: ' + error.message, 'error');
615
  }
616
  }
617
+
618
+ async viewTreeDetails(treeId) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  try {
620
+ const response = await this.authenticatedFetch(`/api/trees/${treeId}`);
621
+ if (!response) return;
 
622
 
623
+ if (!response.ok) {
624
+ throw new Error('Failed to fetch tree data');
625
+ }
626
 
627
+ const tree = await response.json();
 
 
 
 
628
 
629
+ // Create detailed view popup
630
+ const detailContent = `
631
+ <div style="max-width: 350px; font-family: 'Segoe UI', sans-serif;">
632
+ <div style="border-bottom: 2px solid #1e40af; padding-bottom: 12px; margin-bottom: 16px;">
633
+ <h2 style="margin: 0; color: #1e40af; font-size: 18px; font-weight: 700;">
634
+ 🌳 ${tree.scientific_name || tree.common_name || tree.local_name || 'Unknown Tree'}
635
+ </h2>
636
+ <div style="color: #6b7280; font-size: 14px; margin-top: 4px;">
637
+ Tree #${tree.id}${tree.tree_code ? ' • Code: ' + tree.tree_code : ''}
638
+ </div>
639
+ </div>
640
+
641
+ <div style="margin-bottom: 16px;">
642
+ <div style="display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; font-size: 13px;">
643
+ ${tree.local_name ? `<strong>🏷️ Local Name:</strong><span>${tree.local_name}</span>` : ''}
644
+ ${tree.scientific_name ? `<strong>🔬 Scientific:</strong><span><em>${tree.scientific_name}</em></span>` : ''}
645
+ ${tree.common_name ? `<strong>🌍 Common:</strong><span>${tree.common_name}</span>` : ''}
646
+ ${tree.height ? `<strong>📏 Height:</strong><span>${tree.height} meters</span>` : ''}
647
+ ${tree.width ? `<strong>📐 Girth:</strong><span>${tree.width} cm</span>` : ''}
648
+ <strong>📍 Coordinates:</strong><span>${tree.latitude.toFixed(6)}, ${tree.longitude.toFixed(6)}</span>
649
+ <strong>👤 Created by:</strong><span>${tree.created_by || 'Unknown'}</span>
650
+ <strong>📅 Date:</strong><span>${new Date(tree.created_at).toLocaleDateString()}</span>
651
+ </div>
652
+ </div>
653
+
654
+ ${tree.utility && tree.utility.length > 0 ? `
655
+ <div style="margin-bottom: 12px;">
656
+ <strong style="color: #059669; font-size: 14px;">🌿 Ecological Uses:</strong>
657
+ <div style="margin-top: 4px;">
658
+ ${tree.utility.map(u => `<span style="display: inline-block; background: #dcfce7; color: #166534; padding: 2px 6px; border-radius: 4px; font-size: 11px; margin: 2px;">${u}</span>`).join('')}
659
+ </div>
660
+ </div>
661
+ ` : ''}
662
+
663
+ ${tree.phenology_stages && tree.phenology_stages.length > 0 ? `
664
+ <div style="margin-bottom: 12px;">
665
+ <strong style="color: #7c3aed; font-size: 14px;">🌸 Current Stages:</strong>
666
+ <div style="margin-top: 4px;">
667
+ ${tree.phenology_stages.map(stage => `<span style="display: inline-block; background: #ede9fe; color: #6b21a8; padding: 2px 6px; border-radius: 4px; font-size: 11px; margin: 2px;">${stage}</span>`).join('')}
668
+ </div>
669
+ </div>
670
+ ` : ''}
671
+
672
+ ${tree.storytelling_text ? `
673
+ <div style="margin-bottom: 12px;">
674
+ <strong style="color: #ea580c; font-size: 14px;">📖 Stories & Culture:</strong>
675
+ <div style="color: #6b7280; font-size: 12px; margin-top: 4px; line-height: 1.4; max-height: 80px; overflow-y: auto; padding: 8px; background: #f9fafb; border-radius: 6px;">
676
+ ${tree.storytelling_text}
677
+ </div>
678
+ </div>
679
+ ` : ''}
680
+
681
+ ${tree.notes ? `
682
+ <div style="margin-bottom: 12px;">
683
+ <strong style="color: #374151; font-size: 14px;">📝 Notes:</strong>
684
+ <div style="color: #6b7280; font-size: 12px; margin-top: 4px; line-height: 1.4; padding: 8px; background: #f9fafb; border-radius: 6px;">
685
+ ${tree.notes}
686
+ </div>
687
+ </div>
688
+ ` : ''}
689
+ </div>
690
+ `;
691
+
692
+ // Close current popup and show detailed one
693
+ this.map.closePopup();
694
 
695
+ // Find the marker for this tree and open detailed popup
696
+ const treeMarker = this.treeMarkers.find(marker => {
697
+ return marker.getLatLng().lat === tree.latitude && marker.getLatLng().lng === tree.longitude;
698
+ });
699
+
700
+ if (treeMarker) {
701
+ treeMarker.bindPopup(detailContent, {
702
+ maxWidth: 400,
703
+ className: 'tree-popup'
704
+ }).openPopup();
705
+ }
706
 
707
  } catch (error) {
708
+ console.error('Error viewing tree details:', error);
709
+ this.showMessage('Error loading tree details: ' + error.message, 'error');
 
710
  }
711
  }
712
 
713
+ showLoading() {
714
+ document.getElementById('loading').style.display = 'block';
 
 
 
 
 
 
715
  }
716
 
717
+ hideLoading() {
718
+ document.getElementById('loading').style.display = 'none';
 
719
  }
720
 
721
+ showMessage(message, type = 'success') {
722
+ const messageElement = document.getElementById('message');
723
+ messageElement.textContent = message;
724
+ messageElement.className = `message ${type} show`;
 
 
725
 
726
+ setTimeout(() => {
727
+ messageElement.classList.remove('show');
728
+ }, 3000);
 
 
729
  }
730
 
731
+ showGestureHint() {
732
+ const hint = document.querySelector('.gesture-hint');
733
+ if (hint) {
734
+ hint.style.display = 'block';
 
 
 
 
 
 
735
  setTimeout(() => {
736
+ hint.style.display = 'none';
737
+ }, 4000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
739
  }
740
  }
741
 
742
+ // Initialize map when DOM is loaded
743
+ let mapApp;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
  document.addEventListener('DOMContentLoaded', () => {
745
+ console.log('DOM loaded, initializing TreeTrack Map...');
746
+ mapApp = new TreeTrackMap();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
747
  });