Skip to content

Commit

Permalink
Merge pull request #3538 from ever-co/stage
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
evereq authored Jan 22, 2025
2 parents d6e9706 + 21f1802 commit 3001f8b
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,49 @@ import {
SelectValue,
} from "@/components/ui/select";
import { DateRangePicker } from "./date-range-picker";
import { DateRange } from "react-day-picker";
import { ITimeLogReportDailyChartProps } from "@/app/interfaces/timer/ITimerLog";

interface DashboardHeaderProps {
onUpdateDateRange: (startDate: Date, endDate: Date) => void;
onUpdateFilters: (filters: Partial<Omit<ITimeLogReportDailyChartProps, 'organizationId' | 'tenantId'>>) => void;
}

export function DashboardHeader({ onUpdateDateRange, onUpdateFilters }: DashboardHeaderProps) {
const handleDateRangeChange = (range: DateRange | undefined) => {
if (range?.from && range?.to) {
onUpdateDateRange(range.from, range.to);
}
};

const handleFilterChange = (value: string) => {
const today = new Date();
let startDate = new Date();
const endDate = today;

switch (value) {
case 'today':
startDate = today;
break;
case 'week':
startDate.setDate(today.getDate() - 7);
break;
case 'month':
startDate.setMonth(today.getMonth() - 1);
break;
default:
return;
}

onUpdateDateRange(startDate, endDate);
};

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

import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid } from "recharts";
import { Button } from "@/components/ui/button";
import { chartData } from "../data/mock-data";
import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid } from 'recharts';
import { Button } from '@/components/ui/button';
import { ITimerDailyLog } from '@/app/interfaces/timer/ITimerLog';
import { useState } from 'react';
import { Spinner } from '@/components/ui/loaders/spinner';

interface TooltipProps {
active?: boolean;
Expand All @@ -29,12 +31,48 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps) => {
return null;
};

