Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dev-dist/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
*/
workbox.precacheAndRoute([{
"url": "index.html",
"revision": "0.qebr9bqg7as"
"revision": "0.v4bl8mgtcdg"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
Expand Down
166 changes: 166 additions & 0 deletions src/components/ArchiveFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { X, ChevronDown, ChevronUp } from 'lucide-react';
import { Project } from '@/contexts/TimeTrackingContext';
import { TaskCategory } from '@/config/categories';

export interface ArchiveFilterState {
startDate: string;
endDate: string;
project: string;
category: string;
}

const ALL_PROJECTS = 'all-projects';
const ALL_CATEGORIES = 'all-categories';

interface ArchiveFilterProps {
filters: ArchiveFilterState;
onFilterChange: (filters: ArchiveFilterState) => void;
projects: Project[];
categories: TaskCategory[];
}

export const ArchiveFilter: React.FC<ArchiveFilterProps> = ({
filters,
onFilterChange,
projects,
categories
}) => {
const [isExpanded, setIsExpanded] = useState(true);

const handleReset = () => {
onFilterChange({
startDate: '',
endDate: '',
project: '',
category: ''
});
};

const isFilterActive =
filters.startDate || filters.endDate || filters.project || filters.category;

return (
<Card className="print:hidden">
<CardContent className="pt-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center space-x-2 hover:opacity-70 transition-opacity"
>
<h3 className="font-semibold text-lg">Filter Archive</h3>
{isExpanded ? (
<ChevronUp className="w-5 h-5" />
) : (
<ChevronDown className="w-5 h-5" />
)}
</button>
<Button
variant="outline"
size="sm"
onClick={handleReset}
disabled={!isFilterActive}
className="flex items-center space-x-2"
>
<X className="w-4 h-4" />
<span>Reset</span>
</Button>
</div>

{isExpanded && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Date Range */}
<div className="space-y-2">
<Label htmlFor="startDate">Start Date</Label>
<Input
id="startDate"
type="date"
value={filters.startDate}
onChange={(e) =>
onFilterChange({ ...filters, startDate: e.target.value })
}
/>
</div>

<div className="space-y-2">
<Label htmlFor="endDate">End Date</Label>
<Input
id="endDate"
type="date"
value={filters.endDate}
onChange={(e) =>
onFilterChange({ ...filters, endDate: e.target.value })
}
/>
</div>

{/* Project Filter */}
<div className="space-y-2">
<Label htmlFor="project">Project</Label>
<Select
value={filters.project || ALL_PROJECTS}
onValueChange={(value) =>
onFilterChange({
...filters,
project: value === ALL_PROJECTS ? '' : value
})
}
>
<SelectTrigger id="project">
<SelectValue placeholder="All Projects" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_PROJECTS}>All Projects</SelectItem>
{projects.map((project) => (
<SelectItem key={project.id} value={project.name}>
{project.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

{/* Category Filter */}
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Select
value={filters.category || ALL_CATEGORIES}
onValueChange={(value) =>
onFilterChange({
...filters,
category: value === ALL_CATEGORIES ? '' : value
})
}
>
<SelectTrigger id="category">
<SelectValue placeholder="All Categories" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_CATEGORIES}>All Categories</SelectItem>
{categories.map((category) => (
<SelectItem key={category.id} value={category.name}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
</div>
</CardContent>
</Card>
);
};
137 changes: 102 additions & 35 deletions src/pages/Archive.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useMemo } from 'react';
import {
TimeTrackingProvider,
DayRecord
Expand All @@ -8,6 +8,7 @@ import { ArchiveItem } from '@/components/ArchiveItem';
import { ArchiveEditDialog } from '@/components/ArchiveEditDialog';
import { ExportDialog } from '@/components/ExportDialog';
import { ProjectManagement } from '@/components/ProjectManagement';
import { ArchiveFilter, ArchiveFilterState } from '@/components/ArchiveFilter';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardTitle } from '@/components/ui/card';
import { Archive as ArchiveIcon, Database } from 'lucide-react';
Expand All @@ -18,31 +19,72 @@ import SiteNavigationMenu from '@/components/Navigation';
const ArchiveContent: React.FC = () => {
const {
archivedDays,
getTotalHoursForPeriod,
getRevenueForPeriod,
getHoursWorkedForDay,
getBillableHoursForDay,
getNonBillableHoursForDay
getNonBillableHoursForDay,
getRevenueForDay,
projects,
categories
} = useTimeTracking();
const [editingDay, setEditingDay] = useState<DayRecord | null>(null);
const [showExportDialog, setShowExportDialog] = useState(false);
const [showProjectManagement, setShowProjectManagement] = useState(false);
const [filters, setFilters] = useState<ArchiveFilterState>({
startDate: "",
endDate: "",
project: "",
category: ""
});

// Sort archived days from newest to oldest
const sortedDays = [...archivedDays].sort(
(a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
);
// Filter and sort archived days
const filteredDays = useMemo(() => {
let filtered = [...archivedDays];

// Apply date range filter
if (filters.startDate) {
const startDate = new Date(filters.startDate);
startDate.setHours(0, 0, 0, 0);
filtered = filtered.filter(day => {
const dayDate = new Date(day.startTime);
dayDate.setHours(0, 0, 0, 0);
return dayDate >= startDate;
});
}

if (filters.endDate) {
const endDate = new Date(filters.endDate);
endDate.setHours(23, 59, 59, 999);
filtered = filtered.filter(day => {
const dayDate = new Date(day.startTime);
return dayDate <= endDate;
});
}

// Apply project filter
if (filters.project) {
filtered = filtered.filter(day =>
day.tasks.some(task => task.project === filters.project)
);
}

// Calculate summary stats
const totalHours =
archivedDays.length > 0
? getTotalHoursForPeriod(new Date(0), new Date())
: 0;
const totalHoursWorked = archivedDays.reduce((sum, day) => sum + getHoursWorkedForDay(day), 0);
const totalBillableHours = archivedDays.reduce((sum, day) => sum + getBillableHoursForDay(day), 0);
const totalNonBillableHours = archivedDays.reduce((sum, day) => sum + getNonBillableHoursForDay(day), 0);
const totalRevenue =
archivedDays.length > 0 ? getRevenueForPeriod(new Date(0), new Date()) : 0;
// Apply category filter
if (filters.category) {
filtered = filtered.filter(day =>
day.tasks.some(task => task.category === filters.category)
);
}

// Sort from newest to oldest
return filtered.sort(
(a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
);
}, [archivedDays, filters]);

// Calculate summary stats based on filtered days
const totalHoursWorked = filteredDays.reduce((sum, day) => sum + getHoursWorkedForDay(day), 0);
const totalBillableHours = filteredDays.reduce((sum, day) => sum + getBillableHoursForDay(day), 0);
const totalNonBillableHours = filteredDays.reduce((sum, day) => sum + getNonBillableHoursForDay(day), 0);
const totalRevenue = filteredDays.reduce((sum, day) => sum + getRevenueForDay(day), 0);

const handleEdit = (day: DayRecord) => {
setEditingDay(day);
Expand Down Expand Up @@ -73,7 +115,7 @@ const ArchiveContent: React.FC = () => {
</div>
</div>
<div className="max-w-6xl mx-auto p-6 print:p-2">
{sortedDays.length === 0 ? (
{filteredDays.length === 0 && archivedDays.length === 0 ? (
<Card className="text-center py-12 print:hidden">
<CardContent>
<ArchiveIcon className="w-12 h-12 text-gray-400 mx-auto mb-4" />
Expand All @@ -88,18 +130,41 @@ const ArchiveContent: React.FC = () => {
</Card>
) : (
<div className="space-y-6 print:space-y-0">
{/* Summary Stats */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4 print:hidden">
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-blue-600">
{sortedDays.length}
</div>
<div className="text-sm text-gray-600">
Total Days Tracked
</div>
{/* Filter Component */}
<ArchiveFilter
filters={filters}
onFilterChange={setFilters}
projects={projects}
categories={categories}
/>

{filteredDays.length === 0 ? (
<Card className="text-center py-12">
<CardContent>
<ArchiveIcon className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<CardTitle className="mb-2">No Results Found</CardTitle>
<p className="text-gray-600 mb-4">
No archived days match your filter criteria.
</p>
<Button onClick={() => setFilters({ startDate: "", endDate: "", project: "", category: "" })}>
Reset Filters
</Button>
</CardContent>
</Card>
) : (
<>
{/* Summary Stats */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4 print:hidden">
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-blue-600">
{filteredDays.length}
</div>
<div className="text-sm text-gray-600">
Days Shown
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="text-2xl font-bold text-green-600">
Expand Down Expand Up @@ -137,12 +202,14 @@ const ArchiveContent: React.FC = () => {
</Card>
</div>

{/* Archived Days */}
<div className="space-y-4">
{sortedDays.map((day) => (
<ArchiveItem key={day.id} day={day} onEdit={handleEdit} />
))}
</div>
{/* Archived Days */}
<div className="space-y-4">
{filteredDays.map((day) => (
<ArchiveItem key={day.id} day={day} onEdit={handleEdit} />
))}
</div>
</>
)}
</div>
)}
</div>
Expand Down
Loading