Skip to content

Commit c005c19

Browse files
authored
Feat/shadcn layout theme overhaul (#2)
* chore: harden env secrets and docker health checks * feat: implement recruitment API and hooks for job postings and applications - Added recruitment API functions for job postings: get, create, update, and delete. - Implemented hooks for managing job postings and applications using React Query. - Enhanced Admin Settings page with improved layout and accessibility features. - Updated Audit Trail page with better styling and responsive design. - Introduced new types for leave and attendance management. - Enhanced Tailwind CSS configuration with theme tokens and improved dark mode support. - Updated TypeScript configuration for better path resolution. - Integrated shadcn components and improved sidebar navigation experience. - Fixed layout stability issues and ensured consistent dark mode appearance.
1 parent b032c98 commit c005c19

94 files changed

Lines changed: 6774 additions & 821 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/app/audit/models.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
from datetime import datetime
1+
from datetime import UTC, datetime
22

33
from sqlalchemy import DateTime, ForeignKey, Integer, String, Text
44
from sqlalchemy.orm import Mapped, mapped_column
55

66
from app.database import Base
77

88

9+
def _utcnow() -> datetime:
10+
return datetime.now(UTC)
11+
12+
913
class AuditLog(Base):
1014
__tablename__ = "audit_logs"
1115

@@ -16,4 +20,4 @@ class AuditLog(Base):
1620
entity_type: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
1721
entity_id: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True)
1822
details: Mapped[str | None] = mapped_column(Text, nullable=True)
19-
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow, index=True)
23+
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=_utcnow, index=True)

backend/app/auth/router.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from app.auth.models import User, UserRole
66
from app.auth.schemas import AuthTokenPayload, AuthUserResponse, ChangePasswordRequest, LoginRequest, RegisterRequest, SignupRequest, UpdateProfileRequest
77
from app.auth.service import authenticate_user, change_user_password, create_access_token, create_signup_user, create_user, update_user_profile
8+
from app.config import settings
89
from app.dependencies import get_current_user, get_db, require_roles
910
from app.utils.responses import success_response
1011

