diff --git a/apps/web/app/[locale]/dashboard/app-url/page.tsx b/apps/web/app/[locale]/dashboard/app-url/page.tsx new file mode 100644 index 000000000..fd86ca5ad --- /dev/null +++ b/apps/web/app/[locale]/dashboard/app-url/page.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +function AppUrls() { + return ( +
+ +
+ ) +} + +export default AppUrls diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/components/dashboard-header.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/components/dashboard-header.tsx new file mode 100644 index 000000000..ff2b73248 --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/components/dashboard-header.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { DateRangePicker } from "./date-range-picker"; + +export function DashboardHeader() { + return ( +
+

Team Dashboard

+
+ + + +
+
+ ); +} diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/components/date-range-picker.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/components/date-range-picker.tsx new file mode 100644 index 000000000..fe2255649 --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/components/date-range-picker.tsx @@ -0,0 +1,218 @@ +"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"; + +interface DateRangePickerProps { + className?: string; + onDateRangeChange?: (range: DateRange | undefined) => void; +} + +export function DateRangePicker({ className, onDateRangeChange }: DateRangePickerProps) { + const [dateRange, setDateRange] = React.useState({ + from: new Date(), + to: new Date(), + }); + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + const [currentMonth, setCurrentMonth] = React.useState(new Date()); + + const handleDateRangeChange = (range: DateRange | undefined) => { + setDateRange(range); + onDateRangeChange?.(range); + }; + + const predefinedRanges = [ + { + label: "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: "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: "Current 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: "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: "Current 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: "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 ( + + + + + e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onChange={(e) => e.stopPropagation()} + className="p-0 w-auto" + align="center" + > +
+
+ {predefinedRanges.map((range) => ( + + ))} +
+
+ +
+
+
+ + +
+
+
+ ); +} diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-chart.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-chart.tsx new file mode 100644 index 000000000..941c667fd --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-chart.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid } from "recharts"; +import { Button } from "@/components/ui/button"; +import { chartData } from "../data/mock-data"; + +interface TooltipProps { + active?: boolean; + payload?: { + name: string; + value: number; + color: string; + }[]; + label?: string; +} +const CustomTooltip = ({ active, payload, label }: TooltipProps) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload.map((item, index) => ( +

+ {item.name}: {item.value} +

+ ))} +
+ ); + } + return null; +}; + +export function TeamStatsChart() { + return ( +
+
+ + + + + `${value}`} + padding={{ top: 10, bottom: 10 }} + tickCount={8} + /> + } /> + + + + + +
+
+ + + +
+
+ ); +} diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-grid.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-grid.tsx new file mode 100644 index 000000000..42dc713b9 --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-grid.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { Card } from "@/components/ui/card"; + +const stats = [ + { + title: "Members worked", + value: "17", + type: "number" + }, + { + title: "Tracked", + value: "47:23", + type: "time", + color: "text-blue-500", + progress: 70, + progressColor: "bg-blue-500" + }, + { + title: "Manual", + value: "18:33", + type: "time", + color: "text-red-500", + progress: 30, + progressColor: "bg-red-500" + }, + { + title: "Idle", + value: "05:10", + type: "time", + color: "text-yellow-500", + progress: 10, + progressColor: "bg-yellow-500" + }, + { + title: "Total Hours", + value: "70:66", + type: "time" + } +]; + +export function TeamStatsGrid() { + return ( + <> + +
+ {stats.map((stat) => ( + +
+ {stat.title} + + {stat.value} + + {stat.progress && ( +
+
+
+
+
+ )} +
+ + ))} +
+ + ); +} diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-table.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-table.tsx new file mode 100644 index 000000000..2de3a9c95 --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/components/team-stats-table.tsx @@ -0,0 +1,91 @@ +'use client'; + +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'; +import { members } from '../data/mock-data'; + +const getProgressColor = (activityLevel: string) => { + const level = parseInt(activityLevel, 10); + if (isNaN(level) || level < 0) return 'bg-gray-300'; + if (level > 100) return 'bg-green-500'; + if (level <= 20) return 'bg-red-500'; + if (level <= 50) return 'bg-yellow-500'; + return 'bg-green-500'; +}; + +export function TeamStatsTable() { + return ( +
+
+ + + + Member + Tracked Time + Manual Time + Idle Time + Unknown Activity + Activity Level + + + + {members.map((member) => ( + + +
+ + + {member.name[0]} + + {member.name} +
+
+ {member.trackedTime} + {member.manualTime} + {member.idleTime} + {member.unknownActivity} + +
+
+
+
+ {member.activityLevel} +
+ + + ))} + +
+
+
+
+

