Skip to content

Commit bc55bfb

Browse files
Merge branch 'dev' into dashboard-current-project/fix-bugs
2 parents 2ffb62f + 41fcd2e commit bc55bfb

10 files changed

Lines changed: 223 additions & 19 deletions

File tree

‎apps/codebility/app/home/kanban/[projectId]/[id]/_components/KanbanBoard.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { memo, useCallback, useEffect, useMemo, useState } from "react";
3+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import Link from "next/link";
55
import KanbanBoardsSearch from "@/app/home/kanban/_components/KanbanBoardsSearch";
66
import { getMembers } from "@/app/home/projects/actions";
@@ -18,6 +18,7 @@ import KanbanColumnAddButton from "./kanban_modals/KanbanColumnAddButton";
1818
import KanbanColumnAddModal from "./kanban_modals/KanbanColumnAddModal";
1919
import KanbanBoardColumnContainer from "./KanbanBoardColumnContainer";
2020
import UserTaskFilter from "./UserTaskFilter";
21+
import { useKanbanTaskUrlModal } from "@/hooks/useKanbanTaskUrlModal";
2122

2223
interface KanbanBoardProps {
2324
boardData: KanbanBoardType & { kanban_columns: KanbanColumnType[] };
@@ -32,6 +33,9 @@ interface KanbanBoardProps {
3233
function KanbanBoard({ boardData, projectId }: KanbanBoardProps) {
3334
const user = useUserStore((state) => state.user);
3435

36+
// Sync the Kanban task modal with the URL ?taskId param
37+
useKanbanTaskUrlModal(boardData);
38+
3539
// User permissions - simple boolean checks following YAGNI principle
3640
const canAddColumn = user?.role_id === 1 || user?.role_id === 5;
3741
const canAddMember = canAddColumn;

‎apps/codebility/app/home/kanban/[projectId]/[id]/_components/KanbanColumn.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export default function KanbanColumn({
168168
toast.error(response.error || "Failed to update column name");
169169
}
170170
};
171-
171+
172172
return (
173173
<li
174174
ref={setColumnRef}

‎apps/codebility/app/home/kanban/[projectId]/[id]/_components/kanban_modals/KanbanTaskViewEditModal.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { ReactNode, memo, useCallback } from "react";
44
import { useModal } from "@/hooks/use-modal";
55
import { ExtendedTask } from "@/types/home/codev";
6+
import { usePathname, useRouter } from "next/navigation";
67

78
interface Props {
89
children: ReactNode;
@@ -15,9 +16,12 @@ function KanbanTaskViewEditModal({
1516
task,
1617
onComplete,
1718
}: Props) {
19+
const router = useRouter();
20+
const pathname = usePathname();
1821
const { onOpen } = useModal();
1922

2023
const handleClick = useCallback(() => {
24+
router.push(`${pathname}?taskId=${task.id}`, { scroll: false });
2125
onOpen("taskViewModal", task, onComplete);
2226
}, [onOpen, task, onComplete]);
2327

‎apps/codebility/app/home/kanban/[projectId]/[id]/_components/tasks/TaskViewModal.tsx‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,12 @@ const TaskViewModal = ({
691691
<div className="flex items-center justify-between">
692692
<DialogHeader>
693693
<DialogTitle className="text-xl font-bold text-gray-900 dark:text-white">
694-
{task?.title}
694+
{task?.title}{" "}
695+
{task?.ticket_code && (
696+
<span className="ml-1 text-gray-500 dark:text-gray-400 font-normal">
697+
#{task.ticket_code}
698+
</span>
699+
)}
695700
</DialogTitle>
696701
</DialogHeader>
697702
{canModifyTask && (
@@ -968,6 +973,36 @@ const TaskViewModal = ({
968973

969974
<DialogFooter className="mt-4 flex justify-end">
970975
{/*Previous style: flex gap-6 sm:justify-end */}
976+
{/* Copy URL button */}
977+
<Button
978+
onClick={() => {
979+
if (!task?.id) {
980+
toast.error("Cannot copy URL: Task ID is missing");
981+
return;
982+
}
983+
984+
const baseUrl = window.location.origin; // gets the website domain
985+
const url = task.ticket_code
986+
? `${baseUrl}/home/kanban/ticket/${task.ticket_code}`
987+
: new URL(window.location.href).toString();
988+
989+
navigator.clipboard.writeText(url);
990+
toast.success("Task URL copied to clipboard");
991+
}}
992+
style={{
993+
backgroundColor: "#2563EB",
994+
color: "white",
995+
padding: "6px 16px",
996+
fontSize: "14px",
997+
borderRadius: "4px",
998+
border: "none",
999+
minWidth: "auto",
1000+
width: "auto",
1001+
}}
1002+
>
1003+
Copy URL
1004+
</Button>
1005+
9711006
{/* VIEWING MODE: Show Close when just viewing (no changes) */}
9721007
{!hasUnsavedChanges && (
9731008
<Button

‎apps/codebility/app/home/kanban/[projectId]/[id]/_services/query.ts‎

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import { createClientServerComponent } from "@/utils/supabase/server";
55
export const getBoardData = async (boardId: String) => {
66
try {
77
const supabase = await createClientServerComponent();
8-
9-
// Fetch board with columns
8+
9+
// Fetch the board and its columns
1010
const { data: boardData, error } = await supabase
1111
.from("kanban_boards")
12-
.select(
13-
`
12+
.select(`
1413
id,
1514
name,
1615
description,
@@ -24,17 +23,16 @@ export const getBoardData = async (boardId: String) => {
2423
created_at,
2524
updated_at
2625
)
27-
`,
28-
)
26+
`)
2927
.eq("id", boardId)
3028
.single();
3129

3230
if (error) throw error;
3331

34-
// BUG FIX: Fetch only NON-ARCHIVED tasks (handles NULL values)
35-
if (boardData?.kanban_columns && boardData.kanban_columns.length > 0) {
32+
if (boardData?.kanban_columns) {
3633
const columnIds = boardData.kanban_columns.map((col: any) => col.id);
37-
34+
35+
// Fetch tasks
3836
const { data: tasks, error: tasksError } = await supabase
3937
.from("tasks")
4038
.select(`
@@ -75,15 +73,25 @@ export const getBoardData = async (boardId: String) => {
7573

7674
if (tasksError) throw tasksError;
7775

78-
// BUG FIX: Double-check filtering on client side for extra safety
79-
const activeTasks = tasks?.filter((task: any) =>
80-
task.is_archive !== true
81-
) || [];
76+
// Fetch ticket codes separately
77+
const taskIds = tasks?.map((task: any) => task.id) || [];
78+
const { data: ticketCodes, error: ticketError } = await supabase
79+
.from("task_ticket_codes")
80+
.select(`task_id, ticket_code`)
81+
.in("task_id", taskIds);
82+
83+
if (ticketError) throw ticketError;
84+
85+
// Merge ticket_code into tasks
86+
const tasksWithTicket = tasks?.map((task: any) => ({
87+
...task,
88+
ticket_code: ticketCodes?.find((t: any) => t.task_id === task.id)?.ticket_code || null,
89+
}));
8290

8391
// Attach tasks to their respective columns
8492
boardData.kanban_columns = boardData.kanban_columns.map((column: any) => ({
8593
...column,
86-
tasks: activeTasks.filter((task: any) => task.kanban_column_id === column.id)
94+
tasks: tasksWithTicket?.filter((task: any) => task.kanban_column_id === column.id) || [],
8795
}));
8896
} else {
8997
// If no columns exist, initialize empty tasks array
@@ -92,7 +100,7 @@ export const getBoardData = async (boardId: String) => {
92100
tasks: []
93101
})) || [];
94102
}
95-
103+
96104
return boardData;
97105
} catch (error) {
98106
console.error("Error fetching board data:", error);
@@ -135,4 +143,4 @@ export const getSprintsData = async (projectId: String) => {
135143
console.error("Error fetching sprints data:", error);
136144
throw error;
137145
}
138-
};
146+
};

‎apps/codebility/app/home/kanban/[projectId]/[id]/page.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const mapTask = (task: any): Task => ({
3535
skill_category_id: task.skill_category_id,
3636
codev: task.codev,
3737
skill_category: task.skill_category,
38+
ticket_code: task.ticket_code || null,
3839
});
3940

4041
const mapColumn = (column: any): KanbanColumnType => ({
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"use server";
2+
3+
import { createClientServerComponent } from "@/utils/supabase/server";
4+
5+
interface GetTaskDetailsResult {
6+
success: boolean;
7+
error?: string;
8+
data?: {
9+
project_id: string;
10+
board_id: string;
11+
task_id: string;
12+
};
13+
}
14+
15+
export const getTaskDetailsByTicketCode = async (
16+
ticket_code: string
17+
): Promise<GetTaskDetailsResult> => {
18+
const supabase = await createClientServerComponent();
19+
20+
if (!ticket_code) {
21+
return { success: false, error: "Ticket code is required" };
22+
}
23+
24+
// 1️⃣ Get task_id from task_ticket_codes
25+
const { data: taskTicket, error: taskTicketError } = await supabase
26+
.from("task_ticket_codes")
27+
.select("task_id")
28+
.eq("ticket_code", ticket_code)
29+
.single();
30+
31+
if (taskTicketError || !taskTicket) {
32+
return { success: false, error: taskTicketError?.message || "Ticket not found" };
33+
}
34+
35+
const task_id = taskTicket.task_id;
36+
37+
// 2️⃣ Get kanban_column_id from tasks
38+
const { data: task, error: taskError } = await supabase
39+
.from("tasks")
40+
.select("kanban_column_id")
41+
.eq("id", task_id)
42+
.single();
43+
44+
if (taskError || !task) {
45+
return { success: false, error: taskError?.message || "Task not found" };
46+
}
47+
48+
const kanban_column_id = task.kanban_column_id;
49+
50+
// 3️⃣ Get board_id and project_id from kanban_columns
51+
const { data: column, error: columnError } = await supabase
52+
.from("kanban_columns")
53+
.select("board_id")
54+
.eq("id", kanban_column_id)
55+
.single();
56+
57+
if (columnError || !column) {
58+
return { success: false, error: columnError?.message || "Column not found" };
59+
}
60+
61+
const board_id = column.board_id;
62+
63+
// 4️⃣ Get project_id from kanban_boards
64+
const { data: board, error: boardError } = await supabase
65+
.from("kanban_boards")
66+
.select("project_id")
67+
.eq("id", board_id)
68+
.single();
69+
70+
if (boardError || !board) {
71+
return { success: false, error: boardError?.message || "Board not found" };
72+
}
73+
74+
const project_id = board.project_id;
75+
76+
return {
77+
success: true,
78+
data: { task_id, board_id, project_id },
79+
};
80+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { redirect } from 'next/navigation';
2+
import { getTaskDetailsByTicketCode } from './actions';
3+
4+
interface PageProps {
5+
params: {
6+
ticketCode: string;
7+
};
8+
}
9+
10+
export default async function TicketPage({ params }: PageProps) {
11+
const { ticketCode } = params;
12+
13+
const ticketInfo = await getTaskDetailsByTicketCode(ticketCode);
14+
15+
// Determine where to redirect
16+
const destination = `/home/kanban/${ticketInfo.data?.project_id}/${ticketInfo.data?.board_id}?taskId=${ticketInfo.data?.task_id}`;
17+
18+
redirect(destination);
19+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { useEffect, useMemo, useRef } from "react";
4+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
5+
import { useModal } from "@/hooks/use-modal";
6+
import { KanbanBoardType, KanbanColumnType } from "@/types/home/codev";
7+
8+
/**
9+
* Syncs the Kanban task modal with the URL ?taskId param.
10+
*/
11+
export function useKanbanTaskUrlModal(
12+
boardData: KanbanBoardType & { kanban_columns: KanbanColumnType[] }
13+
) {
14+
const { isOpen, type, onOpen } = useModal();
15+
const router = useRouter();
16+
const pathname = usePathname();
17+
const searchParams = useSearchParams();
18+
19+
// Flatten all tasks once
20+
const tasks = useMemo(
21+
() => boardData.kanban_columns.flatMap((col) => col.tasks ?? []),
22+
[boardData.kanban_columns]
23+
);
24+
25+
// Track whether we've already opened the modal from the URL
26+
const openedFromUrlRef = useRef(false);
27+
28+
useEffect(() => {
29+
const taskId = searchParams.get("taskId");
30+
31+
// Open modal only once from URL
32+
if (taskId && !openedFromUrlRef.current) {
33+
const task = tasks.find((t) => t.id === taskId);
34+
if (task) {
35+
onOpen("taskViewModal", task);
36+
openedFromUrlRef.current = true;
37+
}
38+
}
39+
40+
// Clear URL if modal closed
41+
if (!isOpen && searchParams.has("taskId")) {
42+
router.replace(pathname, { scroll: false });
43+
openedFromUrlRef.current = false; // reset so next URL param works
44+
}
45+
46+
// If modal type changes away from taskViewModal, remove param
47+
if (isOpen && type !== "taskViewModal" && searchParams.has("taskId")) {
48+
router.replace(pathname, { scroll: false });
49+
openedFromUrlRef.current = false;
50+
}
51+
}, [isOpen, type, tasks, onOpen, pathname, searchParams, router]);
52+
}

‎apps/codebility/types/home/codev.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ export interface Task {
350350
id: string;
351351
name: string;
352352
};
353+
ticket_code?: string;
353354
}
354355

355356
export interface ExtendedTask extends Task {

0 commit comments

Comments
 (0)