Skip to content

Commit

Permalink
[Feature] Add Weekly/Daily time limits report view (#3376)
Browse files Browse the repository at this point in the history
* add weekly limit page

* add filter options in the header

add the weekly limit table

* fix spell typo

* fix deepscan/codacy

* add time limits reports page

* add coderabit suggestions
  • Loading branch information
CREDO23 authored Dec 1, 2024
1 parent 9caa7b9 commit 1965f0b
Show file tree
Hide file tree
Showing 29 changed files with 1,189 additions and 33 deletions.
186 changes: 186 additions & 0 deletions apps/web/app/[locale]/reports/weekly-limit/components/data-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
'use client';

import * as React from 'react';
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from '@tanstack/react-table';

import { Checkbox } from '@/components/ui/checkbox';

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useTranslations } from 'next-intl';
import { formatIntegerToHour, formatTimeString } from '@/app/helpers';
import { ProgressBar } from '@/lib/components';

export type WeeklyLimitTableDataType = {
member: string;
timeSpent: number;
limit: number;
percentageUsed: number;
remaining: number;
};

/**
* Renders a data table displaying weekly time limits and usage for team members.
*
* @component
* @param {Object} props - The component props.
* @param {WeeklyLimitTableDataType[]} props.data - Array of data objects containing weekly time usage information.
*
* @returns {JSX.Element} A table showing member-wise weekly time limits, usage, and remaining time.
*
*/

