Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
H
hrsystem
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
hrsystem
Commits
f9cd0fe6
Commit
f9cd0fe6
authored
Apr 01, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 22 files via Son of Anton
parent
84e833db
Changes
22
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
2534 additions
and
0 deletions
+2534
-0
app.module.ts
backend/src/app.module.ts
+4
-0
checklist.service.ts
backend/src/modules/onboarding/checklist.service.ts
+132
-0
checklist-item.dto.ts
backend/src/modules/onboarding/dto/checklist-item.dto.ts
+9
-0
contract-sign.dto.ts
backend/src/modules/onboarding/dto/contract-sign.dto.ts
+24
-0
create-invite.dto.ts
backend/src/modules/onboarding/dto/create-invite.dto.ts
+25
-0
register.dto.ts
backend/src/modules/onboarding/dto/register.dto.ts
+82
-0
schedule-config.dto.ts
backend/src/modules/onboarding/dto/schedule-config.dto.ts
+10
-0
self-assessment.dto.ts
backend/src/modules/onboarding/dto/self-assessment.dto.ts
+22
-0
invite.service.ts
backend/src/modules/onboarding/invite.service.ts
+144
-0
onboarding.controller.ts
backend/src/modules/onboarding/onboarding.controller.ts
+143
-0
onboarding.module.ts
backend/src/modules/onboarding/onboarding.module.ts
+13
-0
onboarding.service.ts
backend/src/modules/onboarding/onboarding.service.ts
+381
-0
salary-calculator.service.ts
backend/src/modules/onboarding/salary-calculator.service.ts
+83
-0
create-user.dto.ts
backend/src/modules/users/dto/create-user.dto.ts
+118
-0
reset-password.dto.ts
backend/src/modules/users/dto/reset-password.dto.ts
+31
-0
update-user.dto.ts
backend/src/modules/users/dto/update-user.dto.ts
+151
-0
user-filter.dto.ts
backend/src/modules/users/dto/user-filter.dto.ts
+24
-0
user-response.dto.ts
backend/src/modules/users/dto/user-response.dto.ts
+55
-0
users.controller.ts
backend/src/modules/users/users.controller.ts
+151
-0
users.module.ts
backend/src/modules/users/users.module.ts
+10
-0
users.service.ts
backend/src/modules/users/users.service.ts
+776
-0
schema-additions.prisma
prisma/schema-additions.prisma
+146
-0
No files found.
backend/src/app.module.ts
View file @
f9cd0fe6
...
...
@@ -15,6 +15,8 @@ import { PrismaModule } from './prisma/prisma.module';
import
{
AuthModule
}
from
'./modules/auth/auth.module'
;
import
{
SettingsModule
}
from
'./modules/settings/settings.module'
;
import
{
AuditTrailModule
}
from
'./modules/audit-trail/audit-trail.module'
;
import
{
UsersModule
}
from
'./modules/users/users.module'
;
import
{
OnboardingModule
}
from
'./modules/onboarding/onboarding.module'
;
import
{
JwtAuthGuard
}
from
'./common/guards/jwt-auth.guard'
;
import
{
RolesGuard
}
from
'./common/guards/roles.guard'
;
...
...
@@ -36,6 +38,8 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';
AuthModule
,
SettingsModule
,
AuditTrailModule
,
UsersModule
,
OnboardingModule
,
],
providers
:
[
{
provide
:
APP_GUARD
,
useClass
:
JwtAuthGuard
},
...
...
backend/src/modules/onboarding/checklist.service.ts
0 → 100644
View file @
f9cd0fe6
import
{
Injectable
,
NotFoundException
,
Logger
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
export
interface
ChecklistItem
{
key
:
string
;
label
:
string
;
completed
:
boolean
;
completedAt
:
string
|
null
;
verificationMethod
:
'AUTOMATIC'
|
'MANUAL'
;
verifiedBy
:
string
|
null
;
}
const
DEFAULT_CHECKLIST_ITEMS
:
Array
<
Omit
<
ChecklistItem
,
'completed'
|
'completedAt'
|
'verifiedBy'
>>
=
[
{
key
:
'profile_photo'
,
label
:
'Profile photo uploaded'
,
verificationMethod
:
'AUTOMATIC'
},
{
key
:
'bank_details'
,
label
:
'Bank details provided'
,
verificationMethod
:
'AUTOMATIC'
},
{
key
:
'contract_signed'
,
label
:
'Contract signed'
,
verificationMethod
:
'AUTOMATIC'
},
{
key
:
'policies_acknowledged'
,
label
:
'All policies acknowledged'
,
verificationMethod
:
'AUTOMATIC'
},
{
key
:
'self_assessment'
,
label
:
'Competency self-assessment completed'
,
verificationMethod
:
'AUTOMATIC'
},
{
key
:
'device_setup'
,
label
:
'Device setup confirmed'
,
verificationMethod
:
'MANUAL'
},
{
key
:
'source_control'
,
label
:
'Source control access configured'
,
verificationMethod
:
'MANUAL'
},
{
key
:
'first_board'
,
label
:
'First board assigned'
,
verificationMethod
:
'MANUAL'
},
{
key
:
'intro_meeting'
,
label
:
'Introduction meeting completed'
,
verificationMethod
:
'MANUAL'
},
];
@
Injectable
()
export
class
ChecklistService
{
private
readonly
logger
=
new
Logger
(
ChecklistService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
getChecklist
(
userId
:
string
):
Promise
<
{
items
:
ChecklistItem
[];
completionPercentage
:
number
}
>
{
const
user
=
await
this
.
prisma
.
user
.
findFirst
({
where
:
{
id
:
userId
,
deletedAt
:
null
},
});
if
(
!
user
)
{
throw
new
NotFoundException
(
'User not found'
);
}
const
storedChecklist
=
(
user
as
any
).
onboardingChecklist
as
Record
<
string
,
any
>
|
null
;
const
items
:
ChecklistItem
[]
=
DEFAULT_CHECKLIST_ITEMS
.
map
((
item
)
=>
{
const
stored
=
storedChecklist
?.[
item
.
key
];
return
{
...
item
,
completed
:
stored
?.
completed
||
false
,
completedAt
:
stored
?.
completedAt
||
null
,
verifiedBy
:
stored
?.
verifiedBy
||
null
,
};
});
// Auto-check automatic items
await
this
.
autoCheckItems
(
userId
,
items
);
const
completedCount
=
items
.
filter
((
i
)
=>
i
.
completed
).
length
;
const
completionPercentage
=
Math
.
round
((
completedCount
/
items
.
length
)
*
100
);
return
{
items
,
completionPercentage
};
}
async
updateItem
(
userId
:
string
,
itemKey
:
string
,
completed
:
boolean
,
verifiedById
:
string
):
Promise
<
void
>
{
const
user
=
await
this
.
prisma
.
user
.
findFirst
({
where
:
{
id
:
userId
,
deletedAt
:
null
}
});
if
(
!
user
)
{
throw
new
NotFoundException
(
'User not found'
);
}
const
item
=
DEFAULT_CHECKLIST_ITEMS
.
find
((
i
)
=>
i
.
key
===
itemKey
);
if
(
!
item
)
{
throw
new
NotFoundException
(
'Checklist item not found'
);
}
const
checklist
=
((
user
as
any
).
onboardingChecklist
as
Record
<
string
,
any
>
)
||
{};
checklist
[
itemKey
]
=
{
completed
,
completedAt
:
completed
?
new
Date
().
toISOString
()
:
null
,
verifiedBy
:
completed
?
verifiedById
:
null
,
};
await
this
.
prisma
.
user
.
update
({
where
:
{
id
:
userId
},
data
:
{
onboardingChecklist
:
checklist
}
as
any
,
});
}
async
isComplete
(
userId
:
string
):
Promise
<
boolean
>
{
const
{
completionPercentage
}
=
await
this
.
getChecklist
(
userId
);
return
completionPercentage
===
100
;
}
private
async
autoCheckItems
(
userId
:
string
,
items
:
ChecklistItem
[]):
Promise
<
void
>
{
const
user
=
await
this
.
prisma
.
user
.
findUnique
({
where
:
{
id
:
userId
},
select
:
{
avatar
:
true
,
bankName
:
true
,
bankAccountNumber
:
true
,
onboardingChecklist
:
true
,
},
});
if
(
!
user
)
return
;
let
updated
=
false
;
const
checklist
=
((
user
as
any
).
onboardingChecklist
as
Record
<
string
,
any
>
)
||
{};
// Profile photo
const
photoItem
=
items
.
find
((
i
)
=>
i
.
key
===
'profile_photo'
);
if
(
photoItem
&&
!
photoItem
.
completed
&&
user
.
avatar
)
{
photoItem
.
completed
=
true
;
photoItem
.
completedAt
=
new
Date
().
toISOString
();
photoItem
.
verifiedBy
=
'SYSTEM'
;
checklist
[
'profile_photo'
]
=
{
completed
:
true
,
completedAt
:
photoItem
.
completedAt
,
verifiedBy
:
'SYSTEM'
};
updated
=
true
;
}
// Bank details
const
bankItem
=
items
.
find
((
i
)
=>
i
.
key
===
'bank_details'
);
if
(
bankItem
&&
!
bankItem
.
completed
&&
user
.
bankName
&&
user
.
bankAccountNumber
)
{
bankItem
.
completed
=
true
;
bankItem
.
completedAt
=
new
Date
().
toISOString
();
bankItem
.
verifiedBy
=
'SYSTEM'
;
checklist
[
'bank_details'
]
=
{
completed
:
true
,
completedAt
:
bankItem
.
completedAt
,
verifiedBy
:
'SYSTEM'
};
updated
=
true
;
}
if
(
updated
)
{
await
this
.
prisma
.
user
.
update
({
where
:
{
id
:
userId
},
data
:
{
onboardingChecklist
:
checklist
}
as
any
,
});
}
}
}
\ No newline at end of file
backend/src/modules/onboarding/dto/checklist-item.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsString
,
IsBoolean
}
from
'class-validator'
;
export
class
UpdateChecklistItemDto
{
@
IsString
()
itemKey
:
string
;
@
IsBoolean
()
completed
:
boolean
;
}
\ No newline at end of file
backend/src/modules/onboarding/dto/contract-sign.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsString
,
IsBoolean
,
IsArray
,
MinLength
}
from
'class-validator'
;
export
class
ContractSignDto
{
@
IsString
()
userId
:
string
;
@
IsArray
()
@
IsString
({
each
:
true
})
acknowledgedClauses
:
string
[];
// ['deduction_policy', 'ip_assignment', 'nda', 'termination_terms', 'code_of_conduct', 'data_security', 'salary_adjustment']
@
IsString
()
@
MinLength
(
4
)
signedFullName
:
string
;
@
IsBoolean
()
confirmedDigitalSignature
:
boolean
;
@
IsString
()
ipAddress
:
string
;
@
IsString
()
userAgent
:
string
;
}
\ No newline at end of file
backend/src/modules/onboarding/dto/create-invite.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsString
,
IsOptional
,
IsEnum
,
IsInt
,
Min
,
Max
,
IsArray
}
from
'class-validator'
;
export
class
CreateInviteDto
{
@
IsEnum
([
'FULL_TIME'
,
'PART_TIME'
,
'PROJECT_BASED'
])
contractorType
:
string
;
@
IsOptional
()
@
IsString
()
assignedProjectLeaderId
?:
string
;
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
assignedBoardIds
?:
string
[];
@
IsOptional
()
@
IsInt
()
@
Min
(
1
)
@
Max
(
30
)
expirationDays
?:
number
;
@
IsOptional
()
@
IsString
()
welcomeNote
?:
string
;
}
\ No newline at end of file
backend/src/modules/onboarding/dto/register.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsString
,
MinLength
,
MaxLength
,
Matches
,
IsOptional
,
IsDateString
,
IsEnum
}
from
'class-validator'
;
export
class
RegisterDto
{
@
IsString
()
inviteCode
:
string
;
@
IsString
()
@
MinLength
(
4
)
nameArabic
:
string
;
@
IsString
()
@
MinLength
(
4
)
firstName
:
string
;
@
IsString
()
@
MinLength
(
4
)
lastName
:
string
;
@
IsString
()
@
Matches
(
/^
\d{14}
$/
,
{
message
:
'National ID must be 14 digits'
})
nationalId
:
string
;
@
IsDateString
()
dateOfBirth
:
string
;
@
IsString
()
@
Matches
(
/^01
\d{9}
$/
,
{
message
:
'Primary phone must be Egyptian format (01XXXXXXXXX)'
})
phone
:
string
;
@
IsOptional
()
@
IsString
()
@
Matches
(
/^01
\d{9}
$/
,
{
message
:
'Secondary phone must be Egyptian format (01XXXXXXXXX)'
})
phoneSecondary
?:
string
;
@
IsString
()
@
MinLength
(
20
,
{
message
:
'Address must be at least 20 characters'
})
address
:
string
;
@
IsString
()
@
MinLength
(
4
)
emergencyContactName
:
string
;
@
IsString
()
@
Matches
(
/^01
\d{9}
$/
)
emergencyContactPhone
:
string
;
@
IsEnum
([
'PARENT'
,
'SIBLING'
,
'SPOUSE'
,
'FRIEND'
,
'OTHER'
])
emergencyContactRelationship
:
string
;
@
IsString
()
bankName
:
string
;
@
IsString
()
bankAccountNumber
:
string
;
@
IsString
()
@
MinLength
(
4
)
bankAccountHolderName
:
string
;
@
IsOptional
()
@
IsString
()
taxRegistrationNumber
?:
string
;
@
IsString
()
@
MinLength
(
3
)
@
MaxLength
(
30
)
@
Matches
(
/^
[
a-zA-Z0-9_
]
+$/
,
{
message
:
'Username must be alphanumeric with underscores'
})
username
:
string
;
@
IsString
()
@
MinLength
(
10
)
@
Matches
(
/
(?=
.*
[
a-z
])
/
,
{
message
:
'Password must contain at least one lowercase letter'
})
@
Matches
(
/
(?=
.*
[
A-Z
])
/
,
{
message
:
'Password must contain at least one uppercase letter'
})
@
Matches
(
/
(?=
.*
\d)
/
,
{
message
:
'Password must contain at least one number'
})
@
Matches
(
/
(?=
.*
[
!@#$%^&*()_+
\-
=
\[\]
{};':"
\\
|,.<>
\/
?
])
/
,
{
message
:
'Password must contain at least one special character'
,
})
password
:
string
;
@
IsString
()
confirmPassword
:
string
;
}
\ No newline at end of file
backend/src/modules/onboarding/dto/schedule-config.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsObject
,
IsString
}
from
'class-validator'
;
export
class
ScheduleConfigDto
{
@
IsString
()
userId
:
string
;
@
IsObject
()
schedule
:
Record
<
string
,
string
>
;
// e.g. { sunday: 'IN_OFFICE', monday: 'IN_OFFICE', tuesday: 'REMOTE', wednesday: 'OFF', thursday: 'OFF' }
}
\ No newline at end of file
backend/src/modules/onboarding/dto/self-assessment.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsString
,
IsArray
,
ValidateNested
,
IsInt
,
Min
,
Max
}
from
'class-validator'
;
import
{
Type
}
from
'class-transformer'
;
export
class
CompetencyRatingDto
{
@
IsString
()
competencyAreaId
:
string
;
@
IsInt
()
@
Min
(
0
)
@
Max
(
5
)
level
:
number
;
}
export
class
SelfAssessmentDto
{
@
IsString
()
userId
:
string
;
@
IsArray
()
@
ValidateNested
({
each
:
true
})
@
Type
(()
=>
CompetencyRatingDto
)
ratings
:
CompetencyRatingDto
[];
}
\ No newline at end of file
backend/src/modules/onboarding/invite.service.ts
0 → 100644
View file @
f9cd0fe6
import
{
Injectable
,
NotFoundException
,
BadRequestException
,
ConflictException
,
Logger
,
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
CreateInviteDto
}
from
'./dto/create-invite.dto'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
*
as
crypto
from
'crypto'
;
@
Injectable
()
export
class
InviteService
{
private
readonly
logger
=
new
Logger
(
InviteService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
create
(
dto
:
CreateInviteDto
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
const
expirationDays
=
dto
.
expirationDays
||
7
;
const
code
=
this
.
generateInviteCode
();
const
token
=
crypto
.
randomUUID
();
const
expiresAt
=
new
Date
(
Date
.
now
()
+
expirationDays
*
24
*
60
*
60
*
1000
);
const
invite
=
await
this
.
prisma
.
invite
.
create
({
data
:
{
code
,
token
,
contractorType
:
dto
.
contractorType
,
assignedProjectLeaderId
:
dto
.
assignedProjectLeaderId
||
null
,
assignedBoardIds
:
dto
.
assignedBoardIds
||
[],
welcomeNote
:
dto
.
welcomeNote
||
null
,
expiresAt
,
status
:
'ACTIVE'
,
createdById
:
currentUser
.
id
,
},
});
this
.
logger
.
log
(
`Invite
${
code
}
created by
${
currentUser
.
email
}
, expires
${
expiresAt
.
toISOString
()}
`
);
return
{
id
:
invite
.
id
,
code
:
invite
.
code
,
token
:
invite
.
token
,
contractorType
:
invite
.
contractorType
,
expiresAt
:
invite
.
expiresAt
,
status
:
invite
.
status
,
createdAt
:
invite
.
createdAt
,
};
}
async
findAll
(
currentUser
:
RequestUser
):
Promise
<
any
[]
>
{
const
invites
=
await
this
.
prisma
.
invite
.
findMany
({
orderBy
:
{
createdAt
:
'desc'
},
include
:
{
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
username
:
true
}
},
usedBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
username
:
true
}
},
},
});
// Auto-expire stale invites
const
now
=
new
Date
();
for
(
const
invite
of
invites
)
{
if
(
invite
.
status
===
'ACTIVE'
&&
invite
.
expiresAt
<
now
)
{
await
this
.
prisma
.
invite
.
update
({
where
:
{
id
:
invite
.
id
},
data
:
{
status
:
'EXPIRED'
},
});
invite
.
status
=
'EXPIRED'
;
}
}
return
invites
;
}
async
findByCode
(
code
:
string
):
Promise
<
any
>
{
const
invite
=
await
this
.
prisma
.
invite
.
findFirst
({
where
:
{
OR
:
[{
code
},
{
token
:
code
}]
},
});
if
(
!
invite
)
{
throw
new
NotFoundException
(
'Invalid invite code'
);
}
if
(
invite
.
status
!==
'ACTIVE'
)
{
throw
new
BadRequestException
(
`This invitation has been
${
invite
.
status
.
toLowerCase
()}
. Contact your administrator.`
);
}
if
(
invite
.
expiresAt
<
new
Date
())
{
await
this
.
prisma
.
invite
.
update
({
where
:
{
id
:
invite
.
id
},
data
:
{
status
:
'EXPIRED'
}
});
throw
new
BadRequestException
(
'This invitation has expired. Contact your administrator.'
);
}
return
invite
;
}
async
revoke
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
void
>
{
const
invite
=
await
this
.
prisma
.
invite
.
findUnique
({
where
:
{
id
}
});
if
(
!
invite
)
{
throw
new
NotFoundException
(
'Invite not found'
);
}
if
(
invite
.
status
!==
'ACTIVE'
)
{
throw
new
BadRequestException
(
'Only active invites can be revoked'
);
}
await
this
.
prisma
.
invite
.
update
({
where
:
{
id
},
data
:
{
status
:
'REVOKED'
},
});
this
.
logger
.
log
(
`Invite
${
invite
.
code
}
revoked by
${
currentUser
.
email
}
`
);
}
async
markAsUsed
(
inviteId
:
string
,
userId
:
string
):
Promise
<
void
>
{
await
this
.
prisma
.
invite
.
update
({
where
:
{
id
:
inviteId
},
data
:
{
status
:
'USED'
,
usedById
:
userId
,
usedAt
:
new
Date
(),
},
});
}
async
delete
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
void
>
{
if
(
currentUser
.
role
!==
'SUPER_ADMIN'
)
{
throw
new
BadRequestException
(
'Only Super Admin can delete invite records'
);
}
await
this
.
prisma
.
invite
.
delete
({
where
:
{
id
}
});
this
.
logger
.
log
(
`Invite
${
id
}
deleted by
${
currentUser
.
email
}
`
);
}
private
generateInviteCode
():
string
{
const
chars
=
'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
;
let
code
=
''
;
const
bytes
=
crypto
.
randomBytes
(
8
);
for
(
let
i
=
0
;
i
<
8
;
i
++
)
{
code
+=
chars
[
bytes
[
i
]
%
chars
.
length
];
}
return
code
;
}
}
\ No newline at end of file
backend/src/modules/onboarding/onboarding.controller.ts
0 → 100644
View file @
f9cd0fe6
import
{
Controller
,
Get
,
Post
,
Put
,
Delete
,
Body
,
Param
,
Query
,
HttpCode
,
HttpStatus
,
}
from
'@nestjs/common'
;
import
{
OnboardingService
}
from
'./onboarding.service'
;
import
{
InviteService
}
from
'./invite.service'
;
import
{
ChecklistService
}
from
'./checklist.service'
;
import
{
SalaryCalculatorService
}
from
'./salary-calculator.service'
;
import
{
CreateInviteDto
}
from
'./dto/create-invite.dto'
;
import
{
RegisterDto
}
from
'./dto/register.dto'
;
import
{
ScheduleConfigDto
}
from
'./dto/schedule-config.dto'
;
import
{
ContractSignDto
}
from
'./dto/contract-sign.dto'
;
import
{
SelfAssessmentDto
}
from
'./dto/self-assessment.dto'
;
import
{
UpdateChecklistItemDto
}
from
'./dto/checklist-item.dto'
;
import
{
Public
}
from
'../../common/decorators/public.decorator'
;
import
{
Roles
}
from
'../../common/decorators/roles.decorator'
;
import
{
CurrentUser
,
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
@
Controller
(
'onboarding'
)
export
class
OnboardingController
{
constructor
(
private
readonly
onboardingService
:
OnboardingService
,
private
readonly
inviteService
:
InviteService
,
private
readonly
checklistService
:
ChecklistService
,
private
readonly
salaryCalculator
:
SalaryCalculatorService
,
)
{}
// ─── INVITES ─────────────────────────────────────────
@
Post
(
'invites'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
createInvite
(@
Body
()
dto
:
CreateInviteDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
inviteService
.
create
(
dto
,
user
);
}
@
Get
(
'invites'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
listInvites
(@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
inviteService
.
findAll
(
user
);
}
@
Delete
(
'invites/:id'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
revokeInvite
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
inviteService
.
revoke
(
id
,
user
);
return
{
message
:
'Invite revoked'
};
}
@
Delete
(
'invites/:id/permanent'
)
@
Roles
(
'SUPER_ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
deleteInvite
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
inviteService
.
delete
(
id
,
user
);
return
{
message
:
'Invite deleted permanently'
};
}
// ─── PUBLIC REGISTRATION FLOW ────────────────────────
@
Public
()
@
Post
(
'validate-invite'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
validateInvite
(@
Body
(
'code'
)
code
:
string
)
{
return
this
.
onboardingService
.
validateInvite
(
code
);
}
@
Public
()
@
Get
(
'check-unique'
)
async
checkUnique
(@
Query
(
'field'
)
field
:
string
,
@
Query
(
'value'
)
value
:
string
)
{
return
this
.
onboardingService
.
checkUniqueField
(
field
,
value
);
}
@
Public
()
@
Post
(
'register'
)
async
register
(@
Body
()
dto
:
RegisterDto
)
{
return
this
.
onboardingService
.
register
(
dto
);
}
// ─── ONBOARDING STEPS (AUTHENTICATED) ────────────────
@
Post
(
'schedule'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
configureSchedule
(@
Body
()
dto
:
ScheduleConfigDto
)
{
return
this
.
onboardingService
.
configureSchedule
(
dto
);
}
@
Post
(
'schedule/preview'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
previewSalary
(@
Body
()
dto
:
ScheduleConfigDto
)
{
const
user
=
await
this
.
salaryCalculator
.
calculateBaseSalary
(
dto
.
schedule
,
'FULL_TIME'
,
// will be overridden by actual user type in full flow
);
return
user
;
}
@
Post
(
'contract'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
signContract
(@
Body
()
dto
:
ContractSignDto
)
{
return
this
.
onboardingService
.
signContract
(
dto
);
}
@
Post
(
'self-assessment'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
submitSelfAssessment
(@
Body
()
dto
:
SelfAssessmentDto
)
{
return
this
.
onboardingService
.
submitSelfAssessment
(
dto
);
}
// ─── CHECKLIST ───────────────────────────────────────
@
Get
(
'checklist/:userId'
)
async
getChecklist
(@
Param
(
'userId'
)
userId
:
string
)
{
return
this
.
checklistService
.
getChecklist
(
userId
);
}
@
Put
(
'checklist/:userId/items'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
updateChecklistItem
(
@
Param
(
'userId'
)
userId
:
string
,
@
Body
()
dto
:
UpdateChecklistItemDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
await
this
.
checklistService
.
updateItem
(
userId
,
dto
.
itemKey
,
dto
.
completed
,
user
.
id
);
return
{
message
:
'Checklist item updated'
};
}
// ─── ACTIVATION ──────────────────────────────────────
@
Post
(
'activate/:userId'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
activate
(@
Param
(
'userId'
)
userId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
onboardingService
.
activate
(
userId
,
user
);
}
}
\ No newline at end of file
backend/src/modules/onboarding/onboarding.module.ts
0 → 100644
View file @
f9cd0fe6
import
{
Module
}
from
'@nestjs/common'
;
import
{
OnboardingController
}
from
'./onboarding.controller'
;
import
{
OnboardingService
}
from
'./onboarding.service'
;
import
{
InviteService
}
from
'./invite.service'
;
import
{
ChecklistService
}
from
'./checklist.service'
;
import
{
SalaryCalculatorService
}
from
'./salary-calculator.service'
;
@
Module
({
controllers
:
[
OnboardingController
],
providers
:
[
OnboardingService
,
InviteService
,
ChecklistService
,
SalaryCalculatorService
],
exports
:
[
OnboardingService
,
InviteService
,
ChecklistService
,
SalaryCalculatorService
],
})
export
class
OnboardingModule
{}
\ No newline at end of file
backend/src/modules/onboarding/onboarding.service.ts
0 → 100644
View file @
f9cd0fe6
This diff is collapsed.
Click to expand it.
backend/src/modules/onboarding/salary-calculator.service.ts
0 → 100644
View file @
f9cd0fe6
import
{
Injectable
,
Logger
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
calculateBaseSalaryPiasters
}
from
'../../common/utils/salary.util'
;
@
Injectable
()
export
class
SalaryCalculatorService
{
private
readonly
logger
=
new
Logger
(
SalaryCalculatorService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
calculateBaseSalary
(
schedule
:
Record
<
string
,
string
>
,
contractorType
:
string
,
):
Promise
<
{
baseSalaryPiasters
:
number
;
breakdown
:
any
}
>
{
const
rates
=
await
this
.
getSalaryRates
();
const
baseSalaryPiasters
=
calculateBaseSalaryPiasters
(
schedule
,
contractorType
,
rates
);
const
isFullTime
=
contractorType
===
'FULL_TIME'
;
const
inOfficeRate
=
isFullTime
?
rates
.
fullTimeInOffice
:
rates
.
internInOffice
;
const
remoteRate
=
isFullTime
?
rates
.
fullTimeRemote
:
rates
.
internRemote
;
const
breakdown
:
any
[]
=
[];
for
(
const
[
day
,
type
]
of
Object
.
entries
(
schedule
))
{
if
(
type
===
'IN_OFFICE'
)
{
breakdown
.
push
({
day
,
type
,
ratePiasters
:
inOfficeRate
});
}
else
if
(
type
===
'REMOTE'
)
{
breakdown
.
push
({
day
,
type
,
ratePiasters
:
remoteRate
});
}
}
return
{
baseSalaryPiasters
,
breakdown
};
}
async
recalculateForUser
(
userId
:
string
):
Promise
<
number
>
{
const
user
=
await
this
.
prisma
.
user
.
findUnique
({
where
:
{
id
:
userId
},
select
:
{
weeklySchedule
:
true
,
contractorType
:
true
},
});
if
(
!
user
||
!
user
.
weeklySchedule
||
!
user
.
contractorType
)
{
return
0
;
}
const
{
baseSalaryPiasters
}
=
await
this
.
calculateBaseSalary
(
user
.
weeklySchedule
as
Record
<
string
,
string
>
,
user
.
contractorType
,
);
await
this
.
prisma
.
user
.
update
({
where
:
{
id
:
userId
},
data
:
{
baseSalaryPiasters
},
});
return
baseSalaryPiasters
;
}
private
async
getSalaryRates
():
Promise
<
{
fullTimeInOffice
:
number
;
fullTimeRemote
:
number
;
internInOffice
:
number
;
internRemote
:
number
;
}
>
{
const
settings
=
await
this
.
prisma
.
setting
.
findMany
({
where
:
{
key
:
{
in
:
[
'fullTimeInOfficeRate'
,
'fullTimeRemoteRate'
,
'internInOfficeRate'
,
'internRemoteRate'
],
},
},
});
const
map
:
Record
<
string
,
any
>
=
{};
for
(
const
s
of
settings
)
{
map
[
s
.
key
]
=
s
.
value
;
}
return
{
fullTimeInOffice
:
(
map
[
'fullTimeInOfficeRate'
]
as
number
)
||
240000
,
fullTimeRemote
:
(
map
[
'fullTimeRemoteRate'
]
as
number
)
||
160000
,
internInOffice
:
(
map
[
'internInOfficeRate'
]
as
number
)
||
100000
,
internRemote
:
(
map
[
'internRemoteRate'
]
as
number
)
||
50000
,
};
}
}
\ No newline at end of file
backend/src/modules/users/dto/create-user.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsString
,
IsEmail
,
IsOptional
,
MinLength
,
MaxLength
,
Matches
,
IsEnum
,
IsDateString
,
}
from
'class-validator'
;
export
class
CreateUserDto
{
@
IsEmail
()
email
:
string
;
@
IsString
()
@
MinLength
(
3
)
@
MaxLength
(
30
)
@
Matches
(
/^
[
a-zA-Z0-9_
]
+$/
,
{
message
:
'Username must be alphanumeric with underscores only'
})
username
:
string
;
@
IsString
()
@
MinLength
(
1
)
firstName
:
string
;
@
IsString
()
@
MinLength
(
1
)
lastName
:
string
;
@
IsOptional
()
@
IsString
()
displayName
?:
string
;
@
IsOptional
()
@
IsString
()
nameArabic
?:
string
;
@
IsOptional
()
@
IsString
()
nationalId
?:
string
;
@
IsOptional
()
@
IsDateString
()
dateOfBirth
?:
string
;
@
IsOptional
()
@
IsString
()
phone
?:
string
;
@
IsOptional
()
@
IsString
()
phoneSecondary
?:
string
;
@
IsOptional
()
@
IsString
()
address
?:
string
;
@
IsOptional
()
@
IsString
()
emergencyContactName
?:
string
;
@
IsOptional
()
@
IsString
()
emergencyContactPhone
?:
string
;
@
IsOptional
()
@
IsString
()
emergencyContactRelationship
?:
string
;
@
IsOptional
()
@
IsString
()
bankName
?:
string
;
@
IsOptional
()
@
IsString
()
bankAccountNumber
?:
string
;
@
IsOptional
()
@
IsString
()
bankAccountHolderName
?:
string
;
@
IsOptional
()
@
IsString
()
taxRegistrationNumber
?:
string
;
@
IsEnum
([
'SUPER_ADMIN'
,
'ADMIN'
,
'TEAM_LEAD'
,
'CONTRACTOR'
])
role
:
string
;
@
IsOptional
()
@
IsEnum
([
'FULL_TIME'
,
'PART_TIME'
,
'PROJECT_BASED'
])
contractorType
?:
string
;
@
IsOptional
()
@
IsString
()
department
?:
string
;
@
IsOptional
()
@
IsString
()
title
?:
string
;
@
IsOptional
()
@
IsString
()
bio
?:
string
;
@
IsOptional
()
@
IsString
()
timezone
?:
string
;
@
IsString
()
@
MinLength
(
10
,
{
message
:
'Password must be at least 10 characters'
})
@
Matches
(
/
(?=
.*
[
a-z
])
/
,
{
message
:
'Password must contain at least one lowercase letter'
})
@
Matches
(
/
(?=
.*
[
A-Z
])
/
,
{
message
:
'Password must contain at least one uppercase letter'
})
@
Matches
(
/
(?=
.*
\d)
/
,
{
message
:
'Password must contain at least one number'
})
@
Matches
(
/
(?=
.*
[
!@#$%^&*()_+
\-
=
\[\]
{};':"
\\
|,.<>
\/
?
])
/
,
{
message
:
'Password must contain at least one special character'
,
})
password
:
string
;
}
\ No newline at end of file
backend/src/modules/users/dto/reset-password.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsOptional
,
IsString
,
MinLength
}
from
'class-validator'
;
export
class
ResetPasswordResponseDto
{
temporaryPassword
:
string
;
message
:
string
;
}
export
class
SetActualSalaryDto
{
@
MinLength
(
0
)
actualSalaryPiasters
:
number
;
@
IsOptional
()
@
IsString
()
@
MinLength
(
1
)
reason
?:
string
;
}
export
class
AddPrivateNoteDto
{
@
IsString
()
@
MinLength
(
1
)
content
:
string
;
}
export
class
ChangeStatusDto
{
@
IsString
()
status
:
string
;
@
IsOptional
()
@
IsString
()
reason
?:
string
;
}
\ No newline at end of file
backend/src/modules/users/dto/update-user.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsString
,
IsOptional
,
IsEmail
,
MinLength
,
MaxLength
,
Matches
,
IsEnum
,
IsDateString
,
IsInt
,
Min
,
IsObject
,
}
from
'class-validator'
;
export
class
UpdateUserDto
{
@
IsOptional
()
@
IsEmail
()
email
?:
string
;
@
IsOptional
()
@
IsString
()
@
MinLength
(
3
)
@
MaxLength
(
30
)
@
Matches
(
/^
[
a-zA-Z0-9_
]
+$/
,
{
message
:
'Username must be alphanumeric with underscores only'
})
username
?:
string
;
@
IsOptional
()
@
IsString
()
firstName
?:
string
;
@
IsOptional
()
@
IsString
()
lastName
?:
string
;
@
IsOptional
()
@
IsString
()
displayName
?:
string
;
@
IsOptional
()
@
IsString
()
nameArabic
?:
string
;
@
IsOptional
()
@
IsString
()
nationalId
?:
string
;
@
IsOptional
()
@
IsDateString
()
dateOfBirth
?:
string
;
@
IsOptional
()
@
IsString
()
phone
?:
string
;
@
IsOptional
()
@
IsString
()
phoneSecondary
?:
string
;
@
IsOptional
()
@
IsString
()
address
?:
string
;
@
IsOptional
()
@
IsString
()
avatar
?:
string
;
@
IsOptional
()
@
IsString
()
emergencyContactName
?:
string
;
@
IsOptional
()
@
IsString
()
emergencyContactPhone
?:
string
;
@
IsOptional
()
@
IsString
()
emergencyContactRelationship
?:
string
;
@
IsOptional
()
@
IsString
()
bankName
?:
string
;
@
IsOptional
()
@
IsString
()
bankAccountNumber
?:
string
;
@
IsOptional
()
@
IsString
()
bankAccountHolderName
?:
string
;
@
IsOptional
()
@
IsString
()
taxRegistrationNumber
?:
string
;
@
IsOptional
()
@
IsEnum
([
'SUPER_ADMIN'
,
'ADMIN'
,
'TEAM_LEAD'
,
'CONTRACTOR'
])
role
?:
string
;
@
IsOptional
()
@
IsEnum
([
'INVITED'
,
'ONBOARDING'
,
'ACTIVE'
,
'ON_PIP'
,
'SUSPENDED'
,
'OFFBOARDING'
,
'OFFBOARDED'
])
status
?:
string
;
@
IsOptional
()
@
IsEnum
([
'FULL_TIME'
,
'PART_TIME'
,
'PROJECT_BASED'
])
contractorType
?:
string
;
@
IsOptional
()
@
IsString
()
department
?:
string
;
@
IsOptional
()
@
IsString
()
title
?:
string
;
@
IsOptional
()
@
IsString
()
bio
?:
string
;
@
IsOptional
()
@
IsString
()
timezone
?:
string
;
@
IsOptional
()
@
IsObject
()
weeklySchedule
?:
Record
<
string
,
string
>
;
@
IsOptional
()
@
IsInt
()
@
Min
(
0
)
actualSalaryPiasters
?:
number
;
@
IsOptional
()
@
IsString
()
salaryChangeReason
?:
string
;
@
IsOptional
()
@
IsDateString
()
startDate
?:
string
;
@
IsOptional
()
@
IsDateString
()
contractStartDate
?:
string
;
@
IsOptional
()
@
IsDateString
()
contractEndDate
?:
string
;
@
IsOptional
()
@
IsString
()
assignedProjectLeaderId
?:
string
;
}
\ No newline at end of file
backend/src/modules/users/dto/user-filter.dto.ts
0 → 100644
View file @
f9cd0fe6
import
{
IsOptional
,
IsString
,
IsEnum
}
from
'class-validator'
;
import
{
PaginationDto
}
from
'../../../common/dto/pagination.dto'
;
export
class
UserFilterDto
extends
PaginationDto
{
@
IsOptional
()
@
IsEnum
([
'SUPER_ADMIN'
,
'ADMIN'
,
'TEAM_LEAD'
,
'CONTRACTOR'
])
role
?:
string
;
@
IsOptional
()
@
IsEnum
([
'INVITED'
,
'ONBOARDING'
,
'ACTIVE'
,
'ON_PIP'
,
'SUSPENDED'
,
'OFFBOARDING'
,
'OFFBOARDED'
])
status
?:
string
;
@
IsOptional
()
@
IsEnum
([
'FULL_TIME'
,
'PART_TIME'
,
'PROJECT_BASED'
])
contractorType
?:
string
;
@
IsOptional
()
@
IsString
()
department
?:
string
;
@
IsOptional
()
@
IsString
()
boardId
?:
string
;
}
\ No newline at end of file
backend/src/modules/users/dto/user-response.dto.ts
0 → 100644
View file @
f9cd0fe6
export
class
UserResponseDto
{
id
:
string
;
email
:
string
;
username
:
string
;
firstName
:
string
;
lastName
:
string
;
displayName
:
string
|
null
;
nameArabic
:
string
|
null
;
avatar
:
string
|
null
;
role
:
string
;
status
:
string
;
contractorType
:
string
|
null
;
department
:
string
|
null
;
title
:
string
|
null
;
phone
:
string
|
null
;
timezone
:
string
|
null
;
bio
:
string
|
null
;
startDate
:
string
|
null
;
lastLoginAt
:
string
|
null
;
createdAt
:
string
;
}
export
class
UserProfileResponseDto
extends
UserResponseDto
{
nationalId
:
string
|
null
;
dateOfBirth
:
string
|
null
;
phoneSecondary
:
string
|
null
;
address
:
string
|
null
;
emergencyContactName
:
string
|
null
;
emergencyContactPhone
:
string
|
null
;
emergencyContactRelationship
:
string
|
null
;
bankName
:
string
|
null
;
bankAccountNumber
:
string
|
null
;
bankAccountHolderName
:
string
|
null
;
taxRegistrationNumber
:
string
|
null
;
weeklySchedule
:
Record
<
string
,
string
>
|
null
;
baseSalaryPiasters
:
number
;
actualSalaryPiasters
:
number
;
contractStartDate
:
string
|
null
;
contractEndDate
:
string
|
null
;
assignedProjectLeaderId
:
string
|
null
;
forcePasswordChange
:
boolean
;
}
export
class
ContractorDirectoryEntryDto
{
id
:
string
;
firstName
:
string
;
lastName
:
string
;
displayName
:
string
|
null
;
avatar
:
string
|
null
;
role
:
string
;
contractorType
:
string
|
null
;
department
:
string
|
null
;
title
:
string
|
null
;
status
:
string
;
}
\ No newline at end of file
backend/src/modules/users/users.controller.ts
0 → 100644
View file @
f9cd0fe6
import
{
Controller
,
Get
,
Post
,
Put
,
Delete
,
Body
,
Param
,
Query
,
HttpCode
,
HttpStatus
,
}
from
'@nestjs/common'
;
import
{
UsersService
}
from
'./users.service'
;
import
{
CreateUserDto
}
from
'./dto/create-user.dto'
;
import
{
UpdateUserDto
}
from
'./dto/update-user.dto'
;
import
{
UserFilterDto
}
from
'./dto/user-filter.dto'
;
import
{
AddPrivateNoteDto
,
ChangeStatusDto
,
SetActualSalaryDto
}
from
'./dto/reset-password.dto'
;
import
{
CurrentUser
,
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
Roles
}
from
'../../common/decorators/roles.decorator'
;
@
Controller
(
'users'
)
export
class
UsersController
{
constructor
(
private
readonly
usersService
:
UsersService
)
{}
@
Post
()
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
create
(@
Body
()
dto
:
CreateUserDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
usersService
.
create
(
dto
,
user
);
}
@
Get
()
async
findAll
(@
Query
()
filter
:
UserFilterDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
usersService
.
findAll
(
filter
,
user
);
}
@
Get
(
'directory'
)
async
getDirectory
(@
Query
()
filter
:
UserFilterDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
usersService
.
getDirectory
(
filter
,
user
);
}
@
Get
(
':id'
)
async
findById
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
usersService
.
findById
(
id
,
user
);
}
@
Put
(
':id'
)
async
update
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
UpdateUserDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
usersService
.
update
(
id
,
dto
,
user
);
}
@
Delete
(
':id'
)
@
Roles
(
'SUPER_ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
softDelete
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
usersService
.
softDelete
(
id
,
user
);
return
{
message
:
'User deleted successfully'
};
}
@
Post
(
':id/reset-password'
)
@
Roles
(
'SUPER_ADMIN'
)
async
resetPassword
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
usersService
.
resetPassword
(
id
,
user
);
}
@
Post
(
':id/force-logout'
)
@
Roles
(
'SUPER_ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
forceLogout
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
usersService
.
forceLogout
(
id
,
user
);
return
{
message
:
'User sessions revoked'
};
}
@
Post
(
'force-logout-all'
)
@
Roles
(
'SUPER_ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
forceLogoutAll
(@
CurrentUser
()
user
:
RequestUser
)
{
const
result
=
await
this
.
usersService
.
forceLogoutAll
(
user
);
return
{
message
:
`
${
result
.
count
}
sessions revoked`
};
}
@
Post
(
':id/unlock'
)
@
Roles
(
'SUPER_ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
unlockAccount
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
usersService
.
unlockAccount
(
id
,
user
);
return
{
message
:
'Account unlocked'
};
}
@
Get
(
':id/sessions'
)
async
getUserSessions
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
usersService
.
getUserSessions
(
id
,
user
);
}
@
Delete
(
':id/sessions/:sessionId'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
revokeSession
(
@
Param
(
'id'
)
userId
:
string
,
@
Param
(
'sessionId'
)
sessionId
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
await
this
.
usersService
.
revokeSession
(
userId
,
sessionId
,
user
);
return
{
message
:
'Session revoked'
};
}
@
Delete
(
':id/sessions'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
revokeAllOtherSessions
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
const
result
=
await
this
.
usersService
.
revokeAllOtherSessions
(
id
,
user
);
return
{
message
:
`
${
result
.
count
}
sessions revoked`
};
}
@
Put
(
':id/status'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
changeStatus
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
ChangeStatusDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
usersService
.
changeStatus
(
id
,
dto
.
status
,
dto
.
reason
,
user
);
}
@
Put
(
':id/salary'
)
@
Roles
(
'SUPER_ADMIN'
)
async
setActualSalary
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
SetActualSalaryDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
usersService
.
setActualSalary
(
id
,
dto
.
actualSalaryPiasters
,
dto
.
reason
,
user
);
}
@
Post
(
':id/notes'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
addPrivateNote
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
AddPrivateNoteDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
usersService
.
addPrivateNote
(
id
,
dto
,
user
);
}
@
Get
(
':id/notes'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
getPrivateNotes
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
usersService
.
getPrivateNotes
(
id
,
user
);
}
}
\ No newline at end of file
backend/src/modules/users/users.module.ts
0 → 100644
View file @
f9cd0fe6
import
{
Module
}
from
'@nestjs/common'
;
import
{
UsersController
}
from
'./users.controller'
;
import
{
UsersService
}
from
'./users.service'
;
@
Module
({
controllers
:
[
UsersController
],
providers
:
[
UsersService
],
exports
:
[
UsersService
],
})
export
class
UsersModule
{}
\ No newline at end of file
backend/src/modules/users/users.service.ts
0 → 100644
View file @
f9cd0fe6
This diff is collapsed.
Click to expand it.
prisma/schema-additions.prisma
0 → 100644
View file @
f9cd0fe6
//
───
ADD
TO
User
MODEL
──────────────────────────────────────────
//
These
fields
must
exist
on
the
User
model
:
//
nameArabic
String
?
//
nationalId
String
?
@
unique
//
dateOfBirth
DateTime
?
//
phone
String
?
@
unique
//
phoneSecondary
String
?
//
address
String
?
//
emergencyContactName
String
?
//
emergencyContactPhone
String
?
//
emergencyContactRelationship
String
?
//
bankName
String
?
//
bankAccountNumber
String
?
//
bankAccountHolderName
String
?
//
taxRegistrationNumber
String
?
//
contractorType
String
?
//
department
String
?
//
title
String
?
//
bio
String
?
//
timezone
String
@
default
(
"Africa/Cairo"
)
//
weeklySchedule
Json
?
//
baseSalaryPiasters
Int
@
default
(
0
)
//
actualSalaryPiasters
Int
@
default
(
0
)
//
startDate
DateTime
?
//
contractStartDate
DateTime
?
//
contractEndDate
DateTime
?
//
assignedProjectLeaderId
String
?
//
onboardingChecklist
Json
?
//
deletedAt
DateTime
?
//
───
NEW
MODELS
─────────────────────────────────────────────────
model
Invite
{
id
String
@
id
@
default
(
uuid
())
code
String
@
unique
token
String
@
unique
contractorType
String
assignedProjectLeaderId
String
?
assignedBoardIds
String
[]
welcomeNote
String
?
expiresAt
DateTime
status
String
@
default
(
"ACTIVE"
)
//
ACTIVE
,
USED
,
EXPIRED
,
REVOKED
createdById
String
createdBy
User
@
relation
(
"InviteCreator"
,
fields
:
[
createdById
],
references
:
[
id
])
usedById
String
?
usedBy
User
?
@
relation
(
"InviteUsed"
,
fields
:
[
usedById
],
references
:
[
id
])
usedAt
DateTime
?
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
@@
index
([
code
])
@@
index
([
token
])
@@
index
([
status
])
}
model
BoardMember
{
id
String
@
id
@
default
(
uuid
())
boardId
String
userId
String
role
String
@
default
(
"MEMBER"
)
//
OWNER
,
ADMIN
,
MEMBER
,
VIEWER
joinedAt
DateTime
@
default
(
now
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
user
User
@
relation
(
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
@@
unique
([
boardId
,
userId
])
@@
index
([
boardId
])
@@
index
([
userId
])
}
model
Contract
{
id
String
@
id
@
default
(
uuid
())
userId
String
user
User
@
relation
(
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
contractType
String
contractText
String
signedAt
DateTime
signedFullName
String
acknowledgedClauses
String
[]
signatureIpAddress
String
signatureUserAgent
String
baseSalaryAtSigning
Int
scheduleAtSigning
Json
?
startDate
DateTime
endDate
DateTime
?
status
String
@
default
(
"ACTIVE"
)
//
DRAFT
,
ACTIVE
,
EXPIRED
,
TERMINATED
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
@@
index
([
userId
])
@@
index
([
status
])
}
model
CompetencyRating
{
id
String
@
id
@
default
(
uuid
())
userId
String
competencyAreaId
String
type
String
//
SELF
,
PL_ASSESSMENT
level
Int
//
0
-
5
assessedAt
DateTime
@
default
(
now
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
user
User
@
relation
(
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
competencyArea
CompetencyArea
@
relation
(
fields
:
[
competencyAreaId
],
references
:
[
id
])
@@
unique
([
userId
,
competencyAreaId
,
type
])
@@
index
([
userId
])
}
model
SalaryChangeLog
{
id
String
@
id
@
default
(
uuid
())
userId
String
oldSalaryPiasters
Int
newSalaryPiasters
Int
reason
String
changedById
String
createdAt
DateTime
@
default
(
now
())
@@
index
([
userId
])
}
model
StatusChangeLog
{
id
String
@
id
@
default
(
uuid
())
userId
String
fromStatus
String
toStatus
String
reason
String
?
changedById
String
createdAt
DateTime
@
default
(
now
())
@@
index
([
userId
])
}
model
PrivateNote
{
id
String
@
id
@
default
(
uuid
())
userId
String
content
String
authorId
String
author
User
@
relation
(
"NoteAuthor"
,
fields
:
[
authorId
],
references
:
[
id
])
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
@@
index
([
userId
])
}
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment