Skip to content

Commit

Permalink
[Feat]: Add new API endpoint for daily time log report charts (#3533)
Browse files Browse the repository at this point in the history
* feat: implement time tracking analytics improvements

* fix: code rabbit

* fix: code rabbit

* refactor: improve report activity state management

- Move report activity state to global store using timeLogsRapportChartState
- Remove local state management in useReportActivity hook
- Improve error handling and data reset logic
- Add better error logging
- Simplify state updates and data flow

* feat: Add input validation for required fields.
  • Loading branch information
Innocent-Akim authored Jan 22, 2025
1 parent 25d0024 commit 1be1d7c
Show file tree
Hide file tree
Showing 20 changed files with 615 additions and 274 deletions.
68 changes: 68 additions & 0 deletions apps/web/app/[locale]/dashboard/app-url/[teamId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client"
import { fullWidthState } from '@/app/stores/fullWidth';
import { withAuthentication } from '@/lib/app/authenticator';
import { MainLayout } from '@/lib/layout';
import { cn } from '@/lib/utils';
import { useOrganizationTeams } from '@app/hooks/features/useOrganizationTeams';
import { useAtomValue } from 'jotai';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import React, { useMemo } from 'react';
import { ArrowLeftIcon } from '@radix-ui/react-icons';
import { useRouter } from 'next/navigation';
import { Breadcrumb, Container } from '@/lib/components';

function AppUrls() {
const { activeTeam, isTrackingEnabled } = useOrganizationTeams();
const router = useRouter();
const t = useTranslations();
const fullWidth = useAtomValue(fullWidthState);
const paramsUrl = useParams<{ locale: string }>();
const currentLocale = paramsUrl?.locale;


const breadcrumbPath = useMemo(
() => [
{ title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' },
{ title: activeTeam?.name || '', href: '/' },
{ title: 'Apps & URLs', href: `/${currentLocale}/dashboard/app-url` }
],
[activeTeam?.name, currentLocale, t]
);

if (!activeTeam) {
return (
<div className="flex justify-center items-center h-screen">
<p className="text-gray-500">Team not found</p>
</div>
);
}

return (
<MainLayout
className="items-start pb-1 !overflow-hidden w-full"
childrenClassName="w-full"
showTimer={isTrackingEnabled}
mainHeaderSlot={
<div className="flex flex-col py-4 bg-gray-100 dark:bg-dark--theme">
<Container fullWidth={fullWidth} className={cn('flex gap-4 items-center w-full')}>
<div className="flex items-center pt-6 w-full dark:bg-dark--theme">
<button
onClick={() => router.back()}
className="p-1 rounded-full transition-colors hover:bg-gray-100"
>
<ArrowLeftIcon className="text-dark dark:text-[#6b7280] h-6 w-6" />
</button>
<Breadcrumb paths={breadcrumbPath} className="text-sm" />
</div>
</Container>
</div>
}
></MainLayout>
);
}

export default withAuthentication(AppUrls, {
displayName: 'Apps & URLs',
showPageSkeleton: true
});
11 changes: 0 additions & 11 deletions apps/web/app/[locale]/dashboard/app-url/page.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function DashboardHeader() {
<div className="flex justify-between items-center">
<h1 className="text-2xl font-semibold">Team Dashboard</h1>
<div className="flex gap-4 items-center">
<DateRangePicker />
<DateRangePicker />
<Select defaultValue="filter">
<SelectTrigger className="w-[100px]">
<SelectValue placeholder="Filter" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
'use client';

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

interface DateRangePickerProps {
className?: string;
onDateRangeChange?: (range: DateRange | undefined) => void;
}

export function DateRangePicker({ className, onDateRangeChange }: DateRangePickerProps) {
const t=useTranslations();
const [dateRange, setDateRange] = React.useState<DateRange | undefined>({
from: new Date(),
to: new Date()
});
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [currentMonth, setCurrentMonth] = React.useState<Date>(new Date());

const handleDateRangeChange = (range: DateRange | undefined) => {
try {
setDateRange(range);
onDateRangeChange?.(range);
} catch (error) {
console.error('Error handling date range change:', error);
}
};

const predefinedRanges = [
{
label: t('common.TODAY'),
action: () => {
const today = new Date();
handleDateRangeChange({ from: today, to: today });
},
isSelected: (range: DateRange | undefined) => {
if (!range?.from || !range?.to) return false;
const today = new Date();
return isEqual(range.from, today) && isEqual(range.to, today);
}
},
{
label: t('common.YESTERDAY'),
action: () => {
const yesterday = subDays(new Date(), 1);
handleDateRangeChange({ from: yesterday, to: yesterday });
},
isSelected: (range: DateRange | undefined) => {
if (!range?.from || !range?.to) return false;
const yesterday = subDays(new Date(), 1);
return isEqual(range.from, yesterday) && isEqual(range.to, yesterday);
}
},
{
label: t('common.THIS_WEEK'),
action: () => {
const today = new Date();
handleDateRangeChange({
from: startOfWeek(today, { weekStartsOn: 1 }),
to: endOfWeek(today, { weekStartsOn: 1 })
});
},
isSelected: (range: DateRange | undefined) => {
if (!range?.from || !range?.to) return false;
const today = new Date();
const weekStart = startOfWeek(today, { weekStartsOn: 1 });
const weekEnd = endOfWeek(today, { weekStartsOn: 1 });
return isEqual(range.from, weekStart) && isEqual(range.to, weekEnd);
}
},
{
label: t('common.LAST_WEEK'),
action: () => {
const lastWeek = subWeeks(new Date(), 1);
handleDateRangeChange({
from: startOfWeek(lastWeek, { weekStartsOn: 1 }),
to: endOfWeek(lastWeek, { weekStartsOn: 1 })
});
},
isSelected: (range: DateRange | undefined) => {
if (!range?.from || !range?.to) return false;
const lastWeek = subWeeks(new Date(), 1);
const weekStart = startOfWeek(lastWeek, { weekStartsOn: 1 });
const weekEnd = endOfWeek(lastWeek, { weekStartsOn: 1 });
return isEqual(range.from, weekStart) && isEqual(range.to, weekEnd);
}
},
{
label:t('common.THIS_MONTH'),
action: () => {
const today = new Date();
handleDateRangeChange({
from: startOfMonth(today),
to: endOfMonth(today)
});
},
isSelected: (range: DateRange | undefined) => {
if (!range?.from || !range?.to) return false;
const today = new Date();
const monthStart = startOfMonth(today);
const monthEnd = endOfMonth(today);
return isEqual(range.from, monthStart) && isEqual(range.to, monthEnd);
}
},
{
label: t('common.LAST_MONTH'),
action: () => {
const lastMonth = subMonths(new Date(), 1);
handleDateRangeChange({
from: startOfMonth(lastMonth),
to: endOfMonth(lastMonth)
});
},
isSelected: (range: DateRange | undefined) => {
if (!range?.from || !range?.to) return false;
const lastMonth = subMonths(new Date(), 1);
const monthStart = startOfMonth(lastMonth);
const monthEnd = endOfMonth(lastMonth);
return isEqual(range.from, monthStart) && isEqual(range.to, monthEnd);
}
}
];

const formatDateRange = (range: DateRange) => {
if (!range.from) return 'Select date range';
if (!range.to) return format(range.from, 'd MMM yyyy');

if (isSameYear(range.from, range.to)) {
if (isSameMonth(range.from, range.to)) {
return `${format(range.from, 'd')} - ${format(range.to, 'd MMM yyyy')}`;
}
return `${format(range.from, 'd MMM')} - ${format(range.to, 'd MMM yyyy')}`;
}
return `${format(range.from, 'd MMM yyyy')} - ${format(range.to, 'd MMM yyyy')}`;
};

return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
'justify-between gap-2 px-3 py-2 min-w-[240px] text-center flex items-center',
!dateRange && 'text-muted-foreground',
className
)}
>
{dateRange ? formatDateRange(dateRange) : t('common.SELECT')}
<ChevronDown className="w-4 h-4" />
</Button>
</PopoverTrigger>
<PopoverContent
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onChange={(e) => e.stopPropagation()}
className="p-0 w-auto"
align="center"
>
<div className="flex flex-row-reverse">
<div className="p-1 space-y-1 border-l max-w-36">
{predefinedRanges.map((range) => (
<Button
key={range.label}
variant={range.isSelected(dateRange) ? 'default' : 'ghost'}
className={cn(
'justify-start w-full font-normal dark:text-gray-100',
range.isSelected(dateRange) &&
'bg-primary text-primary-foreground hover:bg-primary/90'
)}
onClick={() => {
range.action();
}}
>
{range.label}
</Button>
))}
</div>
<div className="p-1">
<Calendar
className="min-w-[240px]"
mode="range"
selected={dateRange}
onSelect={handleDateRangeChange}
numberOfMonths={2}
month={currentMonth}
onMonthChange={setCurrentMonth}
showOutsideDays={false}
fixedWeeks
ISOWeek
initialFocus
/>
</div>
</div>
<div className="flex gap-2 justify-end p-1 border-t">
<Button
variant="outline"
onClick={() => {
handleDateRangeChange(undefined);
setIsPopoverOpen(false);
}}
>
{t('common.CLEAR')}
</Button>
<Button
onClick={() => {
setIsPopoverOpen(false);
}}
>
{t('common.APPLY')}
</Button>
</div>
</PopoverContent>
</Popover>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps) => {
export function TeamStatsChart() {
return (
<div className="flex flex-col">
<div className="h-[300px] w-full">
<div className="h-[250px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 20, right: 0, bottom: 20, left: 0 }}>
<LineChart data={chartData} margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid
vertical={true}
strokeDasharray="3 3"
horizontal={true}
className="stroke-gray-200"
className="stroke-gray-200 dark:stroke-gray-700"
vertical={true}
/>
<XAxis
dataKey="date"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function TeamStatsGrid() {
<Card key={stat.title} className="p-6">
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-500">{stat.title}</span>
<span className={`text-2xl font-semibold mt-2 ${stat.color || "text-gray-900"}`}>
<span className={`text-2xl font-semibold mt-2 ${stat.color || "text-gray-900 dark:text-white"}`}>
{stat.value}
</span>
{stat.progress && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function TeamDashboard() {
() => [
{ title: JSON.parse(t('pages.home.BREADCRUMB')), href: '/' },
{ title: activeTeam?.name || '', href: '/' },
{ title: 'team-dashboard', href: `/${currentLocale}/dashboard/team-dashboard` }
{ title: 'Team-Dashboard', href: `/${currentLocale}/dashboard/team-dashboard` }
],
[activeTeam?.name, currentLocale, t]
);
Expand All @@ -42,16 +42,16 @@ function TeamDashboard() {
mainHeaderSlot={
<div className="flex flex-col py-4 bg-gray-100 dark:bg-dark--theme">
<Container fullWidth={fullWidth} className={cn('flex flex-col gap-4 w-full')}>
<div className="flex pt-6 w-full dark:bg-dark--theme">
<div className="flex items-center pt-6 dark:bg-dark--theme">
<button
onClick={() => router.back()}
className="p-1 rounded-full transition-colors hover:bg-gray-100"
>
<ArrowLeftIcon className="text-dark dark:text-[#6b7280] h-6 w-6" />
</button>{' '}
</button>
<Breadcrumb paths={breadcrumbPath} className="text-sm" />
</div>
<div className="flex flex-col gap-4 px-6 pt-4 w-full">
<div className="flex flex-col gap-4 px-4 pt-4 w-full">
<DashboardHeader />
<TeamStatsGrid />
<Card className="p-6 w-full">
Expand All @@ -60,8 +60,7 @@ function TeamDashboard() {
</div>
</Container>
</div>
}
>
}>
<Container fullWidth={fullWidth} className={cn('flex flex-col gap-8 py-6 w-full')}>
<Card className="p-6 w-full">
<TeamStatsTable />
Expand Down
Loading

0 comments on commit 1be1d7c

Please sign in to comment.