@@ -64,12 +65,13 @@ def login(payload: LoginRequest, response: Response, db: Session = Depends(get_d
6465
actor=user,
6566
)
6667

68+
is_production = settings.app_env.lower() == "production"
6769
response.set_cookie(
6870
key="access_token",
6971
value=access_token,
7072
httponly=True,
71-
secure=False,
72-
samesite="lax",
73+
secure=is_production,
74+
samesite="strict" if is_production else "lax",
7375
)
7476

7577
token_payload = AuthTokenPayload(

backend/app/auth/schemas.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1-
from pydantic import BaseModel, EmailStr
1+
from pydantic import BaseModel, EmailStr, field_validator
22

33
from app.auth.models import UserRole
44

55

6+
def _validate_password_strength(value: str) -> str:
7+
if len(value) < 8:
8+
raise ValueError("Password must be at least 8 characters")
9+
if not any(c.isupper() for c in value):
10+
raise ValueError("Password must contain at least one uppercase letter")
11+
if not any(c.islower() for c in value):
12+
raise ValueError("Password must contain at least one lowercase letter")
13+
if not any(c.isdigit() for c in value):
14+
raise ValueError("Password must contain at least one digit")
15+
return value
16+
17+
618
class LoginRequest(BaseModel):
719
email: EmailStr
820
password: str
@@ -14,13 +26,23 @@ class RegisterRequest(BaseModel):
1426
password: str
1527
role: UserRole = UserRole.employee
1628

29+
@field_validator("password")
30+
@classmethod
31+
def password_strength(cls, v: str) -> str:
32+
return _validate_password_strength(v)
33+
1734

1835
class SignupRequest(BaseModel):
1936
email: EmailStr
2037
full_name: str
2138
password: str
2239
department: str
2340

41+
@field_validator("password")
42+
@classmethod
43+
def password_strength(cls, v: str) -> str:
44+
return _validate_password_strength(v)
45+
2446

2547
class AuthUserResponse(BaseModel):
2648
id: int
@@ -52,3 +74,8 @@ class UpdateProfileRequest(BaseModel):
5274
class ChangePasswordRequest(BaseModel):
5375
current_password: str
5476
new_password: str
77+
78+
@field_validator("new_password")
79+
@classmethod
80+
def password_strength(cls, v: str) -> str:
81+
return _validate_password_strength(v)

backend/app/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ def validate_security_settings(self) -> "Settings":
3737
raise ValueError("DEFAULT_ADMIN_PASSWORD and DEFAULT_HR_PASSWORD must be different in production")
3838
return self
3939

40-
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
40+
model_config = SettingsConfigDict(
41+
env_file=(".env", "backend/.env"),
42+
env_file_encoding="utf-8",
43+
extra="ignore",
44+
)
4145

4246

4347
settings = Settings()

backend/app/employees/router.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import APIRouter, Depends, status
1+
from fastapi import APIRouter, Depends, Query, status
22
from sqlalchemy.orm import Session
33

44
from app.audit.service import create_audit_log
@@ -46,10 +46,18 @@ def create_employee_endpoint(
4646

4747
@router.get("/")
4848
def list_employees_endpoint(
49+
skip: int = Query(default=0, ge=0),
50+
limit: int = Query(default=50, ge=1, le=200),
51+
search: str | None = None,
52+
department: str | None = None,
53+
employment_status: str | None = None,
4954
db: Session = Depends(get_db),
5055
_: User = Depends(require_roles(UserRole.admin, UserRole.hr_manager, UserRole.employee)),
5156
):
52-
employees = list_employees(db)
57+
employees, total = list_employees(
58+
db, skip=skip, limit=limit, search=search,
59+
department=department, employment_status=employment_status,
60+
)
5361
user_ids = [item.user_id for item in employees if item.user_id is not None]
5462
users_by_id = {
5563
item.id: item
@@ -63,7 +71,7 @@ def list_employees_endpoint(
6371
)
6472
for item in employees
6573
]
66-
return success_response("Employees fetched successfully", data)
74+
return success_response("Employees fetched successfully", {"items": data, "total": total})
6775

6876

6977
@router.get("/{employee_id}")

backend/app/employees/schemas.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from decimal import Decimal
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, ConfigDict, Field
44

55
from app.employees.models import EmploymentType, RateType
66

@@ -62,5 +62,4 @@ class EmployeeResponse(EmployeeBase):
6262
city: str | None = None
6363
region: str | None = None
6464

65-
class Config:
66-
from_attributes = True
65+
model_config = ConfigDict(from_attributes=True)

backend/app/employees/service.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,30 @@ def create_employee(db: Session, payload: EmployeeCreate) -> Employee:
1717
return employee
1818

1919

20-
def list_employees(db: Session) -> list[Employee]:
21-
return db.query(Employee).order_by(Employee.id.desc()).all()
20+
def list_employees(
21+
db: Session,
22+
*,
23+
skip: int = 0,
24+
limit: int = 50,
25+
search: str | None = None,
26+
department: str | None = None,
27+
employment_status: str | None = None,
28+
) -> tuple[list[Employee], int]:
29+
query = db.query(Employee)
30+
31+
if search:
32+
term = f"%{search}%"
33+
query = query.filter(
34+
(Employee.profile_name.ilike(term)) | (Employee.employee_code.ilike(term))
35+
)
36+
if department:
37+
query = query.filter(Employee.department == department)
38+
if employment_status:
39+
query = query.filter(Employee.employment_status == employment_status)
40+
41+
total = query.count()
42+
items = query.order_by(Employee.id.desc()).offset(skip).limit(limit).all()
43+
return items, total
2244

2345

2446
def get_employee(db: Session, employee_id: int) -> Employee:

backend/app/leave/models.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,49 @@
1-
from datetime import datetime
1+
import enum
2+
from datetime import UTC, datetime
23

3-
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint
4+
from sqlalchemy import Date, DateTime, Enum, ForeignKey, Integer, String, Text, UniqueConstraint
45
from sqlalchemy.orm import Mapped, mapped_column
56

67
from app.database import Base
78

89

10+
def _utcnow() -> datetime:
11+
return datetime.now(UTC)
12+
13+
14+
class LeaveType(str, enum.Enum):
15+
vacation = "vacation"
16+
sick = "sick"
17+
personal = "personal"
18+
maternity = "maternity"
19+
paternity = "paternity"
20+
bereavement = "bereavement"
21+
unpaid = "unpaid"
22+
23+
24+
class LeaveStatus(str, enum.Enum):
25+
pending = "pending"
26+
approved = "approved"
27+
rejected = "rejected"
28+
cancelled = "cancelled"
29+
30+
31+
class LeaveRequest(Base):
32+
__tablename__ = "leave_requests"
33+
34+
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
35+
employee_id: Mapped[int] = mapped_column(ForeignKey("employees.id"), nullable=False, index=True)
36+
leave_type: Mapped[LeaveType] = mapped_column(Enum(LeaveType), nullable=False)
37+
start_date: Mapped[str] = mapped_column(String(20), nullable=False)
38+
end_date: Mapped[str] = mapped_column(String(20), nullable=False)
39+
reason: Mapped[str | None] = mapped_column(Text, nullable=True)
40+
status: Mapped[LeaveStatus] = mapped_column(Enum(LeaveStatus), nullable=False, default=LeaveStatus.pending)
41+
reviewer_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)
42+
review_note: Mapped[str | None] = mapped_column(Text, nullable=True)
43+
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=_utcnow)
44+
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=_utcnow, onupdate=_utcnow)
45+
46+
947
class AttendancePunch(Base):
1048
__tablename__ = "attendance_punches"
1149
__table_args__ = (UniqueConstraint("punch_key", name="uq_attendance_punch_key"),)
@@ -19,4 +57,4 @@ class AttendancePunch(Base):
1957
punch_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
2058
source: Mapped[str] = mapped_column(String(30), nullable=False, default="biometric")
2159
punch_key: Mapped[str] = mapped_column(String(255), nullable=False)
22-
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
60+
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=_utcnow)

0 commit comments

Comments
 (0)