Skip to content

Commit 8ad90d8

Browse files
committed
feat: add summary to each archived day
1 parent 1850307 commit 8ad90d8

File tree

4 files changed

+171
-58
lines changed

4 files changed

+171
-58
lines changed

dev-dist/sw.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
7979
*/
8080
workbox.precacheAndRoute([{
8181
"url": "index.html",
82-
"revision": "0.puuvtj6d5ts"
82+
"revision": "0.qebr9bqg7as"
8383
}], {});
8484
workbox.cleanupOutdatedCaches();
8585
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

src/components/ArchiveItem.tsx

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import React from 'react';
2-
import { Button } from '@/components/ui/button';
3-
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
1+
import React from "react";
2+
import { Button } from "@/components/ui/button";
3+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
44
import {
5-
Table,
6-
TableBody,
7-
TableCell,
8-
TableHead,
9-
TableHeader,
10-
TableRow
11-
} from '@/components/ui/table';
12-
import { Calendar, Clock, Edit, RotateCcw } from 'lucide-react';
13-
import { formatDuration, formatDurationLong, formatTime, formatDate } from '@/utils/timeUtil';
14-
import { DayRecord } from '@/contexts/TimeTrackingContext';
15-
import { useTimeTracking } from '@/hooks/useTimeTracking';
5+
Table,
6+
TableBody,
7+
TableCell,
8+
TableHead,
9+
TableHeader,
10+
TableRow
11+
} from "@/components/ui/table";
12+
import { Calendar, Clock, Edit, RotateCcw, FileText } from "lucide-react";
13+
import { formatDuration, formatDurationLong, formatTime, formatDate, generateDailySummary } from "@/utils/timeUtil";
14+
import { DayRecord } from "@/contexts/TimeTrackingContext";
15+
import { useTimeTracking } from "@/hooks/useTimeTracking";
1616

1717
interface ArchiveItemProps {
1818
day: DayRecord;
@@ -118,6 +118,30 @@ export const ArchiveItem: React.FC<ArchiveItemProps> = ({ day, onEdit }) => {
118118
</div>
119119
</div>
120120

121+
{/* Daily Summary */}
122+
{(() => {
123+
const descriptions = day.tasks
124+
.filter((task) => task.description)
125+
.map((task) => task.description!);
126+
const summary = generateDailySummary(descriptions);
127+
128+
if (!summary) return null;
129+
130+
return (
131+
<div className="space-y-2 border-t pt-4">
132+
<h4 className="font-medium text-gray-900 flex items-center mb-2">
133+
<FileText className="w-4 h-4 mr-2" />
134+
Daily Summary
135+
</h4>
136+
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-md print:bg-white print:border print:border-gray-300">
137+
<p className="text-sm text-gray-700 dark:text-gray-300 print:text-gray-800 leading-relaxed">
138+
{summary}
139+
</p>
140+
</div>
141+
</div>
142+
);
143+
})()}
144+
121145
{/* Tasks Table */}
122146
<div className="print:mt-2">
123147
<h4 className="font-medium text-gray-900 print:hidden mb-2">

src/contexts/TimeTrackingContext.tsx

Lines changed: 86 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import React, {
2-
createContext,
3-
useContext,
4-
useState,
5-
useEffect,
6-
useCallback,
7-
useRef
8-
} from 'react';
9-
import { DEFAULT_CATEGORIES, TaskCategory } from '@/config/categories';
10-
import { DEFAULT_PROJECTS, ProjectCategory } from '@/config/projects';
11-
import { useAuth } from '@/hooks/useAuth';
12-
import { createDataService, DataService } from '@/services/dataService';
13-
import { useRealtimeSync } from '@/hooks/useRealtimeSync';
2+
createContext,
3+
useContext,
4+
useState,
5+
useEffect,
6+
useCallback,
7+
useRef
8+
} from "react";
9+
import { DEFAULT_CATEGORIES, TaskCategory } from "@/config/categories";
10+
import { DEFAULT_PROJECTS, ProjectCategory } from "@/config/projects";
11+
import { useAuth } from "@/hooks/useAuth";
12+
import { createDataService, DataService } from "@/services/dataService";
13+
import { useRealtimeSync } from "@/hooks/useRealtimeSync";
14+
import { generateDailySummary } from "@/utils/timeUtil";
1415

1516
export interface Task {
1617
id: string;
@@ -58,14 +59,15 @@ export interface TimeEntry {
5859
}
5960

6061
export interface InvoiceData {
61-
client: string;
62-
period: { startDate: Date; endDate: Date };
63-
projects: { [key: string]: { hours: number; rate: number; amount: number } };
64-
summary: {
65-
totalHours: number;
66-
totalAmount: number;
67-
};
68-
tasks: Task[];
62+
client: string;
63+
period: { startDate: Date; endDate: Date };
64+
projects: { [key: string]: { hours: number; rate: number; amount: number } };
65+
summary: {
66+
totalHours: number;
67+
totalAmount: number;
68+
};
69+
tasks: (Task & { dayId: string; dayDate: string; dailySummary: string })[];
70+
dailySummaries: { [dayId: string]: { date: string; summary: string } };
6971
}
7072

7173
interface TimeTrackingContextType {
@@ -1032,11 +1034,18 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
10321034
'day_record_id',
10331035
'is_current',
10341036
'inserted_at',
1035-
'updated_at'
1037+
'updated_at',
1038+
'daily_summary'
10361039
];
10371040
const rows = [headers.join(',')];
10381041

10391042
filteredDays.forEach((day) => {
1043+
// Generate daily summary once per day
1044+
const dayDescriptions = day.tasks
1045+
.filter((t) => t.description)
1046+
.map((t) => t.description!);
1047+
const dailySummary = generateDailySummary(dayDescriptions);
1048+
10401049
day.tasks.forEach((task) => {
10411050
if (task.duration) {
10421051
const project = projects.find((p) => p.name === task.project);
@@ -1066,7 +1075,8 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
10661075
`"${day.id}"`, // day_record_id
10671076
'false', // is_current - archived tasks are not current
10681077
`"${insertedAtISO}"`, // inserted_at - actual database timestamp
1069-
`"${updatedAtISO}"` // updated_at - actual database timestamp
1078+
`"${updatedAtISO}"`, // updated_at - actual database timestamp
1079+
`"${dailySummary.replace(/"/g, '""')}"` // daily_summary - escape quotes for CSV
10701080
];
10711081
rows.push(row.join(','));
10721082
}
@@ -1086,6 +1096,19 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
10861096
});
10871097
}
10881098

1099+
// Add daily summary to each day
1100+
const daysWithSummary = filteredDays.map((day) => {
1101+
const dayDescriptions = day.tasks
1102+
.filter((t) => t.description)
1103+
.map((t) => t.description!);
1104+
const dailySummary = generateDailySummary(dayDescriptions);
1105+
1106+
return {
1107+
...day,
1108+
dailySummary
1109+
};
1110+
});
1111+
10891112
const exportData = {
10901113
exportDate: new Date().toISOString(),
10911114
period: {
@@ -1103,7 +1126,7 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
11031126
endDate || new Date()
11041127
)
11051128
},
1106-
days: filteredDays,
1129+
days: daysWithSummary,
11071130
projects: projects
11081131
};
11091132

@@ -1124,26 +1147,49 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
11241147
return dayDate >= startDate && dayDate <= endDate;
11251148
});
11261149

1150+
// Generate daily summaries for all days in the period
1151+
const dailySummaries: { [dayId: string]: { date: string; summary: string } } = {};
1152+
filteredDays.forEach((day) => {
1153+
const dayDescriptions = day.tasks
1154+
.filter((t) => t.description)
1155+
.map((t) => t.description!);
1156+
const summary = generateDailySummary(dayDescriptions);
1157+
1158+
if (summary) {
1159+
dailySummaries[day.id] = {
1160+
date: day.date,
1161+
summary
1162+
};
1163+
}
1164+
});
1165+
11271166
const clientTasks = filteredDays.flatMap((day) =>
1128-
day.tasks.filter((task) => {
1129-
if (!task.client || task.client !== clientName || !task.duration) {
1130-
return false;
1131-
}
1167+
day.tasks
1168+
.filter((task) => {
1169+
if (!task.client || task.client !== clientName || !task.duration) {
1170+
return false;
1171+
}
11321172

1133-
// Only include billable tasks in invoices
1134-
if (task.project && task.category) {
1135-
const project = projectMap.get(task.project);
1136-
const category = categoryMap.get(task.category);
1173+
// Only include billable tasks in invoices
1174+
if (task.project && task.category) {
1175+
const project = projectMap.get(task.project);
1176+
const category = categoryMap.get(task.category);
11371177

1138-
const projectIsBillable = project?.isBillable !== false;
1139-
const categoryIsBillable = category?.isBillable !== false;
1178+
const projectIsBillable = project?.isBillable !== false;
1179+
const categoryIsBillable = category?.isBillable !== false;
11401180

1141-
// Task must be billable to appear on invoice
1142-
return projectIsBillable && categoryIsBillable;
1143-
}
1181+
// Task must be billable to appear on invoice
1182+
return projectIsBillable && categoryIsBillable;
1183+
}
11441184

1145-
return false;
1146-
})
1185+
return false;
1186+
})
1187+
.map((task) => ({
1188+
...task,
1189+
dayId: day.id,
1190+
dayDate: day.date,
1191+
dailySummary: dailySummaries[day.id]?.summary || ""
1192+
}))
11471193
);
11481194

11491195
const projectSummary: {
@@ -1181,7 +1227,8 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
11811227
totalHours: Math.round(totalHours * 100) / 100,
11821228
totalAmount: Math.round(totalAmount * 100) / 100
11831229
},
1184-
tasks: clientTasks
1230+
tasks: clientTasks,
1231+
dailySummaries
11851232
};
11861233
};
11871234

src/utils/timeUtil.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,51 @@ export const formatHoursDecimal = (milliseconds: number): number => {
5555
};
5656

5757
export const calculateHourlyRate = (
58-
totalDuration: number,
59-
rate: number
58+
totalDuration: number,
59+
rate: number
6060
): number => {
61-
const hours = formatHoursDecimal(totalDuration);
62-
return Math.round(hours * rate * 100) / 100;
61+
const hours = formatHoursDecimal(totalDuration);
62+
return Math.round(hours * rate * 100) / 100;
63+
};
64+
65+
/**
66+
* Generates a readable summary paragraph from task descriptions
67+
* @param descriptions - Array of task descriptions
68+
* @returns A formatted paragraph combining all descriptions
69+
*/
70+
export const generateDailySummary = (descriptions: string[]): string => {
71+
// Filter out empty or whitespace-only descriptions
72+
const validDescriptions = descriptions
73+
.filter((desc) => desc && desc.trim().length > 0)
74+
.map((desc) => desc.trim());
75+
76+
if (validDescriptions.length === 0) {
77+
return "";
78+
}
79+
80+
// Connectors to use between sentences (not used for first sentence)
81+
const connectors = ["Additionally,", "Also,", "Furthermore,", "Moreover,"];
82+
83+
// Format each description into a proper sentence
84+
const formattedSentences = validDescriptions.map((desc, index) => {
85+
// Capitalize first letter
86+
let sentence = desc.charAt(0).toUpperCase() + desc.slice(1);
87+
88+
// Add period at the end if missing punctuation
89+
const lastChar = sentence.charAt(sentence.length - 1);
90+
if (![".", "!", "?"].includes(lastChar)) {
91+
sentence += ".";
92+
}
93+
94+
// Add connector for sentences after the first one (vary the connectors)
95+
if (index > 0 && index < validDescriptions.length) {
96+
const connectorIndex = (index - 1) % connectors.length;
97+
sentence = `${connectors[connectorIndex]} ${sentence.charAt(0).toLowerCase()}${sentence.slice(1)}`;
98+
}
99+
100+
return sentence;
101+
});
102+
103+
// Join all sentences with a space
104+
return formattedSentences.join(" ");
63105
};

0 commit comments

Comments
 (0)