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
a791a680
Commit
a791a680
authored
Apr 01, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 7 files via Son of Anton
parent
1a902ad7
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1895 additions
and
0 deletions
+1895
-0
app.module.ts
backend/src/app.module.ts
+5
-0
analytics.controller.ts
backend/src/modules/analytics/analytics.controller.ts
+181
-0
analytics.module.ts
backend/src/modules/analytics/analytics.module.ts
+12
-0
analytics.service.ts
backend/src/modules/analytics/analytics.service.ts
+943
-0
data-export.service.ts
backend/src/modules/analytics/data-export.service.ts
+320
-0
analytics-filter.dto.ts
backend/src/modules/analytics/dto/analytics-filter.dto.ts
+100
-0
report-builder.service.ts
backend/src/modules/analytics/report-builder.service.ts
+334
-0
No files found.
backend/src/app.module.ts
View file @
a791a680
...
...
@@ -55,6 +55,9 @@ import { MeetingsModule } from './modules/meetings/meetings.module';
// ─── Phase 2D: Reports & Daily Operations ───────────────────
import
{
ReportsModule
}
from
'./modules/reports/reports.module'
;
// ─── Phase 3A: Admin & Intelligence ─────────────────────────
import
{
AnalyticsModule
}
from
'./modules/analytics/analytics.module'
;
import
{
JwtAuthGuard
}
from
'./common/guards/jwt-auth.guard'
;
import
{
RolesGuard
}
from
'./common/guards/roles.guard'
;
import
{
TransformInterceptor
}
from
'./common/interceptors/transform.interceptor'
;
...
...
@@ -108,6 +111,8 @@ import { RateLimitMiddleware } from './common/middleware/rate-limit.middleware';
MeetingsModule
,
// Phase 2D
ReportsModule
,
// Phase 3A
AnalyticsModule
,
],
providers
:
[
{
provide
:
APP_GUARD
,
useClass
:
JwtAuthGuard
},
...
...
backend/src/modules/analytics/analytics.controller.ts
0 → 100644
View file @
a791a680
import
{
Controller
,
Get
,
Post
,
Body
,
Param
,
Query
,
Res
,
HttpCode
,
HttpStatus
,
}
from
'@nestjs/common'
;
import
{
Response
}
from
'express'
;
import
{
AnalyticsService
}
from
'./analytics.service'
;
import
{
ReportBuilderService
}
from
'./report-builder.service'
;
import
{
DataExportService
}
from
'./data-export.service'
;
import
{
AnalyticsFilterDto
,
ReportBuilderQueryDto
,
ExportRequestDto
}
from
'./dto/analytics-filter.dto'
;
import
{
CurrentUser
,
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
Roles
}
from
'../../common/decorators/roles.decorator'
;
@
Controller
(
'analytics'
)
export
class
AnalyticsController
{
constructor
(
private
readonly
analyticsService
:
AnalyticsService
,
private
readonly
reportBuilderService
:
ReportBuilderService
,
private
readonly
dataExportService
:
DataExportService
,
)
{}
// ─── DASHBOARDS ──────────────────────────────────────────
@
Get
(
'dashboard'
)
async
getDashboard
(@
CurrentUser
()
user
:
RequestUser
)
{
switch
(
user
.
role
)
{
case
'SUPER_ADMIN'
:
return
this
.
analyticsService
.
getSuperAdminDashboard
();
case
'ADMIN'
:
return
this
.
analyticsService
.
getAdminDashboard
();
case
'TEAM_LEAD'
:
return
this
.
analyticsService
.
getProjectLeaderDashboard
(
user
.
id
);
case
'CONTRACTOR'
:
return
this
.
analyticsService
.
getContractorDashboard
(
user
.
id
);
default
:
return
{};
}
}
@
Get
(
'dashboard/contractor'
)
async
getContractorDashboard
(@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
analyticsService
.
getContractorDashboard
(
user
.
id
);
}
@
Get
(
'dashboard/contractor/:userId'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
getContractorDashboardAdmin
(@
Param
(
'userId'
)
userId
:
string
)
{
return
this
.
analyticsService
.
getContractorDashboard
(
userId
);
}
@
Get
(
'dashboard/project-leader'
)
@
Roles
(
'SUPER_ADMIN'
,
'TEAM_LEAD'
)
async
getProjectLeaderDashboard
(@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
analyticsService
.
getProjectLeaderDashboard
(
user
.
id
);
}
@
Get
(
'dashboard/admin'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
getAdminDashboard
()
{
return
this
.
analyticsService
.
getAdminDashboard
();
}
@
Get
(
'dashboard/super-admin'
)
@
Roles
(
'SUPER_ADMIN'
)
async
getSuperAdminDashboard
()
{
return
this
.
analyticsService
.
getSuperAdminDashboard
();
}
// ─── ANALYTICS ENDPOINTS ─────────────────────────────────
@
Get
(
'deductions'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
getDeductionAnalytics
(@
Query
()
filter
:
AnalyticsFilterDto
)
{
return
this
.
analyticsService
.
getDeductionAnalytics
(
filter
);
}
@
Get
(
'tasks'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
,
'TEAM_LEAD'
)
async
getTaskAnalytics
(@
Query
()
filter
:
AnalyticsFilterDto
)
{
return
this
.
analyticsService
.
getTaskAnalytics
(
filter
);
}
@
Get
(
'system-health'
)
@
Roles
(
'SUPER_ADMIN'
)
async
getSystemHealth
()
{
return
this
.
analyticsService
.
getSystemHealth
();
}
// ─── CUSTOM REPORT BUILDER ───────────────────────────────
@
Post
(
'report-builder'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
,
'TEAM_LEAD'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
executeReportQuery
(@
Body
()
query
:
ReportBuilderQueryDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
reportBuilderService
.
executeQuery
(
query
,
user
);
}
// ─── DATA EXPORT ─────────────────────────────────────────
@
Post
(
'export'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
,
'TEAM_LEAD'
)
async
exportData
(
@
Body
()
dto
:
ExportRequestDto
,
@
CurrentUser
()
user
:
RequestUser
,
@
Res
()
res
:
Response
,
)
{
const
result
=
await
this
.
dataExportService
.
exportData
(
dto
,
user
);
const
format
=
dto
.
format
||
'CSV'
;
if
(
format
===
'JSON'
)
{
res
.
setHeader
(
'Content-Type'
,
'application/json'
);
res
.
setHeader
(
'Content-Disposition'
,
`attachment; filename=
${
result
.
filename
}
.json`
);
res
.
send
(
JSON
.
stringify
(
result
.
data
,
null
,
2
));
}
else
{
// CSV
const
csv
=
this
.
convertToCSV
(
result
.
data
);
res
.
setHeader
(
'Content-Type'
,
'text/csv'
);
res
.
setHeader
(
'Content-Disposition'
,
`attachment; filename=
${
result
.
filename
}
.csv`
);
res
.
send
(
csv
);
}
}
@
Get
(
'export/contractor/:userId'
)
@
Roles
(
'SUPER_ADMIN'
)
async
exportContractorPackage
(
@
Param
(
'userId'
)
userId
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
@
Res
()
res
:
Response
,
)
{
const
data
=
await
this
.
dataExportService
.
exportContractorPackage
(
userId
,
user
);
res
.
setHeader
(
'Content-Type'
,
'application/json'
);
res
.
setHeader
(
'Content-Disposition'
,
`attachment; filename=contractor-
${
userId
}
-
${
new
Date
().
toISOString
().
split
(
'T'
)[
0
]}
.json`
);
res
.
send
(
JSON
.
stringify
(
data
,
null
,
2
));
}
private
convertToCSV
(
data
:
any
[]):
string
{
if
(
!
data
||
data
.
length
===
0
)
return
''
;
const
flattenObject
=
(
obj
:
any
,
prefix
=
''
):
Record
<
string
,
any
>
=>
{
const
flat
:
Record
<
string
,
any
>
=
{};
for
(
const
[
key
,
value
]
of
Object
.
entries
(
obj
))
{
const
fullKey
=
prefix
?
`
${
prefix
}
.
${
key
}
`
:
key
;
if
(
value
!==
null
&&
typeof
value
===
'object'
&&
!
Array
.
isArray
(
value
)
&&
!
(
value
instanceof
Date
))
{
Object
.
assign
(
flat
,
flattenObject
(
value
,
fullKey
));
}
else
if
(
Array
.
isArray
(
value
))
{
flat
[
fullKey
]
=
value
.
map
((
v
)
=>
(
typeof
v
===
'object'
?
JSON
.
stringify
(
v
)
:
v
)).
join
(
'; '
);
}
else
{
flat
[
fullKey
]
=
value
;
}
}
return
flat
;
};
const
flatData
=
data
.
map
((
row
)
=>
flattenObject
(
row
));
const
allKeys
=
new
Set
<
string
>
();
flatData
.
forEach
((
row
)
=>
Object
.
keys
(
row
).
forEach
((
k
)
=>
allKeys
.
add
(
k
)));
const
headers
=
Array
.
from
(
allKeys
);
const
escapeCSV
=
(
val
:
any
):
string
=>
{
if
(
val
===
null
||
val
===
undefined
)
return
''
;
const
str
=
String
(
val
);
if
(
str
.
includes
(
','
)
||
str
.
includes
(
'"'
)
||
str
.
includes
(
'
\
n'
))
{
return
`"
${
str
.
replace
(
/"/g
,
'""'
)}
"`
;
}
return
str
;
};
const
rows
=
[
headers
.
join
(
','
),
...
flatData
.
map
((
row
)
=>
headers
.
map
((
h
)
=>
escapeCSV
(
row
[
h
])).
join
(
','
)),
];
return
rows
.
join
(
'
\
n'
);
}
}
\ No newline at end of file
backend/src/modules/analytics/analytics.module.ts
0 → 100644
View file @
a791a680
import
{
Module
}
from
'@nestjs/common'
;
import
{
AnalyticsController
}
from
'./analytics.controller'
;
import
{
AnalyticsService
}
from
'./analytics.service'
;
import
{
ReportBuilderService
}
from
'./report-builder.service'
;
import
{
DataExportService
}
from
'./data-export.service'
;
@
Module
({
controllers
:
[
AnalyticsController
],
providers
:
[
AnalyticsService
,
ReportBuilderService
,
DataExportService
],
exports
:
[
AnalyticsService
,
ReportBuilderService
,
DataExportService
],
})
export
class
AnalyticsModule
{}
\ No newline at end of file
backend/src/modules/analytics/analytics.service.ts
0 → 100644
View file @
a791a680
import
{
Injectable
,
ForbiddenException
,
Logger
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
AnalyticsFilterDto
}
from
'./dto/analytics-filter.dto'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
piasterToEgp
}
from
'../../common/utils/salary.util'
;
@
Injectable
()
export
class
AnalyticsService
{
private
readonly
logger
=
new
Logger
(
AnalyticsService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
// ─── CONTRACTOR DASHBOARD ─────────────────────────────────
async
getContractorDashboard
(
userId
:
string
):
Promise
<
any
>
{
const
now
=
new
Date
();
const
month
=
now
.
getMonth
()
+
1
;
const
year
=
now
.
getFullYear
();
const
monthStart
=
new
Date
(
year
,
month
-
1
,
1
);
const
monthEnd
=
new
Date
(
year
,
month
,
0
,
23
,
59
,
59
,
999
);
const
user
=
await
this
.
prisma
.
user
.
findUnique
({
where
:
{
id
:
userId
},
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
actualSalaryPiasters
:
true
,
baseSalaryPiasters
:
true
,
weeklySchedule
:
true
,
currentStreak
:
true
,
bestStreak
:
true
,
status
:
true
,
},
});
if
(
!
user
)
return
null
;
// Tasks assigned & in active columns
const
myCards
=
await
this
.
prisma
.
card
.
findMany
({
where
:
{
assignees
:
{
some
:
{
id
:
userId
}
},
deletedAt
:
null
,
isArchived
:
false
,
},
include
:
{
column
:
{
select
:
{
id
:
true
,
name
:
true
,
type
:
true
,
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
}
}
},
labels
:
{
select
:
{
id
:
true
,
name
:
true
,
color
:
true
}
},
},
orderBy
:
[{
dueDate
:
{
sort
:
'asc'
,
nulls
:
'last'
}
},
{
position
:
'asc'
}],
});
// Group by column type priority
const
columnPriority
:
Record
<
string
,
number
>
=
{
DOING
:
0
,
TODO
:
1
,
IN_REVIEW
:
2
,
FROZEN
:
3
,
CUSTOM
:
4
,
BACKLOG
:
5
,
DONE
:
6
,
};
const
sortedCards
=
myCards
.
filter
((
c
)
=>
c
.
column
.
type
!==
'DONE'
)
.
sort
((
a
,
b
)
=>
(
columnPriority
[
a
.
column
.
type
]
??
4
)
-
(
columnPriority
[
b
.
column
.
type
]
??
4
));
// Upcoming deadlines (next 7 days)
const
sevenDaysOut
=
new
Date
(
now
.
getTime
()
+
7
*
24
*
60
*
60
*
1000
);
const
upcomingDeadlines
=
myCards
.
filter
(
(
c
)
=>
c
.
dueDate
&&
new
Date
(
c
.
dueDate
)
>=
now
&&
new
Date
(
c
.
dueDate
)
<=
sevenDaysOut
&&
!
c
.
completedAt
,
);
// This month stats
const
{
getScheduledDaysOfWeek
,
getWorkingDaysInMonth
}
=
await
import
(
'../../common/utils/date.util'
);
const
schedule
=
(
user
.
weeklySchedule
as
Record
<
string
,
string
>
)
||
{};
const
scheduledDays
=
getScheduledDaysOfWeek
(
schedule
);
const
expectedDays
=
getWorkingDaysInMonth
(
year
,
month
,
scheduledDays
);
let
daysReported
=
0
;
try
{
const
dailyReportModel
=
(
this
.
prisma
as
any
).
dailyReport
;
if
(
dailyReportModel
&&
typeof
dailyReportModel
.
count
===
'function'
)
{
daysReported
=
await
dailyReportModel
.
count
({
where
:
{
userId
,
reportDate
:
{
gte
:
monthStart
,
lte
:
monthEnd
},
status
:
{
not
:
'DRAFT'
},
},
});
}
}
catch
{
/* Phase 2D may not be migrated yet */
}
// Learning goals
const
learningGoals
=
await
this
.
prisma
.
learningGoal
.
findMany
({
where
:
{
userId
,
status
:
{
in
:
[
'ACTIVE'
,
'OVERDUE'
,
'EXTENDED'
]
},
},
include
:
{
competencyArea
:
{
select
:
{
name
:
true
}
}
},
orderBy
:
{
deadline
:
'asc'
},
take
:
5
,
});
// Upcoming meetings
const
upcomingMeetings
=
await
this
.
prisma
.
meeting
.
findMany
({
where
:
{
startTime
:
{
gte
:
now
},
status
:
'SCHEDULED'
,
invitees
:
{
some
:
{
userId
}
},
},
orderBy
:
{
startTime
:
'asc'
},
take
:
3
,
});
// Unread notifications count
const
unreadNotifications
=
await
this
.
prisma
.
notification
.
count
({
where
:
{
userId
,
isRead
:
false
},
});
return
{
user
:
{
id
:
user
.
id
,
firstName
:
user
.
firstName
,
lastName
:
user
.
lastName
,
status
:
user
.
status
,
},
tasks
:
{
total
:
sortedCards
.
length
,
doing
:
sortedCards
.
filter
((
c
)
=>
c
.
column
.
type
===
'DOING'
).
length
,
todo
:
sortedCards
.
filter
((
c
)
=>
c
.
column
.
type
===
'TODO'
).
length
,
inReview
:
sortedCards
.
filter
((
c
)
=>
c
.
column
.
type
===
'IN_REVIEW'
).
length
,
frozen
:
sortedCards
.
filter
((
c
)
=>
c
.
column
.
type
===
'FROZEN'
).
length
,
cards
:
sortedCards
.
slice
(
0
,
20
).
map
((
c
)
=>
({
id
:
c
.
id
,
cardNumber
:
c
.
cardNumber
,
title
:
c
.
title
,
columnName
:
c
.
column
.
name
,
columnType
:
c
.
column
.
type
,
boardName
:
c
.
column
.
board
.
name
,
boardKey
:
c
.
column
.
board
.
key
,
dueDate
:
c
.
dueDate
,
isOverdue
:
c
.
dueDate
?
new
Date
(
c
.
dueDate
)
<
now
:
false
,
priority
:
c
.
priority
,
bountyPiasters
:
c
.
bountyPiasters
,
labels
:
c
.
labels
,
})),
},
upcomingDeadlines
:
upcomingDeadlines
.
map
((
c
)
=>
({
id
:
c
.
id
,
cardNumber
:
c
.
cardNumber
,
title
:
c
.
title
,
dueDate
:
c
.
dueDate
,
boardName
:
c
.
column
.
board
.
name
,
priority
:
c
.
priority
,
})),
thisMonth
:
{
daysReported
,
expectedDays
,
month
,
year
,
},
streak
:
{
current
:
user
.
currentStreak
||
0
,
best
:
user
.
bestStreak
||
0
,
},
learningGoals
:
learningGoals
.
map
((
g
)
=>
({
id
:
g
.
id
,
title
:
g
.
title
,
competencyArea
:
g
.
competencyArea
?.
name
,
deadline
:
g
.
deadline
,
status
:
g
.
status
,
isOverdue
:
g
.
deadline
<
now
&&
g
.
status
===
'ACTIVE'
,
})),
upcomingMeetings
:
upcomingMeetings
.
map
((
m
)
=>
({
id
:
m
.
id
,
title
:
m
.
title
,
startTime
:
m
.
startTime
,
location
:
m
.
location
,
})),
unreadNotifications
,
};
}
// ─── PROJECT LEADER DASHBOARD ─────────────────────────────
async
getProjectLeaderDashboard
(
userId
:
string
):
Promise
<
any
>
{
const
now
=
new
Date
();
const
month
=
now
.
getMonth
()
+
1
;
const
year
=
now
.
getFullYear
();
const
today
=
new
Date
(
now
.
getFullYear
(),
now
.
getMonth
(),
now
.
getDate
());
const
todayEnd
=
new
Date
(
today
.
getTime
()
+
24
*
60
*
60
*
1000
-
1
);
// Get boards the PL manages
const
plBoards
=
await
this
.
prisma
.
boardMember
.
findMany
({
where
:
{
userId
},
select
:
{
boardId
:
true
,
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
}
},
});
const
boardIds
=
plBoards
.
map
((
b
)
=>
b
.
boardId
);
// Team members (contractors on PL's boards)
const
teamMemberIds
=
new
Set
<
string
>
();
for
(
const
boardId
of
boardIds
)
{
const
members
=
await
this
.
prisma
.
boardMember
.
findMany
({
where
:
{
boardId
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
,
role
:
true
,
status
:
true
,
assignedProjectLeaderId
:
true
}
}
},
});
for
(
const
m
of
members
)
{
if
(
m
.
user
.
role
===
'CONTRACTOR'
&&
(
m
.
user
.
status
===
'ACTIVE'
||
m
.
user
.
status
===
'ON_PIP'
))
{
teamMemberIds
.
add
(
m
.
userId
);
}
}
}
const
teamIds
=
Array
.
from
(
teamMemberIds
);
// Team report status today
let
reportStatus
:
any
[]
=
[];
try
{
const
dailyReportModel
=
(
this
.
prisma
as
any
).
dailyReport
;
if
(
dailyReportModel
&&
typeof
dailyReportModel
.
findMany
===
'function'
)
{
const
todaysReports
=
await
dailyReportModel
.
findMany
({
where
:
{
userId
:
{
in
:
teamIds
},
reportDate
:
{
gte
:
today
,
lte
:
todayEnd
},
status
:
{
not
:
'DRAFT'
},
},
select
:
{
userId
:
true
,
status
:
true
},
});
const
reportedUserIds
=
new
Set
(
todaysReports
.
map
((
r
:
any
)
=>
r
.
userId
));
const
teamUsers
=
await
this
.
prisma
.
user
.
findMany
({
where
:
{
id
:
{
in
:
teamIds
}
},
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
},
});
reportStatus
=
teamUsers
.
map
((
u
)
=>
({
...
u
,
reported
:
reportedUserIds
.
has
(
u
.
id
),
reportStatus
:
todaysReports
.
find
((
r
:
any
)
=>
r
.
userId
===
u
.
id
)?.
status
||
null
,
}));
}
}
catch
{
/* Reports module may not exist yet */
}
// Pending report reviews
let
pendingReviews
=
0
;
try
{
const
dailyReportModel
=
(
this
.
prisma
as
any
).
dailyReport
;
if
(
dailyReportModel
&&
typeof
dailyReportModel
.
count
===
'function'
)
{
pendingReviews
=
await
dailyReportModel
.
count
({
where
:
{
userId
:
{
in
:
teamIds
},
status
:
{
in
:
[
'SUBMITTED'
,
'LATE'
]
},
},
});
}
}
catch
{
/* ok */
}
// Board overview - card counts by column
const
boardOverview
=
[];
for
(
const
b
of
plBoards
)
{
const
columns
=
await
this
.
prisma
.
column
.
findMany
({
where
:
{
boardId
:
b
.
boardId
},
orderBy
:
{
position
:
'asc'
},
select
:
{
id
:
true
,
name
:
true
,
type
:
true
,
_count
:
{
select
:
{
cards
:
{
where
:
{
deletedAt
:
null
,
isArchived
:
false
}
}
}
},
},
});
boardOverview
.
push
({
board
:
b
.
board
,
columns
:
columns
.
map
((
c
)
=>
({
name
:
c
.
name
,
type
:
c
.
type
,
cardCount
:
(
c
as
any
).
_count
?.
cards
||
0
,
})),
});
}
// At-risk tasks (overdue or due within 2 days)
const
twoDaysOut
=
new
Date
(
now
.
getTime
()
+
2
*
24
*
60
*
60
*
1000
);
const
atRiskCards
=
await
this
.
prisma
.
card
.
findMany
({
where
:
{
column
:
{
boardId
:
{
in
:
boardIds
}
},
dueDate
:
{
lte
:
twoDaysOut
},
completedAt
:
null
,
deletedAt
:
null
,
isArchived
:
false
,
},
include
:
{
assignees
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
}
},
column
:
{
select
:
{
name
:
true
,
board
:
{
select
:
{
key
:
true
}
}
}
},
},
orderBy
:
{
dueDate
:
'asc'
},
take
:
20
,
});
// Deductions initiated by PL pending review
const
pendingDeductions
=
await
this
.
prisma
.
deduction
.
count
({
where
:
{
initiatedById
:
userId
,
status
:
'PENDING_ADMIN_REVIEW'
,
},
});
// Evaluations due (if in evaluation period)
const
pendingEvaluations
=
await
this
.
prisma
.
evaluation
.
count
({
where
:
{
user
:
{
assignedProjectLeaderId
:
userId
},
status
:
'PENDING_TECHNICAL'
,
month
,
year
,
},
});
// Upcoming meetings
const
upcomingMeetings
=
await
this
.
prisma
.
meeting
.
findMany
({
where
:
{
startTime
:
{
gte
:
now
},
status
:
'SCHEDULED'
,
OR
:
[
{
createdById
:
userId
},
{
invitees
:
{
some
:
{
userId
}
}
},
],
},
orderBy
:
{
startTime
:
'asc'
},
take
:
3
,
include
:
{
invitees
:
{
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
}
},
},
},
});
return
{
teamReportStatus
:
reportStatus
,
pendingReviews
,
boardOverview
,
atRiskTasks
:
atRiskCards
.
map
((
c
)
=>
({
id
:
c
.
id
,
cardNumber
:
c
.
cardNumber
,
title
:
c
.
title
,
dueDate
:
c
.
dueDate
,
isOverdue
:
c
.
dueDate
?
new
Date
(
c
.
dueDate
)
<
now
:
false
,
columnName
:
c
.
column
.
name
,
boardKey
:
c
.
column
.
board
.
key
,
assignees
:
c
.
assignees
,
})),
pendingDeductions
,
pendingEvaluations
,
upcomingMeetings
:
upcomingMeetings
.
map
((
m
)
=>
({
id
:
m
.
id
,
title
:
m
.
title
,
startTime
:
m
.
startTime
,
inviteeCount
:
m
.
invitees
.
length
,
})),
teamSize
:
teamIds
.
length
,
};
}
// ─── ADMIN DASHBOARD ──────────────────────────────────────
async
getAdminDashboard
():
Promise
<
any
>
{
const
now
=
new
Date
();
const
month
=
now
.
getMonth
()
+
1
;
const
year
=
now
.
getFullYear
();
const
monthStart
=
new
Date
(
year
,
month
-
1
,
1
);
const
monthEnd
=
new
Date
(
year
,
month
,
0
,
23
,
59
,
59
,
999
);
// Active contractors by type and status
const
contractorsByStatus
=
await
this
.
prisma
.
user
.
groupBy
({
by
:
[
'status'
,
'contractorType'
],
where
:
{
role
:
'CONTRACTOR'
,
deletedAt
:
null
},
_count
:
true
,
});
const
fullTimerCount
=
contractorsByStatus
.
filter
((
c
)
=>
c
.
contractorType
===
'FULL_TIME'
&&
c
.
status
!==
'OFFBOARDED'
)
.
reduce
((
sum
,
c
)
=>
sum
+
c
.
_count
,
0
);
const
internCount
=
contractorsByStatus
.
filter
((
c
)
=>
c
.
contractorType
===
'INTERN'
&&
c
.
status
!==
'OFFBOARDED'
)
.
reduce
((
sum
,
c
)
=>
sum
+
c
.
_count
,
0
);
// Onboarding pipeline
const
onboarding
=
await
this
.
prisma
.
user
.
findMany
({
where
:
{
status
:
'ONBOARDING'
,
deletedAt
:
null
},
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
createdAt
:
true
},
});
// Payroll status
const
currentPayroll
=
await
this
.
prisma
.
payroll
.
findUnique
({
where
:
{
month_year
:
{
month
,
year
}
},
select
:
{
id
:
true
,
status
:
true
,
totalNetPiasters
:
true
,
contractorCount
:
true
},
});
// Deductions this month
const
deductionsThisMonth
=
await
this
.
prisma
.
deduction
.
aggregate
({
where
:
{
payrollMonth
:
month
,
payrollYear
:
year
,
status
:
{
in
:
[
'UPHELD'
,
'REDUCED'
,
'AUTO_APPLIED'
]
},
appliedAmountPiasters
:
{
not
:
null
},
},
_sum
:
{
appliedAmountPiasters
:
true
},
_count
:
true
,
});
// Bounties this month
const
bountiesThisMonth
=
await
this
.
prisma
.
bountyPayout
.
aggregate
({
where
:
{
payrollMonth
:
month
,
payrollYear
:
year
,
revokedAt
:
null
,
},
_sum
:
{
amountPiasters
:
true
},
_count
:
true
,
});
// Active PIPs
const
activePips
=
await
this
.
prisma
.
pip
.
findMany
({
where
:
{
status
:
'ACTIVE'
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
});
// Contract expirations
const
thirtyDaysOut
=
new
Date
(
now
.
getTime
()
+
30
*
24
*
60
*
60
*
1000
);
const
sixtyDaysOut
=
new
Date
(
now
.
getTime
()
+
60
*
24
*
60
*
60
*
1000
);
const
ninetyDaysOut
=
new
Date
(
now
.
getTime
()
+
90
*
24
*
60
*
60
*
1000
);
const
expiringContracts
=
await
this
.
prisma
.
contract
.
findMany
({
where
:
{
endDate
:
{
lte
:
ninetyDaysOut
,
gte
:
now
},
status
:
'ACTIVE'
,
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
orderBy
:
{
endDate
:
'asc'
},
});
// Report compliance
let
reportCompliance
=
{
submitted
:
0
,
expected
:
0
,
onTime
:
0
};
try
{
const
dailyReportModel
=
(
this
.
prisma
as
any
).
dailyReport
;
if
(
dailyReportModel
&&
typeof
dailyReportModel
.
count
===
'function'
)
{
const
submitted
=
await
dailyReportModel
.
count
({
where
:
{
reportDate
:
{
gte
:
monthStart
,
lte
:
monthEnd
},
status
:
{
not
:
'DRAFT'
},
},
});
const
onTime
=
await
dailyReportModel
.
count
({
where
:
{
reportDate
:
{
gte
:
monthStart
,
lte
:
monthEnd
},
status
:
{
in
:
[
'SUBMITTED'
,
'APPROVED'
,
'AUTO_APPROVED'
,
'AMENDED'
]
},
},
});
reportCompliance
=
{
submitted
,
expected
:
0
,
onTime
};
}
}
catch
{
/* ok */
}
// Pending actions
const
pendingDeductionReviews
=
await
this
.
prisma
.
deduction
.
count
({
where
:
{
status
:
'PENDING_ADMIN_REVIEW'
},
});
const
pendingAdjustments
=
await
this
.
prisma
.
adjustment
.
count
({
where
:
{
status
:
'PENDING_APPROVAL'
},
});
const
pendingScheduleChanges
=
await
this
.
prisma
.
scheduleChangeRequest
.
count
({
where
:
{
status
:
'PENDING'
},
});
// Unreported contractors in last 7 days
const
unreportedDeductions
=
await
this
.
prisma
.
deduction
.
findMany
({
where
:
{
subCategory
:
'B2'
,
createdAt
:
{
gte
:
new
Date
(
now
.
getTime
()
-
7
*
24
*
60
*
60
*
1000
)
},
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
distinct
:
[
'userId'
],
});
return
{
contractors
:
{
total
:
fullTimerCount
+
internCount
,
fullTimers
:
fullTimerCount
,
interns
:
internCount
,
byStatus
:
contractorsByStatus
.
map
((
g
)
=>
({
status
:
g
.
status
,
type
:
g
.
contractorType
,
count
:
g
.
_count
,
})),
},
onboarding
:
{
count
:
onboarding
.
length
,
list
:
onboarding
,
},
payroll
:
currentPayroll
?
{
status
:
currentPayroll
.
status
,
totalNetPiasters
:
currentPayroll
.
totalNetPiasters
,
contractorCount
:
currentPayroll
.
contractorCount
,
}
:
{
status
:
'PENDING_CALCULATION'
,
totalNetPiasters
:
0
,
contractorCount
:
0
},
deductionsThisMonth
:
{
count
:
deductionsThisMonth
.
_count
,
totalPiasters
:
deductionsThisMonth
.
_sum
.
appliedAmountPiasters
||
0
,
},
bountiesThisMonth
:
{
count
:
bountiesThisMonth
.
_count
,
totalPiasters
:
bountiesThisMonth
.
_sum
.
amountPiasters
||
0
,
},
activePips
:
activePips
.
map
((
p
)
=>
({
id
:
p
.
id
,
user
:
p
.
user
,
startDate
:
p
.
startDate
,
endDate
:
p
.
endDate
,
status
:
p
.
status
,
})),
expiringContracts
:
expiringContracts
.
map
((
c
)
=>
({
id
:
c
.
id
,
user
:
c
.
user
,
endDate
:
c
.
endDate
,
daysRemaining
:
Math
.
ceil
((
new
Date
(
c
.
endDate
!
).
getTime
()
-
now
.
getTime
())
/
(
1000
*
60
*
60
*
24
)),
})),
reportCompliance
,
pendingActions
:
{
deductionReviews
:
pendingDeductionReviews
,
adjustments
:
pendingAdjustments
,
scheduleChanges
:
pendingScheduleChanges
,
total
:
pendingDeductionReviews
+
pendingAdjustments
+
pendingScheduleChanges
,
},
unreportedContractors
:
unreportedDeductions
.
map
((
d
)
=>
({
userId
:
d
.
userId
,
user
:
d
.
user
,
})),
};
}
// ─── SUPER ADMIN DASHBOARD ────────────────────────────────
async
getSuperAdminDashboard
():
Promise
<
any
>
{
const
now
=
new
Date
();
const
month
=
now
.
getMonth
()
+
1
;
const
year
=
now
.
getFullYear
();
const
lastMonth
=
month
===
1
?
12
:
month
-
1
;
const
lastMonthYear
=
month
===
1
?
year
-
1
:
year
;
// Get admin dashboard as base
const
adminData
=
await
this
.
getAdminDashboard
();
// Total monthly expense
const
allActiveContractors
=
await
this
.
prisma
.
user
.
findMany
({
where
:
{
role
:
'CONTRACTOR'
,
status
:
{
in
:
[
'ACTIVE'
,
'ON_PIP'
]
},
deletedAt
:
null
,
},
select
:
{
id
:
true
,
actualSalaryPiasters
:
true
,
baseSalaryPiasters
:
true
},
});
const
totalSalaryPiasters
=
allActiveContractors
.
reduce
(
(
sum
,
c
)
=>
sum
+
(
c
.
actualSalaryPiasters
||
c
.
baseSalaryPiasters
||
0
),
0
,
);
const
totalBountyPiasters
=
adminData
.
bountiesThisMonth
.
totalPiasters
;
const
totalDeductionPiasters
=
adminData
.
deductionsThisMonth
.
totalPiasters
;
// Adjustments this month
const
positiveAdjustments
=
await
this
.
prisma
.
adjustment
.
aggregate
({
where
:
{
effectiveMonth
:
month
,
effectiveYear
:
year
,
type
:
'POSITIVE'
,
status
:
'APPROVED'
,
},
_sum
:
{
amountPiasters
:
true
},
});
const
negativeAdjustments
=
await
this
.
prisma
.
adjustment
.
aggregate
({
where
:
{
effectiveMonth
:
month
,
effectiveYear
:
year
,
type
:
'NEGATIVE'
,
status
:
'APPROVED'
,
},
_sum
:
{
amountPiasters
:
true
},
});
const
totalExpense
=
totalSalaryPiasters
+
totalBountyPiasters
+
(
positiveAdjustments
.
_sum
.
amountPiasters
||
0
)
-
totalDeductionPiasters
-
(
negativeAdjustments
.
_sum
.
amountPiasters
||
0
);
// Last month comparison
const
lastMonthPayroll
=
await
this
.
prisma
.
payroll
.
findUnique
({
where
:
{
month_year
:
{
month
:
lastMonth
,
year
:
lastMonthYear
}
},
select
:
{
totalNetPiasters
:
true
},
});
// Average evaluation score (last 3 months)
const
recentEvals
=
await
this
.
prisma
.
evaluation
.
aggregate
({
where
:
{
overallScore
:
{
not
:
null
},
year
,
month
:
{
gte
:
month
-
3
},
},
_avg
:
{
overallScore
:
true
},
});
// Team growth: last 6 months
const
sixMonthsAgo
=
new
Date
(
year
,
month
-
7
,
1
);
const
hires
=
await
this
.
prisma
.
user
.
groupBy
({
by
:
[
'createdAt'
],
where
:
{
role
:
'CONTRACTOR'
,
createdAt
:
{
gte
:
sixMonthsAgo
},
},
_count
:
true
,
});
// Top performers (highest eval + most bounties)
const
topByEval
=
await
this
.
prisma
.
evaluation
.
findMany
({
where
:
{
month
,
year
,
overallScore
:
{
not
:
null
}
},
orderBy
:
{
overallScore
:
'desc'
},
take
:
5
,
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
}
},
},
});
const
topByBounty
=
await
this
.
prisma
.
bountyPayout
.
groupBy
({
by
:
[
'userId'
],
where
:
{
payrollMonth
:
month
,
payrollYear
:
year
,
revokedAt
:
null
},
_sum
:
{
amountPiasters
:
true
},
orderBy
:
{
_sum
:
{
amountPiasters
:
'desc'
}
},
take
:
5
,
});
const
topBountyUsers
=
[];
for
(
const
tb
of
topByBounty
)
{
const
user
=
await
this
.
prisma
.
user
.
findUnique
({
where
:
{
id
:
tb
.
userId
},
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
},
});
topBountyUsers
.
push
({
user
,
totalPiasters
:
tb
.
_sum
.
amountPiasters
});
}
// At-risk contractors (highest deductions, lowest evals)
const
highDeductionUsers
=
await
this
.
prisma
.
deduction
.
groupBy
({
by
:
[
'userId'
],
where
:
{
payrollMonth
:
month
,
payrollYear
:
year
,
status
:
{
in
:
[
'UPHELD'
,
'REDUCED'
,
'AUTO_APPLIED'
]
},
appliedAmountPiasters
:
{
not
:
null
},
},
_sum
:
{
appliedAmountPiasters
:
true
},
_count
:
true
,
orderBy
:
{
_sum
:
{
appliedAmountPiasters
:
'desc'
}
},
take
:
5
,
});
const
atRiskContractors
=
[];
for
(
const
hd
of
highDeductionUsers
)
{
const
user
=
await
this
.
prisma
.
user
.
findUnique
({
where
:
{
id
:
hd
.
userId
},
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
,
actualSalaryPiasters
:
true
},
});
if
(
user
)
{
const
salary
=
user
.
actualSalaryPiasters
||
0
;
const
deductionPct
=
salary
>
0
?
((
hd
.
_sum
.
appliedAmountPiasters
||
0
)
/
salary
)
*
100
:
0
;
atRiskContractors
.
push
({
user
,
totalDeductionPiasters
:
hd
.
_sum
.
appliedAmountPiasters
,
deductionCount
:
hd
.
_count
,
deductionPercentage
:
Math
.
round
(
deductionPct
*
10
)
/
10
,
});
}
}
// Active sessions
const
activeSessions
=
await
this
.
prisma
.
session
.
count
({
where
:
{
revokedAt
:
null
,
expiresAt
:
{
gt
:
now
}
},
});
return
{
...
adminData
,
financials
:
{
totalSalaryPiasters
,
totalBountyPiasters
,
totalDeductionPiasters
,
totalPositiveAdjustmentPiasters
:
positiveAdjustments
.
_sum
.
amountPiasters
||
0
,
totalNegativeAdjustmentPiasters
:
negativeAdjustments
.
_sum
.
amountPiasters
||
0
,
netExpensePiasters
:
totalExpense
,
lastMonthNetPiasters
:
lastMonthPayroll
?.
totalNetPiasters
||
0
,
monthOverMonthChange
:
lastMonthPayroll
?.
totalNetPiasters
?
Math
.
round
(((
totalExpense
-
lastMonthPayroll
.
totalNetPiasters
)
/
lastMonthPayroll
.
totalNetPiasters
)
*
1000
)
/
10
:
0
,
},
performance
:
{
averageEvaluationScore
:
recentEvals
.
_avg
.
overallScore
?
Math
.
round
(
recentEvals
.
_avg
.
overallScore
*
100
)
/
100
:
null
,
topPerformersByEval
:
topByEval
.
map
((
e
)
=>
({
user
:
e
.
user
,
score
:
e
.
overallScore
,
rating
:
e
.
rating
,
})),
topPerformersByBounty
:
topBountyUsers
,
atRiskContractors
,
},
system
:
{
activeSessions
,
activeContractors
:
allActiveContractors
.
length
,
},
};
}
// ─── DEDUCTION ANALYTICS ──────────────────────────────────
async
getDeductionAnalytics
(
filter
:
AnalyticsFilterDto
):
Promise
<
any
>
{
const
month
=
filter
.
month
||
new
Date
().
getMonth
()
+
1
;
const
year
=
filter
.
year
||
new
Date
().
getFullYear
();
// By category
const
byCategory
=
await
this
.
prisma
.
deduction
.
groupBy
({
by
:
[
'category'
],
where
:
{
payrollMonth
:
month
,
payrollYear
:
year
,
status
:
{
in
:
[
'UPHELD'
,
'REDUCED'
,
'AUTO_APPLIED'
]
},
},
_sum
:
{
appliedAmountPiasters
:
true
},
_count
:
true
,
});
// By sub-category
const
bySubCategory
=
await
this
.
prisma
.
deduction
.
groupBy
({
by
:
[
'subCategory'
],
where
:
{
payrollMonth
:
month
,
payrollYear
:
year
,
status
:
{
in
:
[
'UPHELD'
,
'REDUCED'
,
'AUTO_APPLIED'
]
},
},
_sum
:
{
appliedAmountPiasters
:
true
},
_count
:
true
,
orderBy
:
{
_count
:
{
subCategory
:
'desc'
}
},
});
// Monthly trend (last 6 months)
const
trends
=
[];
for
(
let
i
=
5
;
i
>=
0
;
i
--
)
{
let
m
=
month
-
i
;
let
y
=
year
;
if
(
m
<=
0
)
{
m
+=
12
;
y
-=
1
;
}
const
agg
=
await
this
.
prisma
.
deduction
.
aggregate
({
where
:
{
payrollMonth
:
m
,
payrollYear
:
y
,
status
:
{
in
:
[
'UPHELD'
,
'REDUCED'
,
'AUTO_APPLIED'
]
},
},
_sum
:
{
appliedAmountPiasters
:
true
},
_count
:
true
,
});
trends
.
push
({
month
:
m
,
year
:
y
,
count
:
agg
.
_count
,
totalPiasters
:
agg
.
_sum
.
appliedAmountPiasters
||
0
,
});
}
return
{
byCategory
:
byCategory
.
map
((
c
)
=>
({
category
:
c
.
category
,
count
:
c
.
_count
,
totalPiasters
:
c
.
_sum
.
appliedAmountPiasters
||
0
,
})),
bySubCategory
:
bySubCategory
.
map
((
c
)
=>
({
subCategory
:
c
.
subCategory
,
count
:
c
.
_count
,
totalPiasters
:
c
.
_sum
.
appliedAmountPiasters
||
0
,
})),
monthlyTrend
:
trends
,
month
,
year
,
};
}
// ─── TASK ANALYTICS ───────────────────────────────────────
async
getTaskAnalytics
(
filter
:
AnalyticsFilterDto
):
Promise
<
any
>
{
const
month
=
filter
.
month
||
new
Date
().
getMonth
()
+
1
;
const
year
=
filter
.
year
||
new
Date
().
getFullYear
();
const
monthStart
=
new
Date
(
year
,
month
-
1
,
1
);
const
monthEnd
=
new
Date
(
year
,
month
,
0
,
23
,
59
,
59
,
999
);
const
baseWhere
:
any
=
{
deletedAt
:
null
,
};
if
(
filter
.
boardId
)
{
baseWhere
.
column
=
{
boardId
:
filter
.
boardId
};
}
// Cards completed this month
const
completed
=
await
this
.
prisma
.
card
.
count
({
where
:
{
...
baseWhere
,
completedAt
:
{
gte
:
monthStart
,
lte
:
monthEnd
}
},
});
// Cards created this month
const
created
=
await
this
.
prisma
.
card
.
count
({
where
:
{
...
baseWhere
,
createdAt
:
{
gte
:
monthStart
,
lte
:
monthEnd
}
},
});
// Cards currently overdue
const
overdue
=
await
this
.
prisma
.
card
.
count
({
where
:
{
...
baseWhere
,
dueDate
:
{
lt
:
new
Date
()
},
completedAt
:
null
,
isArchived
:
false
,
},
});
// Average cycle time for completed cards this month
const
completedCards
=
await
this
.
prisma
.
card
.
findMany
({
where
:
{
...
baseWhere
,
completedAt
:
{
gte
:
monthStart
,
lte
:
monthEnd
},
cycleTimeHours
:
{
not
:
null
},
},
select
:
{
cycleTimeHours
:
true
,
leadTimeHours
:
true
},
});
const
avgCycleHours
=
completedCards
.
length
>
0
?
completedCards
.
reduce
((
sum
,
c
)
=>
sum
+
(
c
.
cycleTimeHours
||
0
),
0
)
/
completedCards
.
length
:
0
;
const
avgLeadHours
=
completedCards
.
length
>
0
?
completedCards
.
reduce
((
sum
,
c
)
=>
sum
+
(
c
.
leadTimeHours
||
0
),
0
)
/
completedCards
.
length
:
0
;
// Completion by assignee
const
completionByAssignee
=
await
this
.
prisma
.
card
.
findMany
({
where
:
{
...
baseWhere
,
completedAt
:
{
gte
:
monthStart
,
lte
:
monthEnd
},
},
select
:
{
assignees
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
});
const
assigneeCounts
:
Record
<
string
,
{
user
:
any
;
count
:
number
}
>
=
{};
for
(
const
card
of
completionByAssignee
)
{
for
(
const
a
of
card
.
assignees
)
{
if
(
!
assigneeCounts
[
a
.
id
])
{
assigneeCounts
[
a
.
id
]
=
{
user
:
a
,
count
:
0
};
}
assigneeCounts
[
a
.
id
].
count
++
;
}
}
const
topCompletors
=
Object
.
values
(
assigneeCounts
)
.
sort
((
a
,
b
)
=>
b
.
count
-
a
.
count
)
.
slice
(
0
,
10
);
return
{
month
,
year
,
completed
,
created
,
overdue
,
avgCycleTimeHours
:
Math
.
round
(
avgCycleHours
*
10
)
/
10
,
avgLeadTimeHours
:
Math
.
round
(
avgLeadHours
*
10
)
/
10
,
topCompletors
,
};
}
// ─── SYSTEM HEALTH ────────────────────────────────────────
async
getSystemHealth
():
Promise
<
any
>
{
const
now
=
new
Date
();
const
activeSessions
=
await
this
.
prisma
.
session
.
count
({
where
:
{
revokedAt
:
null
,
expiresAt
:
{
gt
:
now
}
},
});
const
totalUsers
=
await
this
.
prisma
.
user
.
groupBy
({
by
:
[
'status'
],
where
:
{
deletedAt
:
null
},
_count
:
true
,
});
// Recent errors in audit trail
const
recentErrors
=
await
this
.
prisma
.
auditTrail
.
count
({
where
:
{
errorMessage
:
{
not
:
null
},
createdAt
:
{
gte
:
new
Date
(
now
.
getTime
()
-
24
*
60
*
60
*
1000
)
},
},
});
// Total audit trail entries
const
totalAuditEntries
=
await
this
.
prisma
.
auditTrail
.
count
();
// Total attachments
const
totalAttachments
=
await
this
.
prisma
.
attachment
.
count
();
const
totalAttachmentSize
=
await
this
.
prisma
.
attachment
.
aggregate
({
_sum
:
{
sizeBytes
:
true
},
});
// Total cards
const
totalCards
=
await
this
.
prisma
.
card
.
count
({
where
:
{
deletedAt
:
null
}
});
const
totalBoards
=
await
this
.
prisma
.
board
.
count
({
where
:
{
deletedAt
:
null
}
});
// Database rough size estimation (count of major tables)
const
tableCounts
=
{
users
:
await
this
.
prisma
.
user
.
count
(),
boards
:
totalBoards
,
cards
:
totalCards
,
comments
:
await
this
.
prisma
.
comment
.
count
(),
deductions
:
await
this
.
prisma
.
deduction
.
count
(),
notifications
:
await
this
.
prisma
.
notification
.
count
(),
messages
:
await
this
.
prisma
.
message
.
count
(),
auditTrail
:
totalAuditEntries
,
attachments
:
totalAttachments
,
};
return
{
activeSessions
,
usersByStatus
:
totalUsers
.
map
((
u
)
=>
({
status
:
u
.
status
,
count
:
u
.
_count
})),
recentErrors24h
:
recentErrors
,
storage
:
{
totalAttachments
,
totalSizeBytes
:
totalAttachmentSize
.
_sum
.
sizeBytes
||
0
,
totalSizeMB
:
Math
.
round
((
totalAttachmentSize
.
_sum
.
sizeBytes
||
0
)
/
1048576
),
},
entityCounts
:
tableCounts
,
uptime
:
process
.
uptime
(),
memoryUsage
:
process
.
memoryUsage
(),
nodeVersion
:
process
.
version
,
timestamp
:
now
.
toISOString
(),
};
}
}
\ No newline at end of file
backend/src/modules/analytics/data-export.service.ts
0 → 100644
View file @
a791a680
import
{
Injectable
,
ForbiddenException
,
BadRequestException
,
Logger
,
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
ExportRequestDto
}
from
'./dto/analytics-filter.dto'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
@
Injectable
()
export
class
DataExportService
{
private
readonly
logger
=
new
Logger
(
DataExportService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
exportData
(
dto
:
ExportRequestDto
,
currentUser
:
RequestUser
):
Promise
<
{
data
:
any
[];
filename
:
string
}
>
{
if
(
currentUser
.
role
!==
'SUPER_ADMIN'
&&
currentUser
.
role
!==
'ADMIN'
)
{
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
const
allowedEntities
=
[
'CARDS'
];
if
(
!
allowedEntities
.
includes
(
dto
.
entityType
))
{
throw
new
ForbiddenException
(
'Project Leaders can only export card data for their boards'
);
}
}
else
{
throw
new
ForbiddenException
(
'Insufficient permissions to export data'
);
}
}
const
timestamp
=
new
Date
().
toISOString
().
split
(
'T'
)[
0
];
switch
(
dto
.
entityType
)
{
case
'CONTRACTORS'
:
return
{
data
:
await
this
.
exportContractors
(
dto
),
filename
:
`contractors-
${
timestamp
}
`
,
};
case
'CARDS'
:
return
{
data
:
await
this
.
exportCards
(
dto
,
currentUser
),
filename
:
`cards-
${
timestamp
}
`
,
};
case
'DEDUCTIONS'
:
return
{
data
:
await
this
.
exportDeductions
(
dto
),
filename
:
`deductions-
${
timestamp
}
`
,
};
case
'BOUNTIES'
:
return
{
data
:
await
this
.
exportBounties
(
dto
),
filename
:
`bounties-
${
timestamp
}
`
,
};
case
'EVALUATIONS'
:
return
{
data
:
await
this
.
exportEvaluations
(
dto
),
filename
:
`evaluations-
${
timestamp
}
`
,
};
case
'PAYROLL'
:
return
{
data
:
await
this
.
exportPayroll
(
dto
),
filename
:
`payroll-
${
timestamp
}
`
,
};
case
'ADJUSTMENTS'
:
return
{
data
:
await
this
.
exportAdjustments
(
dto
),
filename
:
`adjustments-
${
timestamp
}
`
,
};
case
'AUDIT_TRAIL'
:
if
(
currentUser
.
role
!==
'SUPER_ADMIN'
)
{
throw
new
ForbiddenException
(
'Only Super Admin can export audit trail'
);
}
return
{
data
:
await
this
.
exportAuditTrail
(
dto
),
filename
:
`audit-trail-
${
timestamp
}
`
,
};
default
:
throw
new
BadRequestException
(
`Unsupported entity type:
${
dto
.
entityType
}
`
);
}
}
async
exportContractorPackage
(
userId
:
string
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
if
(
currentUser
.
role
!==
'SUPER_ADMIN'
)
{
throw
new
ForbiddenException
(
'Only Super Admin can export full contractor data packages'
);
}
const
user
=
await
this
.
prisma
.
user
.
findUnique
({
where
:
{
id
:
userId
},
include
:
{
contracts
:
true
,
sessions
:
{
take
:
50
,
orderBy
:
{
createdAt
:
'desc'
}
},
},
});
if
(
!
user
)
throw
new
BadRequestException
(
'Contractor not found'
);
const
deductions
=
await
this
.
prisma
.
deduction
.
findMany
({
where
:
{
userId
},
orderBy
:
{
createdAt
:
'desc'
},
});
const
bounties
=
await
this
.
prisma
.
bountyPayout
.
findMany
({
where
:
{
userId
},
orderBy
:
{
paidAt
:
'desc'
},
});
const
evaluations
=
await
this
.
prisma
.
evaluation
.
findMany
({
where
:
{
userId
},
orderBy
:
[{
year
:
'desc'
},
{
month
:
'desc'
}],
});
const
pips
=
await
this
.
prisma
.
pip
.
findMany
({
where
:
{
userId
},
include
:
{
checkIns
:
true
},
});
const
learningGoals
=
await
this
.
prisma
.
learningGoal
.
findMany
({
where
:
{
userId
},
});
const
adjustments
=
await
this
.
prisma
.
adjustment
.
findMany
({
where
:
{
userId
},
orderBy
:
{
createdAt
:
'desc'
},
});
const
unavailability
=
await
this
.
prisma
.
unavailability
.
findMany
({
where
:
{
userId
},
orderBy
:
{
startDate
:
'desc'
},
});
const
payrollLines
=
await
this
.
prisma
.
payrollLine
.
findMany
({
where
:
{
userId
},
orderBy
:
{
createdAt
:
'desc'
},
});
// Strip password hash from user data
const
{
passwordHash
,
...
safeUser
}
=
user
;
return
{
exportDate
:
new
Date
().
toISOString
(),
contractor
:
safeUser
,
deductions
,
bounties
,
evaluations
,
pips
,
learningGoals
,
adjustments
,
unavailability
,
payrollLines
,
};
}
private
async
exportContractors
(
dto
:
ExportRequestDto
):
Promise
<
any
[]
>
{
const
where
:
any
=
{
role
:
'CONTRACTOR'
,
deletedAt
:
null
};
if
(
dto
.
userId
)
where
.
id
=
dto
.
userId
;
return
this
.
prisma
.
user
.
findMany
({
where
,
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
username
:
true
,
email
:
true
,
contractorType
:
true
,
status
:
true
,
actualSalaryPiasters
:
true
,
baseSalaryPiasters
:
true
,
currentStreak
:
true
,
bestStreak
:
true
,
createdAt
:
true
,
activatedAt
:
true
,
lastLoginAt
:
true
,
},
orderBy
:
{
createdAt
:
'desc'
},
take
:
10000
,
});
}
private
async
exportCards
(
dto
:
ExportRequestDto
,
currentUser
:
RequestUser
):
Promise
<
any
[]
>
{
const
where
:
any
=
{
deletedAt
:
null
};
if
(
dto
.
dateFrom
||
dto
.
dateTo
)
{
where
.
createdAt
=
{};
if
(
dto
.
dateFrom
)
where
.
createdAt
.
gte
=
new
Date
(
dto
.
dateFrom
);
if
(
dto
.
dateTo
)
where
.
createdAt
.
lte
=
new
Date
(
dto
.
dateTo
);
}
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
const
plBoards
=
await
this
.
prisma
.
boardMember
.
findMany
({
where
:
{
userId
:
currentUser
.
id
},
select
:
{
boardId
:
true
},
});
where
.
column
=
{
boardId
:
{
in
:
plBoards
.
map
((
b
)
=>
b
.
boardId
)
}
};
}
return
this
.
prisma
.
card
.
findMany
({
where
,
select
:
{
id
:
true
,
cardNumber
:
true
,
title
:
true
,
priority
:
true
,
dueDate
:
true
,
completedAt
:
true
,
bountyPiasters
:
true
,
estimatedHours
:
true
,
actualHours
:
true
,
leadTimeHours
:
true
,
cycleTimeHours
:
true
,
frozenTimeHours
:
true
,
isArchived
:
true
,
createdAt
:
true
,
column
:
{
select
:
{
name
:
true
,
type
:
true
,
board
:
{
select
:
{
name
:
true
,
key
:
true
}
}
}
},
assignees
:
{
select
:
{
firstName
:
true
,
lastName
:
true
}
},
},
orderBy
:
{
createdAt
:
'desc'
},
take
:
50000
,
});
}
private
async
exportDeductions
(
dto
:
ExportRequestDto
):
Promise
<
any
[]
>
{
const
where
:
any
=
{};
if
(
dto
.
userId
)
where
.
userId
=
dto
.
userId
;
if
(
dto
.
dateFrom
||
dto
.
dateTo
)
{
where
.
violationDate
=
{};
if
(
dto
.
dateFrom
)
where
.
violationDate
.
gte
=
new
Date
(
dto
.
dateFrom
);
if
(
dto
.
dateTo
)
where
.
violationDate
.
lte
=
new
Date
(
dto
.
dateTo
);
}
return
this
.
prisma
.
deduction
.
findMany
({
where
,
include
:
{
user
:
{
select
:
{
firstName
:
true
,
lastName
:
true
}
},
initiatedBy
:
{
select
:
{
firstName
:
true
,
lastName
:
true
}
},
},
orderBy
:
{
createdAt
:
'desc'
},
take
:
50000
,
});
}
private
async
exportBounties
(
dto
:
ExportRequestDto
):
Promise
<
any
[]
>
{
const
where
:
any
=
{};
if
(
dto
.
userId
)
where
.
userId
=
dto
.
userId
;
if
(
dto
.
dateFrom
||
dto
.
dateTo
)
{
where
.
paidAt
=
{};
if
(
dto
.
dateFrom
)
where
.
paidAt
.
gte
=
new
Date
(
dto
.
dateFrom
);
if
(
dto
.
dateTo
)
where
.
paidAt
.
lte
=
new
Date
(
dto
.
dateTo
);
}
return
this
.
prisma
.
bountyPayout
.
findMany
({
where
,
orderBy
:
{
paidAt
:
'desc'
},
take
:
50000
,
});
}
private
async
exportEvaluations
(
dto
:
ExportRequestDto
):
Promise
<
any
[]
>
{
const
where
:
any
=
{};
if
(
dto
.
userId
)
where
.
userId
=
dto
.
userId
;
return
this
.
prisma
.
evaluation
.
findMany
({
where
,
include
:
{
user
:
{
select
:
{
firstName
:
true
,
lastName
:
true
}
}
},
orderBy
:
[{
year
:
'desc'
},
{
month
:
'desc'
}],
take
:
50000
,
});
}
private
async
exportPayroll
(
dto
:
ExportRequestDto
):
Promise
<
any
[]
>
{
return
this
.
prisma
.
payroll
.
findMany
({
orderBy
:
[{
year
:
'desc'
},
{
month
:
'desc'
}],
include
:
{
lines
:
{
include
:
{
user
:
{
select
:
{
firstName
:
true
,
lastName
:
true
}
},
},
},
},
take
:
1000
,
});
}
private
async
exportAdjustments
(
dto
:
ExportRequestDto
):
Promise
<
any
[]
>
{
const
where
:
any
=
{};
if
(
dto
.
userId
)
where
.
userId
=
dto
.
userId
;
if
(
dto
.
dateFrom
||
dto
.
dateTo
)
{
where
.
createdAt
=
{};
if
(
dto
.
dateFrom
)
where
.
createdAt
.
gte
=
new
Date
(
dto
.
dateFrom
);
if
(
dto
.
dateTo
)
where
.
createdAt
.
lte
=
new
Date
(
dto
.
dateTo
);
}
return
this
.
prisma
.
adjustment
.
findMany
({
where
,
include
:
{
user
:
{
select
:
{
firstName
:
true
,
lastName
:
true
}
},
createdBy
:
{
select
:
{
firstName
:
true
,
lastName
:
true
}
},
},
orderBy
:
{
createdAt
:
'desc'
},
take
:
50000
,
});
}
private
async
exportAuditTrail
(
dto
:
ExportRequestDto
):
Promise
<
any
[]
>
{
const
where
:
any
=
{};
if
(
dto
.
userId
)
where
.
userId
=
dto
.
userId
;
if
(
dto
.
dateFrom
||
dto
.
dateTo
)
{
where
.
createdAt
=
{};
if
(
dto
.
dateFrom
)
where
.
createdAt
.
gte
=
new
Date
(
dto
.
dateFrom
);
if
(
dto
.
dateTo
)
where
.
createdAt
.
lte
=
new
Date
(
dto
.
dateTo
);
}
return
this
.
prisma
.
auditTrail
.
findMany
({
where
,
include
:
{
user
:
{
select
:
{
firstName
:
true
,
lastName
:
true
,
username
:
true
}
},
},
orderBy
:
{
createdAt
:
'desc'
},
take
:
50000
,
});
}
}
\ No newline at end of file
backend/src/modules/analytics/dto/analytics-filter.dto.ts
0 → 100644
View file @
a791a680
import
{
IsOptional
,
IsString
,
IsDateString
,
IsInt
,
IsArray
}
from
'class-validator'
;
import
{
Type
}
from
'class-transformer'
;
import
{
PaginationDto
}
from
'../../../common/dto/pagination.dto'
;
export
class
AnalyticsFilterDto
{
@
IsOptional
()
@
IsDateString
()
dateFrom
?:
string
;
@
IsOptional
()
@
IsDateString
()
dateTo
?:
string
;
@
IsOptional
()
@
IsString
()
userId
?:
string
;
@
IsOptional
()
@
IsString
()
boardId
?:
string
;
@
IsOptional
()
@
Type
(()
=>
Number
)
@
IsInt
()
month
?:
number
;
@
IsOptional
()
@
Type
(()
=>
Number
)
@
IsInt
()
year
?:
number
;
}
export
class
ReportBuilderQueryDto
extends
PaginationDto
{
@
IsString
()
dataSource
:
string
;
// CONTRACTORS, CARDS, DEDUCTIONS, BOUNTIES, EVALUATIONS, REPORTS, PAYROLL, ADJUSTMENTS, UNAVAILABILITY, LEARNING_GOALS
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
columns
?:
string
[];
@
IsOptional
()
@
IsString
()
groupBy
?:
string
;
@
IsOptional
()
@
IsString
()
aggregation
?:
string
;
// SUM, AVG, COUNT, MIN, MAX
@
IsOptional
()
@
IsString
()
aggregationField
?:
string
;
@
IsOptional
()
@
IsDateString
()
dateFrom
?:
string
;
@
IsOptional
()
@
IsDateString
()
dateTo
?:
string
;
@
IsOptional
()
@
IsString
()
userId
?:
string
;
@
IsOptional
()
@
IsString
()
boardId
?:
string
;
@
IsOptional
()
@
IsString
()
status
?:
string
;
@
IsOptional
()
@
IsString
()
category
?:
string
;
}
export
class
ExportRequestDto
{
@
IsString
()
entityType
:
string
;
// CONTRACTORS, CARDS, DEDUCTIONS, BOUNTIES, EVALUATIONS, REPORTS, PAYROLL, ADJUSTMENTS, AUDIT_TRAIL, ALL
@
IsOptional
()
@
IsString
()
format
?:
string
;
// CSV, JSON (default: CSV)
@
IsOptional
()
@
IsDateString
()
dateFrom
?:
string
;
@
IsOptional
()
@
IsDateString
()
dateTo
?:
string
;
@
IsOptional
()
@
IsString
()
userId
?:
string
;
}
\ No newline at end of file
backend/src/modules/analytics/report-builder.service.ts
0 → 100644
View file @
a791a680
import
{
Injectable
,
BadRequestException
,
ForbiddenException
,
Logger
,
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
ReportBuilderQueryDto
}
from
'./dto/analytics-filter.dto'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
getSkip
,
buildPaginatedResponse
,
PaginatedResult
}
from
'../../common/utils/pagination.util'
;
@
Injectable
()
export
class
ReportBuilderService
{
private
readonly
logger
=
new
Logger
(
ReportBuilderService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
executeQuery
(
query
:
ReportBuilderQueryDto
,
currentUser
:
RequestUser
):
Promise
<
PaginatedResult
<
any
>>
{
const
validSources
=
[
'CONTRACTORS'
,
'CARDS'
,
'DEDUCTIONS'
,
'BOUNTIES'
,
'EVALUATIONS'
,
'REPORTS'
,
'PAYROLL'
,
'ADJUSTMENTS'
,
'UNAVAILABILITY'
,
'LEARNING_GOALS'
,
];
if
(
!
validSources
.
includes
(
query
.
dataSource
))
{
throw
new
BadRequestException
(
`Invalid data source. Must be one of:
${
validSources
.
join
(
', '
)}
`
);
}
// PLs can only query their own team's data
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
const
allowedSources
=
[
'CARDS'
,
'EVALUATIONS'
,
'LEARNING_GOALS'
];
if
(
!
allowedSources
.
includes
(
query
.
dataSource
))
{
throw
new
ForbiddenException
(
'Project Leaders can only query cards, evaluations, and learning goals for their team'
);
}
}
const
page
=
query
.
page
||
1
;
const
limit
=
query
.
limit
||
50
;
switch
(
query
.
dataSource
)
{
case
'CONTRACTORS'
:
return
this
.
queryContractors
(
query
,
page
,
limit
);
case
'CARDS'
:
return
this
.
queryCards
(
query
,
page
,
limit
,
currentUser
);
case
'DEDUCTIONS'
:
return
this
.
queryDeductions
(
query
,
page
,
limit
);
case
'BOUNTIES'
:
return
this
.
queryBounties
(
query
,
page
,
limit
);
case
'EVALUATIONS'
:
return
this
.
queryEvaluations
(
query
,
page
,
limit
,
currentUser
);
case
'PAYROLL'
:
return
this
.
queryPayroll
(
query
,
page
,
limit
);
case
'ADJUSTMENTS'
:
return
this
.
queryAdjustments
(
query
,
page
,
limit
);
case
'UNAVAILABILITY'
:
return
this
.
queryUnavailability
(
query
,
page
,
limit
);
case
'LEARNING_GOALS'
:
return
this
.
queryLearningGoals
(
query
,
page
,
limit
,
currentUser
);
default
:
throw
new
BadRequestException
(
'Unsupported data source'
);
}
}
private
async
queryContractors
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{
role
:
'CONTRACTOR'
,
deletedAt
:
null
};
if
(
query
.
status
)
where
.
status
=
query
.
status
;
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
user
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
createdAt
:
'desc'
},
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
username
:
true
,
contractorType
:
true
,
status
:
true
,
actualSalaryPiasters
:
true
,
baseSalaryPiasters
:
true
,
currentStreak
:
true
,
bestStreak
:
true
,
createdAt
:
true
,
lastLoginAt
:
true
,
},
}),
this
.
prisma
.
user
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
private
async
queryCards
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
,
currentUser
:
RequestUser
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{
deletedAt
:
null
};
if
(
query
.
dateFrom
||
query
.
dateTo
)
{
where
.
createdAt
=
{};
if
(
query
.
dateFrom
)
where
.
createdAt
.
gte
=
new
Date
(
query
.
dateFrom
);
if
(
query
.
dateTo
)
where
.
createdAt
.
lte
=
new
Date
(
query
.
dateTo
);
}
if
(
query
.
boardId
)
{
where
.
column
=
{
boardId
:
query
.
boardId
};
}
if
(
query
.
userId
)
{
where
.
assignees
=
{
some
:
{
id
:
query
.
userId
}
};
}
if
(
query
.
status
)
{
where
.
column
=
{
...
where
.
column
,
type
:
query
.
status
};
}
// PL restriction
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
const
plBoards
=
await
this
.
prisma
.
boardMember
.
findMany
({
where
:
{
userId
:
currentUser
.
id
},
select
:
{
boardId
:
true
},
});
where
.
column
=
{
...
where
.
column
,
boardId
:
{
in
:
plBoards
.
map
((
b
)
=>
b
.
boardId
)
}
};
}
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
card
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
createdAt
:
'desc'
},
select
:
{
id
:
true
,
cardNumber
:
true
,
title
:
true
,
priority
:
true
,
dueDate
:
true
,
completedAt
:
true
,
bountyPiasters
:
true
,
estimatedHours
:
true
,
actualHours
:
true
,
leadTimeHours
:
true
,
cycleTimeHours
:
true
,
frozenTimeHours
:
true
,
createdAt
:
true
,
column
:
{
select
:
{
name
:
true
,
type
:
true
,
board
:
{
select
:
{
name
:
true
,
key
:
true
}
}
}
},
assignees
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
}),
this
.
prisma
.
card
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
private
async
queryDeductions
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{};
if
(
query
.
dateFrom
||
query
.
dateTo
)
{
where
.
violationDate
=
{};
if
(
query
.
dateFrom
)
where
.
violationDate
.
gte
=
new
Date
(
query
.
dateFrom
);
if
(
query
.
dateTo
)
where
.
violationDate
.
lte
=
new
Date
(
query
.
dateTo
);
}
if
(
query
.
userId
)
where
.
userId
=
query
.
userId
;
if
(
query
.
status
)
where
.
status
=
query
.
status
;
if
(
query
.
category
)
where
.
category
=
query
.
category
;
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
deduction
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
createdAt
:
'desc'
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
initiatedBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
}),
this
.
prisma
.
deduction
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
private
async
queryBounties
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{
revokedAt
:
null
};
if
(
query
.
dateFrom
||
query
.
dateTo
)
{
where
.
paidAt
=
{};
if
(
query
.
dateFrom
)
where
.
paidAt
.
gte
=
new
Date
(
query
.
dateFrom
);
if
(
query
.
dateTo
)
where
.
paidAt
.
lte
=
new
Date
(
query
.
dateTo
);
}
if
(
query
.
userId
)
where
.
userId
=
query
.
userId
;
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
bountyPayout
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
paidAt
:
'desc'
},
}),
this
.
prisma
.
bountyPayout
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
private
async
queryEvaluations
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
,
currentUser
:
RequestUser
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{};
if
(
query
.
userId
)
where
.
userId
=
query
.
userId
;
if
(
query
.
status
)
where
.
status
=
query
.
status
;
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
where
.
user
=
{
assignedProjectLeaderId
:
currentUser
.
id
};
}
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
evaluation
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
[{
year
:
'desc'
},
{
month
:
'desc'
}],
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
}),
this
.
prisma
.
evaluation
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
private
async
queryPayroll
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{};
if
(
query
.
status
)
where
.
status
=
query
.
status
;
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
payroll
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
[{
year
:
'desc'
},
{
month
:
'desc'
}],
}),
this
.
prisma
.
payroll
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
private
async
queryAdjustments
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{};
if
(
query
.
userId
)
where
.
userId
=
query
.
userId
;
if
(
query
.
status
)
where
.
status
=
query
.
status
;
if
(
query
.
category
)
where
.
category
=
query
.
category
;
if
(
query
.
dateFrom
||
query
.
dateTo
)
{
where
.
createdAt
=
{};
if
(
query
.
dateFrom
)
where
.
createdAt
.
gte
=
new
Date
(
query
.
dateFrom
);
if
(
query
.
dateTo
)
where
.
createdAt
.
lte
=
new
Date
(
query
.
dateTo
);
}
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
adjustment
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
createdAt
:
'desc'
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
}),
this
.
prisma
.
adjustment
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
private
async
queryUnavailability
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{};
if
(
query
.
userId
)
where
.
userId
=
query
.
userId
;
if
(
query
.
dateFrom
||
query
.
dateTo
)
{
if
(
query
.
dateFrom
)
where
.
startDate
=
{
...(
where
.
startDate
||
{}),
gte
:
new
Date
(
query
.
dateFrom
)
};
if
(
query
.
dateTo
)
where
.
endDate
=
{
...(
where
.
endDate
||
{}),
lte
:
new
Date
(
query
.
dateTo
)
};
}
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
unavailability
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
startDate
:
'desc'
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
}),
this
.
prisma
.
unavailability
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
private
async
queryLearningGoals
(
query
:
ReportBuilderQueryDto
,
page
:
number
,
limit
:
number
,
currentUser
:
RequestUser
):
Promise
<
PaginatedResult
<
any
>>
{
const
where
:
any
=
{};
if
(
query
.
userId
)
where
.
userId
=
query
.
userId
;
if
(
query
.
status
)
where
.
status
=
query
.
status
;
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
where
.
user
=
{
assignedProjectLeaderId
:
currentUser
.
id
};
}
const
[
data
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
learningGoal
.
findMany
({
where
,
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
deadline
:
'asc'
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
competencyArea
:
{
select
:
{
id
:
true
,
name
:
true
}
},
},
}),
this
.
prisma
.
learningGoal
.
count
({
where
}),
]);
return
buildPaginatedResponse
(
data
,
total
,
{
page
,
limit
,
sortOrder
:
'asc'
});
}
}
\ 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