Shri commited on
Commit
e8f820d
·
1 Parent(s): fd50b36

feat: added leave and userdevice models

Browse files
alembic/versions/e95f62f91348_added_leave_and_userdevice_table.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """added: leave and userdevice table
2
+
3
+ Revision ID: e95f62f91348
4
+ Revises: a3c79664f866
5
+ Create Date: 2025-11-22 15:42:12.098237
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ import sqlmodel.sql.sqltypes
13
+ from sqlalchemy.dialects import postgresql
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = 'e95f62f91348'
17
+ down_revision: Union[str, Sequence[str], None] = 'a3c79664f866'
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Upgrade schema."""
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ op.drop_column('users', 'device_tokens')
26
+ # ### end Alembic commands ###
27
+
28
+
29
+ def downgrade() -> None:
30
+ """Downgrade schema."""
31
+ # ### commands auto generated by Alembic - please adjust! ###
32
+ op.add_column('users', sa.Column('device_tokens', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True))
33
+ # ### end Alembic commands ###
src/core/models.py CHANGED
@@ -4,7 +4,6 @@ import uuid
4
  from datetime import date, datetime
5
  from enum import Enum
6
  from typing import List, Optional
7
- from src.profile.models import Leaves
8
 
9
 
10
  from sqlalchemy import CheckConstraint, UniqueConstraint
 
4
  from datetime import date, datetime
5
  from enum import Enum
6
  from typing import List, Optional
 
7
 
8
 
9
  from sqlalchemy import CheckConstraint, UniqueConstraint
src/main.py CHANGED
@@ -1,4 +1,4 @@
1
- from src.profile.router import router as profile
2
  from fastapi import FastAPI
3
 
4
 
@@ -7,17 +7,17 @@ from src.chatbot.router import router as chatbot
7
  from src.core.database import init_db
8
  from src.home.router import router as home_router
9
 
10
- from src.profile.router import router as profile
11
 
12
  app = FastAPI(title="Yuvabe App API")
13
 
14
  app.include_router(home_router, prefix="/home", tags=["Home"])
15
 
16
- # init_db()
17
 
18
  app.include_router(auth_router)
19
 
20
- app.include_router(profile)
21
 
22
  app.include_router(chatbot)
23
 
 
1
+ # from src.profile.router import router as profile
2
  from fastapi import FastAPI
3
 
4
 
 
7
  from src.core.database import init_db
8
  from src.home.router import router as home_router
9
 
10
+ # from src.profile.router import router as profile
11
 
12
  app = FastAPI(title="Yuvabe App API")
13
 
14
  app.include_router(home_router, prefix="/home", tags=["Home"])
15
 
16
+ init_db()
17
 
18
  app.include_router(auth_router)
19
 
20
+ # app.include_router(profile)
21
 
22
  app.include_router(chatbot)
23
 
src/profile/models.py CHANGED
@@ -1,39 +1,45 @@
 
 
1
  import uuid
2
  from datetime import date, datetime
3
- from typing import Optional
4
- from sqlmodel import SQLModel, Field
5
  from enum import Enum
 
6
 
 
7
 
8
- class LeaveStatus(str, Enum):
9
- PENDING = "PENDING"
10
- APPROVED = "APPROVED"
11
- REJECTED = "REJECTED"
12
-
13
 
14
- class Leaves(SQLModel, table=True):
15
- __tablename__ = "leaves"
 
 
16
 
 
 
17
  id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
18
-
19
- # Foreign keys (users table)
20
  user_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
21
  mentor_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
22
  lead_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
23
-
24
- leave_type: str = Field(nullable=False)
25
  from_date: date = Field(nullable=False)
26
  to_date: date = Field(nullable=False)
27
- days: int = Field(nullable=False)
28
- reason: Optional[str] = None
29
-
30
  status: LeaveStatus = Field(default=LeaveStatus.PENDING)
31
-
32
- approved_by: Optional[uuid.UUID] = Field(foreign_key="users.id", default=None)
33
- approved_at: Optional[datetime] = None
34
  reject_reason: Optional[str] = None
35
 
36
- comment: Optional[str] = None
37
-
38
- created_at: datetime = Field(default_factory=datetime.now)
39
- updated_at: datetime = Field(default_factory=datetime.now)
 
 
 
 
 
 
1
+ from sqlalchemy.dialects.postgresql import ARRAY
2
+ from sqlalchemy import Column, String
3
  import uuid
4
  from datetime import date, datetime
 
 
5
  from enum import Enum
6
+ from typing import List, Optional
7
 
8
+ from sqlmodel import Field, Relationship, SQLModel
9
 
10
+ class LeaveType(str, Enum):
11
+ SICK = "Sick"
12
+ CASUAL = "Casual"
13
+ EMERGENCY = "Emergency"
 
14
 
15
+ class LeaveStatus(str, Enum):
16
+ APPROVED = "Approved"
17
+ REJECTED = "Rejected"
18
+ PENDING = "Pending"
19
 
20
+ class Leave(SQLModel, table=True):
21
+ __tablename__ = "leave"
22
  id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
 
 
23
  user_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
24
  mentor_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
25
  lead_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
26
+ leave_type: LeaveType = Field(default=LeaveType.SICK)
 
27
  from_date: date = Field(nullable=False)
28
  to_date: date = Field(nullable=False)
29
+ days: Optional[int] = 1
30
+ reason: str = Field(nullable=True)
 
31
  status: LeaveStatus = Field(default=LeaveStatus.PENDING)
32
+ is_delivered: bool = Field(default= False)
33
+ requested_at: date = Field(default_factory=date.today)
34
+ updated_at: date = Field(default_factory=date.today)
35
  reject_reason: Optional[str] = None
36
 
