Skip to content
Open
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
34 changes: 34 additions & 0 deletions path/to/docs/issue-50.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Issue 50: Align Analytics with Real Meeting and Action Item Data

## Summary

The analytics page currently derives completion and duration-related insights from assumptions that do not match actual UI behavior. For example, action item completion is tracked only in local component state, while analytics reads `action_items[].completed`. This makes parts of the analytics dashboard misleading or non-functional.

## Current Problem

* Action item completion in analytics is disconnected from the actual checklist UI
* “Completion rate” may remain empty or inaccurate
* Pending action item counts may be misleading
* Duration is estimated only from transcript word count, which may be too rough if real duration metadata exists or becomes available

## Expected Behavior

* Analytics should reflect the same source of truth as the meeting details UI
* Completion metrics should be based on persisted action item state
* Duration logic should prefer real duration metadata when available
* Derived charts should degrade gracefully when data is incomplete

## Suggested Scope

This can be one PR focused on analytics correctness:
* Align action-item completion storage and analytics computation
* Define expected meeting/action-item shape
* Improve fallback rules for duration calculation
* Add defensive handling for partial/malformed meeting payloads

## Acceptance Criteria

* [ ] Completion rate reflects real persisted action item state
* [ ] Pending action item count is accurate
* [ ] Duration uses a better source of truth when available
* [ ] Analytics UI handles incomplete meeting data safely
18 changes: 18 additions & 0 deletions path/to/src/components/AnalyticsPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { getAnalyticsData } from '../utils/analyticsHelpers';

const AnalyticsPage = ({ meeting }) => {
const analyticsData = getAnalyticsData(meeting);
if (!analyticsData) {
return <div>Loading...</div>;
}
return (
<div>
<h2>Completion Rate: {analyticsData.completionRate.toFixed(2)}%</h2>
<h2>Pending Action Items: {analyticsData.pendingActionItems}</h2>
<h2>Duration: {analyticsData.duration.toFixed(2)} minutes</h2>
</div>
);
};

export default AnalyticsPage;
19 changes: 19 additions & 0 deletions path/to/src/components/MeetingDetails.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { useState, useEffect } from 'react';
import { expectedActionItemShape } from '../utils/meetingShape';

const MeetingDetails = ({ meeting }) => {
const [actionItems, setActionItems] = useState(meeting.actionItems);

useEffect(() => {
setActionItems(meeting.actionItems.map((item) => ({
...item,
completed: item.completed === true,
})));
}, [meeting.actionItems]);

return (
// Meeting details UI
);
};

export default MeetingDetails;
14 changes: 14 additions & 0 deletions path/to/src/utils/analyticsHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { getMeetingData } from './meetingShape';
import { calculateDuration } from './durationCalculator';

const getAnalyticsData = (meeting) => {
const meetingData = getMeetingData(meeting);
if (!meetingData) {
return null;
}
return {
completionRate: meetingData.actionItems.filter((item) => item.completed).length / meetingData.actionItems.length,
pendingActionItems: meetingData.actionItems.filter((item) => !item.completed).length,
duration: meetingData.actionItems.reduce((acc, item) => acc + item.duration, 0),
};
};
10 changes: 10 additions & 0 deletions path/to/src/utils/durationCalculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Calculates duration based on real duration metadata when available.
*/
export const calculateDuration = (meeting) => {
if (meeting.realDuration) {
return meeting.realDuration;
}
// Fallback to estimated duration based on transcript word count
return meeting.transcriptWordCount * 0.01;
};
24 changes: 24 additions & 0 deletions path/to/src/utils/meetingShape.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Expected shape of meeting data.
*/
export const expectedMeetingShape = {
id: String,
title: String,
date: Date,
actionItems: Array.of({
id: String,
title: String,
completed: Boolean,
duration: Number,
}),
};

/**
* Expected shape of action item data.
*/
export const expectedActionItemShape = {
id: String,
title: String,
completed: Boolean,
duration: Number,
};
22 changes: 22 additions & 0 deletions path/to/tests/integration/analytics.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import AnalyticsPage from './AnalyticsPage';
import { expectedMeetingShape } from '../utils/meetingShape';

describe('AnalyticsPage', () => {
it('renders analytics data', () => {
const meeting = {
id: 'meeting-1',
title: 'Meeting 1',
date: new Date(),
actionItems: [
{ id: 'item-1', title: 'Item 1', completed: true },
{ id: 'item-2', title: 'Item 2', completed: false },
],
};
const { getByText } = render(<AnalyticsPage meeting={meeting} />);
expect(getByText('Completion Rate: 50.00%')).toBeInTheDocument();
expect(getByText('Pending Action Items: 1')).toBeInTheDocument();
expect(getByText('Duration: 0.00 minutes')).toBeInTheDocument();
});
});
48 changes: 48 additions & 0 deletions path/to/tests/units/analytics.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import AnalyticsPage from './AnalyticsPage';
import { expectedMeetingShape } from '../utils/meetingShape';

describe('AnalyticsPage', () => {
it('renders completion rate', () => {
const meeting = {
id: 'meeting-1',
title: 'Meeting 1',
date: new Date(),
actionItems: [
{ id: 'item-1', title: 'Item 1', completed: true },
{ id: 'item-2', title: 'Item 2', completed: false },
],
};
const { getByText } = render(<AnalyticsPage meeting={meeting} />);
expect(getByText('Completion Rate: 50.00%')).toBeInTheDocument();
});

it('renders pending action items', () => {
const meeting = {
id: 'meeting-1',
title: 'Meeting 1',
date: new Date(),
actionItems: [
{ id: 'item-1', title: 'Item 1', completed: true },
{ id: 'item-2', title: 'Item 2', completed: false },
],
};
const { getByText } = render(<AnalyticsPage meeting={meeting} />);
expect(getByText('Pending Action Items: 1')).toBeInTheDocument();
});

it('renders duration', () => {
const meeting = {
id: 'meeting-1',
title: 'Meeting 1',
date: new Date(),
actionItems: [
{ id: 'item-1', title: 'Item 1', completed: true, duration: 10 },
{ id: 'item-2', title: 'Item 2', completed: false, duration: 20 },
],
};
const { getByText } = render(<AnalyticsPage meeting={meeting} />);
expect(getByText('Duration: 30.00 minutes')).toBeInTheDocument();
});
});