Skip to content
GitLab
Projects
Groups
Topics
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Register
Sign in
Toggle navigation
Menu
Paulo Avila
petruz-tasks
Commits
7c57e406
Commit
7c57e406
authored
Jul 10, 2026
by
Paulo Avila
Browse files
criação de nova data de conclusao e formatação de data
parent
987e8471
Changes
8
Hide whitespace changes
Inline
Side-by-side
src/app/(app)/dashboard/page.tsx
View file @
7c57e406
...
...
@@ -14,7 +14,7 @@ import { Progress } from "@/components/ui/progress";
import
{
Button
}
from
"
@/components/ui/button
"
;
import
{
DueBadge
}
from
"
@/components/tasks/due-badge
"
;
import
{
STATUS_CHIPS
}
from
"
@/components/tasks/task-utils
"
;
import
{
formatDate
}
from
"
@/lib/dates
"
;
import
{
formatDate
Short
}
from
"
@/lib/dates
"
;
import
{
cn
}
from
"
@/lib/utils
"
;
export
const
metadata
=
{
title
:
"
Painel — Petruz Tasks
"
};
...
...
@@ -147,7 +147,7 @@ export default async function DashboardPage() {
</
p
>
<
p
className
=
"text-xs text-muted-foreground"
>
{
task
.
AssigneeName
??
"
Sem responsável
"
}
·
{
"
"
}
{
formatDate
(
task
.
DueDate
)
}
{
formatDate
Short
(
task
.
DueDate
)
}
</
p
>
</
div
>
<
DueBadge
...
...
src/components/tasks/task-board.tsx
View file @
7c57e406
import
{
CalendarClock
,
CalendarPlus
,
Pencil
}
from
"
lucide-react
"
;
import
{
CalendarCheck
,
CalendarClock
,
CalendarPlus
,
Pencil
}
from
"
lucide-react
"
;
import
{
TASK_STATUSES
}
from
"
@/lib/constants
"
;
import
type
{
Task
,
WorkspaceMember
}
from
"
@/lib/data
"
;
import
{
formatDate
}
from
"
@/lib/dates
"
;
import
{
formatDate
Short
}
from
"
@/lib/dates
"
;
import
{
canEditTask
}
from
"
@/lib/permissions
"
;
import
{
Button
}
from
"
@/components/ui/button
"
;
import
{
Card
,
CardContent
}
from
"
@/components/ui/card
"
;
...
...
@@ -106,16 +106,25 @@ export function TaskBoard({
title
=
"Início"
>
<
CalendarPlus
className
=
"h-3 w-3"
/>
{
formatDate
(
task
.
StartDate
)
}
{
formatDate
Short
(
task
.
StartDate
)
}
</
span
>
)
}
{
task
.
DueDate
&&
(
<
span
className
=
"flex items-center gap-1"
title
=
"
Venciment
o"
title
=
"
Previsão de términ
o"
>
<
CalendarClock
className
=
"h-3 w-3"
/>
{
formatDate
(
task
.
DueDate
)
}
{
formatDateShort
(
task
.
DueDate
)
}
</
span
>
)
}
{
task
.
CompletedDate
&&
(
<
span
className
=
"flex items-center gap-1 text-green-600 dark:text-green-400"
title
=
"Concluída em"
>
<
CalendarCheck
className
=
"h-3 w-3"
/>
{
formatDateShort
(
task
.
CompletedDate
)
}
</
span
>
)
}
</
div
>
...
...
src/components/tasks/task-dialog.tsx
View file @
7c57e406
...
...
@@ -40,6 +40,7 @@ export interface TaskFormValues {
assigneeId
:
number
|
null
;
startDate
:
string
|
null
;
// yyyy-MM-dd
dueDate
:
string
|
null
;
// yyyy-MM-dd
completedDate
:
string
|
null
;
// yyyy-MM-dd
progress
:
number
;
}
...
...
@@ -224,18 +225,40 @@ export function TaskDialog({
name
=
"startDate"
type
=
"date"
defaultValue
=
{
task
?.
startDate
??
""
}
disabled
=
{
Boolean
(
task
)
}
/>
</
div
>
<
div
className
=
"space-y-2"
>
<
Label
htmlFor
=
"task-due"
>
Venciment
o
</
Label
>
<
Label
htmlFor
=
"task-due"
>
Previsão de términ
o
</
Label
>
<
Input
id
=
"task-due"
name
=
"dueDate"
type
=
"date"
defaultValue
=
{
task
?.
dueDate
??
""
}
disabled
=
{
Boolean
(
task
)
}
/>
</
div
>
</
div
>
{
task
&&
(
<
p
className
=
"-mt-2 text-xs text-muted-foreground"
>
Início e previsão de término não podem ser alterados após a
criação.
</
p
>
)
}
{
task
?.
completedDate
&&
(
<
div
className
=
"space-y-2"
>
<
Label
htmlFor
=
"task-completed"
>
Concluída em
</
Label
>
<
Input
id
=
"task-completed"
type
=
"date"
defaultValue
=
{
task
.
completedDate
}
disabled
/>
<
p
className
=
"text-xs text-muted-foreground"
>
Preenchida automaticamente ao concluir a tarefa.
</
p
>
</
div
>
)
}
<
div
className
=
"space-y-2"
>
<
Label
htmlFor
=
"task-progress"
>
Progresso (%)
</
Label
>
<
Input
...
...
src/components/tasks/task-table.tsx
View file @
7c57e406
...
...
@@ -4,7 +4,7 @@ import { Fragment, useState } from "react";
import
{
AlertTriangle
,
ChevronRight
,
Pencil
,
Plus
}
from
"
lucide-react
"
;
import
type
{
Task
,
WorkspaceMember
}
from
"
@/lib/data
"
;
import
{
TASK_STATUSES
}
from
"
@/lib/constants
"
;
import
{
formatDate
,
getDueInfo
}
from
"
@/lib/dates
"
;
import
{
formatDate
Short
,
getDueInfo
}
from
"
@/lib/dates
"
;
import
{
canEditTask
}
from
"
@/lib/permissions
"
;
import
{
initials
,
toFormValues
}
from
"
./task-utils
"
;
import
{
Avatar
,
AvatarFallback
}
from
"
@/components/ui/avatar
"
;
...
...
@@ -27,7 +27,7 @@ import { TaskProgress } from "./task-progress";
import
{
TaskProgressEditor
}
from
"
./task-progress-editor
"
;
import
{
TaskStatusSelect
}
from
"
./task-status-select
"
;
const
COLUMN_COUNT
=
7
;
const
COLUMN_COUNT
=
8
;
export
function
TaskTable
({
tasks
,
...
...
@@ -147,7 +147,10 @@ export function TaskTable({
Início
</
TableHead
>
<
TableHead
className
=
"whitespace-nowrap text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
>
Vencimento
Previsão
</
TableHead
>
<
TableHead
className
=
"whitespace-nowrap text-[11px] font-semibold uppercase tracking-wider text-muted-foreground"
>
Conclusão
</
TableHead
>
<
TableHead
className
=
"w-[80px]"
/>
</
TableRow
>
...
...
@@ -252,18 +255,29 @@ export function TaskTable({
)
}
</
TableCell
>
<
TableCell
className
=
"py-3"
>
<
span
className
=
"text-sm text-muted-foreground"
>
{
formatDate
(
task
.
StartDate
)
}
<
span
className
=
"
whitespace-nowrap
text-sm text-muted-foreground"
>
{
formatDate
Short
(
task
.
StartDate
)
}
</
span
>
</
TableCell
>
<
TableCell
className
=
"py-3"
>
<
div
className
=
"flex items-center gap-2"
>
<
span
className
=
"text-sm text-muted-foreground"
>
{
formatDate
(
task
.
DueDate
)
}
<
span
className
=
"
whitespace-nowrap
text-sm text-muted-foreground"
>
{
formatDate
Short
(
task
.
DueDate
)
}
</
span
>
<
DueBadge
dueDate
=
{
task
.
DueDate
}
status
=
{
task
.
Status
}
/>
</
div
>
</
TableCell
>
<
TableCell
className
=
"py-3"
>
<
span
className
=
"whitespace-nowrap text-sm text-muted-foreground"
>
{
task
.
CompletedDate
?
(
<
span
className
=
"text-green-600 dark:text-green-400"
>
{
formatDateShort
(
task
.
CompletedDate
)
}
</
span
>
)
:
(
"
—
"
)
}
</
span
>
</
TableCell
>
<
TableCell
className
=
"py-3"
>
{
editable
&&
(
<
div
className
=
"flex items-center justify-end transition-opacity md:opacity-0 md:group-hover:opacity-100"
>
...
...
@@ -370,8 +384,10 @@ export function TaskTable({
</
span
>
)
}
<
div
className
=
"flex items-center gap-1.5"
>
<
span
className
=
"text-xs text-muted-foreground"
>
{
formatDate
(
sub
.
DueDate
)
}
<
span
className
=
"whitespace-nowrap text-xs text-muted-foreground"
>
{
sub
.
CompletedDate
?
`✓
${
formatDateShort
(
sub
.
CompletedDate
)}
`
:
formatDateShort
(
sub
.
DueDate
)
}
</
span
>
<
DueBadge
dueDate
=
{
sub
.
DueDate
}
...
...
src/components/tasks/task-utils.ts
View file @
7c57e406
...
...
@@ -22,6 +22,7 @@ export function toFormValues(task: Task): TaskFormValues {
assigneeId
:
task
.
AssigneeId
,
startDate
:
toISODate
(
task
.
StartDate
),
dueDate
:
toISODate
(
task
.
DueDate
),
completedDate
:
toISODate
(
task
.
CompletedDate
),
progress
:
task
.
Progress
,
};
}
...
...
src/lib/data.ts
View file @
7c57e406
...
...
@@ -41,6 +41,7 @@ export interface Task {
CreatedById
:
number
;
StartDate
:
Date
|
null
;
DueDate
:
Date
|
null
;
CompletedDate
:
Date
|
null
;
Progress
:
number
;
CreatedAt
:
Date
;
SubtaskCount
:
number
;
...
...
@@ -257,7 +258,7 @@ const TASK_SELECT = `
SELECT t.Id, t.WorkspaceId, t.Title, t.Description, t.Status, t.Priority,
t.Entry,
t.AssigneeId, a.Name AS AssigneeName, t.CreatedById,
t.StartDate, t.DueDate, t.Progress, t.CreatedAt, w.Name AS WorkspaceName,
t.StartDate, t.DueDate,
t.CompletedDate,
t.Progress, t.CreatedAt, w.Name AS WorkspaceName,
(SELECT COUNT(*) FROM dbo.Tasks s WHERE s.Entry = t.Id) AS SubtaskCount,
(SELECT COUNT(*) FROM dbo.Tasks s WHERE s.Entry = t.Id AND s.Status = 'done') AS SubtaskDone
FROM dbo.Tasks t
...
...
@@ -319,13 +320,17 @@ export async function createTask(
.
input
(
"
progress
"
,
sql
.
Int
,
input
.
progress
)
.
input
(
"
createdById
"
,
sql
.
Int
,
createdById
)
.
query
(
`INSERT INTO dbo.Tasks (WorkspaceId, Title, Description, Status, Priority, Entry, AssigneeId, StartDate, DueDate, Progress, CreatedById)
VALUES (@workspaceId, @title, @description, @status, @priority, @entry, @assigneeId, @startDate, @dueDate, @progress, @createdById)`
`INSERT INTO dbo.Tasks (WorkspaceId, Title, Description, Status, Priority, Entry, AssigneeId, StartDate, DueDate, CompletedDate, Progress, CreatedById)
VALUES (@workspaceId, @title, @description, @status, @priority, @entry, @assigneeId, @startDate, @dueDate,
CASE WHEN @status = 'done' THEN CAST(GETDATE() AS DATE) ELSE NULL END,
@progress, @createdById)`
);
}
export
async
function
updateTask
(
id
:
number
,
input
:
TaskInput
):
Promise
<
void
>
{
const
pool
=
await
getPool
();
// Início e vencimento são imutáveis após a criação: não entram no UPDATE.
// CompletedDate é gerenciada pelo status (preenche ao concluir, limpa ao reabrir).
await
pool
.
request
()
.
input
(
"
id
"
,
sql
.
Int
,
id
)
...
...
@@ -334,14 +339,14 @@ export async function updateTask(id: number, input: TaskInput): Promise<void> {
.
input
(
"
status
"
,
sql
.
NVarChar
(
20
),
input
.
status
)
.
input
(
"
priority
"
,
sql
.
NVarChar
(
10
),
input
.
priority
)
.
input
(
"
assigneeId
"
,
sql
.
Int
,
input
.
assigneeId
)
.
input
(
"
startDate
"
,
sql
.
Date
,
input
.
startDate
)
.
input
(
"
dueDate
"
,
sql
.
Date
,
input
.
dueDate
)
.
input
(
"
progress
"
,
sql
.
Int
,
input
.
progress
)
.
query
(
`UPDATE dbo.Tasks
SET Title = @title, Description = @description, Status = @status,
Priority = @priority, AssigneeId = @assigneeId,
StartDate = @startDate, DueDate = @dueDate, Progress = @progress,
Priority = @priority, AssigneeId = @assigneeId, Progress = @progress,
CompletedDate = CASE WHEN @status = 'done'
THEN COALESCE(CompletedDate, CAST(GETDATE() AS DATE))
ELSE NULL END,
UpdatedAt = SYSUTCDATETIME()
WHERE Id = @id`
);
...
...
@@ -357,7 +362,13 @@ export async function updateTaskStatus(
.
input
(
"
id
"
,
sql
.
Int
,
id
)
.
input
(
"
status
"
,
sql
.
NVarChar
(
20
),
status
)
.
query
(
`UPDATE dbo.Tasks SET Status = @status, UpdatedAt = SYSUTCDATETIME() WHERE Id = @id`
`UPDATE dbo.Tasks
SET Status = @status,
CompletedDate = CASE WHEN @status = 'done'
THEN COALESCE(CompletedDate, CAST(GETDATE() AS DATE))
ELSE NULL END,
UpdatedAt = SYSUTCDATETIME()
WHERE Id = @id`
);
}
...
...
src/lib/dates.ts
View file @
7c57e406
...
...
@@ -18,6 +18,14 @@ export function formatDate(date: Date | string | null): string {
return
`
${
day
}
/
${
month
}
/
${
year
}
`
;
}
/** Formato curto dd/MM para exibição resumida das datas das tarefas. */
export
function
formatDateShort
(
date
:
Date
|
string
|
null
):
string
{
const
iso
=
toISODate
(
date
);
if
(
!
iso
)
return
"
—
"
;
const
[,
month
,
day
]
=
iso
.
split
(
"
-
"
);
return
`
${
day
}
/
${
month
}
`
;
}
function
isoToLocalDate
(
iso
:
string
):
Date
{
const
[
year
,
month
,
day
]
=
iso
.
split
(
"
-
"
).
map
(
Number
);
return
new
Date
(
year
,
month
-
1
,
day
);
...
...
src/lib/db-init.ts
View file @
7c57e406
...
...
@@ -47,6 +47,7 @@ const DDL_STATEMENTS: string[] = [
CreatedById INT NOT NULL REFERENCES dbo.Users(Id),
StartDate DATE NULL,
DueDate DATE NULL,
CompletedDate DATE NULL,
Progress INT NOT NULL DEFAULT 0,
CreatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
UpdatedAt DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
...
...
@@ -56,6 +57,15 @@ const DDL_STATEMENTS: string[] = [
`IF COL_LENGTH('dbo.Tasks', 'Progress') IS NULL
ALTER TABLE dbo.Tasks ADD Progress INT NOT NULL DEFAULT 0`
,
// Data de conclusão (preenchida ao mudar o status para 'done')
`IF COL_LENGTH('dbo.Tasks', 'CompletedDate') IS NULL
ALTER TABLE dbo.Tasks ADD CompletedDate DATE NULL`
,
// Preenche a data de conclusão de tarefas já concluídas sem data
`UPDATE dbo.Tasks
SET CompletedDate = CAST(UpdatedAt AS DATE)
WHERE Status = 'done' AND CompletedDate IS NULL`
,
`IF OBJECT_ID('dbo.Subtasks', 'U') IS NULL
CREATE TABLE dbo.Subtasks (
Id INT IDENTITY(1,1) PRIMARY KEY,
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new 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