export function DataTableWeeklyLimits(props: { data: WeeklyLimitTableDataType[] }) {
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 columns: ColumnDef<WeeklyLimitTableDataType>[] = [
{
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: 'member',
header: () => <div className="">{t('common.MEMBER')}</div>,
cell: ({ row }) => <div className="capitalize">{row.getValue('member')}</div>
},
{
accessorKey: 'timeSpent',
header: () => <div className="">{t('pages.timeLimitReport.TIME_SPENT')}</div>,
cell: ({ row }) => (
<div className="lowercase">
{formatTimeString(formatIntegerToHour(Number(row.getValue('timeSpent')) / 3600))}
</div>
)
},
{
accessorKey: 'limit',
header: () => <div className="">{t('pages.timeLimitReport.LIMIT')}</div>,
cell: ({ row }) => (
<div className="lowercase">
{formatTimeString(formatIntegerToHour(Number(row.getValue('limit')) / 3600))}
</div>
)
},
{
accessorKey: 'percentageUsed',
header: () => <div className="">{t('pages.timeLimitReport.PERCENTAGE_USED')}</div>,
cell: ({ row }) => (
<div className="lowercase flex gap-2 items-center">
<ProgressBar
width={'10rem'}
progress={`${Number(row.getValue('percentageUsed')) < 100 ? Number(row.getValue('percentageUsed')).toFixed(2) : 100}%`}
/>{' '}
<span>{`${Number(row.getValue('percentageUsed')).toFixed(2)}%`}</span>
</div>
)
},
{
accessorKey: 'remaining',
header: () => <div className="">{t('pages.timeLimitReport.REMAINING')}</div>,
cell: ({ row }) => (
<div className="lowercase">
{Number(row.getValue('percentageUsed')) > 100 && '-'}
{formatTimeString(formatIntegerToHour(Number(row.getValue('remaining')) / 3600))}
</div>
)
}
];

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

return (
<div className="w-full">
{table?.getRowModel()?.rows.length ? (
<div className="rounded-md">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead 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>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
'use client';

import * as React from 'react';
import { endOfMonth, endOfWeek, format, isSameDay, startOfMonth, startOfWeek, subDays, subMonths } from 'date-fns';
import { Calendar as CalendarIcon } from 'lucide-react';
import { DateRange } from 'react-day-picker';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { useTranslations } from 'next-intl';
import { useEffect, useMemo } from 'react';

/**
* DatePickerWithRange component provides a date range picker with preset ranges.
* Users can select a custom date range or choose from predefined ranges.
*
* @component
* @param {Object} props - The component props.
* @param {DateRange} props.defaultValue - The initial default date range.
* @param {(dateRange: DateRange) => void} props.onChange - Callback function invoked when the date range is changed.
*
* @returns {JSX.Element} A date range picker with custom and preset options.
*/

export function DatePickerWithRange({
onChange,
defaultValue
}: {
defaultValue: DateRange;
onChange: (dateRange: DateRange) => void;
}) {
const [date, setDate] = React.useState<DateRange | undefined>(defaultValue);
const [selectedDate, setSelectedDate] = React.useState<DateRange | undefined>(defaultValue);
const t = useTranslations();

return (
<div className={cn('grid gap-2')}>
<Popover>
<PopoverTrigger asChild>
<Button
id="date"
variant={'outline'}
className={cn(
'w-[16rem] h-[2.2rem] justify-start text-left font-light',
!date && 'text-muted-foreground',
'overflow-hidden h-[2.2rem] text-clip border border-gray-200 dark:border-gray-700 bg-white dark:bg-dark--theme-light focus:ring-2 focus:ring-transparent'
)}
>
<CalendarIcon />
{date?.from ? (
date.to ? (
<>
{format(date.from, 'LLL dd, y')} - {format(date.to, 'LLL dd, y')}
</>
) : (
format(date.from, 'LLL dd, y')
)
) : (
<span>{t('common.PICK_A_DATE')}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0 flex" align="start">
<Calendar
initialFocus
mode="range"
defaultMonth={date?.from}
selected={selectedDate}
onSelect={setSelectedDate}
numberOfMonths={2}
/>
<div className="flex flex-col gap-1 w-44 border-l">
<PresetDates date={selectedDate} setDate={setSelectedDate} />
<div className="flex p-2 items-center flex-1 gap-1 justify-between">
<Button className=" grow text-xs h-8" variant={'outline'} size={'sm'}>
{t('common.CANCEL')}
</Button>
<Button
onClick={() => {
if (selectedDate?.from && selectedDate?.to) {
onChange(selectedDate);
setDate(selectedDate);
} else {
console.warn('Invalid date range selected');
}
}}
className=" grow text-xs h-8 dark:text-white"
size={'sm'}
>
{t('common.APPLY')}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
);
}

/**
* PresetDates component displays a list of predefined date ranges that users can select.
* It updates the selected date range in the parent DatePickerWithRange component.
*
* @component
* @param {Object} props - The component props.
* @param {React.Dispatch<React.SetStateAction<DateRange | undefined>>} props.setDate - Function to set the selected date range.
* @param {DateRange | undefined} props.date - The currently selected date range.
*
* @returns {JSX.Element} A list of buttons representing preset date ranges.
*/

const PresetDates = ({
setDate,
date
}: {
setDate: React.Dispatch<React.SetStateAction<DateRange | undefined>>;
date: DateRange | undefined;
}) => {
const t = useTranslations();

const presets = useMemo(
() => [
{ label: t('common.TODAY'), range: { from: new Date(), to: new Date() } },
{ label: t('common.YESTERDAY'), range: { from: subDays(new Date(), 1), to: subDays(new Date(), 1) } },
{ label: t('common.THIS_WEEK'), range: { from: startOfWeek(new Date()), to: endOfWeek(new Date()) } },
{
label: t('common.LAST_WEEK'),
range: { from: startOfWeek(subDays(new Date(), 7)), to: endOfWeek(subDays(new Date(), 7)) }
},
{ label: t('common.THIS_MONTH'), range: { from: startOfMonth(new Date()), to: endOfMonth(new Date()) } },
{
label: t('common.LAST_MONTH'),
range: {
from: startOfMonth(subMonths(new Date(), 1)),
to: endOfMonth(subMonths(new Date(), 1))
}
},
{ label: t('common.FILTER_LAST_7_DAYS'), range: { from: subDays(new Date(), 7), to: new Date() } },
{ label: t('common.LAST_TWO_WEEKS'), range: { from: subDays(new Date(), 14), to: new Date() } }
],
[t]
);

const [selected, setSelected] = React.useState<DateRange>();

useEffect(() => {
setSelected(
presets.find((preset) => {
return (
date?.from &&
date.to &&
isSameDay(date?.from, preset.range.from) &&
isSameDay(date?.to, preset.range.to)
);
})?.range
);
}, [date?.from, date?.to, presets, date]);

return (
<div className="flex flex-col w-full p-2 gap-1">
{presets.map((preset) => (
<Button
key={preset.label}
onClick={() => setDate(preset.range)}
variant={
selected?.from &&
selected.to &&
isSameDay(selected?.from, preset.range.from) &&
isSameDay(selected?.to, preset.range.to)
? 'default'
: 'outline'
}
className={cn(
' truncate text-left text-sm h-8 px-2 py-1 font-normal border rounded',
selected?.from &&
selected.to &&
isSameDay(selected?.from, preset.range.from) &&
isSameDay(selected?.to, preset.range.to) &&
'dark:text-white'
)}
>
{preset.label}
</Button>
))}
</div>
);
};
Loading

0 comments on commit 1965f0b

Please sign in to comment.