export function TeamStatsChart() {
interface TeamStatsChartProps {
rapportChartActivity: ITimerDailyLog[];
isLoading: boolean;
}

export function TeamStatsChart({ rapportChartActivity, isLoading }: TeamStatsChartProps) {
const [visibleLines, setVisibleLines] = useState({
tracked: true,
manual: true,
idle: true,
resumed: true
});

const toggleLine = (line: keyof typeof visibleLines) => {
setVisibleLines((prev) => ({
...prev,
[line]: !prev[line]
}));
};

const formattedData =
rapportChartActivity?.map((item: ITimerDailyLog) => ({
date: new Date(item.date).toLocaleDateString(),
tracked: item.value.TRACKED || 0,
manual: item.value.MANUAL || 0,
idle: item.value.IDLE || 0,
resumed: item.value.RESUMED || 0
})) || [];

if (isLoading) {
return (
<div className="flex justify-center items-center h-[250px]">
<Spinner />
</div>
);
}

return (
<div className="flex flex-col">
<div className="h-[250px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<LineChart data={formattedData} margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<CartesianGrid
strokeDasharray="3 3"
horizontal={true}
Expand All @@ -59,58 +97,86 @@ export function TeamStatsChart() {
tickCount={8}
/>
<Tooltip content={<CustomTooltip />} />
<Line
type="monotone"
dataKey="tracked"
stroke="#2563eb"
strokeWidth={2}
dot={{ fill: "#2563eb", r: 4 }}
activeDot={{ r: 6, fill: "#2563eb" }}
/>
<Line
type="monotone"
dataKey="manual"
stroke="#dc2626"
strokeWidth={2}
dot={{ fill: "#dc2626", r: 4 }}
activeDot={{ r: 6, fill: "#dc2626" }}
/>
<Line
type="monotone"
dataKey="idle"
stroke="#eab308"
strokeWidth={2}
dot={{ fill: "#eab308", r: 4 }}
activeDot={{ r: 6, fill: "#eab308" }}
/>
{visibleLines.tracked && (
<Line
type="monotone"
dataKey="tracked"
stroke="#2563eb"
strokeWidth={2}
dot={{ fill: '#2563eb', r: 4 }}
activeDot={{ r: 6, fill: '#2563eb' }}
/>
)}
{visibleLines.manual && (
<Line
type="monotone"
dataKey="manual"
stroke="#dc2626"
strokeWidth={2}
dot={{ fill: '#dc2626', r: 4 }}
activeDot={{ r: 6, fill: '#dc2626' }}
/>
)}
{visibleLines.idle && (
<Line
type="monotone"
dataKey="idle"
stroke="#eab308"
strokeWidth={2}
dot={{ fill: '#eab308', r: 4 }}
activeDot={{ r: 6, fill: '#eab308' }}
/>
)}
{visibleLines.resumed && (
<Line
type="monotone"
dataKey="resumed"
stroke="#34c759"
strokeWidth={2}
dot={{ fill: '#34c759', r: 4 }}
activeDot={{ r: 6, fill: '#34c759' }}
/>
)}
</LineChart>
</ResponsiveContainer>
</div>
<div className="flex gap-3 justify-center -mt-2">
<Button
size="sm"
variant="outline"
className="gap-2 px-3 py-1.5 h-8 text-xs font-normal hover:bg-transparent hover:text-inherit"
className={`gap-2 px-3 py-1.5 h-8 text-xs font-normal hover:bg-transparent ${!visibleLines.tracked ? 'line-through' : ''}`}
onClick={() => toggleLine('tracked')}
>
<div className="w-2 h-2 bg-blue-500 rounded-full" />
Tracked
</Button>
<Button
size="sm"
variant="outline"
className="gap-2 px-3 py-1.5 h-8 text-xs font-normal hover:bg-transparent hover:text-inherit"
className={`gap-2 px-3 py-1.5 h-8 text-xs font-normal hover:bg-transparent ${!visibleLines.manual ? 'line-through' : ''}`}
onClick={() => toggleLine('manual')}
>
<div className="w-2 h-2 bg-red-500 rounded-full" />
Manual
</Button>
<Button
size="sm"
variant="outline"
className="gap-2 px-3 py-1.5 h-8 text-xs font-normal hover:bg-transparent hover:text-inherit"
className={`gap-2 px-3 py-1.5 h-8 text-xs font-normal hover:bg-transparent ${!visibleLines.idle ? 'line-through' : ''}`}
onClick={() => toggleLine('idle')}
>
<div className="w-2 h-2 bg-yellow-500 rounded-full" />
Idle
</Button>
<Button
size="sm"
variant="outline"
className={`gap-2 px-3 py-1.5 h-8 text-xs font-normal hover:bg-transparent ${!visibleLines.resumed ? 'line-through' : ''}`}
onClick={() => toggleLine('resumed')}
>
<div className="w-2 h-2 bg-green-500 rounded-full" />
Resumed
</Button>
</div>
</div>
);
Expand Down
22 changes: 15 additions & 7 deletions apps/web/app/[locale]/dashboard/team-dashboard/[teamId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useMemo } from 'react';
import { useParams,useRouter } from 'next/navigation';
import { useParams, useRouter } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { Card } from '@/components/ui/card';
import { ArrowLeftIcon } from 'lucide-react';
Expand All @@ -16,9 +16,11 @@ import { cn } from '@/lib/utils';
import { useAtomValue } from 'jotai';
import { fullWidthState } from '@app/stores/fullWidth';
import { withAuthentication } from '@/lib/app/authenticator';
import { useReportActivity } from '@app/hooks/features/useReportActivity';

function TeamDashboard() {
const { activeTeam, isTrackingEnabled } = useOrganizationTeams();
const { rapportChartActivity, updateDateRange, updateFilters, loadingTimeLogReportDailyChart } = useReportActivity();
const router = useRouter();
const t = useTranslations();
const fullWidth = useAtomValue(fullWidthState);
Expand All @@ -33,7 +35,6 @@ function TeamDashboard() {
],
[activeTeam?.name, currentLocale, t]
);

return (
<MainLayout
className="items-start pb-1 !overflow-hidden w-full"
Expand All @@ -49,18 +50,25 @@ function TeamDashboard() {
>
<ArrowLeftIcon className="text-dark dark:text-[#6b7280] h-6 w-6" />
</button>
<Breadcrumb paths={breadcrumbPath} className="text-sm" />
<Breadcrumb paths={breadcrumbPath} />
</div>
<div className="flex flex-col gap-4 px-4 pt-4 w-full">
<DashboardHeader />
<div className="flex flex-col gap-6 pb-6">
<DashboardHeader
onUpdateDateRange={updateDateRange}
onUpdateFilters={updateFilters}
/>
<TeamStatsGrid />
<Card className="p-6 w-full">
<TeamStatsChart />
<TeamStatsChart
rapportChartActivity={rapportChartActivity}
isLoading={loadingTimeLogReportDailyChart}
/>
</Card>
</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
70 changes: 56 additions & 14 deletions apps/web/app/hooks/features/useReportActivity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { ITimeLogReportDailyChartProps } from '@/app/interfaces/timer/ITimerLog';
import { getTimeLogReportDailyChart } from '@/app/services/client/api/timer/timer-log';
import { useAuthenticateUser } from './useAuthenticateUser';
Expand All @@ -8,45 +8,87 @@ import { timeLogsRapportChartState } from '@/app/stores';

type UseReportActivityProps = Omit<ITimeLogReportDailyChartProps, 'organizationId' | 'tenantId'>;

const getDefaultDates = () => {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);

return {
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0]
};
};

const defaultProps: UseReportActivityProps = {
activityLevel: { start: 0, end: 100 },
...getDefaultDates(),
start: 0,
end: 100
};

export function useReportActivity(props: UseReportActivityProps) {
export function useReportActivity() {
const { user } = useAuthenticateUser();
const [rapportChartActivity, setRapportChartActivity] = useAtom(timeLogsRapportChartState);
const { loading: loadingTimeLogReportDailyChart, queryCall: queryTimeLogReportDailyChart } = useQuery(getTimeLogReportDailyChart);
const [currentFilters, setCurrentFilters] = useState<Partial<UseReportActivityProps>>(defaultProps);

const fetchReportActivity = useCallback(async () => {
if (!user) {
return;
}
const fetchReportActivity = useCallback(async (customProps?: Partial<UseReportActivityProps>) => {
if (!user) return;
try {
const { data } = await queryTimeLogReportDailyChart({
...props,
const mergedProps = {
...defaultProps,
...currentFilters,
...(customProps || {}),
organizationId: user?.employee.organizationId,
tenantId: user?.tenantId ?? ''
});

};
const { data } = await queryTimeLogReportDailyChart(mergedProps);
if (data) {
setRapportChartActivity(data);
if (customProps) {
setCurrentFilters(prev => ({
...prev,
...customProps
}));
}
} else {
setRapportChartActivity([]);
}

} catch (err) {
console.error('Failed to fetch activity report:', err);
setRapportChartActivity([]);
}
}, [user, queryTimeLogReportDailyChart, props, setRapportChartActivity]);
}, [user, queryTimeLogReportDailyChart, currentFilters, setRapportChartActivity]);

const updateDateRange = useCallback((startDate: Date, endDate: Date) => {
const newProps = {
...currentFilters,
startDate: startDate.toISOString().split('T')[0],
endDate: endDate.toISOString().split('T')[0]
};
fetchReportActivity(newProps);
}, [fetchReportActivity, currentFilters]);

useEffect(() => {
const updateFilters = useCallback((newFilters: Partial<UseReportActivityProps>) => {
fetchReportActivity(newFilters);
}, [fetchReportActivity]);

const initialFetch = useCallback(() => {
if (user) {
fetchReportActivity();
}
}, [user, fetchReportActivity]);

useEffect(() => {
initialFetch();
}, [initialFetch]);


return {
loadingTimeLogReportDailyChart,
rapportChartActivity
rapportChartActivity,
updateDateRange,
updateFilters,
currentFilters
};
}

0 comments on commit 3001f8b

Please sign in to comment.