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
8494e6c4
Commit
8494e6c4
authored
Apr 01, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 14 files via Son of Anton
parent
dbe7775f
Changes
14
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
1419 additions
and
154 deletions
+1419
-154
boards.controller.ts
backend/src/modules/boards/boards.controller.ts
+35
-20
boards.module.ts
backend/src/modules/boards/boards.module.ts
+3
-2
saved-filter.dto.ts
backend/src/modules/boards/dto/saved-filter.dto.ts
+33
-0
saved-filter.service.ts
backend/src/modules/boards/saved-filter.service.ts
+80
-0
card-dependency.service.ts
backend/src/modules/cards/card-dependency.service.ts
+236
-0
card-template.service.ts
backend/src/modules/cards/card-template.service.ts
+157
-0
cards.controller.ts
backend/src/modules/cards/cards.controller.ts
+154
-30
cards.module.ts
backend/src/modules/cards/cards.module.ts
+22
-2
card-template.dto.ts
backend/src/modules/cards/dto/card-template.dto.ts
+83
-0
recurring-card.dto.ts
backend/src/modules/cards/dto/recurring-card.dto.ts
+99
-0
recurring-card.service.ts
backend/src/modules/cards/recurring-card.service.ts
+205
-0
views.service.ts
backend/src/modules/cards/views.service.ts
+169
-0
schema-boards.prisma
prisma/schema-boards.prisma
+86
-67
board.types.ts
shared/src/types/board.types.ts
+57
-33
No files found.
backend/src/modules/boards/boards.controller.ts
View file @
8494e6c4
...
...
@@ -12,6 +12,7 @@ import {
}
from
'@nestjs/common'
;
import
{
BoardsService
}
from
'./boards.service'
;
import
{
BoardTemplateService
}
from
'./board-template.service'
;
import
{
SavedFilterService
}
from
'./saved-filter.service'
;
import
{
CreateBoardDto
}
from
'./dto/create-board.dto'
;
import
{
UpdateBoardDto
}
from
'./dto/update-board.dto'
;
import
{
BoardFilterDto
}
from
'./dto/board-filter.dto'
;
...
...
@@ -21,6 +22,7 @@ import {
UpdateBoardMemberRoleDto
,
}
from
'./dto/board-member.dto'
;
import
{
CreateBoardTemplateDto
,
UpdateBoardTemplateDto
}
from
'./dto/board-template.dto'
;
import
{
CreateSavedFilterDto
,
UpdateSavedFilterDto
}
from
'./dto/saved-filter.dto'
;
import
{
Roles
}
from
'../../common/decorators/roles.decorator'
;
import
{
CurrentUser
,
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
...
...
@@ -29,6 +31,7 @@ export class BoardsController {
constructor
(
private
readonly
boardsService
:
BoardsService
,
private
readonly
boardTemplateService
:
BoardTemplateService
,
private
readonly
savedFilterService
:
SavedFilterService
,
)
{}
// ─── BOARD CRUD ──────────────────────────────────────────
...
...
@@ -51,11 +54,7 @@ export class BoardsController {
@
Put
(
':id'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
update
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
UpdateBoardDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
update
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
UpdateBoardDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
boardsService
.
update
(
id
,
dto
,
user
);
}
...
...
@@ -92,22 +91,14 @@ export class BoardsController {
@
Post
(
':id/members'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
async
addMember
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
AddBoardMemberDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
addMember
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
AddBoardMemberDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
boardsService
.
addMember
(
id
,
dto
,
user
);
}
@
Post
(
':id/members/bulk'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
addMembersBulk
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
AddBoardMembersBulkDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
addMembersBulk
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
AddBoardMembersBulkDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
boardsService
.
addMembersBulk
(
id
,
dto
,
user
);
}
...
...
@@ -125,11 +116,7 @@ export class BoardsController {
@
Delete
(
':id/members/:userId'
)
@
Roles
(
'SUPER_ADMIN'
,
'ADMIN'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
removeMember
(
@
Param
(
'id'
)
id
:
string
,
@
Param
(
'userId'
)
userId
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
removeMember
(@
Param
(
'id'
)
id
:
string
,
@
Param
(
'userId'
)
userId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
boardsService
.
removeMember
(
id
,
userId
,
user
);
return
{
message
:
'Member removed from board'
};
}
...
...
@@ -171,4 +158,32 @@ export class BoardsController {
await
this
.
boardTemplateService
.
delete
(
templateId
,
user
);
return
{
message
:
'Board template deleted'
};
}
// ─── SAVED FILTERS ──────────────────────────────────────
@
Get
(
':id/filters'
)
async
getSavedFilters
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
savedFilterService
.
findAll
(
id
,
user
);
}
@
Post
(
'filters'
)
async
createSavedFilter
(@
Body
()
dto
:
CreateSavedFilterDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
savedFilterService
.
create
(
dto
,
user
);
}
@
Put
(
'filters/:filterId'
)
async
updateSavedFilter
(
@
Param
(
'filterId'
)
filterId
:
string
,
@
Body
()
dto
:
UpdateSavedFilterDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
savedFilterService
.
update
(
filterId
,
dto
,
user
);
}
@
Delete
(
'filters/:filterId'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
deleteSavedFilter
(@
Param
(
'filterId'
)
filterId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
savedFilterService
.
delete
(
filterId
,
user
);
return
{
message
:
'Saved filter deleted'
};
}
}
\ No newline at end of file
backend/src/modules/boards/boards.module.ts
View file @
8494e6c4
...
...
@@ -2,10 +2,11 @@ import { Module } from '@nestjs/common';
import
{
BoardsController
}
from
'./boards.controller'
;
import
{
BoardsService
}
from
'./boards.service'
;
import
{
BoardTemplateService
}
from
'./board-template.service'
;
import
{
SavedFilterService
}
from
'./saved-filter.service'
;
@
Module
({
controllers
:
[
BoardsController
],
providers
:
[
BoardsService
,
BoardTemplateService
],
exports
:
[
BoardsService
,
BoardTemplateService
],
providers
:
[
BoardsService
,
BoardTemplateService
,
SavedFilterService
],
exports
:
[
BoardsService
,
BoardTemplateService
,
SavedFilterService
],
})
export
class
BoardsModule
{}
\ No newline at end of file
backend/src/modules/boards/dto/saved-filter.dto.ts
0 → 100644
View file @
8494e6c4
import
{
IsString
,
IsOptional
,
IsBoolean
,
MinLength
,
MaxLength
}
from
'class-validator'
;
export
class
CreateSavedFilterDto
{
@
IsOptional
()
@
IsString
()
boardId
?:
string
;
@
IsString
()
@
MinLength
(
1
)
@
MaxLength
(
50
)
name
:
string
;
filterConfig
:
any
;
// The filter object
@
IsOptional
()
@
IsBoolean
()
isDefault
?:
boolean
;
}
export
class
UpdateSavedFilterDto
{
@
IsOptional
()
@
IsString
()
@
MinLength
(
1
)
@
MaxLength
(
50
)
name
?:
string
;
@
IsOptional
()
filterConfig
?:
any
;
@
IsOptional
()
@
IsBoolean
()
isDefault
?:
boolean
;
}
\ No newline at end of file
backend/src/modules/boards/saved-filter.service.ts
0 → 100644
View file @
8494e6c4
import
{
Injectable
,
NotFoundException
,
ForbiddenException
,
Logger
,
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
CreateSavedFilterDto
,
UpdateSavedFilterDto
}
from
'./dto/saved-filter.dto'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
@
Injectable
()
export
class
SavedFilterService
{
private
readonly
logger
=
new
Logger
(
SavedFilterService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
create
(
dto
:
CreateSavedFilterDto
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
// If setting as default, unset existing default for this board
if
(
dto
.
isDefault
&&
dto
.
boardId
)
{
await
this
.
prisma
.
savedFilter
.
updateMany
({
where
:
{
userId
:
currentUser
.
id
,
boardId
:
dto
.
boardId
,
isDefault
:
true
},
data
:
{
isDefault
:
false
},
});
}
return
this
.
prisma
.
savedFilter
.
create
({
data
:
{
userId
:
currentUser
.
id
,
boardId
:
dto
.
boardId
||
null
,
name
:
dto
.
name
,
filterConfig
:
dto
.
filterConfig
,
isDefault
:
dto
.
isDefault
||
false
,
},
});
}
async
findAll
(
boardId
:
string
|
undefined
,
currentUser
:
RequestUser
):
Promise
<
any
[]
>
{
const
where
:
any
=
{
userId
:
currentUser
.
id
};
if
(
boardId
)
where
.
boardId
=
boardId
;
return
this
.
prisma
.
savedFilter
.
findMany
({
where
,
orderBy
:
[{
isDefault
:
'desc'
},
{
name
:
'asc'
}],
});
}
async
update
(
id
:
string
,
dto
:
UpdateSavedFilterDto
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
const
filter
=
await
this
.
prisma
.
savedFilter
.
findUnique
({
where
:
{
id
}
});
if
(
!
filter
)
throw
new
NotFoundException
(
'Saved filter not found'
);
if
(
filter
.
userId
!==
currentUser
.
id
)
{
throw
new
ForbiddenException
(
'You can only edit your own saved filters'
);
}
if
(
dto
.
isDefault
&&
filter
.
boardId
)
{
await
this
.
prisma
.
savedFilter
.
updateMany
({
where
:
{
userId
:
currentUser
.
id
,
boardId
:
filter
.
boardId
,
isDefault
:
true
,
id
:
{
not
:
id
}
},
data
:
{
isDefault
:
false
},
});
}
const
updateData
:
any
=
{};
if
(
dto
.
name
!==
undefined
)
updateData
.
name
=
dto
.
name
;
if
(
dto
.
filterConfig
!==
undefined
)
updateData
.
filterConfig
=
dto
.
filterConfig
;
if
(
dto
.
isDefault
!==
undefined
)
updateData
.
isDefault
=
dto
.
isDefault
;
return
this
.
prisma
.
savedFilter
.
update
({
where
:
{
id
},
data
:
updateData
});
}
async
delete
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
void
>
{
const
filter
=
await
this
.
prisma
.
savedFilter
.
findUnique
({
where
:
{
id
}
});
if
(
!
filter
)
throw
new
NotFoundException
(
'Saved filter not found'
);
if
(
filter
.
userId
!==
currentUser
.
id
&&
currentUser
.
role
!==
'SUPER_ADMIN'
)
{
throw
new
ForbiddenException
(
'You can only delete your own saved filters'
);
}
await
this
.
prisma
.
savedFilter
.
delete
({
where
:
{
id
}
});
}
}
\ No newline at end of file
backend/src/modules/cards/card-dependency.service.ts
0 → 100644
View file @
8494e6c4
import
{
Injectable
,
NotFoundException
,
ForbiddenException
,
BadRequestException
,
ConflictException
,
Logger
,
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
@
Injectable
()
export
class
CardDependencyService
{
private
readonly
logger
=
new
Logger
(
CardDependencyService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
addDependency
(
cardId
:
string
,
blockedByCardId
:
string
,
currentUser
:
RequestUser
,
):
Promise
<
any
>
{
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot manage card dependencies'
);
}
if
(
cardId
===
blockedByCardId
)
{
throw
new
BadRequestException
(
'A card cannot block itself'
);
}
const
card
=
await
this
.
prisma
.
card
.
findFirst
({
where
:
{
id
:
cardId
,
deletedAt
:
null
},
include
:
{
column
:
{
select
:
{
boardId
:
true
}
}
},
});
if
(
!
card
)
throw
new
NotFoundException
(
'Card not found'
);
const
blockingCard
=
await
this
.
prisma
.
card
.
findFirst
({
where
:
{
id
:
blockedByCardId
,
deletedAt
:
null
},
include
:
{
column
:
{
select
:
{
boardId
:
true
}
}
},
});
if
(
!
blockingCard
)
throw
new
NotFoundException
(
'Blocking card not found'
);
// PL can only manage on their boards
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
const
membership
=
await
this
.
prisma
.
boardMember
.
findUnique
({
where
:
{
boardId_userId
:
{
boardId
:
card
.
column
.
boardId
,
userId
:
currentUser
.
id
}
},
});
if
(
!
membership
)
{
throw
new
ForbiddenException
(
'You can only manage dependencies on your boards'
);
}
}
// Check for existing
const
existing
=
await
this
.
prisma
.
cardDependency
.
findUnique
({
where
:
{
blockingCardId_blockedCardId
:
{
blockingCardId
:
blockedByCardId
,
blockedCardId
:
cardId
}
},
});
if
(
existing
)
{
throw
new
ConflictException
(
'This dependency already exists'
);
}
// Check for circular dependencies (A blocks B, B blocks A)
const
reverse
=
await
this
.
prisma
.
cardDependency
.
findUnique
({
where
:
{
blockingCardId_blockedCardId
:
{
blockingCardId
:
cardId
,
blockedCardId
:
blockedByCardId
}
},
});
if
(
reverse
)
{
throw
new
BadRequestException
(
'Circular dependency detected. This card already blocks the other card.'
);
}
// Deep circular check — walk the dependency chain
const
isCircular
=
await
this
.
checkCircularDependency
(
blockedByCardId
,
cardId
);
if
(
isCircular
)
{
throw
new
BadRequestException
(
'Adding this dependency would create a circular chain'
);
}
const
dependency
=
await
this
.
prisma
.
cardDependency
.
create
({
data
:
{
blockingCardId
:
blockedByCardId
,
blockedCardId
:
cardId
,
createdById
:
currentUser
.
id
,
},
});
// Log activity on both cards
try
{
const
blockingCardData
=
await
this
.
prisma
.
card
.
findUnique
({
where
:
{
id
:
blockedByCardId
},
select
:
{
cardNumber
:
true
,
title
:
true
},
});
const
blockedCardData
=
await
this
.
prisma
.
card
.
findUnique
({
where
:
{
id
:
cardId
},
select
:
{
cardNumber
:
true
,
title
:
true
},
});
await
this
.
prisma
.
cardActivity
.
create
({
data
:
{
cardId
,
userId
:
currentUser
.
id
,
action
:
'DEPENDENCY_ADDED'
,
metadata
:
{
blockedBy
:
blockedByCardId
,
blockedByNumber
:
blockingCardData
?.
cardNumber
,
blockedByTitle
:
blockingCardData
?.
title
,
},
},
});
await
this
.
prisma
.
cardActivity
.
create
({
data
:
{
cardId
:
blockedByCardId
,
userId
:
currentUser
.
id
,
action
:
'DEPENDENCY_ADDED'
,
metadata
:
{
blocks
:
cardId
,
blocksNumber
:
blockedCardData
?.
cardNumber
,
blocksTitle
:
blockedCardData
?.
title
,
},
},
});
}
catch
(
err
)
{
this
.
logger
.
warn
(
`Failed to log dependency activity:
${
err
.
message
}
`
);
}
this
.
logger
.
log
(
`Dependency added:
${
blockedByCardId
}
blocks
${
cardId
}
by
${
currentUser
.
email
}
`
);
return
this
.
getDependencies
(
cardId
);
}
async
removeDependency
(
cardId
:
string
,
blockedByCardId
:
string
,
currentUser
:
RequestUser
,
):
Promise
<
any
>
{
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot manage card dependencies'
);
}
const
dep
=
await
this
.
prisma
.
cardDependency
.
findUnique
({
where
:
{
blockingCardId_blockedCardId
:
{
blockingCardId
:
blockedByCardId
,
blockedCardId
:
cardId
}
},
});
if
(
!
dep
)
throw
new
NotFoundException
(
'Dependency not found'
);
await
this
.
prisma
.
cardDependency
.
delete
({
where
:
{
blockingCardId_blockedCardId
:
{
blockingCardId
:
blockedByCardId
,
blockedCardId
:
cardId
}
},
});
try
{
await
this
.
prisma
.
cardActivity
.
create
({
data
:
{
cardId
,
userId
:
currentUser
.
id
,
action
:
'DEPENDENCY_REMOVED'
,
metadata
:
{
removedBlockedBy
:
blockedByCardId
},
},
});
}
catch
{
/* non-critical */
}
this
.
logger
.
log
(
`Dependency removed:
${
blockedByCardId
}
no longer blocks
${
cardId
}
`
);
return
this
.
getDependencies
(
cardId
);
}
async
getDependencies
(
cardId
:
string
):
Promise
<
{
blockedBy
:
any
[];
blocks
:
any
[]
}
>
{
const
[
blockedByRels
,
blocksRels
]
=
await
Promise
.
all
([
this
.
prisma
.
cardDependency
.
findMany
({
where
:
{
blockedCardId
:
cardId
},
include
:
{
blockingCard
:
{
select
:
{
id
:
true
,
cardNumber
:
true
,
title
:
true
,
completedAt
:
true
,
columnId
:
true
,
column
:
{
select
:
{
name
:
true
,
type
:
true
}
}
},
},
},
}),
this
.
prisma
.
cardDependency
.
findMany
({
where
:
{
blockingCardId
:
cardId
},
include
:
{
blockedCard
:
{
select
:
{
id
:
true
,
cardNumber
:
true
,
title
:
true
,
completedAt
:
true
,
columnId
:
true
,
column
:
{
select
:
{
name
:
true
,
type
:
true
}
}
},
},
},
}),
]);
return
{
blockedBy
:
blockedByRels
.
map
((
r
)
=>
({
id
:
r
.
id
,
card
:
r
.
blockingCard
,
isResolved
:
!!
r
.
blockingCard
.
completedAt
,
createdAt
:
r
.
createdAt
,
})),
blocks
:
blocksRels
.
map
((
r
)
=>
({
id
:
r
.
id
,
card
:
r
.
blockedCard
,
isResolved
:
!!
r
.
blockedCard
.
completedAt
,
createdAt
:
r
.
createdAt
,
})),
};
}
async
isBlocked
(
cardId
:
string
):
Promise
<
boolean
>
{
const
unresolvedBlockers
=
await
this
.
prisma
.
cardDependency
.
count
({
where
:
{
blockedCardId
:
cardId
,
blockingCard
:
{
completedAt
:
null
,
deletedAt
:
null
},
},
});
return
unresolvedBlockers
>
0
;
}
private
async
checkCircularDependency
(
startCardId
:
string
,
targetCardId
:
string
):
Promise
<
boolean
>
{
const
visited
=
new
Set
<
string
>
();
const
queue
=
[
startCardId
];
while
(
queue
.
length
>
0
)
{
const
current
=
queue
.
shift
()
!
;
if
(
visited
.
has
(
current
))
continue
;
visited
.
add
(
current
);
const
deps
=
await
this
.
prisma
.
cardDependency
.
findMany
({
where
:
{
blockedCardId
:
current
},
select
:
{
blockingCardId
:
true
},
});
for
(
const
dep
of
deps
)
{
if
(
dep
.
blockingCardId
===
targetCardId
)
return
true
;
queue
.
push
(
dep
.
blockingCardId
);
}
// Safety: don't traverse more than 100 nodes
if
(
visited
.
size
>
100
)
return
false
;
}
return
false
;
}
}
\ No newline at end of file
backend/src/modules/cards/card-template.service.ts
0 → 100644
View file @
8494e6c4
import
{
Injectable
,
NotFoundException
,
ForbiddenException
,
BadRequestException
,
Logger
,
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
CreateCardTemplateDto
,
UpdateCardTemplateDto
}
from
'./dto/card-template.dto'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
@
Injectable
()
export
class
CardTemplateService
{
private
readonly
logger
=
new
Logger
(
CardTemplateService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
create
(
dto
:
CreateCardTemplateDto
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot manage card templates'
);
}
if
(
!
[
'BOARD'
,
'ORGANIZATION'
].
includes
(
dto
.
scope
))
{
throw
new
BadRequestException
(
'Scope must be BOARD or ORGANIZATION'
);
}
if
(
dto
.
scope
===
'ORGANIZATION'
&&
currentUser
.
role
!==
'SUPER_ADMIN'
&&
currentUser
.
role
!==
'ADMIN'
)
{
throw
new
ForbiddenException
(
'Only Super Admin and Admin can create organization-level templates'
);
}
if
(
dto
.
scope
===
'BOARD'
)
{
if
(
!
dto
.
boardId
)
{
throw
new
BadRequestException
(
'Board ID is required for board-level templates'
);
}
const
board
=
await
this
.
prisma
.
board
.
findFirst
({
where
:
{
id
:
dto
.
boardId
,
deletedAt
:
null
}
});
if
(
!
board
)
throw
new
NotFoundException
(
'Board not found'
);
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
const
membership
=
await
this
.
prisma
.
boardMember
.
findUnique
({
where
:
{
boardId_userId
:
{
boardId
:
dto
.
boardId
,
userId
:
currentUser
.
id
}
},
});
if
(
!
membership
)
{
throw
new
ForbiddenException
(
'You can only create templates on your boards'
);
}
}
}
const
template
=
await
this
.
prisma
.
cardTemplate
.
create
({
data
:
{
name
:
dto
.
name
,
description
:
dto
.
description
||
null
,
titleTemplate
:
dto
.
titleTemplate
,
bodyTemplate
:
dto
.
bodyTemplate
||
null
,
priority
:
dto
.
priority
||
'NONE'
,
estimatedHours
:
dto
.
estimatedHours
||
null
,
checklistConfig
:
dto
.
checklistConfig
||
null
,
labelIds
:
dto
.
labelIds
||
null
,
scope
:
dto
.
scope
,
boardId
:
dto
.
scope
===
'BOARD'
?
dto
.
boardId
:
null
,
createdById
:
currentUser
.
id
,
},
include
:
{
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
},
});
this
.
logger
.
log
(
`Card template "
${
dto
.
name
}
" created by
${
currentUser
.
email
}
(
${
dto
.
scope
}
)`
);
return
template
;
}
async
findAll
(
boardId
:
string
|
undefined
,
currentUser
:
RequestUser
):
Promise
<
any
[]
>
{
const
where
:
any
=
{};
if
(
boardId
)
{
where
.
OR
=
[
{
scope
:
'ORGANIZATION'
},
{
scope
:
'BOARD'
,
boardId
},
];
}
else
{
if
(
currentUser
.
role
===
'SUPER_ADMIN'
||
currentUser
.
role
===
'ADMIN'
)
{
// See all templates
}
else
{
where
.
OR
=
[{
scope
:
'ORGANIZATION'
}];
}
}
return
this
.
prisma
.
cardTemplate
.
findMany
({
where
,
orderBy
:
[{
scope
:
'asc'
},
{
name
:
'asc'
}],
include
:
{
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
},
});
}
async
findById
(
id
:
string
):
Promise
<
any
>
{
const
template
=
await
this
.
prisma
.
cardTemplate
.
findUnique
({
where
:
{
id
},
include
:
{
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
},
});
if
(
!
template
)
throw
new
NotFoundException
(
'Card template not found'
);
return
template
;
}
async
update
(
id
:
string
,
dto
:
UpdateCardTemplateDto
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot manage card templates'
);
}
const
template
=
await
this
.
prisma
.
cardTemplate
.
findUnique
({
where
:
{
id
}
});
if
(
!
template
)
throw
new
NotFoundException
(
'Card template not found'
);
if
(
currentUser
.
role
===
'TEAM_LEAD'
&&
template
.
createdById
!==
currentUser
.
id
)
{
throw
new
ForbiddenException
(
'You can only edit templates you created'
);
}
const
updateData
:
any
=
{};
if
(
dto
.
name
!==
undefined
)
updateData
.
name
=
dto
.
name
;
if
(
dto
.
description
!==
undefined
)
updateData
.
description
=
dto
.
description
;
if
(
dto
.
titleTemplate
!==
undefined
)
updateData
.
titleTemplate
=
dto
.
titleTemplate
;
if
(
dto
.
bodyTemplate
!==
undefined
)
updateData
.
bodyTemplate
=
dto
.
bodyTemplate
;
if
(
dto
.
priority
!==
undefined
)
updateData
.
priority
=
dto
.
priority
;
if
(
dto
.
estimatedHours
!==
undefined
)
updateData
.
estimatedHours
=
dto
.
estimatedHours
;
if
(
dto
.
checklistConfig
!==
undefined
)
updateData
.
checklistConfig
=
dto
.
checklistConfig
;
if
(
dto
.
labelIds
!==
undefined
)
updateData
.
labelIds
=
dto
.
labelIds
;
return
this
.
prisma
.
cardTemplate
.
update
({
where
:
{
id
},
data
:
updateData
,
include
:
{
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
},
});
}
async
delete
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
void
>
{
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot manage card templates'
);
}
const
template
=
await
this
.
prisma
.
cardTemplate
.
findUnique
({
where
:
{
id
}
});
if
(
!
template
)
throw
new
NotFoundException
(
'Card template not found'
);
if
(
currentUser
.
role
===
'TEAM_LEAD'
&&
template
.
createdById
!==
currentUser
.
id
)
{
throw
new
ForbiddenException
(
'You can only delete templates you created'
);
}
await
this
.
prisma
.
cardTemplate
.
delete
({
where
:
{
id
}
});
this
.
logger
.
log
(
`Card template
${
id
}
deleted by
${
currentUser
.
email
}
`
);
}
}
\ No newline at end of file
backend/src/modules/cards/cards.controller.ts
View file @
8494e6c4
...
...
@@ -13,12 +13,18 @@ import {
import
{
CardsService
}
from
'./cards.service'
;
import
{
CardMovementService
}
from
'./card-movement.service'
;
import
{
CardAssignmentService
}
from
'./card-assignment.service'
;
import
{
CardTemplateService
}
from
'./card-template.service'
;
import
{
RecurringCardService
}
from
'./recurring-card.service'
;
import
{
CardDependencyService
}
from
'./card-dependency.service'
;
import
{
ViewsService
}
from
'./views.service'
;
import
{
CreateCardDto
}
from
'./dto/create-card.dto'
;
import
{
UpdateCardDto
}
from
'./dto/update-card.dto'
;
import
{
MoveCardDto
}
from
'./dto/move-card.dto'
;
import
{
CardFilterDto
}
from
'./dto/card-filter.dto'
;
import
{
DuplicateCardDto
}
from
'./dto/duplicate-card.dto'
;
import
{
AssignCardDto
,
UnassignCardDto
,
SetBountyDto
}
from
'./dto/assign-card.dto'
;
import
{
CreateCardTemplateDto
,
UpdateCardTemplateDto
}
from
'./dto/card-template.dto'
;
import
{
CreateRecurringCardDto
,
UpdateRecurringCardDto
}
from
'./dto/recurring-card.dto'
;
import
{
CurrentUser
,
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
Roles
}
from
'../../common/decorators/roles.decorator'
;
...
...
@@ -28,8 +34,14 @@ export class CardsController {
private
readonly
cardsService
:
CardsService
,
private
readonly
cardMovementService
:
CardMovementService
,
private
readonly
cardAssignmentService
:
CardAssignmentService
,
private
readonly
cardTemplateService
:
CardTemplateService
,
private
readonly
recurringCardService
:
RecurringCardService
,
private
readonly
cardDependencyService
:
CardDependencyService
,
private
readonly
viewsService
:
ViewsService
,
)
{}
// ─── CORE CARD CRUD ──────────────────────────────────
@
Post
()
async
create
(@
Body
()
dto
:
CreateCardDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardsService
.
create
(
dto
,
user
);
...
...
@@ -40,67 +52,48 @@ export class CardsController {
return
this
.
cardsService
.
findAll
(
filter
,
user
);
}
@
Get
(
'my-tasks'
)
async
getMyTasks
(@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
viewsService
.
getMyTasks
(
user
);
}
@
Get
(
':id'
)
async
findById
(@
Param
(
'id'
)
id
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardsService
.
findById
(
id
,
user
);
}
@
Put
(
':id'
)
async
update
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
UpdateCardDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
update
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
UpdateCardDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardsService
.
update
(
id
,
dto
,
user
);
}
@
Put
(
':id/move'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
moveCard
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
MoveCardDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
moveCard
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
MoveCardDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
cardMovementService
.
moveCard
(
id
,
dto
,
user
);
return
this
.
cardsService
.
findById
(
id
,
user
);
}
@
Put
(
':id/assign'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
assignCard
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
AssignCardDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
assignCard
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
AssignCardDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardAssignmentService
.
assign
(
id
,
dto
.
assigneeIds
,
user
);
}
@
Put
(
':id/unassign'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
unassignCard
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
UnassignCardDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
unassignCard
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
UnassignCardDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardAssignmentService
.
unassign
(
id
,
dto
.
userIds
,
user
);
}
@
Put
(
':id/bounty'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
setBounty
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
SetBountyDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
setBounty
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
SetBountyDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardsService
.
setBounty
(
id
,
dto
.
bountyPiasters
,
dto
.
bountySplitJson
,
user
);
}
@
Post
(
':id/duplicate'
)
async
duplicateCard
(
@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
DuplicateCardDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
async
duplicateCard
(@
Param
(
'id'
)
id
:
string
,
@
Body
()
dto
:
DuplicateCardDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardsService
.
duplicate
(
id
,
dto
,
user
);
}
...
...
@@ -143,4 +136,135 @@ export class CardsController {
await
this
.
cardsService
.
unwatchCard
(
id
,
user
);
return
{
message
:
'Stopped watching this card'
};
}
// ─── DEPENDENCIES ────────────────────────────────────
@
Get
(
':id/dependencies'
)
async
getDependencies
(@
Param
(
'id'
)
id
:
string
)
{
return
this
.
cardDependencyService
.
getDependencies
(
id
);
}
@
Post
(
':id/dependencies'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
addDependency
(
@
Param
(
'id'
)
id
:
string
,
@
Body
(
'blockedByCardId'
)
blockedByCardId
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
cardDependencyService
.
addDependency
(
id
,
blockedByCardId
,
user
);
}
@
Delete
(
':id/dependencies/:blockedByCardId'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
removeDependency
(
@
Param
(
'id'
)
id
:
string
,
@
Param
(
'blockedByCardId'
)
blockedByCardId
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
cardDependencyService
.
removeDependency
(
id
,
blockedByCardId
,
user
);
}
// ─── CARD TEMPLATES ──────────────────────────────────
@
Post
(
'templates'
)
async
createTemplate
(@
Body
()
dto
:
CreateCardTemplateDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardTemplateService
.
create
(
dto
,
user
);
}
@
Get
(
'templates/list'
)
async
listTemplates
(@
Query
(
'boardId'
)
boardId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
cardTemplateService
.
findAll
(
boardId
,
user
);
}
@
Get
(
'templates/:templateId'
)
async
getTemplate
(@
Param
(
'templateId'
)
templateId
:
string
)
{
return
this
.
cardTemplateService
.
findById
(
templateId
);
}
@
Put
(
'templates/:templateId'
)
async
updateTemplate
(
@
Param
(
'templateId'
)
templateId
:
string
,
@
Body
()
dto
:
UpdateCardTemplateDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
cardTemplateService
.
update
(
templateId
,
dto
,
user
);
}
@
Delete
(
'templates/:templateId'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
deleteTemplate
(@
Param
(
'templateId'
)
templateId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
cardTemplateService
.
delete
(
templateId
,
user
);
return
{
message
:
'Card template deleted'
};
}
// ─── RECURRING CARDS ─────────────────────────────────
@
Post
(
'recurring'
)
async
createRecurring
(@
Body
()
dto
:
CreateRecurringCardDto
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
recurringCardService
.
create
(
dto
,
user
);
}
@
Get
(
'recurring/list'
)
async
listRecurring
(@
Query
(
'boardId'
)
boardId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
recurringCardService
.
findAll
(
boardId
,
user
);
}
@
Get
(
'recurring/:recurringId'
)
async
getRecurring
(@
Param
(
'recurringId'
)
recurringId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
recurringCardService
.
findById
(
recurringId
,
user
);
}
@
Put
(
'recurring/:recurringId'
)
async
updateRecurring
(
@
Param
(
'recurringId'
)
recurringId
:
string
,
@
Body
()
dto
:
UpdateRecurringCardDto
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
recurringCardService
.
update
(
recurringId
,
dto
,
user
);
}
@
Post
(
'recurring/:recurringId/pause'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
pauseRecurring
(@
Param
(
'recurringId'
)
recurringId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
recurringCardService
.
pause
(
recurringId
,
user
);
}
@
Post
(
'recurring/:recurringId/resume'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
resumeRecurring
(@
Param
(
'recurringId'
)
recurringId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
return
this
.
recurringCardService
.
resume
(
recurringId
,
user
);
}
@
Delete
(
'recurring/:recurringId'
)
@
HttpCode
(
HttpStatus
.
OK
)
async
deleteRecurring
(@
Param
(
'recurringId'
)
recurringId
:
string
,
@
CurrentUser
()
user
:
RequestUser
)
{
await
this
.
recurringCardService
.
delete
(
recurringId
,
user
);
return
{
message
:
'Recurring card definition deleted'
};
}
// ─── BOARD VIEWS ─────────────────────────────────────
@
Get
(
'board/:boardId/activity'
)
async
getBoardActivity
(
@
Param
(
'boardId'
)
boardId
:
string
,
@
Query
(
'page'
)
page
:
string
,
@
Query
(
'limit'
)
limit
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
viewsService
.
getBoardActivity
(
boardId
,
user
,
page
?
parseInt
(
page
,
10
)
:
1
,
limit
?
parseInt
(
limit
,
10
)
:
50
,
);
}
@
Get
(
'board/:boardId/calendar'
)
async
getBoardCalendar
(
@
Param
(
'boardId'
)
boardId
:
string
,
@
Query
(
'start'
)
start
:
string
,
@
Query
(
'end'
)
end
:
string
,
@
CurrentUser
()
user
:
RequestUser
,
)
{
return
this
.
viewsService
.
getCardsWithDeadlines
(
boardId
,
start
,
end
,
user
);
}
}
\ No newline at end of file
backend/src/modules/cards/cards.module.ts
View file @
8494e6c4
...
...
@@ -3,10 +3,30 @@ import { CardsController } from './cards.controller';
import
{
CardsService
}
from
'./cards.service'
;
import
{
CardMovementService
}
from
'./card-movement.service'
;
import
{
CardAssignmentService
}
from
'./card-assignment.service'
;
import
{
CardTemplateService
}
from
'./card-template.service'
;
import
{
RecurringCardService
}
from
'./recurring-card.service'
;
import
{
CardDependencyService
}
from
'./card-dependency.service'
;
import
{
ViewsService
}
from
'./views.service'
;
@
Module
({
controllers
:
[
CardsController
],
providers
:
[
CardsService
,
CardMovementService
,
CardAssignmentService
],
exports
:
[
CardsService
,
CardMovementService
,
CardAssignmentService
],
providers
:
[
CardsService
,
CardMovementService
,
CardAssignmentService
,
CardTemplateService
,
RecurringCardService
,
CardDependencyService
,
ViewsService
,
],
exports
:
[
CardsService
,
CardMovementService
,
CardAssignmentService
,
CardTemplateService
,
RecurringCardService
,
CardDependencyService
,
ViewsService
,
],
})
export
class
CardsModule
{}
\ No newline at end of file
backend/src/modules/cards/dto/card-template.dto.ts
0 → 100644
View file @
8494e6c4
import
{
IsString
,
IsOptional
,
IsArray
,
IsNumber
,
Min
,
MinLength
,
MaxLength
}
from
'class-validator'
;
export
class
CreateCardTemplateDto
{
@
IsString
()
@
MinLength
(
1
)
@
MaxLength
(
100
)
name
:
string
;
@
IsOptional
()
@
IsString
()
description
?:
string
;
@
IsString
()
@
MinLength
(
1
)
@
MaxLength
(
200
)
titleTemplate
:
string
;
@
IsOptional
()
@
IsString
()
bodyTemplate
?:
string
;
@
IsOptional
()
@
IsString
()
priority
?:
string
;
@
IsOptional
()
@
IsNumber
()
@
Min
(
0
)
estimatedHours
?:
number
;
@
IsOptional
()
checklistConfig
?:
any
;
// Array of { title, items }
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
labelIds
?:
string
[];
@
IsString
()
scope
:
string
;
// BOARD, ORGANIZATION
@
IsOptional
()
@
IsString
()
boardId
?:
string
;
}
export
class
UpdateCardTemplateDto
{
@
IsOptional
()
@
IsString
()
@
MinLength
(
1
)
@
MaxLength
(
100
)
name
?:
string
;
@
IsOptional
()
@
IsString
()
description
?:
string
;
@
IsOptional
()
@
IsString
()
@
MaxLength
(
200
)
titleTemplate
?:
string
;
@
IsOptional
()
@
IsString
()
bodyTemplate
?:
string
;
@
IsOptional
()
@
IsString
()
priority
?:
string
;
@
IsOptional
()
@
IsNumber
()
@
Min
(
0
)
estimatedHours
?:
number
;
@
IsOptional
()
checklistConfig
?:
any
;
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
labelIds
?:
string
[];
}
\ No newline at end of file
backend/src/modules/cards/dto/recurring-card.dto.ts
0 → 100644
View file @
8494e6c4
import
{
IsString
,
IsOptional
,
IsArray
,
IsNumber
,
IsBoolean
,
IsDateString
,
Min
,
MinLength
,
MaxLength
}
from
'class-validator'
;
export
class
CreateRecurringCardDto
{
@
IsString
()
boardId
:
string
;
@
IsString
()
@
MinLength
(
1
)
@
MaxLength
(
200
)
title
:
string
;
@
IsOptional
()
@
IsString
()
titleTemplate
?:
string
;
@
IsOptional
()
@
IsString
()
description
?:
string
;
@
IsOptional
()
@
IsString
()
priority
?:
string
;
@
IsOptional
()
@
IsNumber
()
@
Min
(
0
)
estimatedHours
?:
number
;
@
IsOptional
()
checklistConfig
?:
any
;
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
labelIds
?:
string
[];
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
assigneeIds
?:
string
[];
@
IsString
()
recurrenceType
:
string
;
// DAILY, WEEKLY, BIWEEKLY, MONTHLY, CUSTOM
@
IsOptional
()
recurrenceConfig
?:
any
;
// { dayOfWeek?, dayOfMonth?, intervalDays? }
@
IsOptional
()
@
IsDateString
()
startDate
?:
string
;
}
export
class
UpdateRecurringCardDto
{
@
IsOptional
()
@
IsString
()
@
MaxLength
(
200
)
title
?:
string
;
@
IsOptional
()
@
IsString
()
titleTemplate
?:
string
;
@
IsOptional
()
@
IsString
()
description
?:
string
;
@
IsOptional
()
@
IsString
()
priority
?:
string
;
@
IsOptional
()
@
IsNumber
()
@
Min
(
0
)
estimatedHours
?:
number
;
@
IsOptional
()
checklistConfig
?:
any
;
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
labelIds
?:
string
[];
@
IsOptional
()
@
IsArray
()
@
IsString
({
each
:
true
})
assigneeIds
?:
string
[];
@
IsOptional
()
@
IsString
()
recurrenceType
?:
string
;
@
IsOptional
()
recurrenceConfig
?:
any
;
@
IsOptional
()
@
IsBoolean
()
isActive
?:
boolean
;
}
\ No newline at end of file
backend/src/modules/cards/recurring-card.service.ts
0 → 100644
View file @
8494e6c4
import
{
Injectable
,
NotFoundException
,
ForbiddenException
,
BadRequestException
,
Logger
,
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
CreateRecurringCardDto
,
UpdateRecurringCardDto
}
from
'./dto/recurring-card.dto'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
@
Injectable
()
export
class
RecurringCardService
{
private
readonly
logger
=
new
Logger
(
RecurringCardService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
async
create
(
dto
:
CreateRecurringCardDto
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot create recurring cards'
);
}
const
validTypes
=
[
'DAILY'
,
'WEEKLY'
,
'BIWEEKLY'
,
'MONTHLY'
,
'CUSTOM'
];
if
(
!
validTypes
.
includes
(
dto
.
recurrenceType
))
{
throw
new
BadRequestException
(
`Recurrence type must be one of:
${
validTypes
.
join
(
', '
)}
`
);
}
const
board
=
await
this
.
prisma
.
board
.
findFirst
({
where
:
{
id
:
dto
.
boardId
,
deletedAt
:
null
}
});
if
(
!
board
)
throw
new
NotFoundException
(
'Board not found'
);
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
const
membership
=
await
this
.
prisma
.
boardMember
.
findUnique
({
where
:
{
boardId_userId
:
{
boardId
:
dto
.
boardId
,
userId
:
currentUser
.
id
}
},
});
if
(
!
membership
)
{
throw
new
ForbiddenException
(
'You can only create recurring cards on your boards'
);
}
}
const
startDate
=
dto
.
startDate
?
new
Date
(
dto
.
startDate
)
:
new
Date
();
const
nextCreationDate
=
this
.
calculateNextDate
(
dto
.
recurrenceType
,
dto
.
recurrenceConfig
,
startDate
);
const
definition
=
await
this
.
prisma
.
recurringCardDefinition
.
create
({
data
:
{
boardId
:
dto
.
boardId
,
title
:
dto
.
title
,
titleTemplate
:
dto
.
titleTemplate
||
null
,
description
:
dto
.
description
||
null
,
priority
:
dto
.
priority
||
'NONE'
,
estimatedHours
:
dto
.
estimatedHours
||
null
,
checklistConfig
:
dto
.
checklistConfig
||
null
,
labelIds
:
dto
.
labelIds
||
null
,
assigneeIds
:
dto
.
assigneeIds
||
null
,
recurrenceType
:
dto
.
recurrenceType
,
recurrenceConfig
:
dto
.
recurrenceConfig
||
null
,
isActive
:
true
,
nextCreationDate
,
createdById
:
currentUser
.
id
,
},
include
:
{
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
});
this
.
logger
.
log
(
`Recurring card definition "
${
dto
.
title
}
" created on board
${
board
.
key
}
—
${
dto
.
recurrenceType
}
by
${
currentUser
.
email
}
`
,
);
return
definition
;
}
async
findAll
(
boardId
:
string
|
undefined
,
currentUser
:
RequestUser
):
Promise
<
any
[]
>
{
const
where
:
any
=
{};
if
(
boardId
)
{
where
.
boardId
=
boardId
;
}
if
(
currentUser
.
role
===
'TEAM_LEAD'
)
{
where
.
board
=
{
members
:
{
some
:
{
userId
:
currentUser
.
id
}
}
};
}
else
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot view recurring card definitions'
);
}
return
this
.
prisma
.
recurringCardDefinition
.
findMany
({
where
,
orderBy
:
{
createdAt
:
'desc'
},
include
:
{
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
});
}
async
findById
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
const
def
=
await
this
.
prisma
.
recurringCardDefinition
.
findUnique
({
where
:
{
id
},
include
:
{
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
});
if
(
!
def
)
throw
new
NotFoundException
(
'Recurring card definition not found'
);
return
def
;
}
async
update
(
id
:
string
,
dto
:
UpdateRecurringCardDto
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot manage recurring cards'
);
}
const
def
=
await
this
.
prisma
.
recurringCardDefinition
.
findUnique
({
where
:
{
id
}
});
if
(
!
def
)
throw
new
NotFoundException
(
'Recurring card definition not found'
);
if
(
currentUser
.
role
===
'TEAM_LEAD'
&&
def
.
createdById
!==
currentUser
.
id
)
{
throw
new
ForbiddenException
(
'You can only edit recurring cards you created'
);
}
const
updateData
:
any
=
{};
const
fields
=
[
'title'
,
'titleTemplate'
,
'description'
,
'priority'
,
'estimatedHours'
,
'checklistConfig'
,
'labelIds'
,
'assigneeIds'
,
'recurrenceType'
,
'recurrenceConfig'
,
'isActive'
,
];
for
(
const
field
of
fields
)
{
if
((
dto
as
any
)[
field
]
!==
undefined
)
updateData
[
field
]
=
(
dto
as
any
)[
field
];
}
// Recalculate next creation date if recurrence changed
if
(
dto
.
recurrenceType
||
dto
.
recurrenceConfig
)
{
const
rType
=
dto
.
recurrenceType
||
def
.
recurrenceType
;
const
rConfig
=
dto
.
recurrenceConfig
||
def
.
recurrenceConfig
;
updateData
.
nextCreationDate
=
this
.
calculateNextDate
(
rType
,
rConfig
,
new
Date
());
}
return
this
.
prisma
.
recurringCardDefinition
.
update
({
where
:
{
id
},
data
:
updateData
,
include
:
{
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
createdBy
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
}
},
},
});
}
async
pause
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
return
this
.
update
(
id
,
{
isActive
:
false
},
currentUser
);
}
async
resume
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
any
>
{
const
def
=
await
this
.
prisma
.
recurringCardDefinition
.
findUnique
({
where
:
{
id
}
});
if
(
!
def
)
throw
new
NotFoundException
(
'Recurring card definition not found'
);
const
nextDate
=
this
.
calculateNextDate
(
def
.
recurrenceType
,
def
.
recurrenceConfig
as
any
,
new
Date
());
return
this
.
prisma
.
recurringCardDefinition
.
update
({
where
:
{
id
},
data
:
{
isActive
:
true
,
nextCreationDate
:
nextDate
},
});
}
async
delete
(
id
:
string
,
currentUser
:
RequestUser
):
Promise
<
void
>
{
if
(
currentUser
.
role
===
'CONTRACTOR'
)
{
throw
new
ForbiddenException
(
'Contractors cannot delete recurring cards'
);
}
const
def
=
await
this
.
prisma
.
recurringCardDefinition
.
findUnique
({
where
:
{
id
}
});
if
(
!
def
)
throw
new
NotFoundException
(
'Recurring card definition not found'
);
if
(
currentUser
.
role
===
'TEAM_LEAD'
&&
def
.
createdById
!==
currentUser
.
id
)
{
throw
new
ForbiddenException
(
'You can only delete recurring cards you created'
);
}
await
this
.
prisma
.
recurringCardDefinition
.
delete
({
where
:
{
id
}
});
this
.
logger
.
log
(
`Recurring card definition
${
id
}
deleted by
${
currentUser
.
email
}
`
);
}
private
calculateNextDate
(
recurrenceType
:
string
,
config
:
any
,
fromDate
:
Date
):
Date
{
const
next
=
new
Date
(
fromDate
);
next
.
setHours
(
6
,
0
,
0
,
0
);
// Default creation at 6AM
switch
(
recurrenceType
)
{
case
'DAILY'
:
next
.
setDate
(
next
.
getDate
()
+
1
);
break
;
case
'WEEKLY'
:
next
.
setDate
(
next
.
getDate
()
+
7
);
break
;
case
'BIWEEKLY'
:
next
.
setDate
(
next
.
getDate
()
+
14
);
break
;
case
'MONTHLY'
:
next
.
setMonth
(
next
.
getMonth
()
+
1
);
break
;
case
'CUSTOM'
:
const
intervalDays
=
config
?.
intervalDays
||
7
;
next
.
setDate
(
next
.
getDate
()
+
intervalDays
);
break
;
default
:
next
.
setDate
(
next
.
getDate
()
+
7
);
}
return
next
;
}
}
\ No newline at end of file
backend/src/modules/cards/views.service.ts
0 → 100644
View file @
8494e6c4
import
{
Injectable
,
Logger
}
from
'@nestjs/common'
;
import
{
PrismaService
}
from
'../../prisma/prisma.service'
;
import
{
RequestUser
}
from
'../../common/decorators/current-user.decorator'
;
import
{
getSkip
,
buildPaginatedResponse
,
PaginatedResult
}
from
'../../common/utils/pagination.util'
;
@
Injectable
()
export
class
ViewsService
{
private
readonly
logger
=
new
Logger
(
ViewsService
.
name
);
constructor
(
private
readonly
prisma
:
PrismaService
)
{}
/**
* Cross-board "My Tasks" view — all cards assigned to the current user
* grouped by board, then by column status
*/
async
getMyTasks
(
currentUser
:
RequestUser
):
Promise
<
any
>
{
const
cards
=
await
this
.
prisma
.
card
.
findMany
({
where
:
{
assignees
:
{
some
:
{
id
:
currentUser
.
id
}
},
deletedAt
:
null
,
isArchived
:
false
,
},
orderBy
:
[{
dueDate
:
{
sort
:
'asc'
,
nulls
:
'last'
}
},
{
position
:
'asc'
}],
include
:
{
labels
:
{
select
:
{
id
:
true
,
name
:
true
,
color
:
true
,
textColor
:
true
}
},
assignees
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
}
},
column
:
{
select
:
{
id
:
true
,
name
:
true
,
type
:
true
,
board
:
{
select
:
{
id
:
true
,
name
:
true
,
key
:
true
}
},
},
},
_count
:
{
select
:
{
comments
:
true
,
attachments
:
true
}
},
},
});
// Group by board, then 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
boardMap
=
new
Map
<
string
,
{
board
:
any
;
cards
:
any
[]
}
>
();
for
(
const
card
of
cards
)
{
const
boardId
=
card
.
column
.
board
.
id
;
if
(
!
boardMap
.
has
(
boardId
))
{
boardMap
.
set
(
boardId
,
{
board
:
card
.
column
.
board
,
cards
:
[]
});
}
boardMap
.
get
(
boardId
)
!
.
cards
.
push
({
id
:
card
.
id
,
cardNumber
:
card
.
cardNumber
,
title
:
card
.
title
,
columnId
:
card
.
column
.
id
,
columnName
:
card
.
column
.
name
,
columnType
:
card
.
column
.
type
,
position
:
card
.
position
,
priority
:
card
.
priority
,
dueDate
:
card
.
dueDate
,
isOverdue
:
card
.
dueDate
?
new
Date
(
card
.
dueDate
)
<
new
Date
()
&&
!
card
.
completedAt
:
false
,
bountyPiasters
:
card
.
bountyPiasters
,
coverImage
:
card
.
coverImage
,
frozenReason
:
card
.
frozenReason
,
labels
:
card
.
labels
,
assignees
:
card
.
assignees
,
commentCount
:
card
.
_count
.
comments
,
attachmentCount
:
card
.
_count
.
attachments
,
completedAt
:
card
.
completedAt
,
createdAt
:
card
.
createdAt
,
});
}
const
result
=
Array
.
from
(
boardMap
.
values
()).
map
((
group
)
=>
({
board
:
group
.
board
,
cards
:
group
.
cards
.
sort
((
a
,
b
)
=>
{
const
aPri
=
columnPriority
[
a
.
columnType
]
??
4
;
const
bPri
=
columnPriority
[
b
.
columnType
]
??
4
;
return
aPri
-
bPri
;
}),
cardCount
:
group
.
cards
.
length
,
overdueCount
:
group
.
cards
.
filter
((
c
)
=>
c
.
isOverdue
).
length
,
}));
return
{
totalCards
:
cards
.
length
,
totalOverdue
:
cards
.
filter
((
c
)
=>
c
.
dueDate
&&
new
Date
(
c
.
dueDate
)
<
new
Date
()
&&
!
c
.
completedAt
).
length
,
boards
:
result
,
};
}
/**
* Board Activity Feed — chronological list of all actions on a board
*/
async
getBoardActivity
(
boardId
:
string
,
currentUser
:
RequestUser
,
page
:
number
=
1
,
limit
:
number
=
50
,
):
Promise
<
PaginatedResult
<
any
>>
{
// Get all card IDs on this board
const
columns
=
await
this
.
prisma
.
column
.
findMany
({
where
:
{
boardId
},
select
:
{
id
:
true
},
});
const
columnIds
=
columns
.
map
((
c
)
=>
c
.
id
);
const
cardIds
=
await
this
.
prisma
.
card
.
findMany
({
where
:
{
columnId
:
{
in
:
columnIds
}
},
select
:
{
id
:
true
},
});
const
ids
=
cardIds
.
map
((
c
)
=>
c
.
id
);
if
(
ids
.
length
===
0
)
{
return
buildPaginatedResponse
([],
0
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
const
[
activities
,
total
]
=
await
Promise
.
all
([
this
.
prisma
.
cardActivity
.
findMany
({
where
:
{
cardId
:
{
in
:
ids
}
},
skip
:
getSkip
(
page
,
limit
),
take
:
limit
,
orderBy
:
{
createdAt
:
'desc'
},
include
:
{
user
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
}
},
card
:
{
select
:
{
id
:
true
,
cardNumber
:
true
,
title
:
true
}
},
},
}),
this
.
prisma
.
cardActivity
.
count
({
where
:
{
cardId
:
{
in
:
ids
}
}
}),
]);
return
buildPaginatedResponse
(
activities
,
total
,
{
page
,
limit
,
sortOrder
:
'desc'
});
}
/**
* Cards due within N days for calendar view
*/
async
getCardsWithDeadlines
(
boardId
:
string
,
startDate
:
string
,
endDate
:
string
,
currentUser
:
RequestUser
,
):
Promise
<
any
[]
>
{
const
start
=
new
Date
(
startDate
);
const
end
=
new
Date
(
endDate
);
const
where
:
any
=
{
dueDate
:
{
gte
:
start
,
lte
:
end
},
deletedAt
:
null
,
isArchived
:
false
,
column
:
{
boardId
},
};
return
this
.
prisma
.
card
.
findMany
({
where
,
orderBy
:
{
dueDate
:
'asc'
},
select
:
{
id
:
true
,
cardNumber
:
true
,
title
:
true
,
dueDate
:
true
,
priority
:
true
,
completedAt
:
true
,
bountyPiasters
:
true
,
column
:
{
select
:
{
id
:
true
,
name
:
true
,
type
:
true
}
},
labels
:
{
select
:
{
id
:
true
,
name
:
true
,
color
:
true
}
},
assignees
:
{
select
:
{
id
:
true
,
firstName
:
true
,
lastName
:
true
,
avatar
:
true
}
},
},
});
}
}
\ No newline at end of file
prisma/schema-boards.prisma
View file @
8494e6c4
//
============================================================
//
BOARDS
+
COLUMNS
—
Add
these
models
to
your
main
schema
.
prisma
//
============================================================
model
Board
{
id
String
@
id
@
default
(
uuid
())
name
String
description
String
?
key
String
@
unique
//
e
.
g
.
"PROJ"
—
used
for
card
numbering
visibility
String
@
default
(
"PRIVATE"
)
//
PUBLIC
,
PRIVATE
,
TEAM
color
String
?
icon
String
?
allowContractorCreation
Boolean
@
default
(
true
)
autoArchiveDoneCardsDays
Int
@
default
(
30
)
deadlineExcludesHolidays
Boolean
@
default
(
false
)
nextCardNumber
Int
@
default
(
1
)
isArchived
Boolean
@
default
(
false
)
archivedAt
DateTime
?
deletedAt
DateTime
?
createdById
String
createdBy
User
@
relation
(
"BoardCreator"
,
fields
:
[
createdById
],
references
:
[
id
],
onDelete
:
RESTRICT
)
columns
Column
[]
members
BoardMember
[]
labels
Label
[]
@
relation
(
"BoardLabels"
)
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
//
───
ENHANCED
KANBAN
MODELS
──────────────────────────────────────
//
Phase
2
B
:
Card
templates
,
recurring
cards
,
dependencies
,
saved
filters
@@
index
([
createdById
])
@@
index
([
isArchived
])
@@
index
([
deletedAt
])
@@
index
([
name
])
}
model
CardTemplate
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
model
Column
{
id
String
@
id
@
default
(
uuid
())
boardId
String
bo
ard
Board
@
relation
(
fields
:
[
boardId
],
references
:
[
id
],
onDelete
:
CASCADE
)
name
String
icon
String
?
position
Int
type
String
//
BACKLOG
,
TODO
,
DOING
,
FROZEN
,
IN_REVIEW
,
DONE
,
CUSTOM
isDone
Boolean
@
default
(
false
)
isDefault
Boolean
@
default
(
true
)
wipLimit
Int
?
//
per
-
user
WIP
limit
wipLimitTotal
Int
?
//
total
WIP
limit
for
the
column
color
String
?
created
At
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
name
String
description
String
?
titleTemplate
String
//
e
.
g
.
"Weekly Code Review: [Module Name]"
bo
dyTemplate
String
?
//
Pre
-
filled
description
(
rich
text
)
priority
String
@
default
(
"NONE"
)
estimatedHours
Float
?
checklistConfig
Json
?
//
Array
of
{
title
,
items
:
string
[]
}
labelIds
Json
?
//
Array
of
label
IDs
to
auto
-
apply
scope
String
@
default
(
"BOARD"
)
//
BOARD
,
ORGANIZATION
boardId
String
?
board
Board
?
@
relation
(
"BoardCardTemplates"
,
fields
:
[
boardId
],
references
:
[
id
],
onDelete
:
Cascade
)
created
ById
String
createdBy
User
@
relation
(
"CardTemplateCreatedBy"
,
fields
:
[
createdById
],
references
:
[
id
],
onDelete
:
Restrict
)
@@
index
([
boardId
])
@@
index
([
boardId
,
position
])
@@
index
([
scope
])
@@
index
([
createdById
])
}
model
BoardMember
{
model
RecurringCardDefinition
{
id
String
@
id
@
default
(
uuid
())
boardId
String
board
Board
@
relation
(
fields
:
[
boardId
],
references
:
[
id
],
onDelete
:
CASCADE
)
userId
String
user
User
@
relation
(
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
CASCADE
)
role
String
@
default
(
"MEMBER"
)
//
OWNER
,
ADMIN
,
MEMBER
,
VIEWER
joinedAt
DateTime
@
default
(
now
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
@@
unique
([
boardId
,
userId
])
@@
index
([
userId
])
boardId
String
board
Board
@
relation
(
"BoardRecurringCards"
,
fields
:
[
boardId
],
references
:
[
id
],
onDelete
:
Cascade
)
title
String
titleTemplate
String
?
//
e
.
g
.
"Weekly Review — [DATE]"
description
String
?
priority
String
@
default
(
"NONE"
)
estimatedHours
Float
?
checklistConfig
Json
?
//
Array
of
{
title
,
items
:
string
[]
}
labelIds
Json
?
//
Assignment
assigneeIds
Json
?
//
Array
of
user
IDs
to
auto
-
assign
//
Recurrence
recurrenceType
String
//
DAILY
,
WEEKLY
,
BIWEEKLY
,
MONTHLY
,
CUSTOM
recurrenceConfig
Json
?
//
{
dayOfWeek
?,
dayOfMonth
?,
intervalDays
?
}
//
Tracking
isActive
Boolean
@
default
(
true
)
lastCreatedAt
DateTime
?
nextCreationDate
DateTime
?
createdById
String
createdBy
User
@
relation
(
"RecurringCardCreatedBy"
,
fields
:
[
createdById
],
references
:
[
id
],
onDelete
:
Restrict
)
@@
index
([
boardId
])
@@
index
([
isActive
])
@@
index
([
nextCreationDate
])
}
model
BoardTemplate
{
id
String
@
id
@
default
(
uuid
())
name
String
description
String
?
boardConfig
Json
//
column
config
,
board
settings
labelConfig
Json
?
//
label
definitions
createdById
String
createdBy
User
@
relation
(
"BoardTemplateCreator"
,
fields
:
[
createdById
],
references
:
[
id
],
onDelete
:
RESTRICT
)
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
model
CardDependency
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
@@
index
([
createdById
])
blockingCardId
String
//
This
card
blocks
...
blockingCard
Card
@
relation
(
"CardBlocks"
,
fields
:
[
blockingCardId
],
references
:
[
id
],
onDelete
:
Cascade
)
blockedCardId
String
//
...
this
card
blockedCard
Card
@
relation
(
"CardBlockedBy"
,
fields
:
[
blockedCardId
],
references
:
[
id
],
onDelete
:
Cascade
)
createdById
String
?
@@
unique
([
blockingCardId
,
blockedCardId
])
@@
index
([
blockingCardId
])
@@
index
([
blockedCardId
])
}
model
SavedFilter
{
id
String
@
id
@
default
(
uuid
())
createdAt
DateTime
@
default
(
now
())
updatedAt
DateTime
@
updatedAt
userId
String
user
User
@
relation
(
"UserSavedFilters"
,
fields
:
[
userId
],
references
:
[
id
],
onDelete
:
Cascade
)
boardId
String
?
board
Board
?
@
relation
(
"BoardSavedFilters"
,
fields
:
[
boardId
],
references
:
[
id
],
onDelete
:
Cascade
)
name
String
filterConfig
Json
//
{
assigneeIds
?,
labelIds
?,
priority
?,
deadline
?,
columnIds
?,
hasBounty
?,
search
?,
labelLogic
?
}
isDefault
Boolean
@
default
(
false
)
//
Auto
-
apply
when
opening
the
board
@@
index
([
userId
])
@@
index
([
boardId
])
@@
index
([
userId
,
boardId
])
}
\ No newline at end of file
shared/src/types/board.types.ts
View file @
8494e6c4
import
{
BoardVisibility
,
BoardMemberRole
}
from
'../enums'
;
export
interface
BoardSummary
{
export
interface
SavedFilter
{
id
:
string
;
userId
:
string
;
boardId
:
string
|
null
;
name
:
string
;
description
:
string
|
null
;
visibility
:
BoardVisibility
;
prefix
:
string
|
null
;
color
:
string
|
null
;
icon
:
string
|
null
;
isArchived
:
boolean
;
memberCount
:
number
;
cardCount
:
number
;
filterConfig
:
FilterConfig
;
isDefault
:
boolean
;
createdAt
:
string
;
updatedAt
:
string
;
}
export
interface
BoardDetail
extends
BoardSummary
{
columns
:
ColumnData
[];
labels
:
LabelData
[];
members
:
BoardMemberData
[];
export
interface
FilterConfig
{
assigneeIds
?:
string
[];
labelIds
?:
string
[];
labelLogic
?:
'AND'
|
'OR'
;
priority
?:
string
[];
deadline
?:
'overdue'
|
'today'
|
'this_week'
|
'this_month'
|
'no_deadline'
|
null
;
dueDateFrom
?:
string
;
dueDateTo
?:
string
;
columnIds
?:
string
[];
hasBounty
?:
boolean
|
null
;
search
?:
string
;
createdById
?:
string
;
isArchived
?:
boolean
;
}
export
interface
C
olumnData
{
export
interface
C
ardTemplate
{
id
:
string
;
name
:
string
;
position
:
number
;
color
:
string
|
null
;
wipLimit
:
number
|
null
;
isDone
:
boolean
;
cardCount
:
number
;
description
:
string
|
null
;
titleTemplate
:
string
;
bodyTemplate
:
string
|
null
;
priority
:
string
;
estimatedHours
:
number
|
null
;
checklistConfig
:
any
;
labelIds
:
string
[]
|
null
;
scope
:
string
;
boardId
:
string
|
null
;
createdById
:
string
;
createdAt
:
string
;
}
export
interface
LabelData
{
export
interface
RecurringCardDefinition
{
id
:
string
;
name
:
string
;
color
:
string
;
boardId
:
string
;
title
:
string
;
titleTemplate
:
string
|
null
;
description
:
string
|
null
;
priority
:
string
;
recurrenceType
:
string
;
recurrenceConfig
:
any
;
isActive
:
boolean
;
lastCreatedAt
:
string
|
null
;
nextCreationDate
:
string
|
null
;
createdById
:
string
;
createdAt
:
string
;
}
export
interface
BoardMemberData
{
export
interface
CardDependency
{
id
:
string
;
userId
:
string
;
role
:
BoardMemberRole
;
user
:
{
card
:
{
id
:
string
;
firstName
:
string
;
lastNam
e
:
string
;
displayName
:
string
|
null
;
avatar
:
string
|
null
;
cardNumber
:
string
;
titl
e
:
string
;
completedAt
:
string
|
null
;
column
:
{
name
:
string
;
type
:
string
}
;
};
joinedAt
:
string
;
isResolved
:
boolean
;
createdAt
:
string
;
}
export
interface
CardDependencies
{
blockedBy
:
CardDependency
[];
blocks
:
CardDependency
[];
}
\ 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