Skip to content

Add job revenue widget and bank cash metric#23

Open
mgkcloud wants to merge 1 commit intomasterfrom
codex/add-job-revenue-widget-and-update-cash-available
Open

Add job revenue widget and bank cash metric#23
mgkcloud wants to merge 1 commit intomasterfrom
codex/add-job-revenue-widget-and-update-cash-available

Conversation

@mgkcloud
Copy link
Copy Markdown
Owner

@mgkcloud mgkcloud commented May 28, 2025

Summary

  • implement Job Revenue metric in backend aggregation
  • compute cash available from xero_bank_total values
  • expose dashboard endpoint and use in frontend
  • add JobRevenueWidget component and display in TodayOverview
  • provide Currency/Percentage formatters and metric descriptions
  • add ESLint config for backend

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm build (fails: Failed to fetch Inter from Google Fonts)
  • pnpm dev

Summary by CodeRabbit

  • New Features

    • Added a "Job Revenue (Invoiced)" widget to the dashboard, displaying total invoiced job revenue and its percentage change compared to the previous period.
    • Introduced new formatting components for currency and percentage values throughout the dashboard.
  • Improvements

    • Dashboard metrics are now fetched from a dedicated endpoint for improved accuracy and reliability.
  • Bug Fixes

    • Ensured correct calculation and display of job revenue and cash available metrics on the dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2025

Walkthrough

A new "Job Revenue" metric and associated percentage change were introduced to the dashboard, with supporting backend aggregation logic, a dedicated API endpoint, and frontend display components. The dashboard now fetches metrics from a new /metrics/dashboard route. Additional code quality improvements include a new ESLint configuration for the backend.

Changes

File(s) Change Summary
packages/backend/.eslintrc.json Added ESLint config for backend: enables TypeScript, Node.js, disables some rules, declares global.
packages/backend/src/routes/metrics.ts Added /metrics/dashboard GET route for dashboard metrics with date validation and error handling.
packages/backend/src/services/metricsAggregation.ts Refactored dashboard aggregation: added job revenue, cash available, and their period-over-period changes.
packages/frontend/components/today-overview.tsx Integrated "Job Revenue" widget: state, layout, data fetching, and rendering.
packages/frontend/components/ui/formatters.tsx Added Currency and Percentage React components for consistent formatting.
packages/frontend/components/widgets/JobRevenueWidget.tsx New JobRevenueWidget React component for displaying job revenue and its change.
packages/frontend/lib/api.ts Updated metrics schema and switched dashboard metrics fetch to /metrics/dashboard.
packages/frontend/lib/today-widgets.ts Added "Job Revenue (Invoiced)" to the list of available dashboard widgets.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant MetricsService
    participant Database

    User->>Frontend: Loads dashboard
    Frontend->>Backend: GET /metrics/dashboard?start=...&end=...
    Backend->>MetricsService: aggregateMetric('dashboard', start, end)
    MetricsService->>Database: Query job revenue, bank balance, previous period data
    Database-->>MetricsService: Returns queried data
    MetricsService-->>Backend: Aggregated dashboard metrics (jobRevenue, cashAvailable, changes)
    Backend-->>Frontend: JSON metrics response
    Frontend->>Frontend: Update state with jobRevenue, jobRevenueChange
    Frontend->>User: Render JobRevenueWidget with new data
Loading

Poem

In the dashboard fields where numbers grow,
A rabbit hops where revenues flow.
With widgets new and metrics bright,
The job revenue now comes to light.
Code linted, queries run,
More insights for everyone!
🐇💰✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
packages/backend/.eslintrc.json (2)

14-17: Consider enabling TypeScript-specific linting rules.

While disabling no-unused-vars and no-empty may be intentional for development, consider using TypeScript-specific alternatives for better type safety:

  "rules": {
-   "no-unused-vars": "off",
+   "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
    "no-empty": "off"
  }

This allows unused variables with underscore prefix while maintaining type checking.


18-20: Document the D1Database global usage.

The D1Database global declaration suggests this is a Cloudflare Workers D1 database type. Consider adding a comment to clarify its purpose for future maintainers.

  "globals": {
+   // Cloudflare Workers D1 Database global type
    "D1Database": "readonly"
  }
packages/frontend/components/ui/formatters.tsx (2)

3-5: Improve type safety for the Currency component.

The children prop should have better type constraints to ensure only numbers are passed, and consider handling edge cases.

