feat: Add project and user cards, implement settings and list view components, and update dependencies
This commit is contained in:
26
package-lock.json
generated
Normal file
26
package-lock.json
generated
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "tasker",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
|
||||||
|
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
package.json
Normal file
8
package.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "^4.17.21"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.17.13"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
tasker-client/src/app/components/Modal/index.tsx
Normal file
38
tasker-client/src/app/components/Modal/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import Header from "../Header";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Modal = ({ children, isOpen, onClose, name }: Props) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<div className="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-gray-600 bg-opacity-50 p-4">
|
||||||
|
<div className="w-full max-w-2xl rounded-lg bg-white p-4 shadow-lg dark:bg-dark-secondary">
|
||||||
|
<Header
|
||||||
|
name={name}
|
||||||
|
buttonComponent={
|
||||||
|
<button
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-full bg-blue-primary text-white hover:bg-blue-600"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
isSmallText
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
169
tasker-client/src/app/components/ModalNewTask/index.tsx
Normal file
169
tasker-client/src/app/components/ModalNewTask/index.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import Modal from "@/app/components/Modal";
|
||||||
|
import { Priority, Status, useCreateTaskMutation } from "@/state/api";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { formatISO } from "date-fns";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
id?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalNewTask = ({ isOpen, onClose, id = null }: Props) => {
|
||||||
|
const [createTask, { isLoading }] = useCreateTaskMutation();
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [status, setStatus] = useState<Status>(Status.Backlog);
|
||||||
|
const [priority, setPriority] = useState<Priority>(Priority.Backlog);
|
||||||
|
const [tags, setTags] = useState("");
|
||||||
|
const [startDate, setStartDate] = useState("");
|
||||||
|
const [dueDate, setDueDate] = useState("");
|
||||||
|
const [authorUserId, setAuthorUserId] = useState("");
|
||||||
|
const [assignedUserId, setAssignedUserId] = useState("");
|
||||||
|
const [projectId, setProjectId] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!title || !authorUserId || !(id !== null || projectId)) return;
|
||||||
|
|
||||||
|
const formattedStartDate = formatISO(new Date(startDate), {
|
||||||
|
representation: "complete",
|
||||||
|
});
|
||||||
|
const formattedDueDate = formatISO(new Date(dueDate), {
|
||||||
|
representation: "complete",
|
||||||
|
});
|
||||||
|
|
||||||
|
await createTask({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
status,
|
||||||
|
priority,
|
||||||
|
tags,
|
||||||
|
startDate: formattedStartDate,
|
||||||
|
dueDate: formattedDueDate,
|
||||||
|
authorUserId: parseInt(authorUserId),
|
||||||
|
assignedUserId: parseInt(assignedUserId),
|
||||||
|
projectId: id !== null ? Number(id) : Number(projectId),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFormValid = () => {
|
||||||
|
return title && authorUserId && !(id !== null || projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectStyles =
|
||||||
|
"mb-4 block w-full rounded border border-gray-300 px-3 py-2 dark:border-dark-tertiary dark:bg-dark-tertiary dark:text-white dark:focus:outline-none";
|
||||||
|
|
||||||
|
const inputStyles =
|
||||||
|
"w-full rounded border border-gray-300 p-2 shadow-sm dark:border-dark-tertiary dark:bg-dark-tertiary dark:text-white dark:focus:outline-none";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} name="Create New Task">
|
||||||
|
<form
|
||||||
|
className="mt-4 space-y-6"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={inputStyles}
|
||||||
|
placeholder="Title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className={inputStyles}
|
||||||
|
placeholder="Description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-2">
|
||||||
|
<select
|
||||||
|
className={selectStyles}
|
||||||
|
value={status}
|
||||||
|
onChange={(e) =>
|
||||||
|
setStatus(Status[e.target.value as keyof typeof Status])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Select Status</option>
|
||||||
|
<option value={Status.Backlog}>Backlog</option>
|
||||||
|
<option value={Status.InProgress}>In Progress</option>
|
||||||
|
<option value={Status.TestReview}>Test/Review</option>
|
||||||
|
<option value={Status.Done}>Done</option>
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className={selectStyles}
|
||||||
|
value={priority}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPriority(Priority[e.target.value as keyof typeof Priority])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Select Priority</option>
|
||||||
|
<option value={Priority.Urgent}>Urgent</option>
|
||||||
|
<option value={Priority.High}>High</option>
|
||||||
|
<option value={Priority.Medium}>Medium</option>
|
||||||
|
<option value={Priority.Low}>Low</option>
|
||||||
|
<option value={Priority.Backlog}>Backlog</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={inputStyles}
|
||||||
|
placeholder="Tags (comma separated)"
|
||||||
|
value={tags}
|
||||||
|
onChange={(e) => setTags(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-2">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={inputStyles}
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={inputStyles}
|
||||||
|
value={dueDate}
|
||||||
|
onChange={(e) => setDueDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={inputStyles}
|
||||||
|
placeholder="Author User ID"
|
||||||
|
value={authorUserId}
|
||||||
|
onChange={(e) => setAuthorUserId(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={inputStyles}
|
||||||
|
placeholder="Assigned User ID"
|
||||||
|
value={assignedUserId}
|
||||||
|
onChange={(e) => setAssignedUserId(e.target.value)}
|
||||||
|
/>
|
||||||
|
{id === null && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={inputStyles}
|
||||||
|
placeholder="Project ID"
|
||||||
|
value={projectId}
|
||||||
|
onChange={(e) => setProjectId(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`focus-offset-2 mt-4 flex w-full justify-center rounded-md border border-transparent bg-blue-primary px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 ${
|
||||||
|
!isFormValid() || isLoading ? "cursor-not-allowed opacity-50" : ""
|
||||||
|
}`}
|
||||||
|
disabled={!isFormValid() || isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Creating..." : "Create Task"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalNewTask;
|
||||||
19
tasker-client/src/app/components/ProjectCard/index.tsx
Normal file
19
tasker-client/src/app/components/ProjectCard/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Project } from "@/state/api";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
project: Project;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectCard = ({ project }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="rounded border p-4 shadow">
|
||||||
|
<h3>{project.name}</h3>
|
||||||
|
<p>{project.description}</p>
|
||||||
|
<p>Start Date: {project.startDate}</p>
|
||||||
|
<p>End Date: {project.endDate}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectCard;
|
||||||
68
tasker-client/src/app/components/TaskCard/index.tsx
Normal file
68
tasker-client/src/app/components/TaskCard/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Task } from "@/state/api";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import Image from "next/image";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
task: Task;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskCard = ({ task }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="mb-3 rounded bg-white p-4 shadow dark:bg-dark-secondary dark:text-white">
|
||||||
|
{task.attachments && task.attachments.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<strong>Attachments:</strong>
|
||||||
|
<div className="flex flex-wrap">
|
||||||
|
{task.attachments && task.attachments.length > 0 && (
|
||||||
|
<Image
|
||||||
|
src={`${task.attachments[0].fileURL}`}
|
||||||
|
alt={task.attachments[0].fileName}
|
||||||
|
width={400}
|
||||||
|
height={200}
|
||||||
|
className="rounded-md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
<strong>ID:</strong> {task.id}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Title:</strong> {task.title}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Description:</strong>{" "}
|
||||||
|
{task.description || "No description provided"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Status:</strong> {task.status}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Priority:</strong> {task.priority}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Tags:</strong> {task.tags || "No tags"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Start Date:</strong>{" "}
|
||||||
|
{task.startDate ? format(new Date(task.startDate), "P") : "Not set"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Due Date:</strong>{" "}
|
||||||
|
{task.dueDate ? format(new Date(task.dueDate), "P") : "Not set"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Author:</strong>{" "}
|
||||||
|
{task.author ? task.author.username : "Unknown"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Assignee:</strong>{" "}
|
||||||
|
{task.assignee ? task.assignee.username : "Unassigned"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskCard;
|
||||||
29
tasker-client/src/app/components/UserCard/index.tsx
Normal file
29
tasker-client/src/app/components/UserCard/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { User } from "@/state/api";
|
||||||
|
import Image from "next/image";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCard = ({ user }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center rounded border p-4 shadow">
|
||||||
|
{user.profilePictureUrl && (
|
||||||
|
<Image
|
||||||
|
src={`${user.profilePictureUrl}`}
|
||||||
|
alt="profile picture"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
className="rounded-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3>{user.username}</h3>
|
||||||
|
<p>{user.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserCard;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import Navbar from "@/app/(components)/Navbar";
|
import Navbar from "@/app/components/Navbar";
|
||||||
import Sidebar from "@/app/(components)/Sidebar";
|
import Sidebar from "@/app/components/Sidebar";
|
||||||
import StoreProvider, { useAppSelector } from "./redux";
|
import StoreProvider, { useAppSelector } from "./redux";
|
||||||
|
|
||||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
|||||||
162
tasker-client/src/app/home/page.tsx
Normal file
162
tasker-client/src/app/home/page.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Priority,
|
||||||
|
Project,
|
||||||
|
Task,
|
||||||
|
useGetProjectsQuery,
|
||||||
|
useGetTasksQuery,
|
||||||
|
} from "@/state/api";
|
||||||
|
import React from "react";
|
||||||
|
import { useAppSelector } from "../redux";
|
||||||
|
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||||
|
import Header from "@/app/components/Header";
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
Cell,
|
||||||
|
Legend,
|
||||||
|
Pie,
|
||||||
|
PieChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Tooltip,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from "recharts";
|
||||||
|
import { dataGridSxStyles } from "@/lib/utils";
|
||||||
|
|
||||||
|
const taskColumns: GridColDef[] = [
|
||||||
|
{ field: "title", headerName: "Title", width: 200 },
|
||||||
|
{ field: "status", headerName: "Status", width: 150 },
|
||||||
|
{ field: "priority", headerName: "Priority", width: 150 },
|
||||||
|
{ field: "dueDate", headerName: "Due Date", width: 150 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusColors = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];
|
||||||
|
|
||||||
|
const HomePage = () => {
|
||||||
|
const {
|
||||||
|
data: tasks,
|
||||||
|
isLoading: tasksLoading,
|
||||||
|
isError: tasksError,
|
||||||
|
} = useGetTasksQuery({ projectId: parseInt("1") });
|
||||||
|
const { data: projects, isLoading: isProjectsLoading } =
|
||||||
|
useGetProjectsQuery();
|
||||||
|
|
||||||
|
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
|
||||||
|
|
||||||
|
if (tasksLoading || isProjectsLoading) return <div>Loading..</div>;
|
||||||
|
if (tasksError || !tasks || !projects) return <div>Error fetching data</div>;
|
||||||
|
|
||||||
|
const priorityCount = tasks.reduce(
|
||||||
|
(acc: Record<string, number>, task: Task) => {
|
||||||
|
const { priority } = task;
|
||||||
|
acc[priority as Priority] = (acc[priority as Priority] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const taskDistribution = Object.keys(priorityCount).map((key) => ({
|
||||||
|
name: key,
|
||||||
|
count: priorityCount[key],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const statusCount = projects.reduce(
|
||||||
|
(acc: Record<string, number>, project: Project) => {
|
||||||
|
const status = project.endDate ? "Completed" : "Active";
|
||||||
|
acc[status] = (acc[status] || 0) + 1;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectStatus = Object.keys(statusCount).map((key) => ({
|
||||||
|
name: key,
|
||||||
|
count: statusCount[key],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const chartColors = isDarkMode
|
||||||
|
? {
|
||||||
|
bar: "#8884d8",
|
||||||
|
barGrid: "#303030",
|
||||||
|
pieFill: "#4A90E2",
|
||||||
|
text: "#FFFFFF",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
bar: "#8884d8",
|
||||||
|
barGrid: "#E0E0E0",
|
||||||
|
pieFill: "#82ca9d",
|
||||||
|
text: "#000000",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container h-full w-[100%] bg-gray-100 bg-transparent p-8">
|
||||||
|
<Header name="Project Management Dashboard" />
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div className="rounded-lg bg-white p-4 shadow dark:bg-dark-secondary">
|
||||||
|
<h3 className="mb-4 text-lg font-semibold dark:text-white">
|
||||||
|
Task Priority Distribution
|
||||||
|
</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={taskDistribution}>
|
||||||
|
<CartesianGrid
|
||||||
|
strokeDasharray="3 3"
|
||||||
|
stroke={chartColors.barGrid}
|
||||||
|
/>
|
||||||
|
<XAxis dataKey="name" stroke={chartColors.text} />
|
||||||
|
<YAxis stroke={chartColors.text} />
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
width: "min-content",
|
||||||
|
height: "min-content",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
<Bar dataKey="count" fill={chartColors.bar} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg bg-white p-4 shadow dark:bg-dark-secondary">
|
||||||
|
<h3 className="mb-4 text-lg font-semibold dark:text-white">
|
||||||
|
Project Status
|
||||||
|
</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie dataKey="count" data={projectStatus} fill="#82ca9d" label>
|
||||||
|
{projectStatus.map((entry, index) => (
|
||||||
|
<Cell
|
||||||
|
key={`cell-${index}`}
|
||||||
|
fill={statusColors[index % statusColors.length]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg bg-white p-4 shadow dark:bg-dark-secondary md:col-span-2">
|
||||||
|
<h3 className="mb-4 text-lg font-semibold dark:text-white">
|
||||||
|
Your Tasks
|
||||||
|
</h3>
|
||||||
|
<div style={{ height: 400, width: "100%" }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={tasks}
|
||||||
|
columns={taskColumns}
|
||||||
|
checkboxSelection
|
||||||
|
loading={tasksLoading}
|
||||||
|
getRowClassName={() => "data-grid-row"}
|
||||||
|
getCellClassName={() => "data-grid-cell"}
|
||||||
|
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
|
||||||
|
sx={dataGridSxStyles(isDarkMode)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
|
import HomePage from "./home/page";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return <HomePage />;
|
||||||
<main className="flex flex-col items-center justify-center h-screen">
|
|
||||||
New
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
44
tasker-client/src/app/projects/ListView/index.tsx
Normal file
44
tasker-client/src/app/projects/ListView/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import Header from "@/app/components/Header";
|
||||||
|
import TaskCard from "@/app/components/TaskCard";
|
||||||
|
import { Task, useGetTasksQuery } from "@/state/api";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
setIsModalNewTaskOpen: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListView = ({ id, setIsModalNewTaskOpen }: Props) => {
|
||||||
|
const {
|
||||||
|
data: tasks,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
} = useGetTasksQuery({ projectId: Number(id) });
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (error) return <div>An error occurred while fetching tasks</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 pb-8 xl:px-6">
|
||||||
|
<div className="pt-5">
|
||||||
|
<Header
|
||||||
|
name="Task List"
|
||||||
|
buttonComponent={
|
||||||
|
<button
|
||||||
|
className="flex items-center rounded bg-blue-primary px-3 py-2 text-white hover:bg-blue-600"
|
||||||
|
onClick={() => setIsModalNewTaskOpen(true)}
|
||||||
|
>
|
||||||
|
Add Task
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
isSmallText
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 lg:gap-6">
|
||||||
|
{tasks?.map((task: Task) => <TaskCard key={task.id} task={task} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListView;
|
||||||
93
tasker-client/src/app/projects/ModalNewProject/index.tsx
Normal file
93
tasker-client/src/app/projects/ModalNewProject/index.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import Modal from "@/app/components/Modal";
|
||||||
|
import { useCreateProjectMutation } from "@/state/api";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { formatISO } from "date-fns";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalNewProject = ({ isOpen, onClose }: Props) => {
|
||||||
|
const [createProject, { isLoading }] = useCreateProjectMutation();
|
||||||
|
const [projectName, setProjectName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [startDate, setStartDate] = useState("");
|
||||||
|
const [endDate, setEndDate] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!projectName || !startDate || !endDate) return;
|
||||||
|
|
||||||
|
const formattedStartDate = formatISO(new Date(startDate), {
|
||||||
|
representation: "complete",
|
||||||
|
});
|
||||||
|
const formattedEndDate = formatISO(new Date(endDate), {
|
||||||
|
representation: "complete",
|
||||||
|
});
|
||||||
|
|
||||||
|
await createProject({
|
||||||
|
name: projectName,
|
||||||
|
description,
|
||||||
|
startDate: formattedStartDate,
|
||||||
|
endDate: formattedEndDate,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFormValid = () => {
|
||||||
|
return projectName && description && startDate && endDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputStyles =
|
||||||
|
"w-full rounded border border-gray-300 p-2 shadow-sm dark:border-dark-tertiary dark:bg-dark-tertiary dark:text-white dark:focus:outline-none";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} name="Create New Project">
|
||||||
|
<form
|
||||||
|
className="mt-4 space-y-6"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={inputStyles}
|
||||||
|
placeholder="Project Name"
|
||||||
|
value={projectName}
|
||||||
|
onChange={(e) => setProjectName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className={inputStyles}
|
||||||
|
placeholder="Description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-2">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={inputStyles}
|
||||||
|
value={startDate}
|
||||||
|
onChange={(e) => setStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={inputStyles}
|
||||||
|
value={endDate}
|
||||||
|
onChange={(e) => setEndDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`focus-offset-2 mt-4 flex w-full justify-center rounded-md border border-transparent bg-blue-primary px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 ${
|
||||||
|
!isFormValid() || isLoading ? "cursor-not-allowed opacity-50" : ""
|
||||||
|
}`}
|
||||||
|
disabled={!isFormValid() || isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Creating..." : "Create Project"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalNewProject;
|
||||||
@@ -8,7 +8,8 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Header from "../(components)/Header";
|
import Header from "@/app/components/Header";
|
||||||
|
import ModalNewProject from "./ModalNewProject";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
@@ -20,6 +21,10 @@ const ProjectHeader = ({ activeTab, setActiveTab }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 xl:px-6">
|
<div className="px-4 xl:px-6">
|
||||||
|
<ModalNewProject
|
||||||
|
isOpen={isModalNewProjectOpen}
|
||||||
|
onClose={() => setIsModalNewProjectOpen(false)}
|
||||||
|
/>
|
||||||
<div className="pb-6 pt-6 lg:pb-4 lg:pt-8">
|
<div className="pb-6 pt-6 lg:pb-4 lg:pt-8">
|
||||||
<Header
|
<Header
|
||||||
name="Product Design Development"
|
name="Product Design Development"
|
||||||
|
|||||||
116
tasker-client/src/app/projects/TableView/index.tsx
Normal file
116
tasker-client/src/app/projects/TableView/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { useAppSelector } from "@/app/redux";
|
||||||
|
import Header from "@/app/components/Header";
|
||||||
|
import { dataGridSxStyles } from "@/lib/utils";
|
||||||
|
import { useGetTasksQuery } from "@/state/api";
|
||||||
|
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
setIsModalNewTaskOpen: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusClasses: any = {
|
||||||
|
Backlog: "bg-red-400 text-red-800",
|
||||||
|
"In Progress": "bg-yellow-400 text-yellow-800",
|
||||||
|
"Test/Review": "bg-blue-400 text-blue-800",
|
||||||
|
Done: "bg-green-400 text-green-800",
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{
|
||||||
|
field: "title",
|
||||||
|
headerName: "Title",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "description",
|
||||||
|
headerName: "Description",
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "status",
|
||||||
|
headerName: "Status",
|
||||||
|
width: 130,
|
||||||
|
// Render a colored badge in className based on the status
|
||||||
|
renderCell: (params) => (
|
||||||
|
<span
|
||||||
|
className={`inline-flex rounded-full px-2 text-xs font-semibold leading-5 ${statusClasses[params.value] || "bg-gray-400 text-gray-800"}`}
|
||||||
|
>
|
||||||
|
{params.value}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "priority",
|
||||||
|
headerName: "Priority",
|
||||||
|
width: 75,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "tags",
|
||||||
|
headerName: "Tags",
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "startDate",
|
||||||
|
headerName: "Start Date",
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "dueDate",
|
||||||
|
headerName: "Due Date",
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "author",
|
||||||
|
headerName: "Author",
|
||||||
|
width: 150,
|
||||||
|
renderCell: (params) => params.value?.author || "Unknown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "assignee",
|
||||||
|
headerName: "Assignee",
|
||||||
|
width: 150,
|
||||||
|
renderCell: (params) => params.value?.assignee || "Unassigned",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const TableView = ({ id, setIsModalNewTaskOpen }: Props) => {
|
||||||
|
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
|
||||||
|
const {
|
||||||
|
data: tasks,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
} = useGetTasksQuery({ projectId: Number(id) });
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (error || !tasks) return <div>An error occurred while fetching tasks</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[540px] w-full px-4 pb-8 xl:px-6">
|
||||||
|
<div className="pt-5">
|
||||||
|
<Header
|
||||||
|
name="Table"
|
||||||
|
buttonComponent={
|
||||||
|
<button
|
||||||
|
className="flex items-center rounded bg-blue-primary px-3 py-2 text-white hover:bg-blue-600"
|
||||||
|
onClick={() => setIsModalNewTaskOpen(true)}
|
||||||
|
>
|
||||||
|
Add Task
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
isSmallText
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DataGrid
|
||||||
|
rows={tasks || []}
|
||||||
|
columns={columns}
|
||||||
|
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
|
||||||
|
sx={dataGridSxStyles(isDarkMode)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableView;
|
||||||
96
tasker-client/src/app/projects/TimelineView/index.tsx
Normal file
96
tasker-client/src/app/projects/TimelineView/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { useAppSelector } from "@/app/redux";
|
||||||
|
import { useGetTasksQuery } from "@/state/api";
|
||||||
|
import { DisplayOption, Gantt, ViewMode } from "gantt-task-react";
|
||||||
|
import "gantt-task-react/dist/index.css";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
setIsModalNewTaskOpen: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TaskTypeItems = "task" | "milestone" | "project";
|
||||||
|
|
||||||
|
const Timeline = ({ id, setIsModalNewTaskOpen }: Props) => {
|
||||||
|
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
|
||||||
|
const {
|
||||||
|
data: tasks,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
} = useGetTasksQuery({ projectId: Number(id) });
|
||||||
|
|
||||||
|
const [displayOptions, setDisplayOptions] = useState<DisplayOption>({
|
||||||
|
viewMode: ViewMode.Month,
|
||||||
|
locale: "en-US",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ganttTasks = useMemo(() => {
|
||||||
|
return (
|
||||||
|
tasks?.map((task) => ({
|
||||||
|
start: new Date(task.startDate as string),
|
||||||
|
end: new Date(task.dueDate as string),
|
||||||
|
name: task.title,
|
||||||
|
id: `Task-${task.id}`,
|
||||||
|
type: "task" as TaskTypeItems,
|
||||||
|
progress: task.points ? (task.points / 10) * 100 : 0,
|
||||||
|
isDisabled: false,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [tasks]);
|
||||||
|
|
||||||
|
const handleViewModeChange = (
|
||||||
|
event: React.ChangeEvent<HTMLSelectElement>,
|
||||||
|
) => {
|
||||||
|
setDisplayOptions((prev) => ({
|
||||||
|
...prev,
|
||||||
|
viewMode: event.target.value as ViewMode,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (error || !tasks) return <div>An error occurred while fetching tasks</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 xl:px-6">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2 py-5">
|
||||||
|
<h1 className="me-2 text-lg font-bold dark:text-white">
|
||||||
|
Project Timeline
|
||||||
|
</h1>
|
||||||
|
<div className="relative inline-block w-64">
|
||||||
|
<select
|
||||||
|
className="focus:shadow-outline block w-full appearance-none rounded border border-gray-400 bg-white px-4 py-2 pr-8 leading-tight shadow hover:border-gray-500 focus:outline-none dark:border-dark-secondary dark:bg-dark-secondary dark:text-white"
|
||||||
|
value={displayOptions.viewMode}
|
||||||
|
onChange={handleViewModeChange}
|
||||||
|
>
|
||||||
|
<option value={ViewMode.Day}>Day</option>
|
||||||
|
<option value={ViewMode.Week}>Week</option>
|
||||||
|
<option value={ViewMode.Month}>Month</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-md bg-white shadow dark:bg-dark-secondary dark:text-white">
|
||||||
|
<div className="timeline">
|
||||||
|
<Gantt
|
||||||
|
tasks={ganttTasks}
|
||||||
|
{...displayOptions}
|
||||||
|
columnWidth={displayOptions.viewMode === ViewMode.Month ? 150 : 100}
|
||||||
|
listCellWidth="100px"
|
||||||
|
barBackgroundColor={isDarkMode ? "#101214" : "#aeb8c2"}
|
||||||
|
barBackgroundSelectedColor={isDarkMode ? "#000" : "#9ba1a6"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 pb-5 pt-1">
|
||||||
|
<button
|
||||||
|
className="flex items-center rounded bg-blue-primary px-3 py-2 text-white hover:bg-blue-600"
|
||||||
|
onClick={() => setIsModalNewTaskOpen(true)}
|
||||||
|
>
|
||||||
|
Add New Task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Timeline;
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ProjectHeader from "@/app/projects/ProjectHeader";
|
import ProjectHeader from "@/app/projects/ProjectHeader";
|
||||||
import BoardView from "@/app/projects/BoardView";
|
import Board from "@/app/projects/BoardView";
|
||||||
|
import List from "@/app/projects/ListView";
|
||||||
|
import Timeline from "@/app/projects/TimelineView";
|
||||||
|
import Table from "@/app/projects/TableView";
|
||||||
|
import ModalNewTask from "@/app/components/ModalNewTask";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: { id: string };
|
params: { id: string };
|
||||||
@@ -15,26 +19,25 @@ const Project = ({ params }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* <ModalNewTask
|
<ModalNewTask
|
||||||
isOpen={isModalNewTaskOpen}
|
isOpen={isModalNewTaskOpen}
|
||||||
onClose={() => setIsModalNewTaskOpen(false)}
|
onClose={() => setIsModalNewTaskOpen(false)}
|
||||||
id={id}
|
id={id}
|
||||||
/> */}
|
/>
|
||||||
<ProjectHeader activeTab={activeTab} setActiveTab={setActiveTab} />
|
<ProjectHeader activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||||
{
|
|
||||||
activeTab === "Board" && (
|
{activeTab === "Board" && (
|
||||||
<BoardView id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
<Board id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||||
)
|
)}
|
||||||
// {activeTab === "List" && (
|
{activeTab === "List" && (
|
||||||
// <List id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
<List id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||||
// )}
|
)}
|
||||||
// {activeTab === "Timeline" && (
|
{activeTab === "Timeline" && (
|
||||||
// <Timeline id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
<Timeline id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||||
// )}
|
)}
|
||||||
// {activeTab === "Table" && (
|
{activeTab === "Table" && (
|
||||||
// <Table id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
<Table id={id} setIsModalNewTaskOpen={setIsModalNewTaskOpen} />
|
||||||
// )}
|
)}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
41
tasker-client/src/app/projects/settings/page.tsx
Normal file
41
tasker-client/src/app/projects/settings/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Header from "@/app/components/Header";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Settings = () => {
|
||||||
|
const userSettings = {
|
||||||
|
username: "mockuser",
|
||||||
|
email: "mockuser@example.com",
|
||||||
|
teamName: "mockteam",
|
||||||
|
roleName: "mockrole",
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelStyles = "block text-sm font-medium dark:text-white";
|
||||||
|
const textStyles =
|
||||||
|
"mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 dark:text-white";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<Header name="Settings" />
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className={labelStyles}>Username</label>
|
||||||
|
<div className={textStyles}>{userSettings.username}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelStyles}>Email</label>
|
||||||
|
<div className={textStyles}>{userSettings.email}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelStyles}>Team</label>
|
||||||
|
<div className={textStyles}>{userSettings.teamName}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelStyles}>Role</label>
|
||||||
|
<div className={textStyles}>{userSettings.roleName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
75
tasker-client/src/app/search/page.tsx
Normal file
75
tasker-client/src/app/search/page.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Header from "@/app/components/Header";
|
||||||
|
import ProjectCard from "@/app/components/ProjectCard";
|
||||||
|
import TaskCard from "@/app/components/TaskCard";
|
||||||
|
import UserCard from "@/app/components/UserCard";
|
||||||
|
import { useSearchQuery } from "@/state/api";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const Search = () => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const {
|
||||||
|
data: searchResults,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
} = useSearchQuery(searchTerm, {
|
||||||
|
skip: searchTerm.length < 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSearch = debounce(
|
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearchTerm(event.target.value);
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return handleSearch.cancel;
|
||||||
|
}, [handleSearch.cancel]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<Header name="Search" />
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
className="w-1/2 rounded border p-3 shadow"
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
{isLoading && <p>Loading...</p>}
|
||||||
|
{isError && <p>Error occurred while fetching search results.</p>}
|
||||||
|
{!isLoading && !isError && searchResults && (
|
||||||
|
<div>
|
||||||
|
{searchResults.tasks && searchResults.tasks?.length > 0 && (
|
||||||
|
<h2>Tasks</h2>
|
||||||
|
)}
|
||||||
|
{searchResults.tasks?.map((task) => (
|
||||||
|
<TaskCard key={task.id} task={task} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{searchResults.projects && searchResults.projects?.length > 0 && (
|
||||||
|
<h2>Projects</h2>
|
||||||
|
)}
|
||||||
|
{searchResults.projects?.map((project) => (
|
||||||
|
<ProjectCard key={project.id} project={project} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{searchResults.users && searchResults.users?.length > 0 && (
|
||||||
|
<h2>Users</h2>
|
||||||
|
)}
|
||||||
|
{searchResults.users?.map((user) => (
|
||||||
|
<UserCard key={user.userId} user={user} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Search;
|
||||||
59
tasker-client/src/app/teams/index.tsx
Normal file
59
tasker-client/src/app/teams/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
import { useGetTeamsQuery } from "@/state/api";
|
||||||
|
import React from "react";
|
||||||
|
import { useAppSelector } from "../redux";
|
||||||
|
import Header from "@/app/components/Header";
|
||||||
|
import {
|
||||||
|
DataGrid,
|
||||||
|
GridColDef,
|
||||||
|
GridToolbarContainer,
|
||||||
|
GridToolbarExport,
|
||||||
|
GridToolbarFilterButton,
|
||||||
|
} from "@mui/x-data-grid";
|
||||||
|
import { dataGridSxStyles } from "@/lib/utils";
|
||||||
|
|
||||||
|
const CustomToolbar = () => (
|
||||||
|
<GridToolbarContainer className="toolbar flex gap-2">
|
||||||
|
<GridToolbarFilterButton />
|
||||||
|
<GridToolbarExport />
|
||||||
|
</GridToolbarContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{ field: "id", headerName: "Team ID", width: 100 },
|
||||||
|
{ field: "teamName", headerName: "Team Name", width: 200 },
|
||||||
|
{ field: "productOwnerUsername", headerName: "Product Owner", width: 200 },
|
||||||
|
{
|
||||||
|
field: "projectManagerUsername",
|
||||||
|
headerName: "Project Manager",
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Teams = () => {
|
||||||
|
const { data: teams, isLoading, isError } = useGetTeamsQuery();
|
||||||
|
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (isError || !teams) return <div>Error fetching teams</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col p-8">
|
||||||
|
<Header name="Teams" />
|
||||||
|
<div style={{ height: 650, width: "100%" }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={teams || []}
|
||||||
|
columns={columns}
|
||||||
|
pagination
|
||||||
|
slots={{
|
||||||
|
toolbar: CustomToolbar,
|
||||||
|
}}
|
||||||
|
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
|
||||||
|
sx={dataGridSxStyles(isDarkMode)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Teams;
|
||||||
82
tasker-client/src/app/timeline/page.tsx
Normal file
82
tasker-client/src/app/timeline/page.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useAppSelector } from "@/app/redux";
|
||||||
|
import Header from "@/app/components/Header";
|
||||||
|
import { useGetProjectsQuery } from "@/state/api";
|
||||||
|
import { DisplayOption, Gantt, ViewMode } from "gantt-task-react";
|
||||||
|
import "gantt-task-react/dist/index.css";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
type TaskTypeItems = "task" | "milestone" | "project";
|
||||||
|
|
||||||
|
const Timeline = () => {
|
||||||
|
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
|
||||||
|
const { data: projects, isLoading, isError } = useGetProjectsQuery();
|
||||||
|
|
||||||
|
const [displayOptions, setDisplayOptions] = useState<DisplayOption>({
|
||||||
|
viewMode: ViewMode.Month,
|
||||||
|
locale: "en-US",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ganttTasks = useMemo(() => {
|
||||||
|
return (
|
||||||
|
projects?.map((project) => ({
|
||||||
|
start: new Date(project.startDate as string),
|
||||||
|
end: new Date(project.endDate as string),
|
||||||
|
name: project.name,
|
||||||
|
id: `Project-${project.id}`,
|
||||||
|
type: "project" as TaskTypeItems,
|
||||||
|
progress: 50,
|
||||||
|
isDisabled: false,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [projects]);
|
||||||
|
|
||||||
|
const handleViewModeChange = (
|
||||||
|
event: React.ChangeEvent<HTMLSelectElement>,
|
||||||
|
) => {
|
||||||
|
setDisplayOptions((prev) => ({
|
||||||
|
...prev,
|
||||||
|
viewMode: event.target.value as ViewMode,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (isError || !projects)
|
||||||
|
return <div>An error occurred while fetching projects</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-full p-8">
|
||||||
|
<header className="mb-4 flex items-center justify-between">
|
||||||
|
<Header name="Projects Timeline" />
|
||||||
|
<div className="relative inline-block w-64">
|
||||||
|
<select
|
||||||
|
className="focus:shadow-outline block w-full appearance-none rounded border border-gray-400 bg-white px-4 py-2 pr-8 leading-tight shadow hover:border-gray-500 focus:outline-none dark:border-dark-secondary dark:bg-dark-secondary dark:text-white"
|
||||||
|
value={displayOptions.viewMode}
|
||||||
|
onChange={handleViewModeChange}
|
||||||
|
>
|
||||||
|
<option value={ViewMode.Day}>Day</option>
|
||||||
|
<option value={ViewMode.Week}>Week</option>
|
||||||
|
<option value={ViewMode.Month}>Month</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-md bg-white shadow dark:bg-dark-secondary dark:text-white">
|
||||||
|
<div className="timeline">
|
||||||
|
<Gantt
|
||||||
|
tasks={ganttTasks}
|
||||||
|
{...displayOptions}
|
||||||
|
columnWidth={displayOptions.viewMode === ViewMode.Month ? 150 : 100}
|
||||||
|
listCellWidth="100px"
|
||||||
|
projectBackgroundColor={isDarkMode ? "#101214" : "#1f2937"}
|
||||||
|
projectProgressColor={isDarkMode ? "#1f2937" : "#aeb8c2"}
|
||||||
|
projectProgressSelectedColor={isDarkMode ? "#000" : "#9ba1a6"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Timeline;
|
||||||
73
tasker-client/src/app/users/page.tsx
Normal file
73
tasker-client/src/app/users/page.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
import { useGetUsersQuery } from "@/state/api";
|
||||||
|
import { useAppSelector } from "@/app/redux";
|
||||||
|
import React from "react";
|
||||||
|
import Header from "@/app/components/Header";
|
||||||
|
import {
|
||||||
|
DataGrid,
|
||||||
|
GridColDef,
|
||||||
|
GridToolbarContainer,
|
||||||
|
GridToolbarExport,
|
||||||
|
GridToolbarFilterButton,
|
||||||
|
} from "@mui/x-data-grid";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { dataGridSxStyles } from "@/lib/utils";
|
||||||
|
|
||||||
|
const CustomToolbar = () => (
|
||||||
|
<GridToolbarContainer className="toolbar flex gap-2">
|
||||||
|
<GridToolbarFilterButton />
|
||||||
|
<GridToolbarExport />
|
||||||
|
</GridToolbarContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: GridColDef[] = [
|
||||||
|
{ field: "userId", headerName: "ID", width: 100 },
|
||||||
|
{ field: "username", headerName: "Username", width: 150 },
|
||||||
|
{
|
||||||
|
field: "profilePictureUrl",
|
||||||
|
headerName: "Profile Picture",
|
||||||
|
width: 100,
|
||||||
|
renderCell: (params) => (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div className="h-9 w-9">
|
||||||
|
<Image
|
||||||
|
src={`${params.value}`}
|
||||||
|
alt={params.row.username}
|
||||||
|
width={100}
|
||||||
|
height={50}
|
||||||
|
className="h-full rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const Users = () => {
|
||||||
|
const { data: users, isLoading, isError } = useGetUsersQuery();
|
||||||
|
const isDarkMode = useAppSelector((state) => state.global.isDarkMode);
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (isError || !users) return <div>Error fetching users</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col p-8">
|
||||||
|
<Header name="Users" />
|
||||||
|
<div style={{ height: 650, width: "100%" }}>
|
||||||
|
<DataGrid
|
||||||
|
rows={users || []}
|
||||||
|
columns={columns}
|
||||||
|
getRowId={(row) => row.userId}
|
||||||
|
pagination
|
||||||
|
slots={{
|
||||||
|
toolbar: CustomToolbar,
|
||||||
|
}}
|
||||||
|
className="border border-gray-200 bg-white shadow dark:border-stroke-dark dark:bg-dark-secondary dark:text-gray-200"
|
||||||
|
sx={dataGridSxStyles(isDarkMode)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Users;
|
||||||
29
tasker-client/src/lib/utils.ts
Normal file
29
tasker-client/src/lib/utils.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export const dataGridSxStyles = (isDarkMode: boolean) => {
|
||||||
|
return {
|
||||||
|
"& .MuiDataGrid-columnHeaders": {
|
||||||
|
color: `${isDarkMode ? "#e5e7eb" : ""}`,
|
||||||
|
'& [role="row"] > *': {
|
||||||
|
backgroundColor: `${isDarkMode ? "#1d1f21" : "white"}`,
|
||||||
|
borderColor: `${isDarkMode ? "#2d3135" : ""}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiIconbutton-root": {
|
||||||
|
color: `${isDarkMode ? "#a3a3a3" : ""}`,
|
||||||
|
},
|
||||||
|
"& .MuiTablePagination-root": {
|
||||||
|
color: `${isDarkMode ? "#a3a3a3" : ""}`,
|
||||||
|
},
|
||||||
|
"& .MuiTablePagination-selectIcon": {
|
||||||
|
color: `${isDarkMode ? "#a3a3a3" : ""}`,
|
||||||
|
},
|
||||||
|
"& .MuiDataGrid-cell": {
|
||||||
|
border: "none",
|
||||||
|
},
|
||||||
|
"& .MuiDataGrid-row": {
|
||||||
|
borderBottom: `1px solid ${isDarkMode ? "#2d3135" : "e5e7eb"}`,
|
||||||
|
},
|
||||||
|
"& .MuiDataGrid-withBorderColor": {
|
||||||
|
borderColor: `${isDarkMode ? "#2d3135" : "e5e7eb"}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user