+ Showing 1 to {members.length} of {members.length} entries +

+
+
+ + + + + +
+
+
+ ); +} diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/data/mock-data.ts b/apps/web/app/[locale]/dashboard/team-dashboard/data/mock-data.ts new file mode 100644 index 000000000..8da4efc4b --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/data/mock-data.ts @@ -0,0 +1,74 @@ +export const chartData = [ + { + date: "Mon", + tracked: 4000, + manual: 2400, + idle: 2400, + }, + { + date: "Tue", + tracked: 3000, + manual: 1398, + idle: 2210, + }, + { + date: "Wed", + tracked: 2000, + manual: 9800, + idle: 2290, + }, + { + date: "Thu", + tracked: 2780, + manual: 3908, + idle: 2000, + }, + { + date: "Fri", + tracked: 1890, + manual: 4800, + idle: 2181, + }, + { + date: "Sat", + tracked: 2390, + manual: 3800, + idle: 2500, + }, + { + date: "Sun", + tracked: 3490, + manual: 4300, + idle: 2100, + }, +]; + +export const members = [ + { + name: "Elanor Pena", + avatar: "/avatars/01.png", + trackedTime: "8h 12m", + manualTime: "1h 5m", + idleTime: "45m", + unknownActivity: "15m", + activityLevel: "85%", + }, + { + name: "Devon Lane", + avatar: "/avatars/02.png", + trackedTime: "7h 30m", + manualTime: "2h 15m", + idleTime: "30m", + unknownActivity: "10m", + activityLevel: "45%", + }, + { + name: "Brooklyn Simmons", + avatar: "/avatars/03.png", + trackedTime: "6h 45m", + manualTime: "1h 30m", + idleTime: "1h", + unknownActivity: "20m", + activityLevel: "15%", + }, +]; diff --git a/apps/web/app/[locale]/dashboard/team-dashboard/page.tsx b/apps/web/app/[locale]/dashboard/team-dashboard/page.tsx new file mode 100644 index 000000000..4de204a6b --- /dev/null +++ b/apps/web/app/[locale]/dashboard/team-dashboard/page.tsx @@ -0,0 +1,77 @@ +'use client'; + +import { useMemo } from 'react'; +import { useParams,useRouter } from 'next/navigation'; +import { useTranslations } from 'next-intl'; +import { Card } from '@/components/ui/card'; +import { ArrowLeftIcon } from 'lucide-react'; +import { TeamStatsChart } from './components/team-stats-chart'; +import { TeamStatsGrid } from './components/team-stats-grid'; +import { TeamStatsTable } from './components/team-stats-table'; +import { DashboardHeader } from './components/dashboard-header'; +import { useOrganizationTeams } from '@app/hooks/features/useOrganizationTeams'; +import { MainLayout } from '@/lib/layout'; +import { Breadcrumb, Container } from '@/lib/components'; +import { cn } from '@/lib/utils'; +import { useAtomValue } from 'jotai'; +import { fullWidthState } from '@app/stores/fullWidth'; +import { withAuthentication } from '@/lib/app/authenticator'; + +function TeamDashboard() { + 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: 'team-dashboard', href: `/${currentLocale}/dashboard/team-dashboard` } + ], + [activeTeam?.name, currentLocale, t] + ); + + return ( + + +
+ {' '} + +
+
+ + + + + +
+
+
+ } + > + + + + + + + ); +} + +export default withAuthentication(TeamDashboard, { + displayName: 'Team-dashboard', + showPageSkeleton: true +}); diff --git a/apps/web/components/ui/card.tsx b/apps/web/components/ui/card.tsx index fbb0c3772..e75b9a0d8 100644 --- a/apps/web/components/ui/card.tsx +++ b/apps/web/components/ui/card.tsx @@ -1,43 +1,79 @@ -import * as React from 'react'; - -import { cn } from '@/lib/utils'; - -const Card = React.forwardRef>(({ className, ...props }, ref) => ( -
-)); -Card.displayName = 'Card'; - -const CardHeader = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardHeader.displayName = 'CardHeader'; - -const CardTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardTitle.displayName = 'CardTitle'; - -const CardDescription = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardDescription.displayName = 'CardDescription'; - -const CardContent = React.forwardRef>( - ({ className, ...props }, ref) =>
-); -CardContent.displayName = 'CardContent'; - -const CardFooter = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); -CardFooter.displayName = 'CardFooter'; - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/apps/web/components/ui/chart.tsx b/apps/web/components/ui/chart.tsx new file mode 100644 index 000000000..0c401945f --- /dev/null +++ b/apps/web/components/ui/chart.tsx @@ -0,0 +1,363 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +