kamau1 commited on
Commit
710ac90
·
1 Parent(s): 3ebc5bc

added invited name to user invitaitons

Browse files
alembic/versions/20251120_add_invited_name_to_invitations.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """add invited_name to user_invitations
2
+
3
+ Revision ID: 20251120_invited_name
4
+ Revises:
5
+ Create Date: 2025-11-20 19:15:00
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '20251120_invited_name'
14
+ down_revision = None
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ """Add invited_name column to user_invitations table"""
21
+ op.add_column(
22
+ 'user_invitations',
23
+ sa.Column('invited_name', sa.String(length=200), nullable=True)
24
+ )
25
+
26
+
27
+ def downgrade() -> None:
28
+ """Remove invited_name column from user_invitations table"""
29
+ op.drop_column('user_invitations', 'invited_name')
docs/devlogs/browser/browserconsole.txt CHANGED
@@ -32,22 +32,22 @@ flushPassiveEffects @ chunk-276SZO74.js?v=23244404:19447
32
  workLoop @ chunk-276SZO74.js?v=23244404:197
33
  flushWork @ chunk-276SZO74.js?v=23244404:176
34
  performWorkUntilDeadline @ chunk-276SZO74.js?v=23244404:384
35
- core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me → 200 (2.48s)
36
- core.ts:117 ℹ️ [18:56:52] [COMPONENT] Dashboard component selected {role: 'client_admin', componentName: 'ClientAdminDashboard', componentExists: true}
37
- core.ts:124 ℹ️ [18:56:52] [DATA] Fetching organization admin dashboard data
38
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/users?skip=0&limit=20
39
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/projects?skip=0&limit=20
40
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences
41
- core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/users?skip=0&limit=20 → 200 (6.37s)
42
- core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/projects?skip=0&limit=20 → 200 (6.49s)
43
- core.ts:117 ℹ️ [18:56:59] [DATA] Organization dashboard data fetched {usersCount: 1, projectsCount: 0}
44
- core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences 200 (6.49s)
45
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/audit-logs?skip=0&limit=20
46
- core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/audit-logs?skip=0&limit=20 200 (1.87s)
47
- core.ts:117 ℹ️ [18:57:01] [DATA] Fetching available partner organizations {search: undefined, industry: undefined, isActive: true}
48
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/organizations/partners?is_active=true
49
- core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/organizations/partners?is_active=true → 200 (710ms)
50
- core.ts:117 ℹ️ [18:57:41] [DATA] Creating organization project {title: 'Atomio FTTx'}
 
51
  core.ts:167 %cPOST%c https://kamau1-swiftops-backend.hf.space/api/v1/projects
52
  organization.service.ts:414 POST https://kamau1-swiftops-backend.hf.space/api/v1/projects 500 (Internal Server Error)
53
  createOrgProject @ organization.service.ts:414
@@ -67,8 +67,8 @@ dispatchEventForPluginEventSystem @ chunk-276SZO74.js?v=23244404:7173
67
  dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-276SZO74.js?v=23244404:5478
68
  dispatchEvent @ chunk-276SZO74.js?v=23244404:5472
69
  dispatchDiscreteEvent @ chunk-276SZO74.js?v=23244404:5449
70
- core.ts:167 POST https://kamau1-swiftops-backend.hf.space/api/v1/projects → 500 (1.78s)
71
- core.ts:117 ❌ [18:57:43] [DATA] Failed to create project {title: 'Atomio FTTx', error: {…}}
72
  log @ core.ts:117
73
  error @ core.ts:157
74
  createOrgProject @ organization.service.ts:426
 
32
  workLoop @ chunk-276SZO74.js?v=23244404:197
33
  flushWork @ chunk-276SZO74.js?v=23244404:176
34
  performWorkUntilDeadline @ chunk-276SZO74.js?v=23244404:384
35
+ core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me → 200 (561ms)
36
+ core.ts:117 ℹ️ [19:03:33] [COMPONENT] Dashboard component selected {role: 'client_admin', componentName: 'ClientAdminDashboard', componentExists: true}
37
+ core.ts:124 ℹ️ [19:03:33] [DATA] Fetching organization admin dashboard data
38
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/users?skip=0&limit=20
39
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/projects?skip=0&limit=20
40
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences
41
+ core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/auth/me/preferences → 200 (437ms)
42
+ core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/projects?skip=0&limit=20 → 200 (802ms)
43
+ core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/users?skip=0&limit=20 200 (803ms)
44
+ core.ts:117 ℹ️ [19:03:34] [DATA] Organization dashboard data fetched {usersCount: 1, projectsCount: 0}
45
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/audit-logs?skip=0&limit=20
46
+ core.ts:117 ℹ️ [19:03:35] [DATA] Fetching available partner organizations {search: undefined, industry: undefined, isActive: true}
 
47
  core.ts:167 %cGET%c https://kamau1-swiftops-backend.hf.space/api/v1/organizations/partners?is_active=true
48
+ core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/audit-logs?skip=0&limit=20 → 200 (1.92s)
49
+ core.ts:167 GET https://kamau1-swiftops-backend.hf.space/api/v1/organizations/partners?is_active=true 200 (1.05s)
50
+ core.ts:117 ℹ️ [19:04:00] [DATA] Creating organization project {title: 'Atomio Fttx'}
51
  core.ts:167 %cPOST%c https://kamau1-swiftops-backend.hf.space/api/v1/projects
52
  organization.service.ts:414 POST https://kamau1-swiftops-backend.hf.space/api/v1/projects 500 (Internal Server Error)
53
  createOrgProject @ organization.service.ts:414
 
67
  dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay @ chunk-276SZO74.js?v=23244404:5478
68
  dispatchEvent @ chunk-276SZO74.js?v=23244404:5472
69
  dispatchDiscreteEvent @ chunk-276SZO74.js?v=23244404:5449
70
+ core.ts:167 POST https://kamau1-swiftops-backend.hf.space/api/v1/projects → 500 (781ms)
71
+ core.ts:117 ❌ [19:04:01] [DATA] Failed to create project {title: 'Atomio Fttx', error: {…}}
72
  log @ core.ts:117
73
  error @ core.ts:157
74
  createOrgProject @ organization.service.ts:426
src/app/api/v1/invitations.py CHANGED
@@ -273,6 +273,26 @@ async def validate_invitation_token(
273
  organization_name = 'SwiftOps Platform'
274
  organization_type = 'platform'
275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  return InvitationPublicResponse(
277
  id=invitation.id,
278
  email=invitation.email,
@@ -282,7 +302,9 @@ async def validate_invitation_token(
282
  organization_name=organization_name,
283
  organization_type=organization_type,
284
  is_expired=invitation.is_expired,
285
- is_valid=invitation.is_pending
 
 
286
  )
287
 
288
 
 
273
  organization_name = 'SwiftOps Platform'
274
  organization_type = 'platform'
275
 
276
+ # Split invited_name into suggested first and last names for form pre-filling
277
+ suggested_first_name = None
278
+ suggested_last_name = None
279
+
280
+ if invitation.invited_name:
281
+ # Smart name splitting
282
+ name_parts = invitation.invited_name.strip().split()
283
+ if len(name_parts) == 1:
284
+ # Single name - use as first name
285
+ suggested_first_name = name_parts[0]
286
+ elif len(name_parts) == 2:
287
+ # Two parts - first and last
288
+ suggested_first_name = name_parts[0]
289
+ suggested_last_name = name_parts[1]
290
+ else:
291
+ # Multiple parts - first word is first name, rest is last name
292
+ # e.g., "John Michael Doe" → first="John", last="Michael Doe"
293
+ suggested_first_name = name_parts[0]
294
+ suggested_last_name = " ".join(name_parts[1:])
295
+
296
  return InvitationPublicResponse(
297
  id=invitation.id,
298
  email=invitation.email,
 
302
  organization_name=organization_name,
303
  organization_type=organization_type,
304
  is_expired=invitation.is_expired,
305
+ is_valid=invitation.is_pending,
306
+ suggested_first_name=suggested_first_name,
307
+ suggested_last_name=suggested_last_name
308
  )
309
 
310
 
src/app/api/v1/projects.py CHANGED
@@ -60,12 +60,13 @@ async def create_project(
60
  # Audit log
61
  AuditService.log_action(
62
  db=db,
63
- user_id=current_user.id,
64
  action="create",
65
  entity_type="project",
66
- entity_id=project.id,
 
67
  changes={"status": "created"},
68
- ip_address=request.client.host if request.client else None
69
  )
70
 
71
  # Prepare response with nested data
@@ -214,10 +215,11 @@ async def update_project(
214
  # Audit log
215
  AuditService.log_action(
216
  db=db,
217
- user_id=current_user.id,
218
  action="update",
219
  entity_type="project",
220
- entity_id=project.id,
 
221
  changes={
222
  "old": old_data,
223
  "new": {
@@ -226,7 +228,7 @@ async def update_project(
226
  "service_type": project.service_type
227
  }
228
  },
229
- ip_address=request.client.host if request.client else None
230
  )
231
 
232
  # Prepare response
@@ -282,16 +284,18 @@ async def update_project_status(
282
  # Audit log
283
  AuditService.log_action(
284
  db=db,
285
- user_id=current_user.id,
286
  action="update",
287
  entity_type="project",
288
- entity_id=project.id,
 
289
  changes={
290
  "field": "status",
291
  "old": old_status,
292
  "new": data.status,
293
  "reason": data.reason
294
  },
 
295
  ip_address=request.client.host if request.client else None
296
  )
297
 
@@ -347,15 +351,16 @@ async def close_project(
347
  # Audit log
348
  AuditService.log_action(
349
  db=db,
350
- user_id=current_user.id,
351
  action="archive",
352
  entity_type="project",
353
- entity_id=project.id,
 
354
  changes={
355
  "action": "closed",
356
  "reason": data.reason
357
  },
358
- ip_address=request.client.host if request.client else None
359
  )
360
 
361
  # Prepare response
@@ -401,12 +406,13 @@ async def delete_project(
401
  # Audit log
402
  AuditService.log_action(
403
  db=db,
404
- user_id=current_user.id,
405
  action="delete",
406
  entity_type="project",
407
- entity_id=project_id,
 
408
  changes={"action": "soft_deleted"},
409
- ip_address=request.client.host if request.client else None
410
  )
411
 
412
  return None
 
60
  # Audit log
61
  AuditService.log_action(
62
  db=db,
63
+ user=current_user,
64
  action="create",
65
  entity_type="project",
66
+ entity_id=str(project.id),
67
+ description=f"Created project '{project.title}'",
68
  changes={"status": "created"},
69
+ request=request
70
  )
71
 
72
  # Prepare response with nested data
 
215
  # Audit log
216
  AuditService.log_action(
217
  db=db,
218
+ user=current_user,
219
  action="update",
220
  entity_type="project",
221
+ entity_id=str(project.id),
222
+ description=f"Updated project '{project.title}'",
223
  changes={
224
  "old": old_data,
225
  "new": {
 
228
  "service_type": project.service_type
229
  }
230
  },
231
+ request=request
232
  )
233
 
234
  # Prepare response
 
284
  # Audit log
285
  AuditService.log_action(
286
  db=db,
287
+ user=current_user,
288
  action="update",
289
  entity_type="project",
290
+ entity_id=str(project.id),
291
+ description=f"Changed project '{project.title}' status from {old_status} to {data.status}",
292
  changes={
293
  "field": "status",
294
  "old": old_status,
295
  "new": data.status,
296
  "reason": data.reason
297
  },
298
+ request=request
299
  ip_address=request.client.host if request.client else None
300
  )
301
 
 
351
  # Audit log
352
  AuditService.log_action(
353
  db=db,
354
+ user=current_user,
355
  action="archive",
356
  entity_type="project",
357
+ entity_id=str(project.id),
358
+ description=f"Closed project '{project.title}'",
359
  changes={
360
  "action": "closed",
361
  "reason": data.reason
362
  },
363
+ request=request
364
  )
365
 
366
  # Prepare response
 
406
  # Audit log
407
  AuditService.log_action(
408
  db=db,
409
+ user=current_user,
410
  action="delete",
411
  entity_type="project",
412
+ entity_id=str(project_id),
413
+ description=f"Deleted project (ID: {project_id})",
414
  changes={"action": "soft_deleted"},
415
+ request=request
416
  )
417
 
418
  return None
src/app/models/invitation.py CHANGED
@@ -18,6 +18,7 @@ class UserInvitation(BaseModel):
18
  # Invitation Details
19
  email = Column(String(255), nullable=False)
20
  phone = Column(String(50), nullable=True)
 
21
  invited_role = Column(String(50), nullable=False) # app_role ENUM
22
 
23
  # Organization Links
 
18
  # Invitation Details
19
  email = Column(String(255), nullable=False)
20
  phone = Column(String(50), nullable=True)
21
+ invited_name = Column(String(200), nullable=True) # Optional full name of invitee
22
  invited_role = Column(String(50), nullable=False) # app_role ENUM
23
 
24
  # Organization Links
src/app/schemas/invitation.py CHANGED
@@ -11,6 +11,11 @@ class InvitationCreate(BaseModel):
11
  """Schema for creating a user invitation"""
12
  email: EmailStr
13
  phone: Optional[str] = Field(None, max_length=50)
 
 
 
 
 
14
  invited_role: str = Field(..., description="User role (app_role enum)")
15
  client_id: Optional[UUID] = None
16
  contractor_id: Optional[UUID] = None
@@ -56,6 +61,7 @@ class InvitationResponse(BaseModel):
56
  id: UUID
57
  email: str
58
  phone: Optional[str]
 
59
  invited_role: str
60
  client_id: Optional[UUID]
61
  contractor_id: Optional[UUID]
@@ -89,6 +95,8 @@ class InvitationPublicResponse(BaseModel):
89
  organization_type: Optional[str] = None
90
  is_expired: bool
91
  is_valid: bool
 
 
92
 
93
 
94
  class InvitationValidate(BaseModel):
 
11
  """Schema for creating a user invitation"""
12
  email: EmailStr
13
  phone: Optional[str] = Field(None, max_length=50)
14
+ invited_name: Optional[str] = Field(
15
+ None,
16
+ max_length=200,
17
+ description="Full name of invitee (optional, will derive from email if not provided)"
18
+ )
19
  invited_role: str = Field(..., description="User role (app_role enum)")
20
  client_id: Optional[UUID] = None
21
  contractor_id: Optional[UUID] = None
 
61
  id: UUID
62
  email: str
63
  phone: Optional[str]
64
+ invited_name: Optional[str]
65
  invited_role: str
66
  client_id: Optional[UUID]
67
  contractor_id: Optional[UUID]
 
95
  organization_type: Optional[str] = None
96
  is_expired: bool
97
  is_valid: bool
98
+ suggested_first_name: Optional[str] = Field(None, description="Suggested first name from invited_name")
99
+ suggested_last_name: Optional[str] = Field(None, description="Suggested last name from invited_name")
100
 
101
 
102
  class InvitationValidate(BaseModel):
src/app/services/invitation_service.py CHANGED
@@ -98,6 +98,7 @@ class InvitationService:
98
  invitation = UserInvitation(
99
  email=invitation_data.email,
100
  phone=invitation_data.phone,
 
101
  invited_role=invitation_data.invited_role,
102
  client_id=invitation_data.client_id,
103
  contractor_id=invitation_data.contractor_id,
@@ -113,8 +114,8 @@ class InvitationService:
113
  db.commit()
114
  db.refresh(invitation)
115
 
116
- # Send notification
117
- name = invitation_data.email.split('@')[0].title() # Use email prefix as name
118
  await self._send_invitation_notification(
119
  invitation=invitation,
120
  name=name,
@@ -271,8 +272,8 @@ class InvitationService:
271
  if method:
272
  invitation.invitation_method = method
273
 
274
- # Send notification (with new token if regenerated)
275
- name = invitation.email.split('@')[0].title()
276
  await self._send_invitation_notification(
277
  invitation=invitation,
278
  name=name,
 
98
  invitation = UserInvitation(
99
  email=invitation_data.email,
100
  phone=invitation_data.phone,
101
+ invited_name=invitation_data.invited_name,
102
  invited_role=invitation_data.invited_role,
103
  client_id=invitation_data.client_id,
104
  contractor_id=invitation_data.contractor_id,
 
114
  db.commit()
115
  db.refresh(invitation)
116
 
117
+ # Send notification - use provided name or derive from email
118
+ name = invitation_data.invited_name or invitation_data.email.split('@')[0].title()
119
  await self._send_invitation_notification(
120
  invitation=invitation,
121
  name=name,
 
272
  if method:
273
  invitation.invitation_method = method
274
 
275
+ # Send notification (with new token if regenerated) - use provided name or derive from email
276
+ name = invitation.invited_name or invitation.email.split('@')[0].title()
277
  await self._send_invitation_notification(
278
  invitation=invitation,
279
  name=name,