Skip to content

Commit

Permalink
[Feature] Projects management | View projects in grid and list view (#…
Browse files Browse the repository at this point in the history
…3594)

* add menu in the main sidebar

* add the header slot

* add project view (list & grid)

* add coderabit suggestions

* dark mode, show project for the selected team

* add the filters card modal

* add all search filters

* add filters modal card

* fix merge conflits

---------

Co-authored-by: Chimpaye Credo Thierry <thierry@192.168.1.64>
Co-authored-by: Ruslan Konviser <evereq@gmail.com>
  • Loading branch information
3 people authored Feb 15, 2025
1 parent b9f2a96 commit 453ce79
Show file tree
Hide file tree
Showing 32 changed files with 1,824 additions and 61 deletions.
27 changes: 27 additions & 0 deletions apps/web/app/[locale]/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { MainLayout } from '@/lib/layout';
import { useOrganizationTeams } from '@/app/hooks';
import { withAuthentication } from '@/lib/app/authenticator';

function Page() {
const { isTrackingEnabled } = useOrganizationTeams();
return (
<MainLayout
showTimer={isTrackingEnabled}
className="!p-0 pb-1 !overflow-hidden w-full"
childrenClassName="w-full h-full"
mainHeaderSlot={
<div className="flex flex-col p-4 dark:bg-dark--theme">
<div className="flex flex-col items-start justify-between gap-3">
<div className="flex items-center justify-center h-10 gap-8">
<h3 className=" text-3xl font-medium">Project</h3>
</div>
</div>
</div>
}
></MainLayout>
);
}

export default withAuthentication(Page, { displayName: 'ProjectPage' });
301 changes: 301 additions & 0 deletions apps/web/app/[locale]/projects/components/data-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
'use client';

import * as React from 'react';
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from '@tanstack/react-table';
import Image from 'next/image';
import { Checkbox } from '@/components/ui/checkbox';

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useTranslations } from 'next-intl';
import { IProject } from '@/app/interfaces';
import { cn } from '@/lib/utils';
import { useTaskStatus } from '@/app/hooks';
import { useMemo } from 'react';
import moment from 'moment';
import { ArrowUpDown } from 'lucide-react';
import { Button } from '@components/ui/button';
import AvatarStack from '@components/shared/avatar-stack';
import { SpinnerLoader } from '@/lib/components';

export type ProjectTableDataType = {
project: {
name: IProject['name'];
imageUrl: IProject['imageUrl'];
color: IProject['color'];
};
status: IProject['status'];
startDate: IProject['startDate'];
endDate: IProject['endDate'];
members: IProject['members'];
managers: IProject['members'];
teams: IProject['teams'];
};

/**
* Renders a data table displaying projects.
*
* @component
* @param {Object} props - The component props.
* @param {ProjectTableDataType[]} props.data - Array of data objects projects information.
* @param {boolean} props.loading - Whether to show loading indicator when loading projects data.
*
* @returns {JSX.Element} A table showing projects information.
*
*/

export function DataTableProject(props: { data: ProjectTableDataType[]; loading: boolean }) {
const { data, loading } = props;
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const t = useTranslations();
const { taskStatus } = useTaskStatus();

const statusColorsMap: Map<string | undefined, string | undefined> = useMemo(() => {
return new Map(taskStatus.map((status) => [status.name, status.color]));
}, [taskStatus]);

const columns: ColumnDef<ProjectTableDataType>[] = [
{
id: 'select',
header: ({ table }) => (
<div className="">
<Checkbox
checked={
table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
/>
</div>
),
cell: ({ row }) => (
<div className="">
<Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} />
</div>
),
enableSorting: false,
enableHiding: false
},
{
accessorKey: 'project',
header: ({ column }) => {
return (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}>
{t('pages.projects.projectTitle.PLURAL')}
<ArrowUpDown size={10} />
</Button>
);
},
cell: function ({ row }) {
return (
<div className="capitalize">
<div className="flex items-center font-medium gap-2">
<div
style={{ backgroundColor: row.original?.project?.color }}
className={cn(
'w-10 h-10 border overflow-hidden flex items-center justify-center rounded-xl'
)}
>
{!row.original?.project?.imageUrl ? (
row.original?.project?.name?.substring(0, 2)
) : (
<Image
alt={row.original?.project?.name ?? ''}
height={40}
width={40}
className="w-full h-full"
src={row.original?.project?.imageUrl}
/>
)}
</div>
<p>{row.original?.project?.name}</p>
</div>
</div>
);
}
},
{
accessorKey: 'status',
header: ({ column }) => {
return (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}>
{t('common.STATUS')}
<ArrowUpDown size={10} />
</Button>
);
},
cell: ({ row }) => {
return (
<div className="capitalize flex items-center">
<div
style={{ backgroundColor: statusColorsMap.get(row.original?.status) }}
className="rounded px-4 py-1"
>
{row.original?.status}
</div>
</div>
);
}
},
{
accessorKey: 'startDate',
header: ({ column }) => {
return (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}>
{t('common.START_DATE')}
<ArrowUpDown size={10} />
</Button>
);
},
cell: ({ row }) => (
<div className="">
{row.original?.startDate && moment(row.original?.startDate).format('MMM. DD YYYY')}
</div>
)
},
{
accessorKey: 'endDate',
header: ({ column }) => {
return (
<Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}>
{t('common.END_DATE')}
<ArrowUpDown size={10} />
</Button>
);
},
cell: ({ row }) => (
<div className="">{row.original?.endDate && moment(row.original?.endDate).format('MMM. DD YYYY')}</div>
)
},
{
accessorKey: 'members',
header: () => <div>{t('common.MEMBERS')}</div>,
cell: ({ row }) => {
const members =
row.original?.members
?.filter((el) => !el.isManager)
?.map((el) => ({
imageUrl: el?.employee?.user?.imageUrl,
name: el?.employee?.fullName
})) || [];

return members?.length > 0 ? <AvatarStack avatars={members} /> : null;
}
},
{
accessorKey: 'teams',
header: () => <div>{t('common.TEAMS')}</div>,
cell: ({ row }) => {
const teams =
row.original?.teams?.map((el) => ({
name: el?.name
})) || [];

return teams?.length > 0 ? <AvatarStack avatars={teams} /> : null;
}
},
{
accessorKey: 'managers',
header: () => <div>{t('common.MANAGERS')}</div>,
cell: ({ row }) => {
const managers =
row.original?.managers
?.filter((el) => el.isManager)
?.map((el) => ({
imageUrl: el?.employee?.user?.imageUrl,
name: el?.employee?.fullName
})) || [];

return managers?.length > 0 ? <AvatarStack avatars={managers} /> : null;
}
}
];

const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection
}
});

React.useEffect(() => {
console.log(loading);
}, [loading]);

return (
<div className="w-full">
{loading ? (
<div className="w-full flex justify-center items-center">
<SpinnerLoader />
</div>
) : table?.getRowModel()?.rows.length ? (
<div className="rounded-md">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className=" capitalize" key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table?.getRowModel()?.rows.length ? (
table?.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
{t('common.NO_RESULT')}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
) : (
<div className="w-full h-12 flex items-center justify-center">
<span>{t('common.NO_RESULT')}</span>
</div>
)}
</div>
);
}
Loading

0 comments on commit 453ce79

Please sign in to comment.