-export function Currency({ children }: { children: number }) {
-  return <span>{formatCurrency(children)}</span>;
+export function Currency({ children }: { children: number }) {
+  if (typeof children !== 'number' || !isFinite(children)) {
+    return <span>--</span>;
+  }
+  return <span>{formatCurrency(children)}</span>;
}

7-10: Consider consistent prop naming and add accessibility attributes.

The Percentage component uses value prop while Currency uses children. Consider consistency and add ARIA attributes for better accessibility.

-export function Percentage({ value }: { value: number }) {
+export function Percentage({ value }: { value: number }) {
+  if (typeof value !== 'number' || !isFinite(value)) {
+    return <span className="text-gray-500">--</span>;
+  }
   const color = value > 0 ? 'text-green-500' : value < 0 ? 'text-red-500' : 'text-gray-500';
-  return <span className={color}>{formatPercentage(value)}</span>;
+  return (
+    <span 
+      className={color}
+      aria-label={`${value > 0 ? 'Increase' : value < 0 ? 'Decrease' : 'No change'} of ${formatPercentage(value)}`}
+    >
+      {formatPercentage(value)}
+    </span>
+  );
}
packages/frontend/components/widgets/JobRevenueWidget.tsx (2)

4-12: Enhance type safety and add prop validation.

The component looks well-structured but could benefit from better TypeScript types and error handling for edge cases.

+interface JobRevenueWidgetProps {
+  value: number;
+  change: number;
+}
+
-export default function JobRevenueWidget({ value, change }: { value: number; change: number }) {
+export default function JobRevenueWidget({ value, change }: JobRevenueWidgetProps) {
+  // Handle edge cases for invalid data
+  const displayValue = typeof value === 'number' && isFinite(value) ? value : 0;
+  const displayChange = typeof change === 'number' && isFinite(change) ? change : 0;
+  
   return (
     <Card className="p-4">
       <h3 className="text-sm font-medium text-gray-400 mb-2">Job Revenue (Invoiced)</h3>
-      <h2 className="text-3xl font-semibold"><Currency>{value}</Currency></h2>
-      <Percentage value={change} />
+      <h2 className="text-3xl font-semibold"><Currency>{displayValue}</Currency></h2>
+      <Percentage value={displayChange} />
     </Card>
   );
}

7-7: Consider semantic HTML improvements.

The heading structure could be improved for better accessibility and semantic meaning.

-      <h3 className="text-sm font-medium text-gray-400 mb-2">Job Revenue (Invoiced)</h3>
+      <div className="text-sm font-medium text-gray-400 mb-2" role="heading" aria-level="3">
+        Job Revenue (Invoiced)
+      </div>

Or alternatively, ensure the heading level is appropriate for the page structure where this widget is used.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba0313 and 6445f80.

⛔ Files ignored due to path filters (1)
  • docs/descriptions.csv is excluded by !**/*.csv
📒 Files selected for processing (8)
  • packages/backend/.eslintrc.json (1 hunks)
  • packages/backend/src/routes/metrics.ts (1 hunks)
  • packages/backend/src/services/metricsAggregation.ts (4 hunks)
  • packages/frontend/components/today-overview.tsx (6 hunks)
  • packages/frontend/components/ui/formatters.tsx (1 hunks)
  • packages/frontend/components/widgets/JobRevenueWidget.tsx (1 hunks)
  • packages/frontend/lib/api.ts (2 hunks)
  • packages/frontend/lib/today-widgets.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/frontend/components/today-overview.tsx (2)
packages/frontend/components/today-widgets/WidgetContainer.tsx (1)
  • WidgetContainer (17-40)
packages/frontend/components/widgets/JobRevenueWidget.tsx (1)
  • JobRevenueWidget (4-12)
🔇 Additional comments (10)
packages/frontend/lib/today-widgets.ts (1)

3-3: Consider widget ID naming consistency.

The new widget ID 'jobrevenue' follows camelCase while some existing widgets use different patterns. The implementation looks correct and follows the established structure.

The widget registration is properly implemented and maintains consistency with the existing pattern. The label "Job Revenue (Invoiced)" clearly describes the metric being displayed.

packages/frontend/lib/api.ts (2)

19-20: LGTM! Well-structured schema additions.

The new fields jobRevenue and jobRevenueChange are properly typed and integrate well with the existing schema structure. The nullable nature of jobRevenueChange makes sense as it might not be available when there's insufficient historical data.


178-178: Endpoint change aligns with backend implementation.

The updated endpoint path correctly matches the new /metrics/dashboard route added in the backend, maintaining consistency across the stack.

packages/backend/src/services/metricsAggregation.ts (4)

42-46: Well-structured previous period calculations.

The date range calculations for previous periods are implemented correctly and positioned at the top for reuse throughout the dashboard logic. This is a good architectural decision.


190-203: Job revenue logic is correctly implemented.

The job revenue calculation properly:

  • Sums invoice totals for the current period
  • Calculates percentage change vs. previous period
  • Handles division by zero scenarios appropriately

205-222: Improved cash available calculation using bank balances.

This is a significant improvement over the previous approach. Using the latest bank balance metric (xero_bank_total) provides a more accurate representation of cash available compared to summing invoices, as it reflects actual bank account balances.


319-320: Proper integration of new metrics into result structure.

The new metrics are correctly added to both the result object and the expectedKeys array, ensuring consistent data structure and proper fallback values.

Also applies to: 337-337

packages/frontend/components/today-overview.tsx (3)

19-19: Complete integration of JobRevenueWidget infrastructure.

The widget integration is thorough and follows established patterns:

  • Proper import and component mapping
  • Consistent interface and state structure
  • Type-safe implementation

Also applies to: 68-69, 108-108, 133-134


204-205: Robust data handling with fallback values.

The data fetching correctly uses nullish coalescing (??) to provide fallback values, ensuring the component remains stable even if the backend doesn't return these new metrics.


414-421: Well-positioned widget layout adjustment.

The new JobRevenueWidget is properly positioned at x:1, y:0 and the burnrate widget is appropriately repositioned to x:2, y:0 to maintain a clean grid layout. The widget receives the correct props from the metrics state.

Also applies to: 426-426

Comment on lines +236 to +252
api.get('/metrics/dashboard', async (c: Context<{ Bindings: Bindings; Variables: Variables }>) => {
try {
const db = c.get('db');
const startStr = c.req.query('start');
const endStr = c.req.query('end');
if (!startStr || !endStr) {
return c.json({ error: 'Missing start or end' }, 400);
}
const start = Date.parse(startStr);
const end = Date.parse(endStr) + 24 * 60 * 60 * 1000 - 1;
const res = await aggregateMetric({ db, metric: 'dashboard', start, end });
return c.json(res);
} catch (error) {
console.error('Error in /metrics/dashboard:', error);
return c.json({ error: 'Failed to fetch dashboard metrics' }, 500);
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider adding retry logic and date validation for consistency.

The new dashboard route looks well-structured but is missing two features present in the main metrics route:

  1. Missing retry logic: The main /metrics route uses fetchWithIngestionRetry to handle data freshness and trigger N8N workflows. Consider using the same pattern for consistency.

  2. Date validation: The route doesn't validate date formats before parsing, which could cause runtime errors.

Apply this diff to add date validation:

  api.get('/metrics/dashboard', async (c: Context<{ Bindings: Bindings; Variables: Variables }>) => {
    try {
      const db = c.get('db');
      const startStr = c.req.query('start');
      const endStr = c.req.query('end');
      if (!startStr || !endStr) {
        return c.json({ error: 'Missing start or end' }, 400);
      }
+     const start = Date.parse(startStr);
+     const end = Date.parse(endStr);
+     if (isNaN(start) || isNaN(end)) {
+       return c.json({ error: 'Invalid date format' }, 400);
+     }
-     const start = Date.parse(startStr);
-     const end = Date.parse(endStr) + 24 * 60 * 60 * 1000 - 1;
+     const endInclusive = end + 24 * 60 * 60 * 1000 - 1;
-     const res = await aggregateMetric({ db, metric: 'dashboard', start, end });
+     const res = await aggregateMetric({ db, metric: 'dashboard', start, end: endInclusive });
      return c.json(res);
    } catch (error) {
      console.error('Error in /metrics/dashboard:', error);
      return c.json({ error: 'Failed to fetch dashboard metrics' }, 500);
    }
  });

Consider also using fetchWithIngestionRetry like the main metrics route for data freshness consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
api.get('/metrics/dashboard', async (c: Context<{ Bindings: Bindings; Variables: Variables }>) => {
try {
const db = c.get('db');
const startStr = c.req.query('start');
const endStr = c.req.query('end');
if (!startStr || !endStr) {
return c.json({ error: 'Missing start or end' }, 400);
}
const start = Date.parse(startStr);
const end = Date.parse(endStr) + 24 * 60 * 60 * 1000 - 1;
const res = await aggregateMetric({ db, metric: 'dashboard', start, end });
return c.json(res);
} catch (error) {
console.error('Error in /metrics/dashboard:', error);
return c.json({ error: 'Failed to fetch dashboard metrics' }, 500);
}
});
api.get('/metrics/dashboard', async (c: Context<{ Bindings: Bindings; Variables: Variables }>) => {
try {
const db = c.get('db');
const startStr = c.req.query('start');
const endStr = c.req.query('end');
if (!startStr || !endStr) {
return c.json({ error: 'Missing start or end' }, 400);
}
const start = Date.parse(startStr);
const end = Date.parse(endStr);
if (isNaN(start) || isNaN(end)) {
return c.json({ error: 'Invalid date format' }, 400);
}
const endInclusive = end + 24 * 60 * 60 * 1000 - 1;
const res = await aggregateMetric({ db, metric: 'dashboard', start, end: endInclusive });
return c.json(res);
} catch (error) {
console.error('Error in /metrics/dashboard:', error);
return c.json({ error: 'Failed to fetch dashboard metrics' }, 500);
}
});
🤖 Prompt for AI Agents
In packages/backend/src/routes/metrics.ts around lines 236 to 252, the
/metrics/dashboard route lacks date format validation and retry logic present in
the main /metrics route. To fix this, add validation to ensure the 'start' and
'end' query parameters are valid date strings before parsing to prevent runtime
errors. Also, replace the direct call to aggregateMetric with
fetchWithIngestionRetry to maintain data freshness and trigger ingestion
workflows consistently, mirroring the main metrics route's behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant