Skip to content

Commit a0b31ff

Browse files
committed
fix: category billing
1 parent 23b6ec7 commit a0b31ff

File tree

2 files changed

+117
-19
lines changed

2 files changed

+117
-19
lines changed

docs/chatbot.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,3 +1101,38 @@ The billable time tracking system is fully implemented and consistent across:
11011101
✅ Business logic (revenue calculations)
11021102
✅ Archive display (individual tasks & summaries)
11031103
✅ Migration scripts (database schema updates)
1104+
1105+
## Billable logic noted as incorrect
1106+
1107+
When a task was assigned a non-billable category, it was still being shown as billable due to the project itself being a billable entity.
1108+
1109+
### ✅ Billable Logic Fixed!
1110+
1111+
3 bugs were found:
1112+
1113+
- `getRevenueForPeriod()` **was incomplete** - Only checking category billable status, not project
1114+
- `generateInvoiceData()` **was broken** - Not checking billable status at all for invoices
1115+
- **Inconsistent logic across functions** - Different billable checks in different places
1116+
1117+
**What was fixed:**
1118+
1119+
```typescript
1120+
// Now ALL functions use this logic:
1121+
const projectIsBillable = project?.isBillable !== false;
1122+
const categoryIsBillable = category?.isBillable !== false;
1123+
const isBillable = projectIsBillable && categoryIsBillable;
1124+
```
1125+
1126+
**Updates:**
1127+
1128+
`getRevenueForPeriod()` - Now checks BOTH project AND category
1129+
`generateInvoiceData()` - Now filters out non-billable tasks from invoices
1130+
All functions now have consistent AND logic
1131+
1132+
#### How it works
1133+
1134+
| Project billable | Category billable | Result | Revenue Generated |
1135+
| Yes | Yes | Billable | Yes |
1136+
| No | No | Non-billable | No |
1137+
| No |Yes | Non-billable | No |
1138+
| No | No | Non-billable | No |

src/contexts/TimeTrackingContext.tsx

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -860,16 +860,20 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
860860
filteredDays.forEach((day) => {
861861
day.tasks.forEach((task) => {
862862
if (task.project && task.duration && task.category) {
863-
// Check if the category is billable
864-
const category = categories.find((c) => c.name === task.category);
865-
const isBillable = category?.isBillable !== false; // Default to billable if not specified
866-
867-
if (isBillable) {
868-
const project = projects.find((p) => p.name === task.project);
869-
if (project?.hourlyRate) {
870-
const hours = task.duration / (1000 * 60 * 60);
871-
totalRevenue += hours * project.hourlyRate;
872-
}
863+
// Check if both the project and category are billable
864+
const project = projects.find((p) => p.name === task.project);
865+
// Fix: Look up category by ID, not name
866+
const category = categories.find((c) => c.id === task.category);
867+
868+
const projectIsBillable = project?.isBillable !== false; // Default to billable if not specified
869+
const categoryIsBillable = category?.isBillable !== false; // Default to billable if not specified
870+
871+
// Task is billable only if BOTH project AND category are billable
872+
const isBillable = projectIsBillable && categoryIsBillable;
873+
874+
if (isBillable && project?.hourlyRate) {
875+
const hours = task.duration / (1000 * 60 * 60);
876+
totalRevenue += hours * project.hourlyRate;
873877
}
874878
}
875879
});
@@ -894,11 +898,25 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
894898

895899
const getRevenueForDay = (day: DayRecord): number => {
896900
let totalRevenue = 0;
901+
console.log('💰 Calculating revenue for day:', day.date, 'with', day.tasks.length, 'tasks');
902+
897903
day.tasks.forEach((task) => {
898904
if (task.project && task.duration && task.category) {
899905
// Check if both the project and category are billable
900906
const project = projects.find((p) => p.name === task.project);
901-
const category = categories.find((c) => c.name === task.category);
907+
// Fix: Look up category by ID, not name
908+
const category = categories.find((c) => c.id === task.category);
909+
910+
console.log('🔍 Revenue check for task:', {
911+
title: task.title,
912+
category: task.category,
913+
project: task.project,
914+
foundCategory: !!category,
915+
categoryIsBillable: category?.isBillable,
916+
foundProject: !!project,
917+
projectIsBillable: project?.isBillable,
918+
hourlyRate: project?.hourlyRate
919+
});
902920

903921
const projectIsBillable = project?.isBillable !== false; // Default to billable if not specified
904922
const categoryIsBillable = category?.isBillable !== false; // Default to billable if not specified
@@ -908,28 +926,52 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
908926

909927
if (isBillable && project?.hourlyRate) {
910928
const hours = task.duration / (1000 * 60 * 60);
911-
totalRevenue += hours * project.hourlyRate;
929+
const revenue = hours * project.hourlyRate;
930+
totalRevenue += revenue;
931+
console.log('💰 Adding revenue:', revenue, 'for task:', task.title);
932+
} else {
933+
console.log('🚫 No revenue for task:', task.title, 'because:',
934+
!isBillable ? 'not billable' : 'no hourly rate');
912935
}
913936
}
914937
});
915938

939+
console.log('💰 Total revenue for day:', totalRevenue);
916940
return Math.round(totalRevenue * 100) / 100;
917-
};
918-
919-
const getBillableHoursForDay = (day: DayRecord): number => {
941+
}; const getBillableHoursForDay = (day: DayRecord): number => {
920942
let billableTime = 0;
921943
day.tasks.forEach((task) => {
922944
if (task.duration && task.category && task.project) {
923945
// Check if both the project and category are billable
924946
const project = projects.find((p) => p.name === task.project);
925-
const category = categories.find((c) => c.name === task.category);
947+
// Fix: Look up category by ID, not name
948+
const category = categories.find((c) => c.id === task.category);
949+
950+
// Debug logging
951+
console.log('🔍 Checking task billability:', {
952+
taskTitle: task.title,
953+
taskCategory: task.category,
954+
projectName: task.project,
955+
foundProject: !!project,
956+
foundCategory: !!category,
957+
projectIsBillable: project?.isBillable,
958+
categoryIsBillable: category?.isBillable,
959+
availableCategories: categories.map(c => ({ name: c.name, id: c.id, isBillable: c.isBillable }))
960+
});
926961

927962
const projectIsBillable = project?.isBillable !== false; // Default to billable if not specified
928963
const categoryIsBillable = category?.isBillable !== false; // Default to billable if not specified
929964

930965
// Task is billable only if BOTH project AND category are billable
931966
const isBillable = projectIsBillable && categoryIsBillable;
932967

968+
console.log('💰 Task billability result:', {
969+
taskTitle: task.title,
970+
projectIsBillable,
971+
categoryIsBillable,
972+
finalIsBillable: isBillable
973+
});
974+
933975
if (isBillable) {
934976
billableTime += task.duration;
935977
}
@@ -945,7 +987,8 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
945987
if (task.duration && task.category && task.project) {
946988
// Check if both the project and category are billable
947989
const project = projects.find((p) => p.name === task.project);
948-
const category = categories.find((c) => c.name === task.category);
990+
// Fix: Look up category by ID, not name
991+
const category = categories.find((c) => c.id === task.category);
949992

950993
const projectIsBillable = project?.isBillable !== false; // Default to billable if not specified
951994
const categoryIsBillable = category?.isBillable !== false; // Default to billable if not specified
@@ -999,7 +1042,8 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
9991042
day.tasks.forEach((task) => {
10001043
if (task.duration) {
10011044
const project = projects.find((p) => p.name === task.project);
1002-
const category = categories.find((c) => c.name === task.category);
1045+
// Fix: Look up category by ID, not name
1046+
const category = categories.find((c) => c.id === task.category);
10031047

10041048
// Format timestamps as ISO strings for database compatibility
10051049
const startTimeISO = task.startTime.toISOString();
@@ -1077,7 +1121,26 @@ export const TimeTrackingProvider: React.FC<{ children: React.ReactNode }> = ({
10771121
});
10781122

10791123
const clientTasks = filteredDays.flatMap((day) =>
1080-
day.tasks.filter((task) => task.client === clientName && task.duration)
1124+
day.tasks.filter((task) => {
1125+
if (!task.client || task.client !== clientName || !task.duration) {
1126+
return false;
1127+
}
1128+
1129+
// Only include billable tasks in invoices
1130+
if (task.project && task.category) {
1131+
const project = projects.find((p) => p.name === task.project);
1132+
// Fix: Look up category by ID, not name
1133+
const category = categories.find((c) => c.id === task.category);
1134+
1135+
const projectIsBillable = project?.isBillable !== false;
1136+
const categoryIsBillable = category?.isBillable !== false;
1137+
1138+
// Task must be billable to appear on invoice
1139+
return projectIsBillable && categoryIsBillable;
1140+
}
1141+
1142+
return false;
1143+
})
10811144
);
10821145

10831146
const projectSummary: {

0 commit comments

Comments
 (0)