UIPackage
Menu

Framework

Change language

Boilerplate repo

Time Tracker Timesheet

blockproductivity

Toggl/Harvest-style live time tracker with active running stopwatch, project rate selection, billable status toggle, 4 weekly summary metric cards, and daily grouped timesheet activity ledger with resume actions.

Also available for Vue ->

Installation

$npx shadcn@latest add https://uipkge.dev/r/react/time-tracker-timesheet.json
Named registry:npx shadcn@latest add @uipkge-react/time-tracker-timesheetInstalls to:components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
titlestringoptional
subtitlestringoptional
classNamestringoptional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ProjectOption
interface ProjectOption {
  id: string
  name: string
  client: string
  rate: number
  rateLabel: string
  color: string
  badgeClass: string
  dotClass: string
}
TimeEntry
interface TimeEntry {
  id: string
  task: string
  projectId: string
  projectName: string
  clientName: string
  projectBadgeClass: string
  date: string
  dateLabel: string
  startTime: string
  endTime: string
  durationSeconds: number
  isBillable: boolean
  rate: number
  amount: number
}

Files installed (1)

  • components/blocks/TimeTrackerTimesheet.tsx25.5 kB
    'use client'
    
    import * as React from 'react'
    import {
      Briefcase,
      Calendar,
      CircleDollarSign,
      Clock,
      DollarSign,
      Download,
      Play,
      Plus,
      Square,
      Trash2,
      TrendingUp,
    } from 'lucide-react'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
    import { Input } from '@/components/ui/input'
    import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
    import { Separator } from '@/components/ui/separator'
    import { Switch } from '@/components/ui/switch'
    import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
    
    export interface ProjectOption {
      id: string
      name: string
      client: string
      rate: number
      rateLabel: string
      color: string
      badgeClass: string
      dotClass: string
    }
    
    export interface TimeEntry {
      id: string
      task: string
      projectId: string
      projectName: string
      clientName: string
      projectBadgeClass: string
      date: string
      dateLabel: string
      startTime: string
      endTime: string
      durationSeconds: number
      isBillable: boolean
      rate: number
      amount: number
    }
    
    const projects: ProjectOption[] = [
      {
        id: 'acme',
        name: 'Acme Corp',
        client: 'Acme Enterprises',
        rate: 150,
        rateLabel: '$150/hr',
        color: 'blue',
        badgeClass: 'bg-info/10 text-info border-info/20',
        dotClass: 'bg-info',
      },
      {
        id: 'stripe',
        name: 'Stripe Retainer',
        client: 'Stripe, Inc.',
        rate: 175,
        rateLabel: '$175/hr',
        color: 'chart-2',
        badgeClass: 'bg-chart-2/10 text-chart-2 border-chart-2/20',
        dotClass: 'bg-chart-2',
      },
      {
        id: 'linear',
        name: 'Linear Sync',
        client: 'Linear Orbit',
        rate: 160,
        rateLabel: '$160/hr',
        color: 'chart-3',
        badgeClass: 'bg-chart-3/10 text-chart-3 border-chart-3/20',
        dotClass: 'bg-chart-3',
      },
      {
        id: 'uipkge',
        name: 'UIPKGE Core',
        client: 'Internal OSS',
        rate: 0,
        rateLabel: 'Internal · $0/hr',
        color: 'emerald',
        badgeClass: 'bg-success/10 text-success border-success/20',
        dotClass: 'bg-success',
      },
    ]
    
    const initialEntries: TimeEntry[] = [
      {
        id: 'entry-1',
        task: 'Refactoring OKLCH Color Engine & AST Parser',
        projectId: 'acme',
        projectName: 'Acme Corp',
        clientName: 'Acme Enterprises',
        projectBadgeClass: 'bg-info/10 text-info border-info/20',
        date: '2026-08-21',
        dateLabel: 'Today · Friday, Aug 21',
        startTime: '09:00',
        endTime: '11:30',
        durationSeconds: 9000,
        isBillable: true,
        rate: 150,
        amount: 375,
      },
      {
        id: 'entry-2',
        task: 'Webhook idempotency retry queue architecture',
        projectId: 'stripe',
        projectName: 'Stripe Retainer',
        clientName: 'Stripe, Inc.',
        projectBadgeClass: 'bg-chart-2/10 text-chart-2 border-chart-2/20',
        date: '2026-08-21',
        dateLabel: 'Today · Friday, Aug 21',
        startTime: '13:00',
        endTime: '15:45',
        durationSeconds: 9900,
        isBillable: true,
        rate: 175,
        amount: 481.25,
      },
      {
        id: 'entry-3',
        task: 'Weekly sprint retrospective & design token review',
        projectId: 'uipkge',
        projectName: 'UIPKGE Core',
        clientName: 'Internal OSS',
        projectBadgeClass: 'bg-success/10 text-success border-success/20',
        date: '2026-08-21',
        dateLabel: 'Today · Friday, Aug 21',
        startTime: '16:00',
        endTime: '17:30',
        durationSeconds: 5400,
        isBillable: false,
        rate: 0,
        amount: 0,
      },
      {
        id: 'entry-4',
        task: 'Bidirectional GraphQL sync engine & schema migrations',
        projectId: 'linear',
        projectName: 'Linear Sync',
        clientName: 'Linear Orbit',
        projectBadgeClass: 'bg-chart-3/10 text-chart-3 border-chart-3/20',
        date: '2026-08-20',
        dateLabel: 'Yesterday · Thursday, Aug 20',
        startTime: '09:30',
        endTime: '14:00',
        durationSeconds: 16200,
        isBillable: true,
        rate: 160,
        amount: 720,
      },
      {
        id: 'entry-5',
        task: 'Design system Figma token export automation',
        projectId: 'acme',
        projectName: 'Acme Corp',
        clientName: 'Acme Enterprises',
        projectBadgeClass: 'bg-info/10 text-info border-info/20',
        date: '2026-08-20',
        dateLabel: 'Yesterday · Thursday, Aug 20',
        startTime: '14:45',
        endTime: '17:30',
        durationSeconds: 9900,
        isBillable: true,
        rate: 150,
        amount: 412.5,
      },
    ]
    
    function formatDuration(totalSeconds: number): string {
      const hours = Math.floor(totalSeconds / 3600)
      const minutes = Math.floor((totalSeconds % 3600) / 60)
      const seconds = totalSeconds % 60
      return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
    }
    
    function formatCurrency(val: number): string {
      return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
        minimumFractionDigits: 2,
      }).format(val)
    }
    
    export interface TimeTrackerTimesheetProps {
      title?: string
      subtitle?: string
      className?: string
    }
    
    export function TimeTrackerTimesheet({
      title = 'Time Tracker & Timesheet',
      subtitle = 'Track live project hours, calculate client billing rates, and review weekly activity.',
      className,
    }: TimeTrackerTimesheetProps) {
      const [entries, setEntries] = React.useState<TimeEntry[]>(initialEntries)
      const [activeTask, setActiveTask] = React.useState('Refactoring OKLCH Color Engine & AST Parser')
      const [selectedProjectId, setSelectedProjectId] = React.useState('acme')
      const [isBillable, setIsBillable] = React.useState(true)
      const [isRunning, setIsRunning] = React.useState(true)
      const [timerSeconds, setTimerSeconds] = React.useState(6138) // 01:42:18
    
      React.useEffect(() => {
        let interval: ReturnType<typeof setInterval> | null = null
        if (isRunning) {
          interval = setInterval(() => {
            setTimerSeconds((prev) => prev + 1)
          }, 1000)
        }
        return () => {
          if (interval) clearInterval(interval)
        }
      }, [isRunning])
    
      const toggleTimer = () => {
        setIsRunning((prev) => !prev)
      }
    
      const resumeEntry = (entry: TimeEntry) => {
        setActiveTask(entry.task)
        setSelectedProjectId(entry.projectId)
        setIsBillable(entry.isBillable)
        setIsRunning(true)
      }
    
      const deleteEntry = (id: string) => {
        setEntries((prev) => prev.filter((e) => e.id !== id))
      }
    
      const groupedEntries = React.useMemo(() => {
        const groups: Record<
          string,
          { label: string; date: string; entries: TimeEntry[]; totalSeconds: number; totalAmount: number }
        > = {}
    
        for (const entry of entries) {
          if (!groups[entry.date]) {
            groups[entry.date] = {
              label: entry.dateLabel,
              date: entry.date,
              entries: [],
              totalSeconds: 0,
              totalAmount: 0,
            }
          }
          groups[entry.date].entries.push(entry)
          groups[entry.date].totalSeconds += entry.durationSeconds
          groups[entry.date].totalAmount += entry.amount
        }
    
        return Object.values(groups)
      }, [entries])
    
      const totalWeeklySeconds = React.useMemo(() => entries.reduce((acc, e) => acc + e.durationSeconds, 0), [entries])
      const totalWeeklyAmount = React.useMemo(() => entries.reduce((acc, e) => acc + e.amount, 0), [entries])
    
      const formattedTimer = formatDuration(timerSeconds)
    
      return (
        <div data-slot="time-tracker-timesheet" className={cn('w-full space-y-6', className)}>
          {/* Header Section */}
          <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
            <div className="space-y-1">
              <div className="flex items-center gap-2.5">
                <h2 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">{title}</h2>
                <Badge variant="outline" className="border-border hidden text-xs font-medium sm:inline-flex">
                  Toggl Engine
                </Badge>
              </div>
              <p className="text-muted-foreground text-sm">{subtitle}</p>
            </div>
    
            <div className="flex items-center gap-2.5">
              <Button aria-label="Download attachment" variant="outline" size="sm" className="h-9 gap-1.5 shadow-xs">
                <Download className="size-4" aria-hidden="true" />
                Export CSV
              </Button>
              <Button size="sm" className="h-9 gap-1.5 shadow-xs">
                <Plus className="size-4" aria-hidden="true" />
                Manual Entry
              </Button>
            </div>
          </div>
    
          {/* Live Running Stopwatch Hero Bar */}
          <Card className="border-border bg-card overflow-hidden shadow-xs">
            <CardContent className="p-4 sm:p-5">
              <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
                {/* Task description & Project Tag Select */}
                <div className="flex flex-1 flex-col gap-3 sm:flex-row sm:items-center">
                  {/* Task Description Input */}
                  <div className="relative flex-1">
                    <Input
                      value={activeTask}
                      onChange={(e) => setActiveTask(e.target.value)}
                      type="text"
                      placeholder="What are you working on?"
                      className="h-10 text-sm font-medium focus-visible:ring-2"
                    />
                  </div>
    
                  {/* Project / Client Tag Select */}
                  <div className="w-full shrink-0 sm:w-64">
                    <Select value={selectedProjectId} onValueChange={setSelectedProjectId}>
                      <SelectTrigger className="h-10 text-xs sm:text-sm">
                        <SelectValue placeholder="Select client/project" />
                      </SelectTrigger>
                      <SelectContent>
                        {projects.map((proj) => (
                          <SelectItem key={proj.id} value={proj.id}>
                            <div className="flex items-center gap-2">
                              <span className={cn('size-2 shrink-0 rounded-full', proj.dotClass)} />
                              <span className="text-foreground font-medium">{proj.name}</span>
                              <span className="text-muted-foreground text-xs">· {proj.rateLabel}</span>
                            </div>
                          </SelectItem>
                        ))}
                      </SelectContent>
                    </Select>
                  </div>
                </div>
    
                {/* Controls: Billable Switch, Live Stopwatch, Primary Action Button */}
                <div className="border-border/60 flex flex-wrap items-center justify-between gap-3 border-t pt-3 sm:border-t-0 sm:pt-0 lg:justify-end">
                  {/* Billable Switch */}
                  <div className="flex items-center gap-2">
                    <Switch id="billable-toggle-react" checked={isBillable} onCheckedChange={setIsBillable} size="sm" />
                    <label
                      htmlFor="billable-toggle-react"
                      className="flex cursor-pointer items-center gap-1.5 text-xs font-medium select-none"
                    >
                      <DollarSign
                        className={cn('size-3.5', isBillable ? 'text-success font-semibold' : 'text-muted-foreground')}
                        aria-hidden="true"
                      />
                      <span className={isBillable ? 'text-foreground font-medium' : 'text-muted-foreground'}>Billable</span>
                    </label>
                  </div>
    
                  <Separator orientation="vertical" className="hidden h-7 sm:block" />
    
                  {/* Live Running Stopwatch Timer */}
                  <div className="flex items-center gap-2.5">
                    <div className="border-border/80 bg-muted/50 dark:bg-muted/30 flex items-center gap-2.5 rounded-lg border px-3.5 py-1.5 shadow-xs">
                      <div className="relative flex size-2.5 items-center justify-center">
                        {isRunning && (
                          <span className="bg-success absolute inline-flex h-full w-full rounded-full opacity-75" />
                        )}
                        <span
                          className={cn(
                            'relative inline-flex size-2.5 rounded-full',
                            isRunning ? 'bg-success' : 'bg-muted-foreground/40',
                          )}
                        />
                      </div>
                      <span className="text-foreground font-mono text-xl font-bold tracking-tight tabular-nums sm:text-2xl">
                        {formattedTimer}
                      </span>
                    </div>
    
                    {/* Primary Start / Stop Button */}
                    <Button
                      variant={isRunning ? 'destructive' : 'default'}
                      size="default"
                      className="h-10 gap-2 px-4 font-medium shadow-xs"
                      onClick={toggleTimer}
                    >
                      {isRunning ? (
                        <Square className="size-4 fill-current" aria-hidden="true" />
                      ) : (
                        <Play className="size-4 fill-current" aria-hidden="true" />
                      )}
                      <span>{isRunning ? 'Stop' : 'Start'}</span>
                    </Button>
                  </div>
                </div>
              </div>
            </CardContent>
          </Card>
    
          {/* 4 Timesheet Summary Cards */}
          <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            {/* 1. Total Hours This Week */}
            <Card className="border-border shadow-xs">
              <CardContent className="space-y-2 p-5">
                <div className="flex items-center justify-between">
                  <p className="text-muted-foreground text-sm font-medium">Total Hours This Week</p>
                  <div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg">
                    <Clock className="size-4" aria-hidden="true" />
                  </div>
                </div>
                <div className="flex items-baseline gap-2">
                  <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums">38.5 hrs</p>
                  <span className="text-muted-foreground text-xs tabular-nums">/ 40.0 target</span>
                </div>
                <div className="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <span className="text-success inline-flex items-center font-medium">
                    <TrendingUp className="mr-0.5 size-3.5" aria-hidden="true" />
                    96%
                  </span>
                  <span>of weekly target logged</span>
                </div>
              </CardContent>
            </Card>
    
            {/* 2. Billable Hours */}
            <Card className="border-border shadow-xs">
              <CardContent className="space-y-2 p-5">
                <div className="flex items-center justify-between">
                  <p className="text-muted-foreground text-sm font-medium">Billable Hours</p>
                  <div className="bg-success/10 text-success flex size-8 items-center justify-center rounded-lg">
                    <CircleDollarSign className="size-4" aria-hidden="true" />
                  </div>
                </div>
                <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums">32.0 hrs</p>
                <div className="text-muted-foreground flex items-center justify-between text-xs">
                  <span className="text-success font-medium">83% billable ratio</span>
                  <span>6.5 hrs internal</span>
                </div>
              </CardContent>
            </Card>
    
            {/* 3. Total Earnings */}
            <Card className="border-border shadow-xs">
              <CardContent className="space-y-2 p-5">
                <div className="flex items-center justify-between">
                  <p className="text-muted-foreground text-sm font-medium">Total Earnings</p>
                  <div className="bg-chart-2/10 text-chart-2 flex size-8 items-center justify-center rounded-lg">
                    <DollarSign className="size-4" aria-hidden="true" />
                  </div>
                </div>
                <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums">$4,800.00</p>
                <p className="text-muted-foreground text-xs">Avg $150.00/hr effective rate</p>
              </CardContent>
            </Card>
    
            {/* 4. Active Clients */}
            <Card className="border-border shadow-xs">
              <CardContent className="space-y-2 p-5">
                <div className="flex items-center justify-between">
                  <p className="text-muted-foreground text-sm font-medium">Active Clients</p>
                  <div className="bg-info/10 text-info flex size-8 items-center justify-center rounded-lg">
                    <Briefcase className="size-4" aria-hidden="true" />
                  </div>
                </div>
                <p className="text-foreground text-2xl font-bold tracking-tight tabular-nums">3 active clients</p>
                <p className="text-muted-foreground truncate text-xs">Acme Corp · Stripe · Linear</p>
              </CardContent>
            </Card>
          </div>
    
          {/* Weekly Timesheet & Daily Activity Table */}
          <Card className="border-border bg-card shadow-xs">
            <CardHeader className="border-border/60 border-b pb-3">
              <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <CardTitle className="text-base font-semibold">Weekly Timesheet & Daily Activity</CardTitle>
                  <CardDescription className="text-xs">
                    Detailed time entries for the current billing cycle (Aug 17 – Aug 23, 2026).
                  </CardDescription>
                </div>
                <div className="flex items-center gap-2">
                  <Badge variant="outline" className="border-border font-mono text-xs font-medium">
                    Week 34 · 38h 30m · $4,800.00
                  </Badge>
                </div>
              </div>
            </CardHeader>
    
            <CardContent className="p-0">
              <div className="overflow-x-auto">
                <Table>
                  <TableHeader className="bg-muted/40">
                    <TableRow>
                      <TableHead className="min-w-[160px]">Project & Client</TableHead>
                      <TableHead className="min-w-[280px]">Task Description</TableHead>
                      <TableHead className="min-w-[130px]">Billable</TableHead>
                      <TableHead className="min-w-[130px]">Time Range</TableHead>
                      <TableHead className="min-w-[110px] text-right">Duration</TableHead>
                      <TableHead className="min-w-[110px] text-right">Amount</TableHead>
                      <TableHead className="w-24 text-right">
                        <span className="sr-only">Actions</span>
                      </TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {groupedEntries.map((group) => (
                      <React.Fragment key={group.date}>
                        {/* Day Group Header Subrow */}
                        <TableRow className="border-border bg-muted/20 hover:bg-muted/30 border-y font-medium">
                          <TableCell colSpan={4} className="text-foreground py-2.5 text-xs font-semibold">
                            <div className="flex items-center gap-2">
                              <Calendar className="text-muted-foreground size-3.5" aria-hidden="true" />
                              <span>{group.label}</span>
                            </div>
                          </TableCell>
                          <TableCell className="text-foreground py-2.5 text-right font-mono text-xs font-semibold tabular-nums">
                            {formatDuration(group.totalSeconds)}
                          </TableCell>
                          <TableCell className="text-foreground py-2.5 text-right font-mono text-xs font-semibold tabular-nums">
                            {formatCurrency(group.totalAmount)}
                          </TableCell>
                          <TableCell className="py-2.5" />
                        </TableRow>
    
                        {/* Entries for this day */}
                        {group.entries.map((entry) => (
                          <TableRow key={entry.id} className="hover:bg-muted/50 transition-colors">
                            {/* Project Badge & Client */}
                            <TableCell>
                              <div className="flex flex-col gap-1">
                                <Badge
                                  variant="outline"
                                  className={cn('w-fit text-xs font-normal', entry.projectBadgeClass)}
                                >
                                  {entry.projectName}
                                </Badge>
                                <span className="text-muted-foreground text-xs">{entry.clientName}</span>
                              </div>
                            </TableCell>
    
                            {/* Task description */}
                            <TableCell>
                              <p className="text-foreground text-sm font-medium">{entry.task}</p>
                            </TableCell>
    
                            {/* Billable Status */}
                            <TableCell>
                              <div className="flex items-center gap-1.5">
                                {entry.isBillable ? (
                                  <Badge
                                    variant="outline"
                                    className="border-success/30 bg-success/10 text-success text-xs font-medium"
                                  >
                                    <DollarSign className="mr-0.5 size-3" />
                                    Billable (${entry.rate}/h)
                                  </Badge>
                                ) : (
                                  <Badge
                                    variant="outline"
                                    className="border-border bg-muted/60 text-muted-foreground text-xs font-normal"
                                  >
                                    Non-billable
                                  </Badge>
                                )}
                              </div>
                            </TableCell>
    
                            {/* Time Range */}
                            <TableCell className="text-muted-foreground font-mono text-xs whitespace-nowrap tabular-nums">
                              {entry.startTime}{entry.endTime}
                            </TableCell>
    
                            {/* Duration */}
                            <TableCell className="text-foreground text-right font-mono text-sm font-medium whitespace-nowrap tabular-nums">
                              {formatDuration(entry.durationSeconds)}
                            </TableCell>
    
                            {/* Billable Amount */}
                            <TableCell className="text-right font-mono text-sm font-semibold whitespace-nowrap tabular-nums">
                              <span className={entry.isBillable ? 'text-foreground' : 'text-muted-foreground'}>
                                {formatCurrency(entry.amount)}
                              </span>
                            </TableCell>
    
                            {/* Actions */}
                            <TableCell className="text-right">
                              <div className="flex items-center justify-end gap-1">
                                <Button
                                  variant="ghost"
                                  size="icon-sm"
                                  className="text-muted-foreground hover:text-foreground size-8"
                                  title="Resume Timer"
                                  onClick={() => resumeEntry(entry)}
                                >
                                  <Play className="size-3.5 fill-current" aria-hidden="true" />
                                  <span className="sr-only">Resume timer for {entry.task}</span>
                                </Button>
                                <Button
                                  variant="ghost"
                                  size="icon-sm"
                                  className="text-muted-foreground hover:text-destructive size-8"
                                  title="Delete Entry"
                                  onClick={() => deleteEntry(entry.id)}
                                >
                                  <Trash2 className="size-3.5" aria-hidden="true" />
                                  <span className="sr-only">Delete entry</span>
                                </Button>
                              </div>
                            </TableCell>
                          </TableRow>
                        ))}
                      </React.Fragment>
                    ))}
    
                    {/* Empty state */}
                    {entries.length === 0 && (
                      <TableRow>
                        <TableCell colSpan={7} className="h-32 text-center">
                          <div className="text-muted-foreground flex flex-col items-center justify-center gap-1.5">
                            <Clock className="text-muted-foreground/60 size-6" aria-hidden="true" />
                            <p className="text-foreground text-sm font-medium">No time entries recorded for this week</p>
                            <p className="text-xs">Start the live timer above to log your hours.</p>
                          </div>
                        </TableCell>
                      </TableRow>
                    )}
                  </TableBody>
                </Table>
              </div>
    
              {/* Timesheet Table Footer Strip */}
              <div className="border-border bg-card text-muted-foreground flex flex-col items-center justify-between gap-3 border-t px-4 py-3 text-xs sm:flex-row">
                <p className="tabular-nums">
                  Showing <span className="text-foreground font-medium">{entries.length}</span> time entries across{' '}
                  <span className="text-foreground font-medium">{groupedEntries.length}</span> days
                </p>
                <div className="flex items-center gap-3">
                  <span className="text-foreground font-mono text-xs font-medium tabular-nums">
                    Total: {formatDuration(totalWeeklySeconds)} ({formatCurrency(totalWeeklyAmount)})
                  </span>
                </div>
              </div>
            </CardContent>
          </Card>
        </div>
      )
    }
    

Raw manifest:https://uipkge.dev/r/react/time-tracker-timesheet.json