37
+ class UserDevices(SQLModel, table=True):
38
+ __tablename__ = "user_devices"
39
+ id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
40
+ user_id: uuid.UUID = Field(foreign_key="users.id", nullable=False)
41
+ device_token: str
42
+ last_seen: datetime = Field(default_factory=datetime.now)
43
+ device_model: str
44
+ platform: str
45
+ updated_at: datetime = Field(default_factory=datetime.now)
src/profile/router.py CHANGED
@@ -1,282 +1,282 @@
1
- from src.profile.service import send_email_service
2
- from src.profile.schemas import SendMailRequest
3
- from src.profile.utils import build_auth_url
4
- from src.profile.utils import exchange_code_for_tokens
5
- from src.profile.service import USER_TOKEN_STORE
6
- from src.profile.utils import send_email
7
- from src.profile.service import list_user_assets
8
- from fastapi.routing import APIRouter
9
- from src.core.database import get_async_session
10
- from src.auth.utils import get_current_user
11
- from src.auth.schemas import BaseResponse
12
- from sqlalchemy.ext.asyncio.session import AsyncSession
13
- from fastapi.params import Depends
14
- from .schemas import UpdateProfileRequest
15
- from src.profile.service import update_user_profile
16
- from sqlmodel import select
17
- from src.core.models import Users, Teams, Roles, UserTeamsRole
18
- from fastapi import APIRouter, Query, HTTPException ,BackgroundTasks
19
- from fastapi.responses import RedirectResponse, JSONResponse
20
- import httpx
21
- from fastapi import APIRouter, Depends, HTTPException, status
22
- from sqlmodel.ext.asyncio.session import AsyncSession
23
- from src.auth.utils import get_current_user # adjust path
24
- from src.profile.schemas import (
25
- ApplyLeaveRequest,
26
- ApproveRejectRequest,
27
- LeaveResponse,
28
- BalanceResponse,
29
- DeviceTokenIn,
30
- )
31
- from src.profile import service
32
- from typing import List
33
-
34
-
35
- router = APIRouter(prefix="/profile", tags=["Profile"])
36
-
37
-
38
- @router.post("/apply", response_model=LeaveResponse)
39
- async def apply_leave_endpoint(
40
- payload: ApplyLeaveRequest,
41
- session: AsyncSession = Depends(get_async_session),
42
- current_user=Depends(get_current_user),
43
- ):
44
- user_id = current_user
45
- leave = await service.apply_leave(session, user_id, payload)
46
- return leave
47
-
48
-
49
- @router.get("/pending", response_model=List[LeaveResponse])
50
- async def pending_leaves(
51
- session: AsyncSession = Depends(get_async_session),
52
- current_user=Depends(get_current_user),
53
- ):
54
- user_id = current_user
55
- leaves = await service.get_pending_leaves_for_approver(session, user_id)
56
- return leaves
57
-
58
-
59
- @router.get("/my", response_model=List[LeaveResponse])
60
- async def my_leaves(
61
- session: AsyncSession = Depends(get_async_session),
62
- current_user=Depends(get_current_user),
63
- ):
64
- leaves = await service.get_my_leaves(session, current_user)
65
- return leaves
66
-
67
-
68
- @router.get("/team", response_model=List[LeaveResponse])
69
- async def team_leaves(
70
- session: AsyncSession = Depends(get_async_session),
71
- current_user=Depends(get_current_user),
72
- ):
73
- leaves = await service.get_team_leaves(session, current_user)
74
- return leaves
75
-
76
-
77
- @router.post("/{leave_id}/approve", response_model=LeaveResponse)
78
- async def approve_leave_endpoint(
79
- leave_id: str,
80
- payload: ApproveRejectRequest,
81
- session: AsyncSession = Depends(get_async_session),
82
- current_user=Depends(get_current_user),
83
- ):
84
- leave = await service.approve_leave(
85
- session, current_user, leave_id, comment=payload.comment
86
- )
87
- return leave
88
-
89
-
90
- @router.post("/{leave_id}/reject", response_model=LeaveResponse)
91
- async def reject_leave_endpoint(
92
- leave_id: str,
93
- payload: ApproveRejectRequest,
94
- session: AsyncSession = Depends(get_async_session),
95
- current_user=Depends(get_current_user),
96
- ):
97
- leave = await service.reject_leave(
98
- session,
99
- current_user,
100
- leave_id,
101
- reject_reason=payload.reject_reason,
102
- comment=payload.comment,
103
- )
104
- return leave
105
-
106
-
107
- @router.get("/balance", response_model=List[BalanceResponse])
108
- async def get_balance(
109
- session: AsyncSession = Depends(get_async_session),
110
- current_user=Depends(get_current_user),
111
- ):
112
- return await service.get_leave_balance(session, current_user)
113
-
114
-
115
- @router.post("/device-token")
116
- async def save_device_token(
117
- payload: DeviceTokenIn,
118
- session: AsyncSession = Depends(get_async_session),
119
- current_user=Depends(get_current_user),
120
- ):
121
- tokens = await service.add_device_token(session, current_user, payload.device_token)
122
- return {"status": "ok", "tokens": tokens}
123
-
124
-
125
-
126
-
127
- @router.get("/login")
128
- def google_login(state: str | None = Query(None)):
129
- return RedirectResponse(build_auth_url(state))
130
-
131
-
132
- @router.get("/callback")
133
- async def google_callback(code: str | None = None, state: str | None = None):
134
- if not code:
135
- raise HTTPException(400, "Missing code")
136
-
137
- token_data = await exchange_code_for_tokens(code)
138
- access_token = token_data["access_token"]
139
- refresh_token = token_data.get("refresh_token")
140
-
141
- # Get user info
142
- async with httpx.AsyncClient() as client:
143
- r = await client.get(
144
- "https://www.googleapis.com/oauth2/v3/userinfo",
145
- headers={"Authorization": f"Bearer {access_token}"},
146
- )
147
- userinfo = r.json()
148
-
149
- google_user_id = userinfo["sub"]
150
- user_email = userinfo["email"]
151
-
152
- USER_TOKEN_STORE[google_user_id] = {
153
- "access_token": access_token,
154
- "refresh_token": refresh_token,
155
- "email": user_email,
156
- }
157
-
158
- return JSONResponse(
159
- {
160
- "status": "ok",
161
- "user_id": google_user_id,
162
- "email": user_email,
163
- "state": state,
164
- }
165
- )
166
-
167
-
168
- @router.post("/send-mail")
169
- async def send_mail(req: SendMailRequest):
170
- return await send_email_service(req)
171
-
172
-
173
- @router.get("/", response_model=BaseResponse)
174
- async def get_assets(
175
- user_id: str = Depends(get_current_user),
176
- session: AsyncSession = Depends(get_async_session),
177
- ):
178
- assets = await list_user_assets(session, user_id)
179
-
180
- data = {
181
- "assets": [
182
- {
183
- "id": a.id,
184
- "name": a.name,
185
- "type": a.type,
186
- "status": a.status,
187
- }
188
- for a in assets
189
- ]
190
- }
191
-
192
- return {"code": 200, "data": data}
193
-
194
-
195
- @router.put("/update-profile", response_model=BaseResponse)
196
- async def update_profile(
197
- payload: UpdateProfileRequest,
198
- user_id: str = Depends(get_current_user),
199
- session: AsyncSession = Depends(get_async_session),
200
- ):
201
- result = await update_user_profile(session, user_id, payload)
202
- return {"code": 200, "data": result}
203
-
204
-
205
- @router.get("/contacts", response_model=BaseResponse)
206
- async def get_leave_contacts(
207
- current_user=Depends(get_current_user),
208
- session: AsyncSession = Depends(get_async_session),
209
- ):
210
- # get_current_user returns a STRING user_id
211
- user_id = current_user
212
-
213
- if not user_id:
214
- raise HTTPException(status_code=400, detail="Invalid user token")
215
-
216
- # 1) Get user's team
217
- stmt = select(UserTeamsRole).where(UserTeamsRole.user_id == user_id)
218
- ut = (await session.exec(stmt)).first()
219
-
220
- if not ut:
221
- raise HTTPException(status_code=404, detail="User-Team mapping not found")
222
-
223
- # 2) Get Team Lead role
224
- lead_role = (
225
- await session.exec(select(Roles).where(Roles.name == "Team Lead"))
226
- ).first()
227
-
228
- if not lead_role:
229
- raise HTTPException(status_code=500, detail="Team Lead role not found")
230
-
231
- # 3) Find Team Lead user in same team
232
- lead_user = (
233
- await session.exec(
234
- select(Users)
235
- .join(UserTeamsRole)
236
- .where(UserTeamsRole.team_id == ut.team_id)
237
- .where(UserTeamsRole.role_id == lead_role.id)
238
- )
239
- ).all()
240
-
241
- if not lead_user:
242
- raise HTTPException(status_code=404, detail="Team lead not found")
243
-
244
- to_email = ", ".join([u.email_id for u in lead_user])
245
-
246
- # 4) HR CC emails
247
- hr_team = (await session.exec(select(Teams).where(Teams.name == "HR Team"))).first()
248
-
249
- cc = []
250
- if hr_team:
251
- hr_users = (
252
- await session.exec(
253
- select(Users)
254
- .join(UserTeamsRole)
255
- .where(UserTeamsRole.team_id == hr_team.id)
256
- )
257
- ).all()
258
-
259
- cc = [str(row.email_id) for row in hr_users]
260
-
261
- return BaseResponse(code=200, message="success", data={"to": to_email, "cc": cc})
262
-
263
-
264
- @router.post("/send", response_model=BaseResponse)
265
- async def send_leave_email(
266
- payload: dict,
267
- background: BackgroundTasks,
268
- current_user=Depends(get_current_user),
269
- ):
270
- from_email = payload.get("from_email")
271
- to_email = payload.get("to")
272
- cc = payload.get("cc", [])
273
- subject = payload.get("subject")
274
- body = payload.get("body")
275
-
276
- if not subject or not body:
277
- raise HTTPException(status_code=400, detail="Subject and body required")
278
-
279
- # send in background so API returns fast
280
- background.add_task(send_email, to_email, subject, body, cc, from_email)
281
-
282
- return BaseResponse(code=200, message="Leave request sent", data=None)
 
1
+ # from src.profile.service import send_email_service
2
+ # from src.profile.schemas import SendMailRequest
3
+ # from src.profile.utils import build_auth_url
4
+ # from src.profile.utils import exchange_code_for_tokens
5
+ # from src.profile.service import USER_TOKEN_STORE
6
+ # from src.profile.utils import send_email
7
+ # from src.profile.service import list_user_assets
8
+ # from fastapi.routing import APIRouter
9
+ # from src.core.database import get_async_session
10
+ # from src.auth.utils import get_current_user
11
+ # from src.auth.schemas import BaseResponse
12
+ # from sqlalchemy.ext.asyncio.session import AsyncSession
13
+ # from fastapi.params import Depends
14
+ # from .schemas import UpdateProfileRequest
15
+ # from src.profile.service import update_user_profile
16
+ # from sqlmodel import select
17
+ # from src.core.models import Users, Teams, Roles, UserTeamsRole
18
+ # from fastapi import APIRouter, Query, HTTPException ,BackgroundTasks
19
+ # from fastapi.responses import RedirectResponse, JSONResponse
20
+ # import httpx
21
+ # from fastapi import APIRouter, Depends, HTTPException, status
22
+ # from sqlmodel.ext.asyncio.session import AsyncSession
23
+ # from src.auth.utils import get_current_user # adjust path
24
+ # from src.profile.schemas import (
25
+ # ApplyLeaveRequest,
26
+ # ApproveRejectRequest,
27
+ # LeaveResponse,
28
+ # BalanceResponse,
29
+ # DeviceTokenIn,
30
+ # )
31
+ # from src.profile import service
32
+ # from typing import List
33
+
34
+
35
+ # router = APIRouter(prefix="/profile", tags=["Profile"])
36
+
37
+
38
+ # @router.post("/apply", response_model=LeaveResponse)
39
+ # async def apply_leave_endpoint(
40
+ # payload: ApplyLeaveRequest,
41
+ # session: AsyncSession = Depends(get_async_session),
42
+ # current_user=Depends(get_current_user),
43
+ # ):
44
+ # user_id = current_user
45
+ # leave = await service.apply_leave(session, user_id, payload)
46
+ # return leave
47
+
48
+
49
+ # @router.get("/pending", response_model=List[LeaveResponse])
50
+ # async def pending_leaves(
51
+ # session: AsyncSession = Depends(get_async_session),
52
+ # current_user=Depends(get_current_user),
53
+ # ):
54
+ # user_id = current_user
55
+ # leaves = await service.get_pending_leaves_for_approver(session, user_id)
56
+ # return leaves
57
+
58
+
59
+ # @router.get("/my", response_model=List[LeaveResponse])
60
+ # async def my_leaves(
61
+ # session: AsyncSession = Depends(get_async_session),
62
+ # current_user=Depends(get_current_user),
63
+ # ):
64
+ # leaves = await service.get_my_leaves(session, current_user)
65
+ # return leaves
66
+
67
+
68
+ # @router.get("/team", response_model=List[LeaveResponse])
69
+ # async def team_leaves(
70
+ # session: AsyncSession = Depends(get_async_session),
71
+ # current_user=Depends(get_current_user),
72
+ # ):
73
+ # leaves = await service.get_team_leaves(session, current_user)
74
+ # return leaves
75
+
76
+
77
+ # @router.post("/{leave_id}/approve", response_model=LeaveResponse)
78
+ # async def approve_leave_endpoint(
79
+ # leave_id: str,
80
+ # payload: ApproveRejectRequest,
81
+ # session: AsyncSession = Depends(get_async_session),
82
+ # current_user=Depends(get_current_user),
83
+ # ):
84
+ # leave = await service.approve_leave(
85
+ # session, current_user, leave_id, comment=payload.comment
86
+ # )
87
+ # return leave
88
+
89
+
90
+ # @router.post("/{leave_id}/reject", response_model=LeaveResponse)
91
+ # async def reject_leave_endpoint(
92
+ # leave_id: str,
93
+ # payload: ApproveRejectRequest,
94
+ # session: AsyncSession = Depends(get_async_session),
95
+ # current_user=Depends(get_current_user),
96
+ # ):
97
+ # leave = await service.reject_leave(
98
+ # session,
99
+ # current_user,
100
+ # leave_id,
101
+ # reject_reason=payload.reject_reason,
102
+ # comment=payload.comment,
103
+ # )
104
+ # return leave
105
+
106
+
107
+ # @router.get("/balance", response_model=List[BalanceResponse])
108
+ # async def get_balance(
109
+ # session: AsyncSession = Depends(get_async_session),
110
+ # current_user=Depends(get_current_user),
111
+ # ):
112
+ # return await service.get_leave_balance(session, current_user)
113
+
114
+
115
+ # @router.post("/device-token")
116
+ # async def save_device_token(
117
+ # payload: DeviceTokenIn,
118
+ # session: AsyncSession = Depends(get_async_session),
119
+ # current_user=Depends(get_current_user),
120
+ # ):
121
+ # tokens = await service.add_device_token(session, current_user, payload.device_token)
122
+ # return {"status": "ok", "tokens": tokens}
123
+
124
+
125
+
126
+
127
+ # @router.get("/login")
128
+ # def google_login(state: str | None = Query(None)):
129
+ # return RedirectResponse(build_auth_url(state))
130
+
131
+
132
+ # @router.get("/callback")
133
+ # async def google_callback(code: str | None = None, state: str | None = None):
134
+ # if not code:
135
+ # raise HTTPException(400, "Missing code")
136
+
137
+ # token_data = await exchange_code_for_tokens(code)
138
+ # access_token = token_data["access_token"]
139
+ # refresh_token = token_data.get("refresh_token")
140
+
141
+ # # Get user info
142
+ # async with httpx.AsyncClient() as client:
143
+ # r = await client.get(
144
+ # "https://www.googleapis.com/oauth2/v3/userinfo",
145
+ # headers={"Authorization": f"Bearer {access_token}"},
146
+ # )
147
+ # userinfo = r.json()
148
+
149
+ # google_user_id = userinfo["sub"]
150
+ # user_email = userinfo["email"]
151
+
152
+ # USER_TOKEN_STORE[google_user_id] = {
153
+ # "access_token": access_token,
154
+ # "refresh_token": refresh_token,
155
+ # "email": user_email,
156
+ # }
157
+
158
+ # return JSONResponse(
159
+ # {
160
+ # "status": "ok",
161
+ # "user_id": google_user_id,
162
+ # "email": user_email,
163
+ # "state": state,
164
+ # }
165
+ # )
166
+
167
+
168
+ # @router.post("/send-mail")
169
+ # async def send_mail(req: SendMailRequest):
170
+ # return await send_email_service(req)
171
+
172
+
173
+ # @router.get("/", response_model=BaseResponse)
174
+ # async def get_assets(
175
+ # user_id: str = Depends(get_current_user),
176
+ # session: AsyncSession = Depends(get_async_session),
177
+ # ):
178
+ # assets = await list_user_assets(session, user_id)
179
+
180
+ # data = {
181
+ # "assets": [
182
+ # {
183
+ # "id": a.id,
184
+ # "name": a.name,
185
+ # "type": a.type,
186
+ # "status": a.status,
187
+ # }
188
+ # for a in assets
189
+ # ]
190
+ # }
191
+
192
+ # return {"code": 200, "data": data}
193
+
194
+
195
+ # @router.put("/update-profile", response_model=BaseResponse)
196
+ # async def update_profile(
197
+ # payload: UpdateProfileRequest,
198
+ # user_id: str = Depends(get_current_user),
199
+ # session: AsyncSession = Depends(get_async_session),
200
+ # ):
201
+ # result = await update_user_profile(session, user_id, payload)
202
+ # return {"code": 200, "data": result}
203
+
204
+
205
+ # @router.get("/contacts", response_model=BaseResponse)
206
+ # async def get_leave_contacts(
207
+ # current_user=Depends(get_current_user),
208
+ # session: AsyncSession = Depends(get_async_session),
209
+ # ):
210
+ # # get_current_user returns a STRING user_id
211
+ # user_id = current_user
212
+
213
+ # if not user_id:
214
+ # raise HTTPException(status_code=400, detail="Invalid user token")
215
+
216
+ # # 1) Get user's team
217
+ # stmt = select(UserTeamsRole).where(UserTeamsRole.user_id == user_id)
218
+ # ut = (await session.exec(stmt)).first()
219
+
220
+ # if not ut:
221
+ # raise HTTPException(status_code=404, detail="User-Team mapping not found")
222
+
223
+ # # 2) Get Team Lead role
224
+ # lead_role = (
225
+ # await session.exec(select(Roles).where(Roles.name == "Team Lead"))
226
+ # ).first()
227
+
228
+ # if not lead_role:
229
+ # raise HTTPException(status_code=500, detail="Team Lead role not found")
230
+
231
+ # # 3) Find Team Lead user in same team
232
+ # lead_user = (
233
+ # await session.exec(
234
+ # select(Users)
235
+ # .join(UserTeamsRole)
236
+ # .where(UserTeamsRole.team_id == ut.team_id)
237
+ # .where(UserTeamsRole.role_id == lead_role.id)
238
+ # )
239
+ # ).all()
240
+
241
+ # if not lead_user:
242
+ # raise HTTPException(status_code=404, detail="Team lead not found")
243
+
244
+ # to_email = ", ".join([u.email_id for u in lead_user])
245
+
246
+ # # 4) HR CC emails
247
+ # hr_team = (await session.exec(select(Teams).where(Teams.name == "HR Team"))).first()
248
+
249
+ # cc = []
250
+ # if hr_team:
251
+ # hr_users = (
252
+ # await session.exec(
253
+ # select(Users)
254
+ # .join(UserTeamsRole)
255
+ # .where(UserTeamsRole.team_id == hr_team.id)
256
+ # )
257
+ # ).all()
258
+
259
+ # cc = [str(row.email_id) for row in hr_users]
260
+
261
+ # return BaseResponse(code=200, message="success", data={"to": to_email, "cc": cc})
262
+
263
+
264
+ # @router.post("/send", response_model=BaseResponse)
265
+ # async def send_leave_email(
266
+ # payload: dict,
267
+ # background: BackgroundTasks,
268
+ # current_user=Depends(get_current_user),
269
+ # ):
270
+ # from_email = payload.get("from_email")
271
+ # to_email = payload.get("to")
272
+ # cc = payload.get("cc", [])
273
+ # subject = payload.get("subject")
274
+ # body = payload.get("body")
275
+
276
+ # if not subject or not body:
277
+ # raise HTTPException(status_code=400, detail="Subject and body required")
278
+
279
+ # # send in background so API returns fast
280
+ # background.add_task(send_email, to_email, subject, body, cc, from_email)
281
+
282
+ # return BaseResponse(code=200, message="Leave request sent", data=None)
src/profile/service.py CHANGED
@@ -1,384 +1,383 @@
1
- from src.profile.utils import build_raw_message, refresh_access_token
2
- from src.profile.schemas import SendMailRequest
3
- from src.core.models import Assets, Users
4
- from datetime import datetime
5
- import uuid
6
- from fastapi import HTTPException
7
- from passlib.context import CryptContext
8
- from src.core.models import Users
9
- import uuid
10
- from typing import List
11
- from sqlmodel import select
12
- from sqlmodel.ext.asyncio.session import AsyncSession
13
- import httpx
14
- from src.core.config import settings
15
- from typing import List, Optional
16
- from sqlmodel import select
17
- from sqlmodel.ext.asyncio.session import AsyncSession
18
- from fastapi import HTTPException
19
- from datetime import datetime
20
- from src.profile.models import Leaves
21
- from src.profile.schemas import ApplyLeaveRequest, ApproveRejectRequest
22
- from src.profile.utils import calculate_days, find_mentor_and_lead
23
- from src.profile.utils import get_tokens_for_user, send_push_to_tokens
24
- from src.core.config import settings
25
-
26
-
27
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
28
-
29
- # src/profile/service.py
30
-
31
- # Leave limits (you can move to config)
32
- SICK_LIMIT = getattr(settings, "SICK_LEAVE_LIMIT", 10)
33
- CASUAL_LIMIT = getattr(settings, "CASUAL_LEAVE_LIMIT", 10)
34
-
35
-
36
- async def apply_leave(session: AsyncSession, user_id, payload: ApplyLeaveRequest):
37
- # compute days
38
- days = calculate_days(payload.from_date, payload.to_date)
39
- if days <= 0:
40
- raise HTTPException(status_code=400, detail="Invalid date range")
41
-
42
- # find mentor and lead
43
- mentor, lead = await find_mentor_and_lead(session, user_id)
44
- if not mentor or not lead:
45
- raise HTTPException(status_code=400, detail="Mentor or Lead not found for user")
46
-
47
- # check remaining balance
48
- limit = SICK_LIMIT if payload.leave_type.lower() == "sick" else CASUAL_LIMIT
49
- # sum used days for this leave_type
50
- q = select(Leaves).where(
51
- Leaves.user_id == user_id,
52
- Leaves.leave_type.ilike(payload.leave_type),
53
- Leaves.status == "APPROVED",
54
- )
55
- rows = (await session.exec(q)).all()
56
- used = sum(r.days for r in rows) if rows else 0
57
- remaining = limit - used
58
- if days > remaining:
59
- raise HTTPException(
60
- status_code=400,
61
- detail=f"Insufficient {payload.leave_type} balance. Remaining {remaining}",
62
- )
63
-
64
- leave = Leaves(
65
- user_id=user_id,
66
- mentor_id=mentor.id,
67
- lead_id=lead.id,
68
- leave_type=payload.leave_type,
69
- from_date=payload.from_date,
70
- to_date=payload.to_date,
71
- days=days,
72
- reason=payload.reason,
73
- status="PENDING",
74
- )
75
- session.add(leave)
76
- await session.commit()
77
- await session.refresh(leave)
78
-
79
- # push notifications to mentor & lead
80
- title = "New Leave Request"
81
- body = f"{user_id} applied {payload.leave_type} leave ({days} days)."
82
- mentor_tokens = await get_tokens_for_user(session, mentor.id)
83
- lead_tokens = await get_tokens_for_user(session, lead.id)
84
- await send_push_to_tokens(
85
- mentor_tokens,
86
- title,
87
- body,
88
- data={"leave_id": str(leave.id), "action": "leave_request"},
89
- )
90
- await send_push_to_tokens(
91
- lead_tokens,
92
- title,
93
- body,
94
- data={"leave_id": str(leave.id), "action": "leave_request"},
95
- )
96
-
97
- return leave
98
-
99
-
100
- async def get_pending_leaves_for_approver(
101
- session: AsyncSession, approver_user_id
102
- ) -> List[Leaves]:
103
- # returns pending leaves where mentor_id == approver OR lead_id == approver
104
- stmt = select(Leaves).where(
105
- (Leaves.mentor_id == approver_user_id) | (Leaves.lead_id == approver_user_id),
106
- Leaves.status == "PENDING",
107
- )
108
- return (await session.exec(stmt)).all()
109
-
110
-
111
- async def get_my_leaves(session: AsyncSession, user_id) -> List[Leaves]:
112
- stmt = (
113
- select(Leaves)
114
- .where(Leaves.user_id == user_id)
115
- .order_by(Leaves.created_at.desc())
116
- )
117
- return (await session.exec(stmt)).all()
118
-
119
-
120
- async def get_team_leaves(session: AsyncSession, lead_user_id) -> List[Leaves]:
121
- # lead can view leaves where lead_id == lead_user_id
122
- stmt = (
123
- select(Leaves)
124
- .where(Leaves.lead_id == lead_user_id)
125
- .order_by(Leaves.created_at.desc())
126
- )
127
- return (await session.exec(stmt)).all()
128
-
129
-
130
- async def approve_leave(
131
- session: AsyncSession, approver_id, leave_id: str, comment: Optional[str] = None
132
- ):
133
- # transaction-safe update
134
- async with session.begin():
135
- stmt = select(Leaves).where(Leaves.id == leave_id).with_for_update()
136
- leave = (await session.exec(stmt)).one_or_none()
137
- if not leave:
138
- raise HTTPException(404, "Leave not found")
139
- if leave.status != "PENDING":
140
- raise HTTPException(400, "Leave is not pending")
141
-
142
- # optional: verify approver is mentor or lead for this leave
143
- if str(approver_id) not in (str(leave.mentor_id), str(leave.lead_id)):
144
- # you might want to check roles more thoroughly
145
- raise HTTPException(403, "Not authorized to approve this leave")
146
-
147
- # check balance again before approving
148
- # compute limit and used
149
- limit = SICK_LIMIT if leave.leave_type.lower() == "sick" else CASUAL_LIMIT
150
- q = select(Leaves).where(
151
- Leaves.user_id == leave.user_id,
152
- Leaves.leave_type.ilike(leave.leave_type),
153
- Leaves.status == "APPROVED",
154
- )
155
- approved_rows = (await session.exec(q)).all()
156
- used = sum(r.days for r in approved_rows) if approved_rows else 0
157
- if used + leave.days > limit:
158
- raise HTTPException(400, "Insufficient balance at approval time")
159
-
160
- # update
161
- leave.status = "APPROVED"
162
- leave.approved_by = approver_id
163
- leave.approved_at = datetime.utcnow()
164
- if comment:
165
- leave.comment = comment
166
-
167
- session.add(leave)
168
- # commit done by context manager
169
-
170
- # send push notification to member and lead
171
- title = "Leave Approved"
172
- body = f"Your leave ({leave.leave_type}) has been approved."
173
- member_tokens = await get_tokens_for_user(session, leave.user_id)
174
- lead_tokens = await get_tokens_for_user(session, leave.lead_id)
175
- await send_push_to_tokens(
176
- member_tokens,
177
- title,
178
- body,
179
- data={"leave_id": str(leave.id), "action": "leave_approved"},
180
- )
181
- await send_push_to_tokens(
182
- lead_tokens,
183
- title,
184
- body,
185
- data={"leave_id": str(leave.id), "action": "leave_approved"},
186
- )
187
-
188
- return leave
189
-
190
-
191
- async def reject_leave(
192
- session: AsyncSession,
193
- approver_id,
194
- leave_id: str,
195
- reject_reason: Optional[str] = None,
196
- comment: Optional[str] = None,
197
- ):
198
- async with session.begin():
199
- stmt = select(Leaves).where(Leaves.id == leave_id).with_for_update()
200
- leave = (await session.exec(stmt)).one_or_none()
201
- if not leave:
202
- raise HTTPException(404, "Leave not found")
203
- if leave.status != "PENDING":
204
- raise HTTPException(400, "Leave is not pending")
205
-
206
- if str(approver_id) not in (str(leave.mentor_id), str(leave.lead_id)):
207
- raise HTTPException(403, "Not authorized to reject this leave")
208
-
209
- leave.status = "REJECTED"
210
- leave.approved_by = approver_id
211
- leave.approved_at = datetime.utcnow()
212
- leave.reject_reason = reject_reason
213
- if comment:
214
- leave.comment = comment
215
- session.add(leave)
216
-
217
- # push to member + lead
218
- title = "Leave Rejected"
219
- body = f"Your leave ({leave.leave_type}) has been rejected. Reason: {leave.reject_reason or 'N/A'}"
220
- member_tokens = await get_tokens_for_user(session, leave.user_id)
221
- lead_tokens = await get_tokens_for_user(session, leave.lead_id)
222
- await send_push_to_tokens(
223
- member_tokens,
224
- title,
225
- body,
226
- data={"leave_id": str(leave.id), "action": "leave_rejected"},
227
- )
228
- await send_push_to_tokens(
229
- lead_tokens,
230
- title,
231
- body,
232
- data={"leave_id": str(leave.id), "action": "leave_rejected"},
233
- )
234
-
235
- return leave
236
-
237
-
238
- async def add_device_token(session: AsyncSession, user_id, device_token: str):
239
- """
240
- Add FCM token to Users.device_tokens ARRAY.
241
- Avoid duplicates.
242
- """
243
-
244
- # 1) Fetch user
245
- user = await session.get(Users, user_id)
246
- if not user:
247
- raise HTTPException(404, "User not found")
248
-
249
- # 2) If token not present -> add it
250
- if device_token not in user.device_tokens:
251
- user.device_tokens.append(device_token)
252
- session.add(user)
253
- await session.commit()
254
- await session.refresh(user)
255
-
256
- return user.device_tokens
257
-
258
-
259
- async def get_leave_balance(session: AsyncSession, user_id) -> List[dict]:
260
- # compute used for each leave_type and return
261
- # using constants SICK_LIMIT and CASUAL_LIMIT
262
- stmt = select(Leaves).where(Leaves.user_id == user_id, Leaves.status == "APPROVED")
263
- rows = (await session.exec(stmt)).all()
264
- used_sick = sum(r.days for r in rows if r.leave_type.lower() == "sick")
265
- used_casual = sum(r.days for r in rows if r.leave_type.lower() == "casual")
266
- return [
267
- {
268
- "leave_type": "Sick",
269
- "limit": SICK_LIMIT,
270
- "used": used_sick,
271
- "remaining": max(0, SICK_LIMIT - used_sick),
272
- },
273
- {
274
- "leave_type": "Casual",
275
- "limit": CASUAL_LIMIT,
276
- "used": used_casual,
277
- "remaining": max(0, CASUAL_LIMIT - used_casual),
278
- },
279
- ]
280
-
281
-
282
- # In production, replace with DB storage
283
- USER_TOKEN_STORE = {} # {google_user_id: {tokens}}
284
-
285
-
286
- async def send_email_service(req: SendMailRequest):
287
- record = USER_TOKEN_STORE.get(req.user_id)
288
- if not record:
289
- raise HTTPException(404, "User not logged in with Google OAuth")
290
-
291
- access_token = record["access_token"]
292
- refresh_token = record.get("refresh_token")
293
-
294
- if not access_token and refresh_token:
295
- new_tokens = await refresh_access_token(refresh_token)
296
- access_token = new_tokens["access_token"]
297
- record["access_token"] = access_token
298
-
299
- if not access_token:
300
- raise HTTPException(400, "Re-auth required")
301
-
302
- raw = build_raw_message(
303
- to_email=req.to,
304
- subject=req.subject,
305
- body=req.body,
306
- from_name=req.from_name,
307
- from_email=record["email"],
308
- )
309
-
310
- url = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
311
- payload = {"raw": raw}
312
-
313
- async with httpx.AsyncClient() as client:
314
- r = await client.post(
315
- url, json=payload, headers={"Authorization": f"Bearer {access_token}"}
316
- )
317
-
318
- if r.status_code >= 400:
319
- raise HTTPException(500, f"Gmail error: {r.text}")
320
-
321
- return r.json()
322
-
323
-
324
- async def update_user_profile(session, user_id: str, data):
325
- user = await session.get(Users, uuid.UUID(user_id))
326
-
327
- if not user:
328
- raise HTTPException(status_code=404, detail="User not found")
329
-
330
- # --- Update Name ---
331
- if data.name:
332
- user.user_name = data.name
333
-
334
- # --- Update Email ---
335
- if data.email:
336
- user.email_id = data.email
337
-
338
- # --- Update DOB ---
339
- if data.dob:
340
- try:
341
- # Convert DD.MM.YYYY → Python date
342
- parsed_date = datetime.strptime(data.dob, "%d.%m.%Y").date()
343
- user.dob = parsed_date
344
- except:
345
- raise HTTPException(
346
- status_code=400, detail="DOB must be in DD.MM.YYYY format"
347
- )
348
-
349
- # --- Update Address ---
350
- if data.address:
351
- user.address = data.address
352
-
353
- # --- Change Password ---
354
- if data.new_password:
355
- if not data.current_password:
356
- raise HTTPException(status_code=400, detail="Current password required")
357
-
358
- # Verify old password
359
- if not pwd_context.verify(data.current_password, user.password):
360
- raise HTTPException(status_code=400, detail="Incorrect current password")
361
-
362
- # Set new password
363
- user.password = pwd_context.hash(data.new_password)
364
-
365
- # Commit changes
366
- await session.commit()
367
- await session.refresh(user)
368
-
369
- return {
370
- "message": "Profile updated successfully",
371
- "user": {
372
- "id": str(user.id),
373
- "name": user.user_name,
374
- "email": user.email_id,
375
- "dob": user.dob.isoformat() if user.dob else None,
376
- "address": user.address,
377
- "is_verified": user.is_verified,
378
- },
379
- }
380
 
381
-
382
- async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
383
- q = await session.exec(select(Assets).where(Assets.user_id == uuid.UUID(user_id)))
384
- return q.all()
 
1
+ # from src.profile.utils import build_raw_message, refresh_access_token
2
+ # from src.profile.schemas import SendMailRequest
3
+ # from src.core.models import Assets, Users
4
+ # from datetime import datetime
5
+ # import uuid
6
+ # from fastapi import HTTPException
7
+ # from passlib.context import CryptContext
8
+ # from src.core.models import Users
9
+ # import uuid
10
+ # from typing import List
11
+ # from sqlmodel import select
12
+ # from sqlmodel.ext.asyncio.session import AsyncSession
13
+ # import httpx
14
+ # from src.core.config import settings
15
+ # from typing import List, Optional
16
+ # from sqlmodel import select
17
+ # from sqlmodel.ext.asyncio.session import AsyncSession
18
+ # from fastapi import HTTPException
19
+ # from datetime import datetime
20
+ # from src.profile.schemas import ApplyLeaveRequest, ApproveRejectRequest
21
+ # from src.profile.utils import calculate_days, find_mentor_and_lead
22
+ # from src.profile.utils import get_tokens_for_user, send_push_to_tokens
23
+ # from src.core.config import settings
24
+
25
+
26
+ # pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
27
+
28
+ # # src/profile/service.py
29
+
30
+ # # Leave limits (you can move to config)
31
+ # SICK_LIMIT = getattr(settings, "SICK_LEAVE_LIMIT", 10)
32
+ # CASUAL_LIMIT = getattr(settings, "CASUAL_LEAVE_LIMIT", 10)
33
+
34
+
35
+ # async def apply_leave(session: AsyncSession, user_id, payload: ApplyLeaveRequest):
36
+ # # compute days
37
+ # days = calculate_days(payload.from_date, payload.to_date)
38
+ # if days <= 0:
39
+ # raise HTTPException(status_code=400, detail="Invalid date range")
40
+
41
+ # # find mentor and lead
42
+ # mentor, lead = await find_mentor_and_lead(session, user_id)
43
+ # if not mentor or not lead:
44
+ # raise HTTPException(status_code=400, detail="Mentor or Lead not found for user")
45
+
46
+ # # check remaining balance
47
+ # limit = SICK_LIMIT if payload.leave_type.lower() == "sick" else CASUAL_LIMIT
48
+ # # sum used days for this leave_type
49
+ # q = select(Leaves).where(
50
+ # Leaves.user_id == user_id,
51
+ # Leaves.leave_type.ilike(payload.leave_type),
52
+ # Leaves.status == "APPROVED",
53
+ # )
54
+ # rows = (await session.exec(q)).all()
55
+ # used = sum(r.days for r in rows) if rows else 0
56
+ # remaining = limit - used
57
+ # if days > remaining:
58
+ # raise HTTPException(
59
+ # status_code=400,
60
+ # detail=f"Insufficient {payload.leave_type} balance. Remaining {remaining}",
61
+ # )
62
+
63
+ # leave = Leaves(
64
+ # user_id=user_id,
65
+ # mentor_id=mentor.id,
66
+ # lead_id=lead.id,
67
+ # leave_type=payload.leave_type,
68
+ # from_date=payload.from_date,
69
+ # to_date=payload.to_date,
70
+ # days=days,
71
+ # reason=payload.reason,
72
+ # status="PENDING",
73
+ # )
74
+ # session.add(leave)
75
+ # await session.commit()
76
+ # await session.refresh(leave)
77
+
78
+ # # push notifications to mentor & lead
79
+ # title = "New Leave Request"
80
+ # body = f"{user_id} applied {payload.leave_type} leave ({days} days)."
81
+ # mentor_tokens = await get_tokens_for_user(session, mentor.id)
82
+ # lead_tokens = await get_tokens_for_user(session, lead.id)
83
+ # await send_push_to_tokens(
84
+ # mentor_tokens,
85
+ # title,
86
+ # body,
87
+ # data={"leave_id": str(leave.id), "action": "leave_request"},
88
+ # )
89
+ # await send_push_to_tokens(
90
+ # lead_tokens,
91
+ # title,
92
+ # body,
93
+ # data={"leave_id": str(leave.id), "action": "leave_request"},
94
+ # )
95
+
96
+ # return leave
97
+
98
+
99
+ # async def get_pending_leaves_for_approver(
100
+ # session: AsyncSession, approver_user_id
101
+ # ) -> List[Leaves]:
102
+ # # returns pending leaves where mentor_id == approver OR lead_id == approver
103
+ # stmt = select(Leaves).where(
104
+ # (Leaves.mentor_id == approver_user_id) | (Leaves.lead_id == approver_user_id),
105
+ # Leaves.status == "PENDING",
106
+ # )
107
+ # return (await session.exec(stmt)).all()
108
+
109
+
110
+ # async def get_my_leaves(session: AsyncSession, user_id) -> List[Leaves]:
111
+ # stmt = (
112
+ # select(Leaves)
113
+ # .where(Leaves.user_id == user_id)
114
+ # .order_by(Leaves.created_at.desc())
115
+ # )
116
+ # return (await session.exec(stmt)).all()
117
+
118
+
119
+ # async def get_team_leaves(session: AsyncSession, lead_user_id) -> List[Leaves]:
120
+ # # lead can view leaves where lead_id == lead_user_id
121
+ # stmt = (
122
+ # select(Leaves)
123
+ # .where(Leaves.lead_id == lead_user_id)
124
+ # .order_by(Leaves.created_at.desc())
125
+ # )
126
+ # return (await session.exec(stmt)).all()
127
+
128
+
129
+ # async def approve_leave(
130
+ # session: AsyncSession, approver_id, leave_id: str, comment: Optional[str] = None
131
+ # ):
132
+ # # transaction-safe update
133
+ # async with session.begin():
134
+ # stmt = select(Leaves).where(Leaves.id == leave_id).with_for_update()
135
+ # leave = (await session.exec(stmt)).one_or_none()
136
+ # if not leave:
137
+ # raise HTTPException(404, "Leave not found")
138
+ # if leave.status != "PENDING":
139
+ # raise HTTPException(400, "Leave is not pending")
140
+
141
+ # # optional: verify approver is mentor or lead for this leave
142
+ # if str(approver_id) not in (str(leave.mentor_id), str(leave.lead_id)):
143
+ # # you might want to check roles more thoroughly
144
+ # raise HTTPException(403, "Not authorized to approve this leave")
145
+
146
+ # # check balance again before approving
147
+ # # compute limit and used
148
+ # limit = SICK_LIMIT if leave.leave_type.lower() == "sick" else CASUAL_LIMIT
149
+ # q = select(Leaves).where(
150
+ # Leaves.user_id == leave.user_id,
151
+ # Leaves.leave_type.ilike(leave.leave_type),
152
+ # Leaves.status == "APPROVED",
153
+ # )
154
+ # approved_rows = (await session.exec(q)).all()
155
+ # used = sum(r.days for r in approved_rows) if approved_rows else 0
156
+ # if used + leave.days > limit:
157
+ # raise HTTPException(400, "Insufficient balance at approval time")
158
+
159
+ # # update
160
+ # leave.status = "APPROVED"
161
+ # leave.approved_by = approver_id
162
+ # leave.approved_at = datetime.utcnow()
163
+ # if comment:
164
+ # leave.comment = comment
165
+
166
+ # session.add(leave)
167
+ # # commit done by context manager
168
+
169
+ # # send push notification to member and lead
170
+ # title = "Leave Approved"
171
+ # body = f"Your leave ({leave.leave_type}) has been approved."
172
+ # member_tokens = await get_tokens_for_user(session, leave.user_id)
173
+ # lead_tokens = await get_tokens_for_user(session, leave.lead_id)
174
+ # await send_push_to_tokens(
175
+ # member_tokens,
176
+ # title,
177
+ # body,
178
+ # data={"leave_id": str(leave.id), "action": "leave_approved"},
179
+ # )
180
+ # await send_push_to_tokens(
181
+ # lead_tokens,
182
+ # title,
183
+ # body,
184
+ # data={"leave_id": str(leave.id), "action": "leave_approved"},
185
+ # )
186
+
187
+ # return leave
188
+
189
+
190
+ # async def reject_leave(
191
+ # session: AsyncSession,
192
+ # approver_id,
193
+ # leave_id: str,
194
+ # reject_reason: Optional[str] = None,
195
+ # comment: Optional[str] = None,
196
+ # ):
197
+ # async with session.begin():
198
+ # stmt = select(Leaves).where(Leaves.id == leave_id).with_for_update()
199
+ # leave = (await session.exec(stmt)).one_or_none()
200
+ # if not leave:
201
+ # raise HTTPException(404, "Leave not found")
202
+ # if leave.status != "PENDING":
203
+ # raise HTTPException(400, "Leave is not pending")
204
+
205
+ # if str(approver_id) not in (str(leave.mentor_id), str(leave.lead_id)):
206
+ # raise HTTPException(403, "Not authorized to reject this leave")
207
+
208
+ # leave.status = "REJECTED"
209
+ # leave.approved_by = approver_id
210
+ # leave.approved_at = datetime.utcnow()
211
+ # leave.reject_reason = reject_reason
212
+ # if comment:
213
+ # leave.comment = comment
214
+ # session.add(leave)
215
+
216
+ # # push to member + lead
217
+ # title = "Leave Rejected"
218
+ # body = f"Your leave ({leave.leave_type}) has been rejected. Reason: {leave.reject_reason or 'N/A'}"
219
+ # member_tokens = await get_tokens_for_user(session, leave.user_id)
220
+ # lead_tokens = await get_tokens_for_user(session, leave.lead_id)
221
+ # await send_push_to_tokens(
222
+ # member_tokens,
223
+ # title,
224
+ # body,
225
+ # data={"leave_id": str(leave.id), "action": "leave_rejected"},
226
+ # )
227
+ # await send_push_to_tokens(
228
+ # lead_tokens,
229
+ # title,
230
+ # body,
231
+ # data={"leave_id": str(leave.id), "action": "leave_rejected"},
232
+ # )
233
+
234
+ # return leave
235
+
236
+
237
+ # async def add_device_token(session: AsyncSession, user_id, device_token: str):
238
+ # """
239
+ # Add FCM token to Users.device_tokens ARRAY.
240
+ # Avoid duplicates.
241
+ # """
242
+
243
+ # # 1) Fetch user
244
+ # user = await session.get(Users, user_id)
245
+ # if not user:
246
+ # raise HTTPException(404, "User not found")
247
+
248
+ # # 2) If token not present -> add it
249
+ # if device_token not in user.device_tokens:
250
+ # user.device_tokens.append(device_token)
251
+ # session.add(user)
252
+ # await session.commit()
253
+ # await session.refresh(user)
254
+
255
+ # return user.device_tokens
256
+
257
+
258
+ # async def get_leave_balance(session: AsyncSession, user_id) -> List[dict]:
259
+ # # compute used for each leave_type and return
260
+ # # using constants SICK_LIMIT and CASUAL_LIMIT
261
+ # stmt = select(Leaves).where(Leaves.user_id == user_id, Leaves.status == "APPROVED")
262
+ # rows = (await session.exec(stmt)).all()
263
+ # used_sick = sum(r.days for r in rows if r.leave_type.lower() == "sick")
264
+ # used_casual = sum(r.days for r in rows if r.leave_type.lower() == "casual")
265
+ # return [
266
+ # {
267
+ # "leave_type": "Sick",
268
+ # "limit": SICK_LIMIT,
269
+ # "used": used_sick,
270
+ # "remaining": max(0, SICK_LIMIT - used_sick),
271
+ # },
272
+ # {
273
+ # "leave_type": "Casual",
274
+ # "limit": CASUAL_LIMIT,
275
+ # "used": used_casual,
276
+ # "remaining": max(0, CASUAL_LIMIT - used_casual),
277
+ # },
278
+ # ]
279
+
280
+
281
+ # # In production, replace with DB storage
282
+ # USER_TOKEN_STORE = {} # {google_user_id: {tokens}}
283
+
284
+
285
+ # async def send_email_service(req: SendMailRequest):
286
+ # record = USER_TOKEN_STORE.get(req.user_id)
287
+ # if not record:
288
+ # raise HTTPException(404, "User not logged in with Google OAuth")
289
+
290
+ # access_token = record["access_token"]
291
+ # refresh_token = record.get("refresh_token")
292
+
293
+ # if not access_token and refresh_token:
294
+ # new_tokens = await refresh_access_token(refresh_token)
295
+ # access_token = new_tokens["access_token"]
296
+ # record["access_token"] = access_token
297
+
298
+ # if not access_token:
299
+ # raise HTTPException(400, "Re-auth required")
300
+
301
+ # raw = build_raw_message(
302
+ # to_email=req.to,
303
+ # subject=req.subject,
304
+ # body=req.body,
305
+ # from_name=req.from_name,
306
+ # from_email=record["email"],
307
+ # )
308
+
309
+ # url = "https://gmail.googleapis.com/gmail/v1/users/me/messages/send"
310
+ # payload = {"raw": raw}
311
+
312
+ # async with httpx.AsyncClient() as client:
313
+ # r = await client.post(
314
+ # url, json=payload, headers={"Authorization": f"Bearer {access_token}"}
315
+ # )
316
+
317
+ # if r.status_code >= 400:
318
+ # raise HTTPException(500, f"Gmail error: {r.text}")
319
+
320
+ # return r.json()
321
+
322
+
323
+ # async def update_user_profile(session, user_id: str, data):
324
+ # user = await session.get(Users, uuid.UUID(user_id))
325
+
326
+ # if not user:
327
+ # raise HTTPException(status_code=404, detail="User not found")
328
+
329
+ # # --- Update Name ---
330
+ # if data.name:
331
+ # user.user_name = data.name
332
+
333
+ # # --- Update Email ---
334
+ # if data.email:
335
+ # user.email_id = data.email
336
+
337
+ # # --- Update DOB ---
338
+ # if data.dob:
339
+ # try:
340
+ # # Convert DD.MM.YYYY → Python date
341
+ # parsed_date = datetime.strptime(data.dob, "%d.%m.%Y").date()
342
+ # user.dob = parsed_date
343
+ # except:
344
+ # raise HTTPException(
345
+ # status_code=400, detail="DOB must be in DD.MM.YYYY format"
346
+ # )
347
+
348
+ # # --- Update Address ---
349
+ # if data.address:
350
+ # user.address = data.address
351
+
352
+ # # --- Change Password ---
353
+ # if data.new_password:
354
+ # if not data.current_password:
355
+ # raise HTTPException(status_code=400, detail="Current password required")
356
+
357
+ # # Verify old password
358
+ # if not pwd_context.verify(data.current_password, user.password):
359
+ # raise HTTPException(status_code=400, detail="Incorrect current password")
360
+
361
+ # # Set new password
362
+ # user.password = pwd_context.hash(data.new_password)
363
+
364
+ # # Commit changes
365
+ # await session.commit()
366
+ # await session.refresh(user)
367
+
368
+ # return {
369
+ # "message": "Profile updated successfully",
370
+ # "user": {
371
+ # "id": str(user.id),
372
+ # "name": user.user_name,
373
+ # "email": user.email_id,
374
+ # "dob": user.dob.isoformat() if user.dob else None,
375
+ # "address": user.address,
376
+ # "is_verified": user.is_verified,
377
+ # },
378
+ # }
 
379
 
380
+
381
+ # async def list_user_assets(session: AsyncSession, user_id: str) -> List[Assets]:
382
+ # q = await session.exec(select(Assets).where(Assets.user_id == uuid.UUID(user_id)))
383
+ # return q.all()