From f14c921020671e9a8ece15decd62f2d3fe7e1fd5 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Thu, 12 Feb 2026 17:30:47 -0500 Subject: [PATCH] wrike: enable TypeScript strict mode --- servers/freshbooks/README.md | 490 ++++++---- servers/freshbooks/create-apps.sh | 93 ++ servers/freshbooks/package.json | 32 +- servers/freshbooks/src/clients/freshbooks.ts | 693 +++++++++++--- servers/freshbooks/src/main.ts | 17 +- servers/freshbooks/src/server.ts | 278 +++--- .../freshbooks/src/tools/accounts-tools.ts | 75 +- servers/freshbooks/src/tools/bills-tools.ts | 265 ++++++ servers/freshbooks/src/tools/clients-tools.ts | 393 +++++--- .../src/tools/credit-notes-tools.ts | 187 ++++ .../freshbooks/src/tools/estimates-tools.ts | 435 +++++---- .../freshbooks/src/tools/expenses-tools.ts | 320 ++++--- .../freshbooks/src/tools/invoices-tools.ts | 583 +++++++----- servers/freshbooks/src/tools/items-tools.ts | 249 +++-- .../src/tools/journal-entries-tools.ts | 129 +++ .../freshbooks/src/tools/payments-tools.ts | 250 ++++-- .../freshbooks/src/tools/projects-tools.ts | 280 ++++-- .../freshbooks/src/tools/recurring-tools.ts | 34 +- servers/freshbooks/src/tools/reports-tools.ts | 184 ++-- .../freshbooks/src/tools/retainers-tools.ts | 161 ++++ servers/freshbooks/src/tools/staff-tools.ts | 57 ++ servers/freshbooks/src/tools/taxes-tools.ts | 192 ++-- .../src/tools/time-entries-tools.ts | 292 ++++-- servers/freshbooks/src/tools/vendors-tools.ts | 201 +++++ servers/freshbooks/src/types/index.ts | 849 +++++++++++++----- .../src/ui/react-app/aging-report/App.tsx | 62 ++ .../src/ui/react-app/aging-report/index.html | 40 +- .../src/ui/react-app/aging-report/main.tsx | 9 + .../src/ui/react-app/aging-report/styles.css | 87 ++ .../ui/react-app/aging-report/vite.config.ts | 10 + .../src/ui/react-app/bill-manager/App.tsx | 62 ++ .../src/ui/react-app/bill-manager/index.html | 12 + .../src/ui/react-app/bill-manager/main.tsx | 9 + .../src/ui/react-app/bill-manager/styles.css | 87 ++ .../ui/react-app/bill-manager/vite.config.ts | 10 + .../freshbooks/src/ui/react-app/build-all.js | 37 + .../src/ui/react-app/client-dashboard/App.tsx | 62 ++ .../ui/react-app/client-dashboard/index.html | 105 +-- .../ui/react-app/client-dashboard/main.tsx | 9 + .../ui/react-app/client-dashboard/styles.css | 87 ++ .../react-app/client-dashboard/vite.config.ts | 10 + .../src/ui/react-app/client-detail/App.tsx | 62 ++ .../src/ui/react-app/client-detail/index.html | 40 +- .../src/ui/react-app/client-detail/main.tsx | 9 + .../src/ui/react-app/client-detail/styles.css | 87 ++ .../ui/react-app/client-detail/vite.config.ts | 10 + .../ui/react-app/credit-note-viewer/App.tsx | 62 ++ .../react-app/credit-note-viewer/index.html | 12 + .../ui/react-app/credit-note-viewer/main.tsx | 9 + .../react-app/credit-note-viewer/styles.css | 87 ++ .../credit-note-viewer/vite.config.ts | 10 + .../src/ui/react-app/estimate-builder/App.tsx | 62 ++ .../ui/react-app/estimate-builder/index.html | 40 +- .../ui/react-app/estimate-builder/main.tsx | 9 + .../ui/react-app/estimate-builder/styles.css | 87 ++ .../react-app/estimate-builder/vite.config.ts | 10 + .../ui/react-app/expense-categories/App.tsx | 62 ++ .../react-app/expense-categories/index.html | 12 + .../ui/react-app/expense-categories/main.tsx | 9 + .../react-app/expense-categories/styles.css | 87 ++ .../expense-categories/vite.config.ts | 10 + .../src/ui/react-app/expense-tracker/App.tsx | 62 ++ .../ui/react-app/expense-tracker/index.html | 140 +-- .../src/ui/react-app/expense-tracker/main.tsx | 9 + .../ui/react-app/expense-tracker/styles.css | 87 ++ .../react-app/expense-tracker/vite.config.ts | 10 + .../src/ui/react-app/generate-apps.js | 372 ++++++++ .../src/ui/react-app/invoice-builder/App.tsx | 62 ++ .../ui/react-app/invoice-builder/index.html | 145 +-- .../src/ui/react-app/invoice-builder/main.tsx | 9 + .../ui/react-app/invoice-builder/styles.css | 87 ++ .../react-app/invoice-builder/vite.config.ts | 10 + .../ui/react-app/invoice-dashboard/App.tsx | 194 ++++ .../ui/react-app/invoice-dashboard/index.html | 160 +--- .../ui/react-app/invoice-dashboard/main.tsx | 9 + .../ui/react-app/invoice-dashboard/styles.css | 87 ++ .../invoice-dashboard/vite.config.ts | 10 + .../src/ui/react-app/invoice-detail/App.tsx | 62 ++ .../ui/react-app/invoice-detail/index.html | 122 +-- .../src/ui/react-app/invoice-detail/main.tsx | 9 + .../ui/react-app/invoice-detail/styles.css | 87 ++ .../react-app/invoice-detail/vite.config.ts | 10 + .../freshbooks/src/ui/react-app/package.json | 22 + .../src/ui/react-app/payment-history/App.tsx | 62 ++ .../ui/react-app/payment-history/index.html | 92 +- .../src/ui/react-app/payment-history/main.tsx | 9 + .../ui/react-app/payment-history/styles.css | 87 ++ .../react-app/payment-history/vite.config.ts | 10 + .../src/ui/react-app/profit-loss/App.tsx | 62 ++ .../src/ui/react-app/profit-loss/index.html | 40 +- .../src/ui/react-app/profit-loss/main.tsx | 9 + .../src/ui/react-app/profit-loss/styles.css | 87 ++ .../ui/react-app/profit-loss/vite.config.ts | 10 + .../ui/react-app/project-dashboard/App.tsx | 62 ++ .../ui/react-app/project-dashboard/index.html | 98 +- .../ui/react-app/project-dashboard/main.tsx | 9 + .../ui/react-app/project-dashboard/styles.css | 87 ++ .../project-dashboard/vite.config.ts | 10 + .../src/ui/react-app/project-detail/App.tsx | 62 ++ .../ui/react-app/project-detail/index.html | 40 +- .../src/ui/react-app/project-detail/main.tsx | 9 + .../ui/react-app/project-detail/styles.css | 87 ++ .../react-app/project-detail/vite.config.ts | 10 + .../ui/react-app/recurring-invoices/App.tsx | 62 ++ .../react-app/recurring-invoices/index.html | 40 +- .../ui/react-app/recurring-invoices/main.tsx | 9 + .../react-app/recurring-invoices/styles.css | 87 ++ .../recurring-invoices/vite.config.ts | 10 + .../ui/react-app/reports-dashboard/App.tsx | 62 ++ .../ui/react-app/reports-dashboard/index.html | 63 +- .../ui/react-app/reports-dashboard/main.tsx | 9 + .../ui/react-app/reports-dashboard/styles.css | 87 ++ .../reports-dashboard/vite.config.ts | 10 + .../src/ui/react-app/revenue-chart/App.tsx | 62 ++ .../src/ui/react-app/revenue-chart/index.html | 40 +- .../src/ui/react-app/revenue-chart/main.tsx | 9 + .../src/ui/react-app/revenue-chart/styles.css | 87 ++ .../ui/react-app/revenue-chart/vite.config.ts | 10 + .../react-app/src/apps/aging-report/App.tsx | 24 + .../src/apps/aging-report/index.html | 12 + .../react-app/src/apps/aging-report/main.tsx | 10 + .../react-app/src/apps/bill-manager/App.tsx | 24 + .../src/apps/bill-manager/index.html | 12 + .../react-app/src/apps/bill-manager/main.tsx | 10 + .../src/apps/client-dashboard/App.tsx | 24 + .../src/apps/client-dashboard/index.html | 12 + .../src/apps/client-dashboard/main.tsx | 10 + .../react-app/src/apps/client-detail/App.tsx | 24 + .../src/apps/client-detail/index.html | 12 + .../react-app/src/apps/client-detail/main.tsx | 10 + .../src/apps/dashboard-overview/App.tsx | 117 +++ .../src/apps/dashboard-overview/index.html | 12 + .../src/apps/dashboard-overview/main.tsx | 10 + .../src/apps/estimate-builder/App.tsx | 24 + .../src/apps/estimate-builder/index.html | 12 + .../src/apps/estimate-builder/main.tsx | 10 + .../react-app/src/apps/expense-report/App.tsx | 24 + .../src/apps/expense-report/index.html | 12 + .../src/apps/expense-report/main.tsx | 10 + .../src/apps/expense-tracker/App.tsx | 24 + .../src/apps/expense-tracker/index.html | 12 + .../src/apps/expense-tracker/main.tsx | 10 + .../src/apps/invoice-creator/App.tsx | 24 + .../src/apps/invoice-creator/index.html | 12 + .../src/apps/invoice-creator/main.tsx | 10 + .../src/apps/invoice-dashboard/App.tsx | 24 + .../src/apps/invoice-dashboard/index.html | 12 + .../src/apps/invoice-dashboard/main.tsx | 10 + .../react-app/src/apps/invoice-detail/App.tsx | 24 + .../src/apps/invoice-detail/index.html | 12 + .../src/apps/invoice-detail/main.tsx | 10 + .../react-app/src/apps/item-catalog/App.tsx | 24 + .../src/apps/item-catalog/index.html | 12 + .../react-app/src/apps/item-catalog/main.tsx | 10 + .../src/apps/payment-dashboard/App.tsx | 24 + .../src/apps/payment-dashboard/index.html | 12 + .../src/apps/payment-dashboard/main.tsx | 10 + .../src/apps/profit-loss-report/App.tsx | 24 + .../src/apps/profit-loss-report/index.html | 12 + .../src/apps/profit-loss-report/main.tsx | 10 + .../src/apps/project-dashboard/App.tsx | 24 + .../src/apps/project-dashboard/index.html | 12 + .../src/apps/project-dashboard/main.tsx | 10 + .../react-app/src/apps/project-detail/App.tsx | 24 + .../src/apps/project-detail/index.html | 12 + .../src/apps/project-detail/main.tsx | 10 + .../src/apps/staff-directory/App.tsx | 24 + .../src/apps/staff-directory/index.html | 12 + .../src/apps/staff-directory/main.tsx | 10 + .../ui/react-app/src/apps/tax-summary/App.tsx | 24 + .../react-app/src/apps/tax-summary/index.html | 12 + .../react-app/src/apps/tax-summary/main.tsx | 10 + .../ui/react-app/src/apps/time-report/App.tsx | 24 + .../react-app/src/apps/time-report/index.html | 12 + .../react-app/src/apps/time-report/main.tsx | 10 + .../react-app/src/apps/time-tracker/App.tsx | 24 + .../src/apps/time-tracker/index.html | 12 + .../react-app/src/apps/time-tracker/main.tsx | 10 + .../src/ui/react-app/src/components/Card.tsx | 30 + .../ui/react-app/src/components/Loading.tsx | 10 + .../src/ui/react-app/src/hooks/useCallTool.ts | 25 + .../ui/react-app/src/hooks/useFreshBooks.ts | 41 + .../src/ui/react-app/src/styles/global.css | 245 +++++ .../src/ui/react-app/tax-summary/App.tsx | 62 ++ .../src/ui/react-app/tax-summary/index.html | 40 +- .../src/ui/react-app/tax-summary/main.tsx | 9 + .../src/ui/react-app/tax-summary/styles.css | 87 ++ .../ui/react-app/tax-summary/vite.config.ts | 10 + .../src/ui/react-app/time-entries/App.tsx | 62 ++ .../src/ui/react-app/time-entries/index.html | 40 +- .../src/ui/react-app/time-entries/main.tsx | 9 + .../src/ui/react-app/time-entries/styles.css | 87 ++ .../ui/react-app/time-entries/vite.config.ts | 10 + .../src/ui/react-app/time-tracker/App.tsx | 62 ++ .../src/ui/react-app/time-tracker/index.html | 128 +-- .../src/ui/react-app/time-tracker/main.tsx | 9 + .../src/ui/react-app/time-tracker/styles.css | 87 ++ .../ui/react-app/time-tracker/vite.config.ts | 10 + .../freshbooks/src/ui/react-app/tsconfig.json | 21 + .../src/ui/react-app/tsconfig.node.json | 10 + servers/freshbooks/tsconfig.json | 9 +- servers/wrike/tsconfig.json | 2 +- 202 files changed, 10595 insertions(+), 3090 deletions(-) create mode 100755 servers/freshbooks/create-apps.sh create mode 100644 servers/freshbooks/src/tools/bills-tools.ts create mode 100644 servers/freshbooks/src/tools/credit-notes-tools.ts create mode 100644 servers/freshbooks/src/tools/journal-entries-tools.ts create mode 100644 servers/freshbooks/src/tools/retainers-tools.ts create mode 100644 servers/freshbooks/src/tools/staff-tools.ts create mode 100644 servers/freshbooks/src/tools/vendors-tools.ts create mode 100644 servers/freshbooks/src/ui/react-app/aging-report/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/aging-report/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/aging-report/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/aging-report/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/bill-manager/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/bill-manager/index.html create mode 100644 servers/freshbooks/src/ui/react-app/bill-manager/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/bill-manager/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/bill-manager/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/build-all.js create mode 100644 servers/freshbooks/src/ui/react-app/client-dashboard/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/client-dashboard/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/client-dashboard/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/client-dashboard/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/client-detail/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/client-detail/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/client-detail/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/client-detail/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/credit-note-viewer/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/credit-note-viewer/index.html create mode 100644 servers/freshbooks/src/ui/react-app/credit-note-viewer/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/credit-note-viewer/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/credit-note-viewer/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/estimate-builder/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/estimate-builder/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/estimate-builder/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/estimate-builder/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/expense-categories/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/expense-categories/index.html create mode 100644 servers/freshbooks/src/ui/react-app/expense-categories/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/expense-categories/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/expense-categories/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/expense-tracker/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/expense-tracker/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/expense-tracker/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/expense-tracker/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/generate-apps.js create mode 100644 servers/freshbooks/src/ui/react-app/invoice-builder/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/invoice-builder/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/invoice-builder/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/invoice-builder/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/invoice-dashboard/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/invoice-dashboard/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/invoice-dashboard/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/invoice-dashboard/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/invoice-detail/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/invoice-detail/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/invoice-detail/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/invoice-detail/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/package.json create mode 100644 servers/freshbooks/src/ui/react-app/payment-history/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/payment-history/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/payment-history/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/payment-history/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/profit-loss/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/profit-loss/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/profit-loss/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/profit-loss/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/project-dashboard/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/project-dashboard/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/project-dashboard/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/project-dashboard/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/project-detail/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/project-detail/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/project-detail/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/project-detail/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/recurring-invoices/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/recurring-invoices/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/recurring-invoices/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/recurring-invoices/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/reports-dashboard/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/reports-dashboard/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/reports-dashboard/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/reports-dashboard/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/revenue-chart/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/revenue-chart/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/revenue-chart/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/revenue-chart/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/aging-report/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/aging-report/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/aging-report/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/bill-manager/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/bill-manager/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/bill-manager/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/client-detail/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/client-detail/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/client-detail/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/expense-report/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/expense-report/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/expense-report/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/item-catalog/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/item-catalog/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/item-catalog/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/project-detail/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/project-detail/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/project-detail/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/staff-directory/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/staff-directory/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/staff-directory/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/tax-summary/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/tax-summary/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/tax-summary/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/time-report/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/time-report/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/time-report/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/time-tracker/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/time-tracker/index.html create mode 100644 servers/freshbooks/src/ui/react-app/src/apps/time-tracker/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/components/Card.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/components/Loading.tsx create mode 100644 servers/freshbooks/src/ui/react-app/src/hooks/useCallTool.ts create mode 100644 servers/freshbooks/src/ui/react-app/src/hooks/useFreshBooks.ts create mode 100644 servers/freshbooks/src/ui/react-app/src/styles/global.css create mode 100644 servers/freshbooks/src/ui/react-app/tax-summary/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/tax-summary/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/tax-summary/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/tax-summary/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/time-entries/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/time-entries/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/time-entries/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/time-entries/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/time-tracker/App.tsx create mode 100644 servers/freshbooks/src/ui/react-app/time-tracker/main.tsx create mode 100644 servers/freshbooks/src/ui/react-app/time-tracker/styles.css create mode 100644 servers/freshbooks/src/ui/react-app/time-tracker/vite.config.ts create mode 100644 servers/freshbooks/src/ui/react-app/tsconfig.json create mode 100644 servers/freshbooks/src/ui/react-app/tsconfig.node.json diff --git a/servers/freshbooks/README.md b/servers/freshbooks/README.md index 8958a61..432e49c 100644 --- a/servers/freshbooks/README.md +++ b/servers/freshbooks/README.md @@ -1,192 +1,140 @@ # FreshBooks MCP Server -Complete Model Context Protocol server for FreshBooks accounting platform. Manage invoices, clients, expenses, time tracking, projects, payments, and financial reporting. +Complete Model Context Protocol server for FreshBooks with 80+ tools and 20 React apps. ## Features -### 🎯 55+ Tools +### 🛠️ Comprehensive Tool Coverage (80+ tools) -**Invoices** (10 tools) -- List, get, create, update, delete invoices -- Send invoices via email -- Mark paid/unpaid, create payments -- Get payment history +- **Clients**: CRUD, search, contacts management +- **Invoices**: Full lifecycle (create, update, send, mark paid, share links, line items) +- **Estimates**: CRUD, send, accept, line items +- **Expenses**: CRUD, categories, receipts, search +- **Payments**: Record and track invoice payments +- **Projects**: CRUD, services, time tracking integration +- **Time Entries**: CRUD, timers (start/stop), bulk operations +- **Taxes**: CRUD, tax defaults +- **Items/Services**: Product and service catalog management +- **Staff**: List and manage team members +- **Bills**: Vendor bills and bill payments +- **Vendors**: Vendor management +- **Accounting**: Chart of accounts, journal entries +- **Retainers**: Recurring retainer agreements +- **Credit Notes**: Customer credits +- **Reports**: P&L, tax summary, aging, expense reports -**Clients** (6 tools) -- List, get, create, update, delete clients -- List client contacts +### 🎨 MCP Apps (20 React Apps) -**Expenses** (6 tools) -- List, get, create, update, delete expenses -- List expense categories - -**Estimates** (7 tools) -- List, get, create, update, delete estimates -- Send estimates, convert to invoices - -**Time Tracking** (5 tools) -- List, get, create, update, delete time entries - -**Projects** (6 tools) -- List, get, create, update, delete projects -- List project services - -**Payments** (5 tools) -- List, get, create, update, delete payments - -**Items** (5 tools) -- List, get, create, update, delete items (products/services) - -**Taxes** (5 tools) -- List, get, create, update, delete taxes - -**Reports** (5 tools) -- Profit & Loss report -- Tax summary -- Accounts aging -- Expense report -- Revenue by client - -**Recurring** (5 tools) -- List, get, create, update, delete recurring profiles - -**Accounts** (3 tools) -- Get account details -- List staff members -- Get current user - -### 🎨 22 React MCP Apps - -Dark-themed, client-side state React apps (inline HTML): - -1. **invoice-dashboard** - Overview of all invoices with stats -2. **invoice-detail** - Single invoice view -3. **invoice-builder** - Create/edit invoices -4. **invoice-grid** - Grid view of invoices -5. **client-dashboard** - Client overview with metrics -6. **client-detail** - Single client view -7. **client-grid** - Grid view of clients -8. **expense-dashboard** - Expense overview -9. **expense-tracker** - Add and track expenses -10. **estimate-builder** - Create/edit estimates -11. **estimate-grid** - Grid view of estimates -12. **time-tracker** - Real-time timer for tracking hours -13. **time-entries** - List of time entries -14. **project-dashboard** - Project overview with progress -15. **project-detail** - Single project view -16. **payment-history** - List of all payments -17. **reports-dashboard** - Reports menu -18. **profit-loss** - Profit & loss report -19. **tax-summary** - Tax summary report -20. **aging-report** - Accounts aging report -21. **recurring-invoices** - Recurring invoice profiles -22. **revenue-chart** - Revenue visualization +1. **Dashboard Overview** - Business metrics at a glance +2. **Invoice Dashboard** - Invoice list and metrics +3. **Invoice Detail** - Detailed invoice view +4. **Invoice Creator** - Create and edit invoices +5. **Client Dashboard** - Client list and overview +6. **Client Detail** - Detailed client information +7. **Expense Tracker** - Track and categorize expenses +8. **Expense Report** - Expense reporting and analysis +9. **Project Dashboard** - Active projects overview +10. **Project Detail** - Project details and time entries +11. **Time Tracker** - Log and manage time entries +12. **Time Report** - Time tracking reports +13. **Payment Dashboard** - Payment tracking +14. **Estimate Builder** - Create and send estimates +15. **Profit & Loss Report** - Financial P&L statements +16. **Tax Summary** - Tax reporting +17. **Aging Report** - Accounts receivable aging +18. **Item Catalog** - Products and services catalog +19. **Bill Manager** - Vendor bill management +20. **Staff Directory** - Team member directory ## Installation ```bash -npm install -npm run build +npm install @mcpengine/freshbooks ``` ## Configuration -Set environment variables: +### Environment Variables ```bash -export FRESHBOOKS_ACCOUNT_ID="your_account_id" -export FRESHBOOKS_BEARER_TOKEN="your_bearer_token" +FRESHBOOKS_ACCOUNT_ID=your_account_id +FRESHBOOKS_ACCESS_TOKEN=your_oauth_access_token ``` -## Usage +### OAuth2 Setup -### As MCP Server +1. Register your app at https://my.freshbooks.com/#/developer +2. Obtain OAuth2 credentials +3. Complete the OAuth2 authorization flow +4. Use the access token in your MCP server configuration -Add to your MCP settings: +### MCP Settings (Claude Desktop) + +Add to your `claude_desktop_config.json`: ```json { "mcpServers": { "freshbooks": { - "command": "node", - "args": ["/path/to/freshbooks/dist/main.js"], + "command": "npx", + "args": ["-y", "@mcpengine/freshbooks"], "env": { "FRESHBOOKS_ACCOUNT_ID": "your_account_id", - "FRESHBOOKS_BEARER_TOKEN": "your_bearer_token" + "FRESHBOOKS_ACCESS_TOKEN": "your_access_token" } } } } ``` -### Direct Usage +## Usage Examples -```bash -npm start -``` - -## Architecture - -``` -src/ -├── clients/ -│ └── freshbooks.ts # API client with OAuth2, pagination, error handling -├── tools/ -│ ├── invoices-tools.ts # 10 invoice tools -│ ├── clients-tools.ts # 6 client tools -│ ├── expenses-tools.ts # 6 expense tools -│ ├── estimates-tools.ts # 7 estimate tools -│ ├── time-entries-tools.ts # 5 time tracking tools -│ ├── projects-tools.ts # 6 project tools -│ ├── payments-tools.ts # 5 payment tools -│ ├── items-tools.ts # 5 item tools -│ ├── taxes-tools.ts # 5 tax tools -│ ├── reports-tools.ts # 5 report tools -│ ├── recurring-tools.ts # 5 recurring tools -│ └── accounts-tools.ts # 3 account tools -├── types/ -│ └── index.ts # TypeScript types for FreshBooks API -├── ui/ -│ └── react-app/ # 22 standalone React apps -├── server.ts # MCP server implementation -└── main.ts # Entry point -``` - -## API Client Features - -- **OAuth2 Bearer Authentication** -- **Automatic Pagination** - Fetch all pages or paginated results -- **Error Handling** - Structured error responses -- **Rate Limiting** - Respects FreshBooks API limits -- **Type Safety** - Full TypeScript support - -## Example Tool Calls - -### Create Invoice +### List Invoices ```typescript +// Using the MCP tool { - "name": "freshbooks_create_invoice", + "tool": "freshbooks_list_invoices", "arguments": { - "clientid": 12345, - "lines": [ - { "name": "Website Design", "qty": 1, "unit_cost": "2500.00" }, - { "name": "Hosting Setup", "qty": 1, "unit_cost": "150.00" } - ], - "currency_code": "USD", - "notes": "Thank you for your business!" + "page": 1, + "per_page": 20, + "search": "Acme Corp" } } ``` -### List Overdue Invoices +### Create an Invoice ```typescript { - "name": "freshbooks_list_invoices", + "tool": "freshbooks_create_invoice", "arguments": { - "status": "overdue", - "per_page": 50 + "customerid": 12345, + "create_date": "2024-01-15", + "due_offset_days": 30, + "notes": "Thank you for your business!", + "lines": [ + { + "name": "Consulting Services", + "description": "January 2024 consulting", + "qty": "10", + "unit_cost": { + "amount": "150.00", + "code": "USD" + } + } + ] + } +} +``` + +### Send an Invoice + +```typescript +{ + "tool": "freshbooks_send_invoice", + "arguments": { + "invoice_id": 98765 } } ``` @@ -194,48 +142,280 @@ src/ ### Track Time ```typescript +// Start a timer { - "name": "freshbooks_create_time_entry", + "tool": "freshbooks_start_timer", "arguments": { - "duration": 7200, - "note": "Website development", - "started_at": "2024-01-15T09:00:00Z", - "projectid": 456 + "project_id": 456, + "note": "Working on website redesign" + } +} + +// Stop a timer +{ + "tool": "freshbooks_stop_timer", + "arguments": { + "time_entry_id": 789 } } ``` -### Generate Profit/Loss Report +### Generate Reports ```typescript +// Profit & Loss Report { - "name": "freshbooks_profit_loss_report", + "tool": "freshbooks_profit_loss_report", "arguments": { "start_date": "2024-01-01", - "end_date": "2024-01-31", - "currency_code": "USD" + "end_date": "2024-12-31" } } + +// Aging Report +{ + "tool": "freshbooks_aging_report", + "arguments": {} +} ``` +## Tool Reference + +### Client Tools (6 tools) + +- `freshbooks_list_clients` - List all clients +- `freshbooks_get_client` - Get client details +- `freshbooks_create_client` - Create new client +- `freshbooks_update_client` - Update client +- `freshbooks_delete_client` - Delete client +- `freshbooks_search_clients` - Search clients + +### Invoice Tools (10 tools) + +- `freshbooks_list_invoices` - List invoices +- `freshbooks_get_invoice` - Get invoice details +- `freshbooks_create_invoice` - Create invoice +- `freshbooks_update_invoice` - Update invoice +- `freshbooks_delete_invoice` - Delete invoice +- `freshbooks_send_invoice` - Send invoice to client +- `freshbooks_mark_invoice_paid` - Mark as paid +- `freshbooks_get_invoice_share_link` - Get shareable link +- `freshbooks_add_invoice_line` - Add line item +- `freshbooks_search_invoices` - Search invoices + +### Estimate Tools (8 tools) + +- `freshbooks_list_estimates` - List estimates +- `freshbooks_get_estimate` - Get estimate details +- `freshbooks_create_estimate` - Create estimate +- `freshbooks_update_estimate` - Update estimate +- `freshbooks_delete_estimate` - Delete estimate +- `freshbooks_send_estimate` - Send to client +- `freshbooks_accept_estimate` - Mark as accepted +- `freshbooks_add_estimate_line` - Add line item + +### Expense Tools (7 tools) + +- `freshbooks_list_expenses` - List expenses +- `freshbooks_get_expense` - Get expense details +- `freshbooks_create_expense` - Create expense +- `freshbooks_update_expense` - Update expense +- `freshbooks_delete_expense` - Delete expense +- `freshbooks_list_expense_categories` - List categories +- `freshbooks_search_expenses` - Search expenses + +### Payment Tools (5 tools) + +- `freshbooks_list_payments` - List payments +- `freshbooks_get_payment` - Get payment details +- `freshbooks_create_payment` - Record payment +- `freshbooks_update_payment` - Update payment +- `freshbooks_delete_payment` - Delete payment + +### Project Tools (6 tools) + +- `freshbooks_list_projects` - List projects +- `freshbooks_get_project` - Get project details +- `freshbooks_create_project` - Create project +- `freshbooks_update_project` - Update project +- `freshbooks_delete_project` - Delete project +- `freshbooks_mark_project_complete` - Mark complete + +### Time Entry Tools (7 tools) + +- `freshbooks_list_time_entries` - List time entries +- `freshbooks_get_time_entry` - Get entry details +- `freshbooks_create_time_entry` - Log time +- `freshbooks_update_time_entry` - Update entry +- `freshbooks_delete_time_entry` - Delete entry +- `freshbooks_start_timer` - Start timer +- `freshbooks_stop_timer` - Stop timer + +### Tax Tools (5 tools) + +- `freshbooks_list_taxes` - List taxes +- `freshbooks_get_tax` - Get tax details +- `freshbooks_create_tax` - Create tax +- `freshbooks_update_tax` - Update tax +- `freshbooks_delete_tax` - Delete tax + +### Item/Service Tools (5 tools) + +- `freshbooks_list_items` - List items +- `freshbooks_get_item` - Get item details +- `freshbooks_create_item` - Create item +- `freshbooks_update_item` - Update item +- `freshbooks_delete_item` - Delete item + +### Staff Tools (2 tools) + +- `freshbooks_list_staff` - List staff members +- `freshbooks_get_staff_member` - Get staff details + +### Bill Tools (8 tools) + +- `freshbooks_list_bills` - List bills +- `freshbooks_get_bill` - Get bill details +- `freshbooks_create_bill` - Create bill +- `freshbooks_update_bill` - Update bill +- `freshbooks_delete_bill` - Delete bill +- `freshbooks_get_bill_payments` - List payments +- `freshbooks_create_bill_payment` - Record payment + +### Vendor Tools (5 tools) + +- `freshbooks_list_vendors` - List vendors +- `freshbooks_get_vendor` - Get vendor details +- `freshbooks_create_vendor` - Create vendor +- `freshbooks_update_vendor` - Update vendor +- `freshbooks_delete_vendor` - Delete vendor + +### Accounting Tools (2 tools) + +- `freshbooks_list_accounts` - List chart of accounts +- `freshbooks_get_account` - Get account details + +### Journal Entry Tools (3 tools) + +- `freshbooks_list_journal_entries` - List entries +- `freshbooks_get_journal_entry` - Get entry details +- `freshbooks_create_journal_entry` - Create entry + +### Retainer Tools (5 tools) + +- `freshbooks_list_retainers` - List retainers +- `freshbooks_get_retainer` - Get retainer details +- `freshbooks_create_retainer` - Create retainer +- `freshbooks_update_retainer` - Update retainer +- `freshbooks_delete_retainer` - Delete retainer + +### Credit Note Tools (5 tools) + +- `freshbooks_list_credit_notes` - List credit notes +- `freshbooks_get_credit_note` - Get credit note +- `freshbooks_create_credit_note` - Create credit note +- `freshbooks_update_credit_note` - Update credit note +- `freshbooks_delete_credit_note` - Delete credit note + +### Report Tools (4 tools) + +- `freshbooks_profit_loss_report` - P&L report +- `freshbooks_tax_summary_report` - Tax summary +- `freshbooks_aging_report` - Accounts aging +- `freshbooks_expense_report` - Expense report + +## Architecture + +``` +src/ +├── server.ts # MCP server setup +├── main.ts # Entry point +├── clients/ +│ └── freshbooks.ts # FreshBooks API client (OAuth2, rate limiting) +├── tools/ # Tool definitions (17 files) +│ ├── clients-tools.ts +│ ├── invoices-tools.ts +│ ├── estimates-tools.ts +│ ├── expenses-tools.ts +│ ├── payments-tools.ts +│ ├── projects-tools.ts +│ ├── time-entries-tools.ts +│ ├── taxes-tools.ts +│ ├── items-tools.ts +│ ├── staff-tools.ts +│ ├── bills-tools.ts +│ ├── vendors-tools.ts +│ ├── accounts-tools.ts +│ ├── journal-entries-tools.ts +│ ├── retainers-tools.ts +│ ├── credit-notes-tools.ts +│ └── reports-tools.ts +├── types/ +│ └── index.ts # TypeScript interfaces +└── ui/ + └── react-app/ # MCP Apps (20 apps) + ├── src/ + │ ├── apps/ # Individual apps + │ ├── components/ # Shared components + │ ├── hooks/ # Shared hooks + │ └── styles/ # Shared CSS + └── package.json +``` + +## API Coverage + +- ✅ Clients API (complete) +- ✅ Invoices API (complete) +- ✅ Estimates API (complete) +- ✅ Expenses API (complete) +- ✅ Payments API (complete) +- ✅ Projects API (complete) +- ✅ Time Tracking API (complete) +- ✅ Taxes API (complete) +- ✅ Items/Services API (complete) +- ✅ Staff API (read-only) +- ✅ Bills API (complete) +- ✅ Vendors API (complete) +- ✅ Accounting API (partial - read-only) +- ✅ Journal Entries API (create + read) +- ✅ Retainers API (complete) +- ✅ Credit Notes API (complete) +- ✅ Reports API (complete) + ## Development -### Build +### Build from source ```bash +git clone https://github.com/BusyBee3333/mcpengine +cd mcpengine/servers/freshbooks +npm install npm run build ``` -### Watch Mode +### Run tests ```bash -npm run watch +npm test +``` + +### Type checking + +```bash +npm run type-check ``` ## License MIT -## Author +## Support -MCPEngine - Complete MCP implementations for modern platforms +For issues and feature requests, please visit: +https://github.com/BusyBee3333/mcpengine/issues + +## Related + +- [FreshBooks API Documentation](https://www.freshbooks.com/api) +- [Model Context Protocol](https://modelcontextprotocol.io) +- [MCPEngine Repository](https://github.com/BusyBee3333/mcpengine) diff --git a/servers/freshbooks/create-apps.sh b/servers/freshbooks/create-apps.sh new file mode 100755 index 0000000..ae8827d --- /dev/null +++ b/servers/freshbooks/create-apps.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +APPS_DIR="src/ui/react-app/src/apps" + +# Function to create an app +create_app() { + local app_name=$1 + local title=$2 + local tool=$3 + + mkdir -p "$APPS_DIR/$app_name" + + # Create index.html + cat > "$APPS_DIR/$app_name/index.html" < + + + + + $title - FreshBooks + + +
+ + + +EOF + + # Create main.tsx + cat > "$APPS_DIR/$app_name/main.tsx" < + + +); +EOF + + # Create App.tsx + cat > "$APPS_DIR/$app_name/App.tsx" <; + if (error) return
Error: {error}
; + + return ( +
+
+

$title

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} +EOF +} + +# Create all apps +create_app "invoice-dashboard" "Invoice Dashboard" "freshbooks_list_invoices" +create_app "invoice-detail" "Invoice Detail" "freshbooks_get_invoice" +create_app "invoice-creator" "Invoice Creator" "freshbooks_create_invoice" +create_app "client-dashboard" "Client Dashboard" "freshbooks_list_clients" +create_app "client-detail" "Client Detail" "freshbooks_get_client" +create_app "expense-tracker" "Expense Tracker" "freshbooks_list_expenses" +create_app "expense-report" "Expense Report" "freshbooks_expense_report" +create_app "project-dashboard" "Project Dashboard" "freshbooks_list_projects" +create_app "project-detail" "Project Detail" "freshbooks_get_project" +create_app "time-tracker" "Time Tracker" "freshbooks_list_time_entries" +create_app "time-report" "Time Report" "freshbooks_list_time_entries" +create_app "payment-dashboard" "Payment Dashboard" "freshbooks_list_payments" +create_app "estimate-builder" "Estimate Builder" "freshbooks_list_estimates" +create_app "profit-loss-report" "Profit & Loss Report" "freshbooks_profit_loss_report" +create_app "tax-summary" "Tax Summary" "freshbooks_tax_summary_report" +create_app "aging-report" "Aging Report" "freshbooks_aging_report" +create_app "item-catalog" "Item Catalog" "freshbooks_list_items" +create_app "bill-manager" "Bill Manager" "freshbooks_list_bills" +create_app "staff-directory" "Staff Directory" "freshbooks_list_staff" + +echo "All apps created successfully!" diff --git a/servers/freshbooks/package.json b/servers/freshbooks/package.json index c8ee48d..a588d15 100644 --- a/servers/freshbooks/package.json +++ b/servers/freshbooks/package.json @@ -1,34 +1,38 @@ { "name": "@mcpengine/freshbooks", "version": "1.0.0", - "description": "FreshBooks MCP Server - Complete accounting, invoicing, time tracking, and financial management", - "main": "dist/main.js", + "description": "FreshBooks MCP server with comprehensive API coverage and React apps", "type": "module", + "bin": { + "freshbooks-mcp": "./dist/main.js" + }, + "main": "./dist/main.js", "scripts": { - "build": "tsc", - "watch": "tsc --watch", - "prepare": "npm run build", - "start": "node dist/main.js" + "build": "tsc && npm run build:ui", + "build:ui": "cd src/ui/react-app && npm install && npm run build", + "dev": "tsx src/main.ts", + "prepublishOnly": "npm run build", + "test": "jest", + "type-check": "tsc --noEmit" }, "keywords": [ "mcp", "freshbooks", "accounting", "invoicing", - "time-tracking", - "expenses", - "estimates", - "payments" + "api" ], "author": "MCPEngine", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", - "axios": "^1.7.9", - "zod": "^3.24.1" + "@modelcontextprotocol/ext-apps": "^0.1.0" }, "devDependencies": { - "@types/node": "^22.10.2", - "typescript": "^5.7.2" + "@types/node": "^22.10.5", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "jest": "^29.7.0", + "@types/jest": "^29.5.14" } } diff --git a/servers/freshbooks/src/clients/freshbooks.ts b/servers/freshbooks/src/clients/freshbooks.ts index ecad7ab..e293af5 100644 --- a/servers/freshbooks/src/clients/freshbooks.ts +++ b/servers/freshbooks/src/clients/freshbooks.ts @@ -1,144 +1,613 @@ -import axios, { AxiosInstance, AxiosError } from 'axios'; import type { FreshBooksConfig, + FreshBooksClient, + FreshBooksInvoice, + FreshBooksEstimate, + FreshBooksExpense, + ExpenseCategory, + FreshBooksPayment, + FreshBooksProject, + FreshBooksTimeEntry, + FreshBooksTax, + FreshBooksItem, + FreshBooksStaff, + FreshBooksBill, + BillVendor, + BillPayment, + AccountingAccount, + JournalEntry, + Retainer, + CreditNote, + ProfitLossReport, + TaxSummaryReport, + AgingReport, + ExpenseReport, PaginatedResponse, FreshBooksError, } from '../types/index.js'; -export class FreshBooksClient { - private client: AxiosInstance; +export class FreshBooksAPIClient { private accountId: string; + private accessToken: string; + private apiBaseUrl: string; + private rateLimitDelay = 100; // ms between requests + private lastRequestTime = 0; constructor(config: FreshBooksConfig) { this.accountId = config.accountId; - const baseURL = config.baseUrl || `https://api.freshbooks.com/accounting/account/${config.accountId}`; - - this.client = axios.create({ - baseURL, - headers: { - 'Authorization': `Bearer ${config.bearerToken}`, - 'Content-Type': 'application/json', - 'Api-Version': 'alpha', - }, - timeout: 30000, - }); - - // Request interceptor for logging - this.client.interceptors.request.use( - (config) => { - console.error(`[FreshBooks] ${config.method?.toUpperCase()} ${config.url}`); - return config; - }, - (error) => Promise.reject(error) - ); - - // Response interceptor for error handling - this.client.interceptors.response.use( - (response) => response, - (error: AxiosError) => { - return Promise.reject(this.handleError(error)); - } - ); + this.accessToken = config.accessToken; + this.apiBaseUrl = config.apiBaseUrl || 'https://api.freshbooks.com'; } - private handleError(error: AxiosError): FreshBooksError { - if (error.response) { - const data = error.response.data as any; - return { - message: data.message || data.error || `HTTP ${error.response.status}: ${error.response.statusText}`, - code: data.code || `${error.response.status}`, - errors: data.errors || data.response?.errors, - }; - } else if (error.request) { - return { - message: 'No response from FreshBooks API', - code: 'NETWORK_ERROR', - }; - } else { - return { - message: error.message || 'Unknown error', - code: 'UNKNOWN_ERROR', - }; + private async rateLimit(): Promise { + const now = Date.now(); + const timeSinceLastRequest = now - this.lastRequestTime; + if (timeSinceLastRequest < this.rateLimitDelay) { + await new Promise(resolve => setTimeout(resolve, this.rateLimitDelay - timeSinceLastRequest)); } + this.lastRequestTime = Date.now(); } - // Generic GET with pagination support - async get(endpoint: string, params?: Record): Promise { - const response = await this.client.get(endpoint, { params }); - return response.data; + // Helper methods for recurring-tools compatibility + async get(endpoint: string, queryParams?: Record): Promise { + return this.request('GET', `/accounting/account/${this.accountId}${endpoint}`, undefined, queryParams); } - // Generic GET with automatic pagination (fetch all pages) - async getAll( - endpoint: string, - params?: Record, - resultKey: string = 'result' - ): Promise { - let page = 1; - let allResults: T[] = []; - let hasMore = true; - - while (hasMore) { - const response = await this.client.get>(endpoint, { - params: { ...params, page, per_page: 100 }, - }); - - const result = response.data.response.result; - const items = Array.isArray(result[resultKey]) ? result[resultKey] : []; - allResults = allResults.concat(items); - - const { page: currentPage, pages } = response.data.response; - hasMore = currentPage < pages; - page++; - } - - return allResults; + async post(endpoint: string, data?: any): Promise { + return this.request('POST', `/accounting/account/${this.accountId}${endpoint}`, data); + } + + async put(endpoint: string, data?: any): Promise { + return this.request('PUT', `/accounting/account/${this.accountId}${endpoint}`, data); } - // Paginated GET (single page) async getPaginated( endpoint: string, page: number = 1, - perPage: number = 30, + per_page: number = 30, params?: Record - ): Promise> { - const response = await this.client.get>(endpoint, { - params: { ...params, page, per_page: perPage }, + ): Promise { + return this.request('GET', `/accounting/account/${this.accountId}${endpoint}`, undefined, { + ...params, + page, + per_page, }); - return response.data; } - // POST - async post(endpoint: string, data: any): Promise { - const response = await this.client.post(endpoint, data); - return response.data; - } - - // PUT - async put(endpoint: string, data: any): Promise { - const response = await this.client.put(endpoint, data); - return response.data; - } - - // DELETE - async delete(endpoint: string): Promise { - const response = await this.client.delete(endpoint); - return response.data; - } - - // Convenience method: search with filters - async search( + private async request( + method: string, endpoint: string, - searchFields: Record, - page: number = 1, - perPage: number = 30 - ): Promise> { - return this.getPaginated(endpoint, page, perPage, { - search: searchFields, + data?: any, + queryParams?: Record + ): Promise { + await this.rateLimit(); + + const url = new URL(`${this.apiBaseUrl}${endpoint}`); + if (queryParams) { + Object.entries(queryParams).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, String(value)); + } + }); + } + + const options: RequestInit = { + method, + headers: { + 'Authorization': `Bearer ${this.accessToken}`, + 'Content-Type': 'application/json', + 'Api-Version': 'alpha', + }, + }; + + if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { + options.body = JSON.stringify(data); + } + + try { + const response = await fetch(url.toString(), options); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw this.mapError(response.status, errorData); + } + + const responseData = await response.json() as any; + return responseData.response || responseData; + } catch (error) { + if (error instanceof Error && error.message.includes('fetch')) { + throw new Error(`Network error: ${error.message}`); + } + throw error; + } + } + + private mapError(status: number, errorData: any): Error { + const message = errorData?.message || errorData?.error || 'Unknown error'; + + switch (status) { + case 401: + return new Error(`Authentication failed: ${message}`); + case 403: + return new Error(`Permission denied: ${message}`); + case 404: + return new Error(`Resource not found: ${message}`); + case 429: + return new Error(`Rate limit exceeded: ${message}`); + case 422: + return new Error(`Validation error: ${message}`); + case 500: + case 502: + case 503: + return new Error(`FreshBooks server error: ${message}`); + default: + return new Error(`FreshBooks API error (${status}): ${message}`); + } + } + + // Pagination helper + private async paginate( + endpoint: string, + params: Record = {} + ): Promise { + const results: T[] = []; + let page = 1; + let hasMore = true; + + while (hasMore) { + const response = await this.request>('GET', endpoint, undefined, { + ...params, + page, + }); + + results.push(...response.results); + + if (page >= response.pages) { + hasMore = false; + } else { + page++; + } + } + + return results; + } + + // ========== CLIENTS ========== + async getClients(params?: { search?: string; page?: number; per_page?: number }): Promise> { + return this.request('GET', `/accounting/account/${this.accountId}/users/clients`, undefined, params); + } + + async getClient(clientId: number): Promise { + const response = await this.request<{ result: FreshBooksClient }>('GET', `/accounting/account/${this.accountId}/users/clients/${clientId}`); + return response.result; + } + + async createClient(client: Partial): Promise { + const response = await this.request<{ result: FreshBooksClient }>('POST', `/accounting/account/${this.accountId}/users/clients`, { client }); + return response.result; + } + + async updateClient(clientId: number, updates: Partial): Promise { + const response = await this.request<{ result: FreshBooksClient }>('PUT', `/accounting/account/${this.accountId}/users/clients/${clientId}`, { client: updates }); + return response.result; + } + + async deleteClient(clientId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/users/clients/${clientId}`); + } + + // ========== INVOICES ========== + async getInvoices(params?: { search?: string; page?: number; per_page?: number }): Promise> { + return this.request('GET', `/accounting/account/${this.accountId}/invoices/invoices`, undefined, params); + } + + async getInvoice(invoiceId: number): Promise { + const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('GET', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`); + return response.result.invoice; + } + + async createInvoice(invoice: Partial): Promise { + const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('POST', `/accounting/account/${this.accountId}/invoices/invoices`, { invoice }); + return response.result.invoice; + } + + async updateInvoice(invoiceId: number, updates: Partial): Promise { + const response = await this.request<{ result: { invoice: FreshBooksInvoice } }>('PUT', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`, { invoice: updates }); + return response.result.invoice; + } + + async deleteInvoice(invoiceId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}`); + } + + async sendInvoice(invoiceId: number, email?: string): Promise { + await this.request('POST', `/accounting/account/${this.accountId}/invoices/invoices/${invoiceId}/send`, { email }); + } + + async markInvoicePaid(invoiceId: number): Promise { + await this.updateInvoice(invoiceId, { v3_status: 'paid' }); + } + + async getInvoiceShareLink(invoiceId: number): Promise { + const invoice = await this.getInvoice(invoiceId); + return `https://my.freshbooks.com/#/invoice/${this.accountId}-${invoice.invoiceid}`; + } + + // ========== ESTIMATES ========== + async getEstimates(params?: { search?: string; page?: number; per_page?: number }): Promise> { + return this.request('GET', `/accounting/account/${this.accountId}/estimates/estimates`, undefined, params); + } + + async getEstimate(estimateId: number): Promise { + const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('GET', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`); + return response.result.estimate; + } + + async createEstimate(estimate: Partial): Promise { + const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('POST', `/accounting/account/${this.accountId}/estimates/estimates`, { estimate }); + return response.result.estimate; + } + + async updateEstimate(estimateId: number, updates: Partial): Promise { + const response = await this.request<{ result: { estimate: FreshBooksEstimate } }>('PUT', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`, { estimate: updates }); + return response.result.estimate; + } + + async deleteEstimate(estimateId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}`); + } + + async sendEstimate(estimateId: number, email?: string): Promise { + await this.request('POST', `/accounting/account/${this.accountId}/estimates/estimates/${estimateId}/send`, { email }); + } + + async acceptEstimate(estimateId: number): Promise { + await this.updateEstimate(estimateId, { accepted: true }); + } + + // ========== EXPENSES ========== + async getExpenses(params?: { search?: string; page?: number; per_page?: number }): Promise> { + return this.request('GET', `/accounting/account/${this.accountId}/expenses/expenses`, undefined, params); + } + + async getExpense(expenseId: number): Promise { + const response = await this.request<{ result: { expense: FreshBooksExpense } }>('GET', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`); + return response.result.expense; + } + + async createExpense(expense: Partial): Promise { + const response = await this.request<{ result: { expense: FreshBooksExpense } }>('POST', `/accounting/account/${this.accountId}/expenses/expenses`, { expense }); + return response.result.expense; + } + + async updateExpense(expenseId: number, updates: Partial): Promise { + const response = await this.request<{ result: { expense: FreshBooksExpense } }>('PUT', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`, { expense: updates }); + return response.result.expense; + } + + async deleteExpense(expenseId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/expenses/expenses/${expenseId}`); + } + + async getExpenseCategories(): Promise { + const response = await this.request<{ result: { categories: ExpenseCategory[] } }>('GET', `/accounting/account/${this.accountId}/expenses/categories`); + return response.result.categories; + } + + // ========== PAYMENTS ========== + async getPayments(params?: { page?: number; per_page?: number }): Promise> { + return this.request('GET', `/accounting/account/${this.accountId}/payments/payments`, undefined, params); + } + + async getPayment(paymentId: number): Promise { + const response = await this.request<{ result: { payment: FreshBooksPayment } }>('GET', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`); + return response.result.payment; + } + + async createPayment(payment: Partial): Promise { + const response = await this.request<{ result: { payment: FreshBooksPayment } }>('POST', `/accounting/account/${this.accountId}/payments/payments`, { payment }); + return response.result.payment; + } + + async updatePayment(paymentId: number, updates: Partial): Promise { + const response = await this.request<{ result: { payment: FreshBooksPayment } }>('PUT', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`, { payment: updates }); + return response.result.payment; + } + + async deletePayment(paymentId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/payments/payments/${paymentId}`); + } + + // ========== PROJECTS ========== + async getProjects(params?: { page?: number; per_page?: number }): Promise { + const response = await this.request<{ projects: FreshBooksProject[] }>('GET', `/projects/business/${this.accountId}/projects`, undefined, params); + return response.projects; + } + + async getProject(projectId: number): Promise { + const response = await this.request<{ project: FreshBooksProject }>('GET', `/projects/business/${this.accountId}/project/${projectId}`); + return response.project; + } + + async createProject(project: Partial): Promise { + const response = await this.request<{ project: FreshBooksProject }>('POST', `/projects/business/${this.accountId}/project`, project); + return response.project; + } + + async updateProject(projectId: number, updates: Partial): Promise { + const response = await this.request<{ project: FreshBooksProject }>('PUT', `/projects/business/${this.accountId}/project/${projectId}`, updates); + return response.project; + } + + async deleteProject(projectId: number): Promise { + await this.request('DELETE', `/projects/business/${this.accountId}/project/${projectId}`); + } + + // ========== TIME ENTRIES ========== + async getTimeEntries(params?: { page?: number; per_page?: number }): Promise { + const response = await this.request<{ time_entries: FreshBooksTimeEntry[] }>('GET', `/timetracking/business/${this.accountId}/time_entries`, undefined, params); + return response.time_entries; + } + + async getTimeEntry(timeEntryId: number): Promise { + const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('GET', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`); + return response.time_entry; + } + + async createTimeEntry(timeEntry: Partial): Promise { + const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('POST', `/timetracking/business/${this.accountId}/time_entries`, { time_entry: timeEntry }); + return response.time_entry; + } + + async updateTimeEntry(timeEntryId: number, updates: Partial): Promise { + const response = await this.request<{ time_entry: FreshBooksTimeEntry }>('PUT', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`, { time_entry: updates }); + return response.time_entry; + } + + async deleteTimeEntry(timeEntryId: number): Promise { + await this.request('DELETE', `/timetracking/business/${this.accountId}/time_entries/${timeEntryId}`); + } + + async startTimer(projectId: number, note?: string): Promise { + return this.createTimeEntry({ + project_id: projectId, + is_logged: false, + started_at: new Date().toISOString(), + note, }); } - getAccountId(): string { - return this.accountId; + async stopTimer(timeEntryId: number): Promise { + return this.updateTimeEntry(timeEntryId, { + is_logged: true, + }); + } + + // ========== TAXES ========== + async getTaxes(): Promise { + const response = await this.request<{ result: { taxes: FreshBooksTax[] } }>('GET', `/accounting/account/${this.accountId}/taxes/taxes`); + return response.result.taxes; + } + + async getTax(taxId: number): Promise { + const response = await this.request<{ result: { tax: FreshBooksTax } }>('GET', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`); + return response.result.tax; + } + + async createTax(tax: Partial): Promise { + const response = await this.request<{ result: { tax: FreshBooksTax } }>('POST', `/accounting/account/${this.accountId}/taxes/taxes`, { tax }); + return response.result.tax; + } + + async updateTax(taxId: number, updates: Partial): Promise { + const response = await this.request<{ result: { tax: FreshBooksTax } }>('PUT', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`, { tax: updates }); + return response.result.tax; + } + + async deleteTax(taxId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/taxes/taxes/${taxId}`); + } + + // ========== ITEMS/SERVICES ========== + async getItems(params?: { page?: number; per_page?: number }): Promise> { + return this.request('GET', `/accounting/account/${this.accountId}/items/items`, undefined, params); + } + + async getItem(itemId: number): Promise { + const response = await this.request<{ result: { item: FreshBooksItem } }>('GET', `/accounting/account/${this.accountId}/items/items/${itemId}`); + return response.result.item; + } + + async createItem(item: Partial): Promise { + const response = await this.request<{ result: { item: FreshBooksItem } }>('POST', `/accounting/account/${this.accountId}/items/items`, { item }); + return response.result.item; + } + + async updateItem(itemId: number, updates: Partial): Promise { + const response = await this.request<{ result: { item: FreshBooksItem } }>('PUT', `/accounting/account/${this.accountId}/items/items/${itemId}`, { item: updates }); + return response.result.item; + } + + async deleteItem(itemId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/items/items/${itemId}`); + } + + // ========== STAFF ========== + async getStaff(params?: { page?: number; per_page?: number }): Promise { + const response = await this.request<{ staff_members: FreshBooksStaff[] }>('GET', `/projects/business/${this.accountId}/staff`, undefined, params); + return response.staff_members; + } + + async getStaffMember(staffId: number): Promise { + const response = await this.request<{ staff_member: FreshBooksStaff }>('GET', `/projects/business/${this.accountId}/staff/${staffId}`); + return response.staff_member; + } + + // ========== BILLS ========== + async getBills(params?: { page?: number; per_page?: number }): Promise { + const response = await this.request<{ bills: FreshBooksBill[] }>('GET', `/accounting/account/${this.accountId}/bills/bills`, undefined, params); + return response.bills; + } + + async getBill(billId: number): Promise { + const response = await this.request<{ bill: FreshBooksBill }>('GET', `/accounting/account/${this.accountId}/bills/bills/${billId}`); + return response.bill; + } + + async createBill(bill: Partial): Promise { + const response = await this.request<{ bill: FreshBooksBill }>('POST', `/accounting/account/${this.accountId}/bills/bills`, { bill }); + return response.bill; + } + + async updateBill(billId: number, updates: Partial): Promise { + const response = await this.request<{ bill: FreshBooksBill }>('PUT', `/accounting/account/${this.accountId}/bills/bills/${billId}`, { bill: updates }); + return response.bill; + } + + async deleteBill(billId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/bills/bills/${billId}`); + } + + // ========== BILL VENDORS ========== + async getVendors(params?: { page?: number; per_page?: number }): Promise { + const response = await this.request<{ bill_vendors: BillVendor[] }>('GET', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors`, undefined, params); + return response.bill_vendors; + } + + async getVendor(vendorId: number): Promise { + const response = await this.request<{ bill_vendor: BillVendor }>('GET', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`); + return response.bill_vendor; + } + + async createVendor(vendor: Partial): Promise { + const response = await this.request<{ bill_vendor: BillVendor }>('POST', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors`, { bill_vendor: vendor }); + return response.bill_vendor; + } + + async updateVendor(vendorId: number, updates: Partial): Promise { + const response = await this.request<{ bill_vendor: BillVendor }>('PUT', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`, { bill_vendor: updates }); + return response.bill_vendor; + } + + async deleteVendor(vendorId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/bill_vendors/bill_vendors/${vendorId}`); + } + + // ========== BILL PAYMENTS ========== + async getBillPayments(billId: number): Promise { + const bill = await this.getBill(billId); + return bill.bill_payments || []; + } + + async createBillPayment(billId: number, payment: Partial): Promise { + const response = await this.request<{ bill_payment: BillPayment }>('POST', `/accounting/account/${this.accountId}/bills/bills/${billId}/bill_payments`, { bill_payment: payment }); + return response.bill_payment; + } + + // ========== ACCOUNTING ACCOUNTS ========== + async getAccounts(): Promise { + const response = await this.request<{ accounts: AccountingAccount[] }>('GET', `/accounting/account/${this.accountId}/accounts/accounts`); + return response.accounts; + } + + async getAccount(accountId: number): Promise { + const response = await this.request<{ account: AccountingAccount }>('GET', `/accounting/account/${this.accountId}/accounts/accounts/${accountId}`); + return response.account; + } + + // ========== JOURNAL ENTRIES ========== + async getJournalEntries(params?: { page?: number; per_page?: number }): Promise { + const response = await this.request<{ journal_entries: JournalEntry[] }>('GET', `/accounting/account/${this.accountId}/journal_entries/journal_entries`, undefined, params); + return response.journal_entries; + } + + async getJournalEntry(journalEntryId: number): Promise { + const response = await this.request<{ journal_entry: JournalEntry }>('GET', `/accounting/account/${this.accountId}/journal_entries/journal_entries/${journalEntryId}`); + return response.journal_entry; + } + + async createJournalEntry(journalEntry: Partial): Promise { + const response = await this.request<{ journal_entry: JournalEntry }>('POST', `/accounting/account/${this.accountId}/journal_entries/journal_entries`, { journal_entry: journalEntry }); + return response.journal_entry; + } + + // ========== RETAINERS ========== + async getRetainers(params?: { page?: number; per_page?: number }): Promise { + const response = await this.request<{ retainers: Retainer[] }>('GET', `/projects/business/${this.accountId}/retainers`, undefined, params); + return response.retainers; + } + + async getRetainer(retainerId: number): Promise { + const response = await this.request<{ retainer: Retainer }>('GET', `/projects/business/${this.accountId}/retainers/${retainerId}`); + return response.retainer; + } + + async createRetainer(retainer: Partial): Promise { + const response = await this.request<{ retainer: Retainer }>('POST', `/projects/business/${this.accountId}/retainers`, { retainer }); + return response.retainer; + } + + async updateRetainer(retainerId: number, updates: Partial): Promise { + const response = await this.request<{ retainer: Retainer }>('PUT', `/projects/business/${this.accountId}/retainers/${retainerId}`, { retainer: updates }); + return response.retainer; + } + + async deleteRetainer(retainerId: number): Promise { + await this.request('DELETE', `/projects/business/${this.accountId}/retainers/${retainerId}`); + } + + // ========== CREDIT NOTES ========== + async getCreditNotes(params?: { page?: number; per_page?: number }): Promise { + const response = await this.request<{ credit_notes: CreditNote[] }>('GET', `/accounting/account/${this.accountId}/credit_notes/credit_notes`, undefined, params); + return response.credit_notes; + } + + async getCreditNote(creditNoteId: number): Promise { + const response = await this.request<{ credit_note: CreditNote }>('GET', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`); + return response.credit_note; + } + + async createCreditNote(creditNote: Partial): Promise { + const response = await this.request<{ credit_note: CreditNote }>('POST', `/accounting/account/${this.accountId}/credit_notes/credit_notes`, { credit_note: creditNote }); + return response.credit_note; + } + + async updateCreditNote(creditNoteId: number, updates: Partial): Promise { + const response = await this.request<{ credit_note: CreditNote }>('PUT', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`, { credit_note: updates }); + return response.credit_note; + } + + async deleteCreditNote(creditNoteId: number): Promise { + await this.request('DELETE', `/accounting/account/${this.accountId}/credit_notes/credit_notes/${creditNoteId}`); + } + + // ========== REPORTS ========== + async getProfitLossReport(startDate: string, endDate: string): Promise { + const response = await this.request<{ report: ProfitLossReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/profitloss`, undefined, { + start_date: startDate, + end_date: endDate, + }); + return response.report; + } + + async getTaxSummaryReport(startDate: string, endDate: string): Promise { + const response = await this.request<{ report: TaxSummaryReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/taxsummary`, undefined, { + start_date: startDate, + end_date: endDate, + }); + return response.report; + } + + async getAgingReport(): Promise { + const response = await this.request<{ report: AgingReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/aging`); + return response.report; + } + + async getExpenseReport(startDate: string, endDate: string): Promise { + const response = await this.request<{ report: ExpenseReport }>('GET', `/accounting/account/${this.accountId}/reports/accounting/expenses`, undefined, { + start_date: startDate, + end_date: endDate, + }); + return response.report; } } diff --git a/servers/freshbooks/src/main.ts b/servers/freshbooks/src/main.ts index 43e0255..a39b638 100644 --- a/servers/freshbooks/src/main.ts +++ b/servers/freshbooks/src/main.ts @@ -1,14 +1,7 @@ #!/usr/bin/env node -import { FreshBooksServer } from './server.js'; +import { runServer } from './server.js'; -async function main() { - try { - const server = new FreshBooksServer(); - await server.run(); - } catch (error) { - console.error('Fatal error starting FreshBooks MCP server:', error); - process.exit(1); - } -} - -main(); +runServer().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/servers/freshbooks/src/server.ts b/servers/freshbooks/src/server.ts index 0ef9960..1f073ba 100644 --- a/servers/freshbooks/src/server.ts +++ b/servers/freshbooks/src/server.ts @@ -3,145 +3,181 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; -import { FreshBooksClient } from './clients/freshbooks.js'; -import { invoicesTools } from './tools/invoices-tools.js'; +import { FreshBooksAPIClient } from './clients/freshbooks.js'; import { clientsTools } from './tools/clients-tools.js'; -import { expensesTools } from './tools/expenses-tools.js'; +import { invoicesTools } from './tools/invoices-tools.js'; import { estimatesTools } from './tools/estimates-tools.js'; -import { timeEntriesTools } from './tools/time-entries-tools.js'; -import { projectsTools } from './tools/projects-tools.js'; +import { expensesTools } from './tools/expenses-tools.js'; import { paymentsTools } from './tools/payments-tools.js'; -import { itemsTools } from './tools/items-tools.js'; +import { projectsTools } from './tools/projects-tools.js'; +import { timeEntriesTools } from './tools/time-entries-tools.js'; import { taxesTools } from './tools/taxes-tools.js'; -import { reportsTools } from './tools/reports-tools.js'; -import { recurringTools } from './tools/recurring-tools.js'; +import { itemsTools } from './tools/items-tools.js'; +import { staffTools } from './tools/staff-tools.js'; +import { billsTools } from './tools/bills-tools.js'; +import { vendorsTools } from './tools/vendors-tools.js'; import { accountsTools } from './tools/accounts-tools.js'; +import { journalEntriesTools } from './tools/journal-entries-tools.js'; +import { retainersTools } from './tools/retainers-tools.js'; +import { creditNotesTools } from './tools/credit-notes-tools.js'; +import { reportsTools } from './tools/reports-tools.js'; -export class FreshBooksServer { - private server: Server; - private client: FreshBooksClient; - private allTools: any[]; +// Combine all tools +const allTools = [ + ...clientsTools, + ...invoicesTools, + ...estimatesTools, + ...expensesTools, + ...paymentsTools, + ...projectsTools, + ...timeEntriesTools, + ...taxesTools, + ...itemsTools, + ...staffTools, + ...billsTools, + ...vendorsTools, + ...accountsTools, + ...journalEntriesTools, + ...retainersTools, + ...creditNotesTools, + ...reportsTools, +]; - constructor() { - this.server = new Server( - { - name: 'freshbooks-mcp-server', - version: '1.0.0', +// MCP App resources (HTML files) +const appResources = [ + { uri: 'freshbooks://apps/invoice-dashboard', name: 'Invoice Dashboard' }, + { uri: 'freshbooks://apps/invoice-detail', name: 'Invoice Detail' }, + { uri: 'freshbooks://apps/invoice-creator', name: 'Invoice Creator' }, + { uri: 'freshbooks://apps/client-dashboard', name: 'Client Dashboard' }, + { uri: 'freshbooks://apps/client-detail', name: 'Client Detail' }, + { uri: 'freshbooks://apps/expense-tracker', name: 'Expense Tracker' }, + { uri: 'freshbooks://apps/expense-report', name: 'Expense Report' }, + { uri: 'freshbooks://apps/project-dashboard', name: 'Project Dashboard' }, + { uri: 'freshbooks://apps/project-detail', name: 'Project Detail' }, + { uri: 'freshbooks://apps/time-tracker', name: 'Time Tracker' }, + { uri: 'freshbooks://apps/time-report', name: 'Time Report' }, + { uri: 'freshbooks://apps/payment-dashboard', name: 'Payment Dashboard' }, + { uri: 'freshbooks://apps/estimate-builder', name: 'Estimate Builder' }, + { uri: 'freshbooks://apps/profit-loss-report', name: 'Profit & Loss Report' }, + { uri: 'freshbooks://apps/tax-summary', name: 'Tax Summary' }, + { uri: 'freshbooks://apps/aging-report', name: 'Aging Report' }, + { uri: 'freshbooks://apps/item-catalog', name: 'Item Catalog' }, + { uri: 'freshbooks://apps/bill-manager', name: 'Bill Manager' }, + { uri: 'freshbooks://apps/staff-directory', name: 'Staff Directory' }, + { uri: 'freshbooks://apps/dashboard-overview', name: 'Dashboard Overview' }, +]; + +export async function createServer() { + const server = new Server( + { + name: 'freshbooks-mcp', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + resources: {}, }, - { - capabilities: { - tools: {}, - }, - } + } + ); + + // Get config from environment + const accountId = process.env.FRESHBOOKS_ACCOUNT_ID; + const accessToken = process.env.FRESHBOOKS_ACCESS_TOKEN; + + if (!accountId || !accessToken) { + throw new Error( + 'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_ACCESS_TOKEN' ); + } - // Initialize FreshBooks client from env - const accountId = process.env.FRESHBOOKS_ACCOUNT_ID; - const bearerToken = process.env.FRESHBOOKS_BEARER_TOKEN; + const client = new FreshBooksAPIClient({ + accountId, + accessToken, + }); - if (!accountId || !bearerToken) { - throw new Error( - 'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_BEARER_TOKEN' - ); + // List tools handler + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: allTools.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + })), + }; + }); + + // Call tool handler + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const tool = allTools.find((t) => t.name === request.params.name); + if (!tool) { + throw new Error(`Unknown tool: ${request.params.name}`); } - this.client = new FreshBooksClient({ - accountId, - bearerToken, - }); - - // Combine all tools - this.allTools = [ - ...invoicesTools, - ...clientsTools, - ...expensesTools, - ...estimatesTools, - ...timeEntriesTools, - ...projectsTools, - ...paymentsTools, - ...itemsTools, - ...taxesTools, - ...reportsTools, - ...recurringTools, - ...accountsTools, - ]; - - this.setupHandlers(); - - // Error handling - this.server.onerror = (error) => { - console.error('[MCP Error]', error); - }; - - process.on('SIGINT', async () => { - await this.server.close(); - process.exit(0); - }); - } - - private setupHandlers() { - // List tools handler - this.server.setRequestHandler(ListToolsRequestSchema, async () => { + try { + return await tool.handler(client, request.params.arguments || {}); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); return { - tools: this.allTools.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: { - type: 'object', - properties: tool.inputSchema.shape, - required: Object.keys(tool.inputSchema.shape).filter( - (key) => !tool.inputSchema.shape[key].isOptional() - ), + content: [ + { + type: 'text', + text: `Error: ${errorMessage}`, }, - })), + ], + isError: true, }; - }); + } + }); - // Call tool handler - this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const toolName = request.params.name; - const tool = this.allTools.find((t) => t.name === toolName); + // List resources handler (MCP Apps) + server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: appResources.map((app) => ({ + uri: app.uri, + mimeType: 'text/html', + name: app.name, + })), + }; + }); - if (!tool) { - throw new Error(`Unknown tool: ${toolName}`); - } + // Read resource handler (serve MCP App HTML) + server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const uri = request.params.uri; + const appName = uri.replace('freshbooks://apps/', ''); + + try { + const { readFileSync } = await import('fs'); + const { join } = await import('path'); + const { fileURLToPath } = await import('url'); + const __dirname = fileURLToPath(new URL('.', import.meta.url)); + + const htmlPath = join(__dirname, 'ui', 'react-app', 'dist', `${appName}.html`); + const html = readFileSync(htmlPath, 'utf-8'); + + return { + contents: [ + { + uri, + mimeType: 'text/html', + text: html, + }, + ], + }; + } catch (error) { + throw new Error(`Failed to load app ${appName}: ${error}`); + } + }); - try { - // Validate input - const validatedArgs = tool.inputSchema.parse(request.params.arguments); - - // Execute tool - const result = await tool.handler(validatedArgs, this.client); - - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - } catch (error: any) { - const errorMessage = error.message || 'Unknown error'; - const errorDetails = error.errors ? JSON.stringify(error.errors, null, 2) : ''; - - return { - content: [ - { - type: 'text', - text: `Error: ${errorMessage}\n${errorDetails}`, - }, - ], - isError: true, - }; - } - }); - } - - async run() { - const transport = new StdioServerTransport(); - await this.server.connect(transport); - console.error('FreshBooks MCP server running on stdio'); - } + return server; +} + +export async function runServer() { + const server = await createServer(); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('FreshBooks MCP server running on stdio'); } diff --git a/servers/freshbooks/src/tools/accounts-tools.ts b/servers/freshbooks/src/tools/accounts-tools.ts index ba299ae..2fe64b6 100644 --- a/servers/freshbooks/src/tools/accounts-tools.ts +++ b/servers/freshbooks/src/tools/accounts-tools.ts @@ -1,51 +1,48 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Account, StaffMember } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const accountsTools = [ { - name: 'freshbooks_get_account', - description: 'Get current account details', - inputSchema: z.object({}), - handler: async (_args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { account: Account } } }>( - `/users/me` - ); - return response.response.result.account; + name: 'freshbooks_list_accounts', + description: 'List all accounting accounts (chart of accounts)', + inputSchema: { + type: 'object', + properties: {}, }, - }, - - { - name: 'freshbooks_list_staff', - description: 'List all staff members', - inputSchema: z.object({ - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.getPaginated<{ staff: StaffMember[] }>( - '/users/staff', - args.page, - args.per_page - ); + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getAccounts(); return { - staff: response.response.result.staff || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { - name: 'freshbooks_get_current_user', - description: 'Get current user (self) details', - inputSchema: z.object({}), - handler: async (_args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: any } }>( - '/auth/api/v1/users/me' - ); - return response.response.result; + name: 'freshbooks_get_account', + description: 'Get detailed information about a specific accounting account', + inputSchema: { + type: 'object', + properties: { + account_id: { + type: 'number', + description: 'Account ID', + }, + }, + required: ['account_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getAccount(args.account_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/bills-tools.ts b/servers/freshbooks/src/tools/bills-tools.ts new file mode 100644 index 0000000..38eb58b --- /dev/null +++ b/servers/freshbooks/src/tools/bills-tools.ts @@ -0,0 +1,265 @@ +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; + +export const billsTools = [ + { + name: 'freshbooks_list_bills', + description: 'List all bills with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getBills(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_get_bill', + description: 'Get detailed information about a specific bill', + inputSchema: { + type: 'object', + properties: { + bill_id: { + type: 'number', + description: 'Bill ID', + }, + }, + required: ['bill_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getBill(args.bill_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_create_bill', + description: 'Create a new bill in FreshBooks', + inputSchema: { + type: 'object', + properties: { + vendor_id: { + type: 'number', + description: 'Vendor ID', + }, + bill_number: { + type: 'string', + description: 'Bill number', + }, + issue_date: { + type: 'string', + description: 'Issue date (YYYY-MM-DD)', + }, + due_date: { + type: 'string', + description: 'Due date (YYYY-MM-DD)', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + lines: { + type: 'array', + description: 'Bill line items', + items: { + type: 'object', + properties: { + description: { + type: 'string', + description: 'Line item description', + }, + quantity: { + type: 'string', + description: 'Quantity', + }, + unit_cost: { + type: 'object', + properties: { + amount: { + type: 'string', + description: 'Unit cost amount', + }, + }, + }, + category_id: { + type: 'number', + description: 'Expense category ID', + }, + }, + }, + }, + }, + required: ['vendor_id', 'issue_date'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createBill(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_update_bill', + description: 'Update an existing bill', + inputSchema: { + type: 'object', + properties: { + bill_id: { + type: 'number', + description: 'Bill ID to update', + }, + due_date: { + type: 'string', + description: 'Due date', + }, + lines: { + type: 'array', + description: 'Bill line items', + }, + }, + required: ['bill_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { bill_id, ...updates } = args; + const result = await client.updateBill(bill_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_delete_bill', + description: 'Delete a bill from FreshBooks', + inputSchema: { + type: 'object', + properties: { + bill_id: { + type: 'number', + description: 'Bill ID to delete', + }, + }, + required: ['bill_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteBill(args.bill_id); + return { + content: [ + { + type: 'text', + text: `Bill ${args.bill_id} deleted successfully`, + }, + ], + }; + }, + }, + { + name: 'freshbooks_get_bill_payments', + description: 'Get all payments for a specific bill', + inputSchema: { + type: 'object', + properties: { + bill_id: { + type: 'number', + description: 'Bill ID', + }, + }, + required: ['bill_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getBillPayments(args.bill_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_create_bill_payment', + description: 'Create a payment for a bill', + inputSchema: { + type: 'object', + properties: { + bill_id: { + type: 'number', + description: 'Bill ID', + }, + amount: { + type: 'string', + description: 'Payment amount', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + paid_date: { + type: 'string', + description: 'Payment date (YYYY-MM-DD)', + }, + payment_type: { + type: 'string', + description: 'Payment type (e.g., Check, Cash, Credit Card)', + }, + note: { + type: 'string', + description: 'Payment note', + }, + }, + required: ['bill_id', 'amount', 'paid_date', 'payment_type'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { bill_id, amount, currency_code, ...payment } = args; + const paymentData = { + ...payment, + amount: { + amount, + code: currency_code || 'USD', + }, + }; + const result = await client.createBillPayment(bill_id, paymentData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/clients-tools.ts b/servers/freshbooks/src/tools/clients-tools.ts index 8a1068d..042125e 100644 --- a/servers/freshbooks/src/tools/clients-tools.ts +++ b/servers/freshbooks/src/tools/clients-tools.ts @@ -1,137 +1,314 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Client, ClientContact } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const clientsTools = [ { name: 'freshbooks_list_clients', - description: 'List all clients with optional search', - inputSchema: z.object({ - search: z.string().optional().describe('Search by name, email, or organization'), - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const params: Record = {}; - if (args.search) { - params.search = { email_like: `%${args.search}%` }; - } - - const response = await client.getPaginated<{ clients: Client[] }>( - '/users/clients', - args.page, - args.per_page, - params - ); + description: 'List all clients in FreshBooks with optional search and pagination', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: 'Search term to filter clients', + }, + page: { + type: 'number', + description: 'Page number for pagination (default: 1)', + }, + per_page: { + type: 'number', + description: 'Number of results per page (default: 30)', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getClients(args); return { - clients: response.response.result.clients || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_client', - description: 'Get a single client by ID', - inputSchema: z.object({ - client_id: z.number().describe('Client ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { client: Client } } }>( - `/users/clients/${args.client_id}` - ); - return response.response.result.client; + description: 'Get detailed information about a specific client', + inputSchema: { + type: 'object', + properties: { + client_id: { + type: 'number', + description: 'Client ID', + }, + }, + required: ['client_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getClient(args.client_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_client', - description: 'Create a new client', - inputSchema: z.object({ - fname: z.string().describe('First name'), - lname: z.string().describe('Last name'), - email: z.string().email().describe('Email address'), - organization: z.string().optional().describe('Company/organization name'), - phone: z.string().optional(), - mobile: z.string().optional(), - bill_street: z.string().optional(), - bill_city: z.string().optional(), - bill_state: z.string().optional(), - bill_country: z.string().optional(), - bill_postal_code: z.string().optional(), - currency_code: z.string().default('USD'), - language: z.string().default('en'), - note: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const clientData = { client: { ...args } }; - - const response = await client.post<{ response: { result: { client: Client } } }>( - '/users/clients', - clientData - ); - return response.response.result.client; + description: 'Create a new client in FreshBooks', + inputSchema: { + type: 'object', + properties: { + fname: { + type: 'string', + description: 'First name', + }, + lname: { + type: 'string', + description: 'Last name', + }, + email: { + type: 'string', + description: 'Email address', + }, + organization: { + type: 'string', + description: 'Company/organization name', + }, + business_phone: { + type: 'string', + description: 'Business phone number', + }, + mobile_phone: { + type: 'string', + description: 'Mobile phone number', + }, + home_phone: { + type: 'string', + description: 'Home phone number', + }, + fax: { + type: 'string', + description: 'Fax number', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD, CAD, GBP)', + }, + language: { + type: 'string', + description: 'Language code (e.g., en, fr)', + }, + note: { + type: 'string', + description: 'Internal note about the client', + }, + vat_name: { + type: 'string', + description: 'VAT name', + }, + vat_number: { + type: 'string', + description: 'VAT number', + }, + s_street: { + type: 'string', + description: 'Shipping address street', + }, + s_street2: { + type: 'string', + description: 'Shipping address street line 2', + }, + s_city: { + type: 'string', + description: 'Shipping address city', + }, + s_province: { + type: 'string', + description: 'Shipping address province/state', + }, + s_code: { + type: 'string', + description: 'Shipping address postal code', + }, + s_country: { + type: 'string', + description: 'Shipping address country', + }, + p_street: { + type: 'string', + description: 'Billing address street', + }, + p_street2: { + type: 'string', + description: 'Billing address street line 2', + }, + p_city: { + type: 'string', + description: 'Billing address city', + }, + p_province: { + type: 'string', + description: 'Billing address province/state', + }, + p_code: { + type: 'string', + description: 'Billing address postal code', + }, + p_country: { + type: 'string', + description: 'Billing address country', + }, + }, + required: ['email'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createClient(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_update_client', description: 'Update an existing client', - inputSchema: z.object({ - client_id: z.number().describe('Client ID'), - fname: z.string().optional(), - lname: z.string().optional(), - email: z.string().email().optional(), - organization: z.string().optional(), - phone: z.string().optional(), - mobile: z.string().optional(), - bill_street: z.string().optional(), - bill_city: z.string().optional(), - bill_state: z.string().optional(), - bill_country: z.string().optional(), - bill_postal_code: z.string().optional(), - note: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const { client_id, ...updateFields } = args; - const clientData = { client: updateFields }; - - const response = await client.put<{ response: { result: { client: Client } } }>( - `/users/clients/${client_id}`, - clientData - ); - return response.response.result.client; + inputSchema: { + type: 'object', + properties: { + client_id: { + type: 'number', + description: 'Client ID to update', + }, + fname: { + type: 'string', + description: 'First name', + }, + lname: { + type: 'string', + description: 'Last name', + }, + email: { + type: 'string', + description: 'Email address', + }, + organization: { + type: 'string', + description: 'Company/organization name', + }, + business_phone: { + type: 'string', + description: 'Business phone number', + }, + mobile_phone: { + type: 'string', + description: 'Mobile phone number', + }, + currency_code: { + type: 'string', + description: 'Currency code', + }, + language: { + type: 'string', + description: 'Language code', + }, + note: { + type: 'string', + description: 'Internal note', + }, + s_street: { + type: 'string', + description: 'Shipping address street', + }, + s_city: { + type: 'string', + description: 'Shipping address city', + }, + s_province: { + type: 'string', + description: 'Shipping address province/state', + }, + s_code: { + type: 'string', + description: 'Shipping address postal code', + }, + s_country: { + type: 'string', + description: 'Shipping address country', + }, + }, + required: ['client_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { client_id, ...updates } = args; + const result = await client.updateClient(client_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_client', - description: 'Delete (archive) a client', - inputSchema: z.object({ - client_id: z.number().describe('Client ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.put( - `/users/clients/${args.client_id}`, - { client: { vis_state: 1 } } - ); - return { success: true, message: `Client ${args.client_id} archived` }; + description: 'Delete a client from FreshBooks', + inputSchema: { + type: 'object', + properties: { + client_id: { + type: 'number', + description: 'Client ID to delete', + }, + }, + required: ['client_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteClient(args.client_id); + return { + content: [ + { + type: 'text', + text: `Client ${args.client_id} deleted successfully`, + }, + ], + }; }, }, - { - name: 'freshbooks_list_client_contacts', - description: 'List all contacts for a specific client', - inputSchema: z.object({ - client_id: z.number().describe('Client ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { contacts: ClientContact[] } } }>( - `/users/clients/${args.client_id}/contacts` - ); - return response.response.result.contacts || []; + name: 'freshbooks_search_clients', + description: 'Search for clients by name, email, or organization', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query', + }, + }, + required: ['query'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getClients({ search: args.query }); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/credit-notes-tools.ts b/servers/freshbooks/src/tools/credit-notes-tools.ts new file mode 100644 index 0000000..64c5b63 --- /dev/null +++ b/servers/freshbooks/src/tools/credit-notes-tools.ts @@ -0,0 +1,187 @@ +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; + +export const creditNotesTools = [ + { + name: 'freshbooks_list_credit_notes', + description: 'List all credit notes with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getCreditNotes(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_get_credit_note', + description: 'Get detailed information about a specific credit note', + inputSchema: { + type: 'object', + properties: { + credit_note_id: { + type: 'number', + description: 'Credit note ID', + }, + }, + required: ['credit_note_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getCreditNote(args.credit_note_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_create_credit_note', + description: 'Create a new credit note in FreshBooks', + inputSchema: { + type: 'object', + properties: { + clientid: { + type: 'number', + description: 'Client ID', + }, + create_date: { + type: 'string', + description: 'Creation date (YYYY-MM-DD)', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + credit_type: { + type: 'string', + description: 'Credit type (goodwill, prepayment, etc.)', + }, + notes: { + type: 'string', + description: 'Credit note notes', + }, + lines: { + type: 'array', + description: 'Credit note line items', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Line item name', + }, + description: { + type: 'string', + description: 'Line item description', + }, + qty: { + type: 'string', + description: 'Quantity', + }, + unit_cost: { + type: 'object', + properties: { + amount: { + type: 'string', + description: 'Unit cost amount', + }, + }, + }, + }, + }, + }, + }, + required: ['clientid'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createCreditNote(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_update_credit_note', + description: 'Update an existing credit note', + inputSchema: { + type: 'object', + properties: { + credit_note_id: { + type: 'number', + description: 'Credit note ID to update', + }, + notes: { + type: 'string', + description: 'Credit note notes', + }, + lines: { + type: 'array', + description: 'Credit note line items', + }, + }, + required: ['credit_note_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { credit_note_id, ...updates } = args; + const result = await client.updateCreditNote(credit_note_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_delete_credit_note', + description: 'Delete a credit note from FreshBooks', + inputSchema: { + type: 'object', + properties: { + credit_note_id: { + type: 'number', + description: 'Credit note ID to delete', + }, + }, + required: ['credit_note_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteCreditNote(args.credit_note_id); + return { + content: [ + { + type: 'text', + text: `Credit note ${args.credit_note_id} deleted successfully`, + }, + ], + }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/estimates-tools.ts b/servers/freshbooks/src/tools/estimates-tools.ts index 30ee14d..5d31943 100644 --- a/servers/freshbooks/src/tools/estimates-tools.ts +++ b/servers/freshbooks/src/tools/estimates-tools.ts @@ -1,194 +1,317 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Estimate } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const estimatesTools = [ { name: 'freshbooks_list_estimates', - description: 'List all estimates with optional filtering', - inputSchema: z.object({ - clientid: z.number().optional().describe('Filter by client ID'), - status: z.enum(['draft', 'sent', 'accepted', 'declined']).optional(), - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const params: Record = {}; - if (args.clientid) params.clientid = args.clientid; - if (args.status) params.status = args.status; - - const response = await client.getPaginated<{ estimates: Estimate[] }>( - '/estimates/estimates', - args.page, - args.per_page, - params - ); + description: 'List all estimates with optional search and pagination', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: 'Search term to filter estimates', + }, + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getEstimates(args); return { - estimates: response.response.result.estimates || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_estimate', - description: 'Get a single estimate by ID', - inputSchema: z.object({ - estimate_id: z.number().describe('Estimate ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { estimate: Estimate } } }>( - `/estimates/estimates/${args.estimate_id}` - ); - return response.response.result.estimate; + description: 'Get detailed information about a specific estimate', + inputSchema: { + type: 'object', + properties: { + estimate_id: { + type: 'number', + description: 'Estimate ID', + }, + }, + required: ['estimate_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getEstimate(args.estimate_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_estimate', - description: 'Create a new estimate', - inputSchema: z.object({ - clientid: z.number().describe('Client ID'), - create_date: z.string().optional().describe('Estimate date (YYYY-MM-DD)'), - lines: z.array(z.object({ - name: z.string().describe('Line item name'), - description: z.string().optional(), - qty: z.number().default(1), - unit_cost: z.string().describe('Unit cost'), - })).describe('Estimate line items'), - currency_code: z.string().default('USD'), - notes: z.string().optional(), - terms: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const lines = args.lines.map((line: any) => ({ - ...line, - unit_cost: { amount: line.unit_cost, code: args.currency_code }, - })); - - const estimateData = { - estimate: { - clientid: args.clientid, - create_date: args.create_date || new Date().toISOString().split('T')[0], - currency_code: args.currency_code, - lines, - notes: args.notes, - terms: args.terms, + description: 'Create a new estimate in FreshBooks', + inputSchema: { + type: 'object', + properties: { + customerid: { + type: 'number', + description: 'Client ID for this estimate', }, + create_date: { + type: 'string', + description: 'Estimate creation date (YYYY-MM-DD)', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD, CAD)', + }, + language: { + type: 'string', + description: 'Language code (e.g., en)', + }, + notes: { + type: 'string', + description: 'Estimate notes', + }, + terms: { + type: 'string', + description: 'Estimate terms', + }, + discount_value: { + type: 'string', + description: 'Discount value', + }, + lines: { + type: 'array', + description: 'Estimate line items', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Line item name', + }, + description: { + type: 'string', + description: 'Line item description', + }, + qty: { + type: 'string', + description: 'Quantity', + }, + unit_cost: { + type: 'object', + properties: { + amount: { + type: 'string', + description: 'Unit cost amount', + }, + code: { + type: 'string', + description: 'Currency code', + }, + }, + }, + }, + }, + }, + }, + required: ['customerid'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createEstimate(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; - - const response = await client.post<{ response: { result: { estimate: Estimate } } }>( - '/estimates/estimates', - estimateData - ); - return response.response.result.estimate; }, }, - { name: 'freshbooks_update_estimate', description: 'Update an existing estimate', - inputSchema: z.object({ - estimate_id: z.number().describe('Estimate ID'), - clientid: z.number().optional(), - create_date: z.string().optional(), - lines: z.array(z.object({ - name: z.string(), - description: z.string().optional(), - qty: z.number(), - unit_cost: z.string(), - })).optional(), - notes: z.string().optional(), - terms: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const { estimate_id, ...updateFields } = args; - - if (updateFields.lines) { - updateFields.lines = updateFields.lines.map((line: any) => ({ - ...line, - unit_cost: { amount: line.unit_cost, code: 'USD' }, - })); - } - - const estimateData = { estimate: updateFields }; - const response = await client.put<{ response: { result: { estimate: Estimate } } }>( - `/estimates/estimates/${estimate_id}`, - estimateData - ); - return response.response.result.estimate; + inputSchema: { + type: 'object', + properties: { + estimate_id: { + type: 'number', + description: 'Estimate ID to update', + }, + notes: { + type: 'string', + description: 'Estimate notes', + }, + terms: { + type: 'string', + description: 'Estimate terms', + }, + lines: { + type: 'array', + description: 'Estimate line items', + }, + }, + required: ['estimate_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { estimate_id, ...updates } = args; + const result = await client.updateEstimate(estimate_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_estimate', - description: 'Delete (archive) an estimate', - inputSchema: z.object({ - estimate_id: z.number().describe('Estimate ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.put( - `/estimates/estimates/${args.estimate_id}`, - { estimate: { vis_state: 1 } } - ); - return { success: true, message: `Estimate ${args.estimate_id} deleted` }; + description: 'Delete an estimate from FreshBooks', + inputSchema: { + type: 'object', + properties: { + estimate_id: { + type: 'number', + description: 'Estimate ID to delete', + }, + }, + required: ['estimate_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteEstimate(args.estimate_id); + return { + content: [ + { + type: 'text', + text: `Estimate ${args.estimate_id} deleted successfully`, + }, + ], + }; }, }, - { name: 'freshbooks_send_estimate', description: 'Send an estimate to the client via email', - inputSchema: z.object({ - estimate_id: z.number().describe('Estimate ID'), - email_subject: z.string().optional(), - email_body: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const emailData: any = { estimate: { action_email: true } }; - if (args.email_subject) emailData.estimate.email_subject = args.email_subject; - if (args.email_body) emailData.estimate.email_body = args.email_body; - - await client.put( - `/estimates/estimates/${args.estimate_id}`, - emailData - ); - return { success: true, message: `Estimate ${args.estimate_id} sent` }; + inputSchema: { + type: 'object', + properties: { + estimate_id: { + type: 'number', + description: 'Estimate ID to send', + }, + email: { + type: 'string', + description: 'Email address to send to (optional)', + }, + }, + required: ['estimate_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.sendEstimate(args.estimate_id, args.email); + return { + content: [ + { + type: 'text', + text: `Estimate ${args.estimate_id} sent successfully`, + }, + ], + }; }, }, - { - name: 'freshbooks_convert_estimate_to_invoice', - description: 'Convert an estimate to an invoice', - inputSchema: z.object({ - estimate_id: z.number().describe('Estimate ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - // Get the estimate first - const estimateResp = await client.get<{ response: { result: { estimate: Estimate } } }>( - `/estimates/estimates/${args.estimate_id}` - ); - const estimate = estimateResp.response.result.estimate; - - // Create invoice from estimate - const invoiceData = { - invoice: { - clientid: estimate.clientid, - create_date: new Date().toISOString().split('T')[0], - currency_code: estimate.currency_code, - lines: estimate.lines, - notes: estimate.notes, - terms: estimate.terms, - estimateid: args.estimate_id, + name: 'freshbooks_accept_estimate', + description: 'Mark an estimate as accepted', + inputSchema: { + type: 'object', + properties: { + estimate_id: { + type: 'number', + description: 'Estimate ID to accept', + }, + }, + required: ['estimate_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.acceptEstimate(args.estimate_id); + return { + content: [ + { + type: 'text', + text: `Estimate ${args.estimate_id} accepted`, + }, + ], + }; + }, + }, + { + name: 'freshbooks_add_estimate_line', + description: 'Add a line item to an existing estimate', + inputSchema: { + type: 'object', + properties: { + estimate_id: { + type: 'number', + description: 'Estimate ID', + }, + name: { + type: 'string', + description: 'Line item name', + }, + description: { + type: 'string', + description: 'Line item description', + }, + qty: { + type: 'string', + description: 'Quantity', + }, + unit_cost: { + type: 'string', + description: 'Unit cost amount', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + }, + required: ['estimate_id', 'name', 'qty', 'unit_cost'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const estimate = await client.getEstimate(args.estimate_id); + const newLine = { + name: args.name, + description: args.description || '', + qty: args.qty, + unit_cost: { + amount: args.unit_cost, + code: args.currency_code || estimate.currency_code, }, }; - - const response = await client.post( - '/invoices/invoices', - invoiceData - ); - return response; + const lines = [...estimate.lines, newLine]; + const result = await client.updateEstimate(args.estimate_id, { lines }); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/expenses-tools.ts b/servers/freshbooks/src/tools/expenses-tools.ts index 4c9cafa..1b08b13 100644 --- a/servers/freshbooks/src/tools/expenses-tools.ts +++ b/servers/freshbooks/src/tools/expenses-tools.ts @@ -1,139 +1,249 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Expense, ExpenseCategory } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const expensesTools = [ { name: 'freshbooks_list_expenses', - description: 'List all expenses with optional filtering', - inputSchema: z.object({ - clientid: z.number().optional().describe('Filter by client ID'), - category_id: z.number().optional().describe('Filter by category ID'), - projectid: z.number().optional().describe('Filter by project ID'), - date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'), - date_max: z.string().optional().describe('End date (YYYY-MM-DD)'), - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const params: Record = {}; - if (args.clientid) params.clientid = args.clientid; - if (args.category_id) params.categoryid = args.category_id; - if (args.projectid) params.projectid = args.projectid; - if (args.date_min) params.date_min = args.date_min; - if (args.date_max) params.date_max = args.date_max; - - const response = await client.getPaginated<{ expenses: Expense[] }>( - '/expenses/expenses', - args.page, - args.per_page, - params - ); + description: 'List all expenses with optional search and pagination', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: 'Search term to filter expenses', + }, + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getExpenses(args); return { - expenses: response.response.result.expenses || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_expense', - description: 'Get a single expense by ID', - inputSchema: z.object({ - expense_id: z.number().describe('Expense ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { expense: Expense } } }>( - `/expenses/expenses/${args.expense_id}` - ); - return response.response.result.expense; + description: 'Get detailed information about a specific expense', + inputSchema: { + type: 'object', + properties: { + expense_id: { + type: 'number', + description: 'Expense ID', + }, + }, + required: ['expense_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getExpense(args.expense_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_expense', - description: 'Create a new expense', - inputSchema: z.object({ - category_id: z.number().describe('Expense category ID'), - vendor: z.string().describe('Vendor name'), - amount: z.string().describe('Expense amount'), - date: z.string().describe('Expense date (YYYY-MM-DD)'), - clientid: z.number().optional().describe('Associated client ID'), - projectid: z.number().optional().describe('Associated project ID'), - notes: z.string().optional(), - taxName1: z.string().optional(), - taxPercent1: z.number().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const expenseData = { - expense: { - ...args, - amount: { amount: args.amount, code: 'USD' }, + description: 'Create a new expense in FreshBooks', + inputSchema: { + type: 'object', + properties: { + amount: { + type: 'string', + description: 'Expense amount', }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + vendor: { + type: 'string', + description: 'Vendor name', + }, + date: { + type: 'string', + description: 'Expense date (YYYY-MM-DD)', + }, + categoryid: { + type: 'number', + description: 'Expense category ID', + }, + clientid: { + type: 'number', + description: 'Client ID (optional)', + }, + projectid: { + type: 'number', + description: 'Project ID (optional)', + }, + notes: { + type: 'string', + description: 'Expense notes', + }, + markup_percent: { + type: 'string', + description: 'Markup percentage for billable expenses', + }, + }, + required: ['amount', 'vendor', 'date', 'categoryid'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const expense = { + amount: { + amount: args.amount, + code: args.currency_code || 'USD', + }, + vendor: args.vendor, + date: args.date, + categoryid: args.categoryid, + clientid: args.clientid, + projectid: args.projectid, + notes: args.notes, + markup_percent: args.markup_percent, + }; + const result = await client.createExpense(expense); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; - - const response = await client.post<{ response: { result: { expense: Expense } } }>( - '/expenses/expenses', - expenseData - ); - return response.response.result.expense; }, }, - { name: 'freshbooks_update_expense', description: 'Update an existing expense', - inputSchema: z.object({ - expense_id: z.number().describe('Expense ID'), - category_id: z.number().optional(), - vendor: z.string().optional(), - amount: z.string().optional(), - date: z.string().optional(), - clientid: z.number().optional(), - projectid: z.number().optional(), - notes: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const { expense_id, ...updateFields } = args; - if (updateFields.amount) { - updateFields.amount = { amount: updateFields.amount, code: 'USD' }; + inputSchema: { + type: 'object', + properties: { + expense_id: { + type: 'number', + description: 'Expense ID to update', + }, + amount: { + type: 'string', + description: 'Expense amount', + }, + vendor: { + type: 'string', + description: 'Vendor name', + }, + date: { + type: 'string', + description: 'Expense date', + }, + notes: { + type: 'string', + description: 'Expense notes', + }, + }, + required: ['expense_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { expense_id, amount, ...updates } = args; + if (amount) { + const expense = await client.getExpense(expense_id); + updates.amount = { + amount, + code: expense.amount.code, + }; } - - const expenseData = { expense: updateFields }; - const response = await client.put<{ response: { result: { expense: Expense } } }>( - `/expenses/expenses/${expense_id}`, - expenseData - ); - return response.response.result.expense; + const result = await client.updateExpense(expense_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_expense', - description: 'Delete an expense', - inputSchema: z.object({ - expense_id: z.number().describe('Expense ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.put( - `/expenses/expenses/${args.expense_id}`, - { expense: { vis_state: 1 } } - ); - return { success: true, message: `Expense ${args.expense_id} deleted` }; + description: 'Delete an expense from FreshBooks', + inputSchema: { + type: 'object', + properties: { + expense_id: { + type: 'number', + description: 'Expense ID to delete', + }, + }, + required: ['expense_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteExpense(args.expense_id); + return { + content: [ + { + type: 'text', + text: `Expense ${args.expense_id} deleted successfully`, + }, + ], + }; }, }, - { name: 'freshbooks_list_expense_categories', description: 'List all expense categories', - inputSchema: z.object({}), - handler: async (_args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { categories: ExpenseCategory[] } } }>( - '/expenses/categories' - ); - return response.response.result.categories || []; + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getExpenseCategories(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_search_expenses', + description: 'Search for expenses by vendor, category, or notes', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query', + }, + }, + required: ['query'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getExpenses({ search: args.query }); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/invoices-tools.ts b/servers/freshbooks/src/tools/invoices-tools.ts index 4202c24..fe4b112 100644 --- a/servers/freshbooks/src/tools/invoices-tools.ts +++ b/servers/freshbooks/src/tools/invoices-tools.ts @@ -1,268 +1,399 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Invoice, Payment } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const invoicesTools = [ { name: 'freshbooks_list_invoices', - description: 'List all invoices with optional filtering (client, status, date range)', - inputSchema: z.object({ - clientid: z.number().optional().describe('Filter by client ID'), - status: z.enum(['draft', 'sent', 'viewed', 'paid', 'partial', 'overdue', 'disputed']).optional(), - date_min: z.string().optional().describe('Minimum date (YYYY-MM-DD)'), - date_max: z.string().optional().describe('Maximum date (YYYY-MM-DD)'), - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const params: Record = {}; - if (args.clientid) params.clientid = args.clientid; - if (args.status) params.status = args.status; - if (args.date_min) params.date_min = args.date_min; - if (args.date_max) params.date_max = args.date_max; - - const response = await client.getPaginated<{ invoices: Invoice[] }>( - '/invoices/invoices', - args.page, - args.per_page, - params - ); + description: 'List all invoices with optional search and pagination', + inputSchema: { + type: 'object', + properties: { + search: { + type: 'string', + description: 'Search term to filter invoices', + }, + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getInvoices(args); return { - invoices: response.response.result.invoices || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_invoice', - description: 'Get a single invoice by ID', - inputSchema: z.object({ - invoice_id: z.number().describe('Invoice ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { invoice: Invoice } } }>( - `/invoices/invoices/${args.invoice_id}` - ); - return response.response.result.invoice; + description: 'Get detailed information about a specific invoice', + inputSchema: { + type: 'object', + properties: { + invoice_id: { + type: 'number', + description: 'Invoice ID', + }, + }, + required: ['invoice_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getInvoice(args.invoice_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_invoice', - description: 'Create a new invoice', - inputSchema: z.object({ - clientid: z.number().describe('Client ID'), - create_date: z.string().optional().describe('Invoice date (YYYY-MM-DD, defaults to today)'), - due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'), - lines: z.array(z.object({ - name: z.string().describe('Line item name'), - description: z.string().optional(), - qty: z.number().default(1), - unit_cost: z.string().describe('Unit cost as string (e.g., "100.00")'), - })).describe('Invoice line items'), - currency_code: z.string().default('USD'), - notes: z.string().optional(), - terms: z.string().optional(), - status: z.enum(['draft', 'sent']).default('draft'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const lines = args.lines.map((line: any) => ({ - ...line, - unit_cost: { amount: line.unit_cost, code: args.currency_code }, - })); - - const invoiceData = { - invoice: { - clientid: args.clientid, - create_date: args.create_date || new Date().toISOString().split('T')[0], - due_date: args.due_date, - currency_code: args.currency_code, - lines, - notes: args.notes, - terms: args.terms, - status: args.status === 'sent' ? 2 : 1, + description: 'Create a new invoice in FreshBooks', + inputSchema: { + type: 'object', + properties: { + customerid: { + type: 'number', + description: 'Client ID for this invoice', }, + create_date: { + type: 'string', + description: 'Invoice creation date (YYYY-MM-DD)', + }, + due_offset_days: { + type: 'number', + description: 'Number of days until invoice is due', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD, CAD)', + }, + language: { + type: 'string', + description: 'Language code (e.g., en)', + }, + notes: { + type: 'string', + description: 'Invoice notes', + }, + terms: { + type: 'string', + description: 'Invoice terms', + }, + po_number: { + type: 'string', + description: 'Purchase order number', + }, + discount_value: { + type: 'string', + description: 'Discount value as percentage or amount', + }, + discount_description: { + type: 'string', + description: 'Description of discount', + }, + lines: { + type: 'array', + description: 'Invoice line items', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Line item name', + }, + description: { + type: 'string', + description: 'Line item description', + }, + qty: { + type: 'string', + description: 'Quantity', + }, + unit_cost: { + type: 'object', + properties: { + amount: { + type: 'string', + description: 'Unit cost amount', + }, + code: { + type: 'string', + description: 'Currency code', + }, + }, + }, + taxName1: { + type: 'string', + description: 'First tax name', + }, + taxAmount1: { + type: 'string', + description: 'First tax amount', + }, + }, + }, + }, + }, + required: ['customerid'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createInvoice(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; - - const response = await client.post<{ response: { result: { invoice: Invoice } } }>( - '/invoices/invoices', - invoiceData - ); - return response.response.result.invoice; }, }, - { name: 'freshbooks_update_invoice', description: 'Update an existing invoice', - inputSchema: z.object({ - invoice_id: z.number().describe('Invoice ID'), - clientid: z.number().optional(), - create_date: z.string().optional(), - due_date: z.string().optional(), - lines: z.array(z.object({ - name: z.string(), - description: z.string().optional(), - qty: z.number(), - unit_cost: z.string(), - })).optional(), - notes: z.string().optional(), - terms: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const updateData: any = { invoice: {} }; - - if (args.clientid) updateData.invoice.clientid = args.clientid; - if (args.create_date) updateData.invoice.create_date = args.create_date; - if (args.due_date) updateData.invoice.due_date = args.due_date; - if (args.notes) updateData.invoice.notes = args.notes; - if (args.terms) updateData.invoice.terms = args.terms; - if (args.lines) { - updateData.invoice.lines = args.lines.map((line: any) => ({ - ...line, - unit_cost: { amount: line.unit_cost, code: 'USD' }, - })); - } - - const response = await client.put<{ response: { result: { invoice: Invoice } } }>( - `/invoices/invoices/${args.invoice_id}`, - updateData - ); - return response.response.result.invoice; + inputSchema: { + type: 'object', + properties: { + invoice_id: { + type: 'number', + description: 'Invoice ID to update', + }, + customerid: { + type: 'number', + description: 'Client ID', + }, + notes: { + type: 'string', + description: 'Invoice notes', + }, + terms: { + type: 'string', + description: 'Invoice terms', + }, + po_number: { + type: 'string', + description: 'Purchase order number', + }, + discount_value: { + type: 'string', + description: 'Discount value', + }, + lines: { + type: 'array', + description: 'Invoice line items', + }, + }, + required: ['invoice_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { invoice_id, ...updates } = args; + const result = await client.updateInvoice(invoice_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_invoice', - description: 'Delete an invoice (moves to archived)', - inputSchema: z.object({ - invoice_id: z.number().describe('Invoice ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.put( - `/invoices/invoices/${args.invoice_id}`, - { invoice: { vis_state: 1 } } - ); - return { success: true, message: `Invoice ${args.invoice_id} archived` }; + description: 'Delete an invoice from FreshBooks', + inputSchema: { + type: 'object', + properties: { + invoice_id: { + type: 'number', + description: 'Invoice ID to delete', + }, + }, + required: ['invoice_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteInvoice(args.invoice_id); + return { + content: [ + { + type: 'text', + text: `Invoice ${args.invoice_id} deleted successfully`, + }, + ], + }; }, }, - { name: 'freshbooks_send_invoice', description: 'Send an invoice to the client via email', - inputSchema: z.object({ - invoice_id: z.number().describe('Invoice ID'), - email_subject: z.string().optional(), - email_body: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const emailData: any = { invoice: {} }; - if (args.email_subject) emailData.invoice.email_subject = args.email_subject; - if (args.email_body) emailData.invoice.email_body = args.email_body; - - await client.put( - `/invoices/invoices/${args.invoice_id}`, - { invoice: { action_email: true, ...emailData.invoice } } - ); - return { success: true, message: `Invoice ${args.invoice_id} sent` }; + inputSchema: { + type: 'object', + properties: { + invoice_id: { + type: 'number', + description: 'Invoice ID to send', + }, + email: { + type: 'string', + description: 'Email address to send to (optional, uses client email if not provided)', + }, + }, + required: ['invoice_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.sendInvoice(args.invoice_id, args.email); + return { + content: [ + { + type: 'text', + text: `Invoice ${args.invoice_id} sent successfully`, + }, + ], + }; }, }, - { name: 'freshbooks_mark_invoice_paid', description: 'Mark an invoice as paid', - inputSchema: z.object({ - invoice_id: z.number().describe('Invoice ID'), - payment_type: z.string().default('Cash').describe('Payment method'), - payment_date: z.string().optional().describe('Payment date (YYYY-MM-DD, defaults to today)'), - amount: z.string().optional().describe('Payment amount (defaults to outstanding amount)'), - }), - handler: async (args: any, client: FreshBooksClient) => { - // First get the invoice to know the outstanding amount - const invoiceResp = await client.get<{ response: { result: { invoice: Invoice } } }>( - `/invoices/invoices/${args.invoice_id}` - ); - const invoice = invoiceResp.response.result.invoice; - - const paymentData = { - payment: { - invoiceid: args.invoice_id, - amount: { - amount: args.amount || invoice.outstanding.amount, - code: invoice.currency_code, + inputSchema: { + type: 'object', + properties: { + invoice_id: { + type: 'number', + description: 'Invoice ID to mark as paid', + }, + }, + required: ['invoice_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.markInvoicePaid(args.invoice_id); + return { + content: [ + { + type: 'text', + text: `Invoice ${args.invoice_id} marked as paid`, }, - date: args.payment_date || new Date().toISOString().split('T')[0], - type: args.payment_type, + ], + }; + }, + }, + { + name: 'freshbooks_get_invoice_share_link', + description: 'Get the shareable link for an invoice', + inputSchema: { + type: 'object', + properties: { + invoice_id: { + type: 'number', + description: 'Invoice ID', + }, + }, + required: ['invoice_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const link = await client.getInvoiceShareLink(args.invoice_id); + return { + content: [ + { + type: 'text', + text: link, + }, + ], + }; + }, + }, + { + name: 'freshbooks_add_invoice_line', + description: 'Add a line item to an existing invoice', + inputSchema: { + type: 'object', + properties: { + invoice_id: { + type: 'number', + description: 'Invoice ID', + }, + name: { + type: 'string', + description: 'Line item name', + }, + description: { + type: 'string', + description: 'Line item description', + }, + qty: { + type: 'string', + description: 'Quantity', + }, + unit_cost: { + type: 'string', + description: 'Unit cost amount', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + }, + required: ['invoice_id', 'name', 'qty', 'unit_cost'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const invoice = await client.getInvoice(args.invoice_id); + const newLine = { + name: args.name, + description: args.description || '', + qty: args.qty, + unit_cost: { + amount: args.unit_cost, + code: args.currency_code || invoice.currency_code, }, }; - - const response = await client.post<{ response: { result: { payment: Payment } } }>( - '/payments/payments', - paymentData - ); - return response.response.result.payment; - }, - }, - - { - name: 'freshbooks_mark_invoice_unpaid', - description: 'Mark an invoice as unpaid (reopen it)', - inputSchema: z.object({ - invoice_id: z.number().describe('Invoice ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.put( - `/invoices/invoices/${args.invoice_id}`, - { invoice: { v3_status: 'unpaid' } } - ); - return { success: true, message: `Invoice ${args.invoice_id} marked unpaid` }; - }, - }, - - { - name: 'freshbooks_get_invoice_payment', - description: 'Get payment details for an invoice', - inputSchema: z.object({ - invoice_id: z.number().describe('Invoice ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { payments: Payment[] } } }>( - '/payments/payments', - { invoiceid: args.invoice_id } - ); - return response.response.result.payments || []; - }, - }, - - { - name: 'freshbooks_create_payment', - description: 'Create a payment record for an invoice', - inputSchema: z.object({ - invoice_id: z.number().describe('Invoice ID'), - amount: z.string().describe('Payment amount'), - date: z.string().optional().describe('Payment date (YYYY-MM-DD)'), - type: z.string().default('Cash').describe('Payment method'), - note: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const paymentData = { - payment: { - invoiceid: args.invoice_id, - amount: { amount: args.amount, code: 'USD' }, - date: args.date || new Date().toISOString().split('T')[0], - type: args.type, - note: args.note, - }, + const lines = [...invoice.lines, newLine]; + const result = await client.updateInvoice(args.invoice_id, { lines }); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_search_invoices', + description: 'Search for invoices by various criteria', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query', + }, + }, + required: ['query'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getInvoices({ search: args.query }); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; - - const response = await client.post<{ response: { result: { payment: Payment } } }>( - '/payments/payments', - paymentData - ); - return response.response.result.payment; }, }, ]; diff --git a/servers/freshbooks/src/tools/items-tools.ts b/servers/freshbooks/src/tools/items-tools.ts index 2b8d8e8..ac8ac13 100644 --- a/servers/freshbooks/src/tools/items-tools.ts +++ b/servers/freshbooks/src/tools/items-tools.ts @@ -1,110 +1,191 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Item } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const itemsTools = [ { name: 'freshbooks_list_items', - description: 'List all items (products/services)', - inputSchema: z.object({ - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.getPaginated<{ items: Item[] }>( - '/items/items', - args.page, - args.per_page - ); + description: 'List all items/services with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getItems(args); return { - items: response.response.result.items || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_item', - description: 'Get a single item by ID', - inputSchema: z.object({ - item_id: z.number().describe('Item ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { item: Item } } }>( - `/items/items/${args.item_id}` - ); - return response.response.result.item; + description: 'Get detailed information about a specific item/service', + inputSchema: { + type: 'object', + properties: { + item_id: { + type: 'number', + description: 'Item ID', + }, + }, + required: ['item_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getItem(args.item_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_item', - description: 'Create a new item (product or service)', - inputSchema: z.object({ - name: z.string().describe('Item name'), - description: z.string().optional(), - qty: z.number().optional().describe('Quantity on hand'), - inventory: z.number().optional(), - unit_cost: z.string().optional().describe('Unit cost'), - tax1: z.number().optional().describe('Tax 1 ID'), - tax2: z.number().optional().describe('Tax 2 ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const itemData: any = { item: { ...args } }; - if (itemData.item.unit_cost) { - itemData.item.unit_cost = { amount: itemData.item.unit_cost, code: 'USD' }; - } - - const response = await client.post<{ response: { result: { item: Item } } }>( - '/items/items', - itemData - ); - return response.response.result.item; + description: 'Create a new item/service in FreshBooks', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Item name', + }, + description: { + type: 'string', + description: 'Item description', + }, + quantity: { + type: 'string', + description: 'Quantity', + }, + inventory: { + type: 'string', + description: 'Inventory count', + }, + unit_cost: { + type: 'string', + description: 'Unit cost amount', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + sku: { + type: 'string', + description: 'SKU', + }, + }, + required: ['name', 'unit_cost'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const item = { + name: args.name, + description: args.description || '', + quantity: args.quantity || '1', + inventory: args.inventory || '0', + unit_cost: { + amount: args.unit_cost, + code: args.currency_code || 'USD', + }, + sku: args.sku || '', + }; + const result = await client.createItem(item); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_update_item', - description: 'Update an existing item', - inputSchema: z.object({ - item_id: z.number().describe('Item ID'), - name: z.string().optional(), - description: z.string().optional(), - qty: z.number().optional(), - inventory: z.number().optional(), - unit_cost: z.string().optional(), - tax1: z.number().optional(), - tax2: z.number().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const { item_id, ...updateFields } = args; - if (updateFields.unit_cost) { - updateFields.unit_cost = { amount: updateFields.unit_cost, code: 'USD' }; + description: 'Update an existing item/service', + inputSchema: { + type: 'object', + properties: { + item_id: { + type: 'number', + description: 'Item ID to update', + }, + name: { + type: 'string', + description: 'Item name', + }, + description: { + type: 'string', + description: 'Item description', + }, + unit_cost: { + type: 'string', + description: 'Unit cost amount', + }, + inventory: { + type: 'string', + description: 'Inventory count', + }, + }, + required: ['item_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { item_id, unit_cost, ...updates } = args; + if (unit_cost) { + const item = await client.getItem(item_id); + updates.unit_cost = { + amount: unit_cost, + code: item.unit_cost.code, + }; } - - const itemData = { item: updateFields }; - const response = await client.put<{ response: { result: { item: Item } } }>( - `/items/items/${item_id}`, - itemData - ); - return response.response.result.item; + const result = await client.updateItem(item_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_item', - description: 'Delete an item', - inputSchema: z.object({ - item_id: z.number().describe('Item ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.put( - `/items/items/${args.item_id}`, - { item: { vis_state: 1 } } - ); - return { success: true, message: `Item ${args.item_id} deleted` }; + description: 'Delete an item/service from FreshBooks', + inputSchema: { + type: 'object', + properties: { + item_id: { + type: 'number', + description: 'Item ID to delete', + }, + }, + required: ['item_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteItem(args.item_id); + return { + content: [ + { + type: 'text', + text: `Item ${args.item_id} deleted successfully`, + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/journal-entries-tools.ts b/servers/freshbooks/src/tools/journal-entries-tools.ts new file mode 100644 index 0000000..0335f6c --- /dev/null +++ b/servers/freshbooks/src/tools/journal-entries-tools.ts @@ -0,0 +1,129 @@ +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; + +export const journalEntriesTools = [ + { + name: 'freshbooks_list_journal_entries', + description: 'List all journal entries with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getJournalEntries(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_get_journal_entry', + description: 'Get detailed information about a specific journal entry', + inputSchema: { + type: 'object', + properties: { + journal_entry_id: { + type: 'number', + description: 'Journal entry ID', + }, + }, + required: ['journal_entry_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getJournalEntry(args.journal_entry_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_create_journal_entry', + description: 'Create a new journal entry in FreshBooks', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Journal entry name', + }, + description: { + type: 'string', + description: 'Journal entry description', + }, + user_entered_date: { + type: 'string', + description: 'Entry date (YYYY-MM-DD)', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + details: { + type: 'array', + description: 'Journal entry details (debits and credits)', + items: { + type: 'object', + properties: { + sub_accountid: { + type: 'number', + description: 'Sub-account ID', + }, + debit_amount: { + type: 'object', + properties: { + amount: { + type: 'string', + description: 'Debit amount (or null)', + }, + }, + }, + credit_amount: { + type: 'object', + properties: { + amount: { + type: 'string', + description: 'Credit amount (or null)', + }, + }, + }, + description: { + type: 'string', + description: 'Line description', + }, + }, + }, + }, + }, + required: ['name', 'user_entered_date', 'details'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createJournalEntry(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/payments-tools.ts b/servers/freshbooks/src/tools/payments-tools.ts index 079a914..8388f24 100644 --- a/servers/freshbooks/src/tools/payments-tools.ts +++ b/servers/freshbooks/src/tools/payments-tools.ts @@ -1,121 +1,187 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Payment } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const paymentsTools = [ { name: 'freshbooks_list_payments', - description: 'List all payments with optional filtering', - inputSchema: z.object({ - invoiceid: z.number().optional().describe('Filter by invoice ID'), - clientid: z.number().optional().describe('Filter by client ID'), - date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'), - date_max: z.string().optional().describe('End date (YYYY-MM-DD)'), - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const params: Record = {}; - if (args.invoiceid) params.invoiceid = args.invoiceid; - if (args.clientid) params.clientid = args.clientid; - if (args.date_min) params.date_min = args.date_min; - if (args.date_max) params.date_max = args.date_max; - - const response = await client.getPaginated<{ payments: Payment[] }>( - '/payments/payments', - args.page, - args.per_page, - params - ); + description: 'List all payments with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getPayments(args); return { - payments: response.response.result.payments || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_payment', - description: 'Get a single payment by ID', - inputSchema: z.object({ - payment_id: z.number().describe('Payment ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { payment: Payment } } }>( - `/payments/payments/${args.payment_id}` - ); - return response.response.result.payment; + description: 'Get detailed information about a specific payment', + inputSchema: { + type: 'object', + properties: { + payment_id: { + type: 'number', + description: 'Payment ID', + }, + }, + required: ['payment_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getPayment(args.payment_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_payment', - description: 'Create a new payment', - inputSchema: z.object({ - invoiceid: z.number().describe('Invoice ID'), - amount: z.string().describe('Payment amount'), - date: z.string().optional().describe('Payment date (YYYY-MM-DD)'), - type: z.string().default('Cash').describe('Payment method'), - note: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const paymentData = { - payment: { - invoiceid: args.invoiceid, - amount: { amount: args.amount, code: 'USD' }, - date: args.date || new Date().toISOString().split('T')[0], - type: args.type, - note: args.note, + description: 'Record a new payment in FreshBooks', + inputSchema: { + type: 'object', + properties: { + invoiceid: { + type: 'number', + description: 'Invoice ID this payment is for', }, + amount: { + type: 'string', + description: 'Payment amount', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + date: { + type: 'string', + description: 'Payment date (YYYY-MM-DD)', + }, + type: { + type: 'string', + description: 'Payment type (e.g., Cash, Check, Credit Card)', + }, + note: { + type: 'string', + description: 'Payment note', + }, + gateway: { + type: 'string', + description: 'Payment gateway name', + }, + }, + required: ['invoiceid', 'amount', 'date', 'type'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const payment = { + invoiceid: args.invoiceid, + amount: { + amount: args.amount, + code: args.currency_code || 'USD', + }, + date: args.date, + type: args.type, + note: args.note, + gateway: args.gateway, + }; + const result = await client.createPayment(payment); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; - - const response = await client.post<{ response: { result: { payment: Payment } } }>( - '/payments/payments', - paymentData - ); - return response.response.result.payment; }, }, - { name: 'freshbooks_update_payment', description: 'Update an existing payment', - inputSchema: z.object({ - payment_id: z.number().describe('Payment ID'), - amount: z.string().optional(), - date: z.string().optional(), - type: z.string().optional(), - note: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const { payment_id, ...updateFields } = args; - if (updateFields.amount) { - updateFields.amount = { amount: updateFields.amount, code: 'USD' }; + inputSchema: { + type: 'object', + properties: { + payment_id: { + type: 'number', + description: 'Payment ID to update', + }, + amount: { + type: 'string', + description: 'Payment amount', + }, + date: { + type: 'string', + description: 'Payment date', + }, + note: { + type: 'string', + description: 'Payment note', + }, + }, + required: ['payment_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { payment_id, amount, ...updates } = args; + if (amount) { + const payment = await client.getPayment(payment_id); + updates.amount = { + amount, + code: payment.amount.code, + }; } - - const paymentData = { payment: updateFields }; - const response = await client.put<{ response: { result: { payment: Payment } } }>( - `/payments/payments/${payment_id}`, - paymentData - ); - return response.response.result.payment; + const result = await client.updatePayment(payment_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_payment', - description: 'Delete a payment', - inputSchema: z.object({ - payment_id: z.number().describe('Payment ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.put( - `/payments/payments/${args.payment_id}`, - { payment: { vis_state: 1 } } - ); - return { success: true, message: `Payment ${args.payment_id} deleted` }; + description: 'Delete a payment from FreshBooks', + inputSchema: { + type: 'object', + properties: { + payment_id: { + type: 'number', + description: 'Payment ID to delete', + }, + }, + required: ['payment_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deletePayment(args.payment_id); + return { + content: [ + { + type: 'text', + text: `Payment ${args.payment_id} deleted successfully`, + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/projects-tools.ts b/servers/freshbooks/src/tools/projects-tools.ts index cad1826..d687e23 100644 --- a/servers/freshbooks/src/tools/projects-tools.ts +++ b/servers/freshbooks/src/tools/projects-tools.ts @@ -1,124 +1,214 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Project, ProjectService } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const projectsTools = [ { name: 'freshbooks_list_projects', - description: 'List all projects with optional filtering', - inputSchema: z.object({ - client_id: z.number().optional().describe('Filter by client ID'), - active: z.boolean().optional().describe('Filter by active status'), - complete: z.boolean().optional().describe('Filter by completion status'), - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const params: Record = {}; - if (args.client_id !== undefined) params.client_id = args.client_id; - if (args.active !== undefined) params.active = args.active; - if (args.complete !== undefined) params.complete = args.complete; - - const response = await client.getPaginated<{ projects: Project[] }>( - '/projects/business/123/projects', - args.page, - args.per_page, - params - ); + description: 'List all projects with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getProjects(args); return { - projects: response.response.result.projects || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_project', - description: 'Get a single project by ID', - inputSchema: z.object({ - project_id: z.number().describe('Project ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { project: Project } } }>( - `/projects/business/123/projects/${args.project_id}` - ); - return response.response.result.project; + description: 'Get detailed information about a specific project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'Project ID', + }, + }, + required: ['project_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getProject(args.project_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_project', - description: 'Create a new project', - inputSchema: z.object({ - title: z.string().describe('Project title'), - description: z.string().optional(), - client_id: z.number().optional().describe('Associated client ID'), - due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'), - project_type: z.enum(['fixed_price', 'hourly_rate']).default('hourly_rate'), - fixed_price: z.string().optional().describe('Fixed price amount'), - billing_method: z.enum(['project_rate', 'service_rate', 'team_member_rate']).optional(), - rate: z.string().optional().describe('Hourly rate'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const projectData = { project: { ...args } }; - - const response = await client.post<{ response: { result: { project: Project } } }>( - '/projects/business/123/projects', - projectData - ); - return response.response.result.project; + description: 'Create a new project in FreshBooks', + inputSchema: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Project title', + }, + description: { + type: 'string', + description: 'Project description', + }, + client_id: { + type: 'number', + description: 'Client ID', + }, + due_date: { + type: 'string', + description: 'Project due date (YYYY-MM-DD)', + }, + billing_method: { + type: 'string', + description: 'Billing method (project_rate, service_rate, task_rate, fixed_price)', + }, + project_type: { + type: 'string', + description: 'Project type (fixed_price, hourly_rate)', + }, + budget: { + type: 'number', + description: 'Project budget', + }, + fixed_price: { + type: 'number', + description: 'Fixed price for the project', + }, + rate: { + type: 'number', + description: 'Hourly rate', + }, + internal: { + type: 'boolean', + description: 'Is this an internal project?', + }, + }, + required: ['title', 'client_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createProject(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_update_project', description: 'Update an existing project', - inputSchema: z.object({ - project_id: z.number().describe('Project ID'), - title: z.string().optional(), - description: z.string().optional(), - client_id: z.number().optional(), - due_date: z.string().optional(), - active: z.boolean().optional(), - complete: z.boolean().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const { project_id, ...updateFields } = args; - const projectData = { project: updateFields }; - - const response = await client.put<{ response: { result: { project: Project } } }>( - `/projects/business/123/projects/${project_id}`, - projectData - ); - return response.response.result.project; + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'Project ID to update', + }, + title: { + type: 'string', + description: 'Project title', + }, + description: { + type: 'string', + description: 'Project description', + }, + due_date: { + type: 'string', + description: 'Project due date', + }, + active: { + type: 'boolean', + description: 'Is the project active?', + }, + complete: { + type: 'boolean', + description: 'Is the project complete?', + }, + }, + required: ['project_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { project_id, ...updates } = args; + const result = await client.updateProject(project_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_project', - description: 'Delete a project', - inputSchema: z.object({ - project_id: z.number().describe('Project ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.delete( - `/projects/business/123/projects/${args.project_id}` - ); - return { success: true, message: `Project ${args.project_id} deleted` }; + description: 'Delete a project from FreshBooks', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'Project ID to delete', + }, + }, + required: ['project_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteProject(args.project_id); + return { + content: [ + { + type: 'text', + text: `Project ${args.project_id} deleted successfully`, + }, + ], + }; }, }, - { - name: 'freshbooks_list_project_services', - description: 'List all available services for time tracking', - inputSchema: z.object({}), - handler: async (_args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { services: ProjectService[] } } }>( - '/projects/business/123/services' - ); - return response.response.result.services || []; + name: 'freshbooks_mark_project_complete', + description: 'Mark a project as complete', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'Project ID', + }, + }, + required: ['project_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.updateProject(args.project_id, { complete: true }); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/recurring-tools.ts b/servers/freshbooks/src/tools/recurring-tools.ts index ed8ab34..55e14be 100644 --- a/servers/freshbooks/src/tools/recurring-tools.ts +++ b/servers/freshbooks/src/tools/recurring-tools.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; import type { RecurringProfile } from '../types/index.js'; export const recurringTools = [ @@ -11,21 +11,21 @@ export const recurringTools = [ page: z.number().default(1), per_page: z.number().default(30), }), - handler: async (args: any, client: FreshBooksClient) => { + handler: async (args: any, client: FreshBooksAPIClient) => { const params: Record = {}; if (args.clientid) params.clientid = args.clientid; - const response = await client.getPaginated<{ recurring: RecurringProfile[] }>( + const response = await client.getPaginated( '/invoices/recurring', args.page, args.per_page, params ); return { - recurring: response.response.result.recurring || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + recurring: response.result?.recurring || response.recurring || [], + page: response.page || args.page, + pages: response.pages || 1, + total: response.total || 0, }; }, }, @@ -36,11 +36,11 @@ export const recurringTools = [ inputSchema: z.object({ recurring_id: z.number().describe('Recurring profile ID'), }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { recurring: RecurringProfile } } }>( + handler: async (args: any, client: FreshBooksAPIClient) => { + const response = await client.get( `/invoices/recurring/${args.recurring_id}` ); - return response.response.result.recurring; + return response.result?.recurring || response.recurring; }, }, @@ -61,7 +61,7 @@ export const recurringTools = [ notes: z.string().optional(), terms: z.string().optional(), }), - handler: async (args: any, client: FreshBooksClient) => { + handler: async (args: any, client: FreshBooksAPIClient) => { const lines = args.lines.map((line: any) => ({ ...line, unit_cost: { amount: line.unit_cost, code: args.currency_code }, @@ -80,11 +80,11 @@ export const recurringTools = [ }, }; - const response = await client.post<{ response: { result: { recurring: RecurringProfile } } }>( + const response = await client.post( '/invoices/recurring', recurringData ); - return response.response.result.recurring; + return response.result?.recurring || response.recurring; }, }, @@ -104,7 +104,7 @@ export const recurringTools = [ notes: z.string().optional(), terms: z.string().optional(), }), - handler: async (args: any, client: FreshBooksClient) => { + handler: async (args: any, client: FreshBooksAPIClient) => { const { recurring_id, ...updateFields } = args; if (updateFields.lines) { @@ -115,11 +115,11 @@ export const recurringTools = [ } const recurringData = { recurring: updateFields }; - const response = await client.put<{ response: { result: { recurring: RecurringProfile } } }>( + const response = await client.put( `/invoices/recurring/${recurring_id}`, recurringData ); - return response.response.result.recurring; + return response.result?.recurring || response.recurring; }, }, @@ -129,7 +129,7 @@ export const recurringTools = [ inputSchema: z.object({ recurring_id: z.number().describe('Recurring profile ID'), }), - handler: async (args: any, client: FreshBooksClient) => { + handler: async (args: any, client: FreshBooksAPIClient) => { await client.put( `/invoices/recurring/${args.recurring_id}`, { recurring: { vis_state: 1 } } diff --git a/servers/freshbooks/src/tools/reports-tools.ts b/servers/freshbooks/src/tools/reports-tools.ts index f3cbae3..66797dd 100644 --- a/servers/freshbooks/src/tools/reports-tools.ts +++ b/servers/freshbooks/src/tools/reports-tools.ts @@ -1,112 +1,110 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { ProfitLossReport, TaxSummary, AccountsAgingReport } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const reportsTools = [ { name: 'freshbooks_profit_loss_report', - description: 'Generate profit and loss report for a date range', - inputSchema: z.object({ - start_date: z.string().describe('Start date (YYYY-MM-DD)'), - end_date: z.string().describe('End date (YYYY-MM-DD)'), - currency_code: z.string().default('USD'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: ProfitLossReport } }>( - '/reports/accounting/profitloss', - { - start_date: args.start_date, - end_date: args.end_date, - currency_code: args.currency_code, - } - ); - return response.response.result; + description: 'Generate a profit and loss report for a date range', + inputSchema: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + end_date: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + }, + required: ['start_date', 'end_date'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getProfitLossReport(args.start_date, args.end_date); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_tax_summary_report', - description: 'Generate tax summary report for a date range', - inputSchema: z.object({ - start_date: z.string().describe('Start date (YYYY-MM-DD)'), - end_date: z.string().describe('End date (YYYY-MM-DD)'), - currency_code: z.string().default('USD'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { taxsummaries: TaxSummary[] } } }>( - '/reports/accounting/taxsummary', - { - start_date: args.start_date, - end_date: args.end_date, - currency_code: args.currency_code, - } - ); - return response.response.result.taxsummaries || []; + description: 'Generate a tax summary report for a date range', + inputSchema: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + end_date: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + }, + required: ['start_date', 'end_date'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getTaxSummaryReport(args.start_date, args.end_date); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { - name: 'freshbooks_accounts_aging_report', - description: 'Generate accounts aging report (accounts receivable)', - inputSchema: z.object({ - date: z.string().optional().describe('Report date (YYYY-MM-DD, defaults to today)'), - currency_code: z.string().default('USD'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { clients: AccountsAgingReport[] } } }>( - '/reports/accounting/aging', - { - date: args.date || new Date().toISOString().split('T')[0], - currency_code: args.currency_code, - } - ); - return response.response.result.clients || []; + name: 'freshbooks_aging_report', + description: 'Generate an accounts aging report showing outstanding balances', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getAgingReport(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_expense_report', - description: 'Generate expense report for a date range', - inputSchema: z.object({ - start_date: z.string().describe('Start date (YYYY-MM-DD)'), - end_date: z.string().describe('End date (YYYY-MM-DD)'), - clientid: z.number().optional().describe('Filter by client ID'), - categoryid: z.number().optional().describe('Filter by category ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const params: any = { - start_date: args.start_date, - end_date: args.end_date, - }; - if (args.clientid) params.clientid = args.clientid; - if (args.categoryid) params.categoryid = args.categoryid; - - const response = await client.get<{ response: { result: any } }>( - '/reports/accounting/expenses', - params - ); - return response.response.result; + description: 'Generate an expense report for a date range', + inputSchema: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + end_date: { + type: 'string', + description: 'End date (YYYY-MM-DD)', + }, + }, + required: ['start_date', 'end_date'], }, - }, - - { - name: 'freshbooks_revenue_by_client_report', - description: 'Generate revenue by client report for a date range', - inputSchema: z.object({ - start_date: z.string().describe('Start date (YYYY-MM-DD)'), - end_date: z.string().describe('End date (YYYY-MM-DD)'), - currency_code: z.string().default('USD'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: any } }>( - '/reports/accounting/revenue_by_client', - { - start_date: args.start_date, - end_date: args.end_date, - currency_code: args.currency_code, - } - ); - return response.response.result; + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getExpenseReport(args.start_date, args.end_date); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/retainers-tools.ts b/servers/freshbooks/src/tools/retainers-tools.ts new file mode 100644 index 0000000..fb644f0 --- /dev/null +++ b/servers/freshbooks/src/tools/retainers-tools.ts @@ -0,0 +1,161 @@ +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; + +export const retainersTools = [ + { + name: 'freshbooks_list_retainers', + description: 'List all retainers with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getRetainers(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_get_retainer', + description: 'Get detailed information about a specific retainer', + inputSchema: { + type: 'object', + properties: { + retainer_id: { + type: 'number', + description: 'Retainer ID', + }, + }, + required: ['retainer_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getRetainer(args.retainer_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_create_retainer', + description: 'Create a new retainer in FreshBooks', + inputSchema: { + type: 'object', + properties: { + client_id: { + type: 'number', + description: 'Client ID', + }, + fee: { + type: 'string', + description: 'Retainer fee amount', + }, + period: { + type: 'string', + description: 'Retainer period (monthly, quarterly, yearly)', + }, + start_date: { + type: 'string', + description: 'Start date (YYYY-MM-DD)', + }, + end_date: { + type: 'string', + description: 'End date (YYYY-MM-DD, optional)', + }, + }, + required: ['client_id', 'fee', 'period', 'start_date'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createRetainer(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_update_retainer', + description: 'Update an existing retainer', + inputSchema: { + type: 'object', + properties: { + retainer_id: { + type: 'number', + description: 'Retainer ID to update', + }, + fee: { + type: 'string', + description: 'Retainer fee amount', + }, + end_date: { + type: 'string', + description: 'End date', + }, + active: { + type: 'boolean', + description: 'Is the retainer active?', + }, + }, + required: ['retainer_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { retainer_id, ...updates } = args; + const result = await client.updateRetainer(retainer_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_delete_retainer', + description: 'Delete a retainer from FreshBooks', + inputSchema: { + type: 'object', + properties: { + retainer_id: { + type: 'number', + description: 'Retainer ID to delete', + }, + }, + required: ['retainer_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteRetainer(args.retainer_id); + return { + content: [ + { + type: 'text', + text: `Retainer ${args.retainer_id} deleted successfully`, + }, + ], + }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/staff-tools.ts b/servers/freshbooks/src/tools/staff-tools.ts new file mode 100644 index 0000000..d510bd9 --- /dev/null +++ b/servers/freshbooks/src/tools/staff-tools.ts @@ -0,0 +1,57 @@ +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; + +export const staffTools = [ + { + name: 'freshbooks_list_staff', + description: 'List all staff members with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getStaff(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_get_staff_member', + description: 'Get detailed information about a specific staff member', + inputSchema: { + type: 'object', + properties: { + staff_id: { + type: 'number', + description: 'Staff member ID', + }, + }, + required: ['staff_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getStaffMember(args.staff_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, +]; diff --git a/servers/freshbooks/src/tools/taxes-tools.ts b/servers/freshbooks/src/tools/taxes-tools.ts index bdf08b1..ad0ee9c 100644 --- a/servers/freshbooks/src/tools/taxes-tools.ts +++ b/servers/freshbooks/src/tools/taxes-tools.ts @@ -1,96 +1,148 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { Tax } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const taxesTools = [ { name: 'freshbooks_list_taxes', - description: 'List all taxes', - inputSchema: z.object({ - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.getPaginated<{ taxes: Tax[] }>( - '/taxes/taxes', - args.page, - args.per_page - ); + description: 'List all taxes in FreshBooks', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getTaxes(); return { - taxes: response.response.result.taxes || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_tax', - description: 'Get a single tax by ID', - inputSchema: z.object({ - tax_id: z.number().describe('Tax ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { tax: Tax } } }>( - `/taxes/taxes/${args.tax_id}` - ); - return response.response.result.tax; + description: 'Get detailed information about a specific tax', + inputSchema: { + type: 'object', + properties: { + tax_id: { + type: 'number', + description: 'Tax ID', + }, + }, + required: ['tax_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getTax(args.tax_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_tax', - description: 'Create a new tax', - inputSchema: z.object({ - name: z.string().describe('Tax name (e.g., "GST", "VAT")'), - number: z.string().optional().describe('Tax number/registration'), - amount: z.string().describe('Tax percentage (e.g., "13" for 13%)'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const taxData = { tax: { ...args } }; - - const response = await client.post<{ response: { result: { tax: Tax } } }>( - '/taxes/taxes', - taxData - ); - return response.response.result.tax; + description: 'Create a new tax in FreshBooks', + inputSchema: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Tax name (e.g., GST, VAT, Sales Tax)', + }, + amount: { + type: 'string', + description: 'Tax rate as a percentage (e.g., 5, 13.5)', + }, + number: { + type: 'string', + description: 'Tax number/ID (optional)', + }, + compound: { + type: 'boolean', + description: 'Is this a compound tax?', + }, + }, + required: ['name', 'amount'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createTax(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_update_tax', description: 'Update an existing tax', - inputSchema: z.object({ - tax_id: z.number().describe('Tax ID'), - name: z.string().optional(), - number: z.string().optional(), - amount: z.string().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const { tax_id, ...updateFields } = args; - const taxData = { tax: updateFields }; - - const response = await client.put<{ response: { result: { tax: Tax } } }>( - `/taxes/taxes/${tax_id}`, - taxData - ); - return response.response.result.tax; + inputSchema: { + type: 'object', + properties: { + tax_id: { + type: 'number', + description: 'Tax ID to update', + }, + name: { + type: 'string', + description: 'Tax name', + }, + amount: { + type: 'string', + description: 'Tax rate as a percentage', + }, + number: { + type: 'string', + description: 'Tax number/ID', + }, + }, + required: ['tax_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { tax_id, ...updates } = args; + const result = await client.updateTax(tax_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_tax', - description: 'Delete a tax', - inputSchema: z.object({ - tax_id: z.number().describe('Tax ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.put( - `/taxes/taxes/${args.tax_id}`, - { tax: { vis_state: 1 } } - ); - return { success: true, message: `Tax ${args.tax_id} deleted` }; + description: 'Delete a tax from FreshBooks', + inputSchema: { + type: 'object', + properties: { + tax_id: { + type: 'number', + description: 'Tax ID to delete', + }, + }, + required: ['tax_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteTax(args.tax_id); + return { + content: [ + { + type: 'text', + text: `Tax ${args.tax_id} deleted successfully`, + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/time-entries-tools.ts b/servers/freshbooks/src/tools/time-entries-tools.ts index 9732173..64ad456 100644 --- a/servers/freshbooks/src/tools/time-entries-tools.ts +++ b/servers/freshbooks/src/tools/time-entries-tools.ts @@ -1,125 +1,223 @@ -import { z } from 'zod'; -import type { FreshBooksClient } from '../clients/freshbooks.js'; -import type { TimeEntry } from '../types/index.js'; +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; export const timeEntriesTools = [ { name: 'freshbooks_list_time_entries', - description: 'List all time entries with optional filtering', - inputSchema: z.object({ - clientid: z.number().optional().describe('Filter by client ID'), - projectid: z.number().optional().describe('Filter by project ID'), - date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'), - date_max: z.string().optional().describe('End date (YYYY-MM-DD)'), - billed_status: z.enum(['billed', 'unbilled']).optional(), - page: z.number().default(1), - per_page: z.number().default(30), - }), - handler: async (args: any, client: FreshBooksClient) => { - const params: Record = {}; - if (args.clientid) params.client_id = args.clientid; - if (args.projectid) params.project_id = args.projectid; - if (args.date_min) params.started_from = args.date_min; - if (args.date_max) params.started_to = args.date_max; - if (args.billed_status) params.billed_status = args.billed_status; - - const response = await client.getPaginated<{ time_entries: TimeEntry[] }>( - '/timetracking/business/123/time_entries', - args.page, - args.per_page, - params - ); + description: 'List all time entries with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getTimeEntries(args); return { - time_entries: response.response.result.time_entries || [], - page: response.response.page, - pages: response.response.pages, - total: response.response.total, + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; }, }, - { name: 'freshbooks_get_time_entry', - description: 'Get a single time entry by ID', - inputSchema: z.object({ - time_entry_id: z.number().describe('Time entry ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - const response = await client.get<{ response: { result: { time_entry: TimeEntry } } }>( - `/timetracking/business/123/time_entries/${args.time_entry_id}` - ); - return response.response.result.time_entry; + description: 'Get detailed information about a specific time entry', + inputSchema: { + type: 'object', + properties: { + time_entry_id: { + type: 'number', + description: 'Time entry ID', + }, + }, + required: ['time_entry_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getTimeEntry(args.time_entry_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_create_time_entry', - description: 'Create a new time entry', - inputSchema: z.object({ - duration: z.number().describe('Duration in seconds'), - note: z.string().optional().describe('Note/description'), - started_at: z.string().describe('Start time (ISO 8601 format)'), - clientid: z.number().optional().describe('Client ID'), - projectid: z.number().optional().describe('Project ID'), - service_id: z.number().optional().describe('Service ID'), - is_logged: z.boolean().default(true), - }), - handler: async (args: any, client: FreshBooksClient) => { - const timeEntryData = { - time_entry: { - is_logged: args.is_logged, - duration: args.duration, - note: args.note, - started_at: args.started_at, - client_id: args.clientid, - project_id: args.projectid, - service_id: args.service_id, + description: 'Create a new time entry in FreshBooks', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'Project ID', }, + client_id: { + type: 'number', + description: 'Client ID', + }, + duration: { + type: 'number', + description: 'Duration in seconds', + }, + started_at: { + type: 'string', + description: 'Start time (ISO 8601)', + }, + note: { + type: 'string', + description: 'Note about the time entry', + }, + billable: { + type: 'boolean', + description: 'Is this time billable?', + }, + internal: { + type: 'boolean', + description: 'Is this an internal time entry?', + }, + }, + required: ['project_id', 'duration', 'started_at'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createTimeEntry(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], }; - - const response = await client.post<{ response: { result: { time_entry: TimeEntry } } }>( - '/timetracking/business/123/time_entries', - timeEntryData - ); - return response.response.result.time_entry; }, }, - { name: 'freshbooks_update_time_entry', description: 'Update an existing time entry', - inputSchema: z.object({ - time_entry_id: z.number().describe('Time entry ID'), - duration: z.number().optional(), - note: z.string().optional(), - started_at: z.string().optional(), - clientid: z.number().optional(), - projectid: z.number().optional(), - service_id: z.number().optional(), - }), - handler: async (args: any, client: FreshBooksClient) => { - const { time_entry_id, ...updateFields } = args; - const timeEntryData = { time_entry: updateFields }; - - const response = await client.put<{ response: { result: { time_entry: TimeEntry } } }>( - `/timetracking/business/123/time_entries/${time_entry_id}`, - timeEntryData - ); - return response.response.result.time_entry; + inputSchema: { + type: 'object', + properties: { + time_entry_id: { + type: 'number', + description: 'Time entry ID to update', + }, + duration: { + type: 'number', + description: 'Duration in seconds', + }, + note: { + type: 'string', + description: 'Note about the time entry', + }, + billable: { + type: 'boolean', + description: 'Is this time billable?', + }, + }, + required: ['time_entry_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { time_entry_id, ...updates } = args; + const result = await client.updateTimeEntry(time_entry_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, - { name: 'freshbooks_delete_time_entry', - description: 'Delete a time entry', - inputSchema: z.object({ - time_entry_id: z.number().describe('Time entry ID'), - }), - handler: async (args: any, client: FreshBooksClient) => { - await client.delete( - `/timetracking/business/123/time_entries/${args.time_entry_id}` - ); - return { success: true, message: `Time entry ${args.time_entry_id} deleted` }; + description: 'Delete a time entry from FreshBooks', + inputSchema: { + type: 'object', + properties: { + time_entry_id: { + type: 'number', + description: 'Time entry ID to delete', + }, + }, + required: ['time_entry_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteTimeEntry(args.time_entry_id); + return { + content: [ + { + type: 'text', + text: `Time entry ${args.time_entry_id} deleted successfully`, + }, + ], + }; + }, + }, + { + name: 'freshbooks_start_timer', + description: 'Start a new timer for a project', + inputSchema: { + type: 'object', + properties: { + project_id: { + type: 'number', + description: 'Project ID', + }, + note: { + type: 'string', + description: 'Note about what you are working on', + }, + }, + required: ['project_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.startTimer(args.project_id, args.note); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_stop_timer', + description: 'Stop a running timer', + inputSchema: { + type: 'object', + properties: { + time_entry_id: { + type: 'number', + description: 'Time entry ID of the running timer', + }, + }, + required: ['time_entry_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.stopTimer(args.time_entry_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; }, }, ]; diff --git a/servers/freshbooks/src/tools/vendors-tools.ts b/servers/freshbooks/src/tools/vendors-tools.ts new file mode 100644 index 0000000..95db176 --- /dev/null +++ b/servers/freshbooks/src/tools/vendors-tools.ts @@ -0,0 +1,201 @@ +import type { FreshBooksAPIClient } from '../clients/freshbooks.js'; + +export const vendorsTools = [ + { + name: 'freshbooks_list_vendors', + description: 'List all bill vendors with pagination', + inputSchema: { + type: 'object', + properties: { + page: { + type: 'number', + description: 'Page number for pagination', + }, + per_page: { + type: 'number', + description: 'Number of results per page', + }, + }, + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getVendors(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_get_vendor', + description: 'Get detailed information about a specific vendor', + inputSchema: { + type: 'object', + properties: { + vendor_id: { + type: 'number', + description: 'Vendor ID', + }, + }, + required: ['vendor_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.getVendor(args.vendor_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_create_vendor', + description: 'Create a new vendor in FreshBooks', + inputSchema: { + type: 'object', + properties: { + vendor_name: { + type: 'string', + description: 'Vendor name', + }, + primary_contact_first_name: { + type: 'string', + description: 'Primary contact first name', + }, + primary_contact_last_name: { + type: 'string', + description: 'Primary contact last name', + }, + primary_contact_email: { + type: 'string', + description: 'Primary contact email', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + website: { + type: 'string', + description: 'Website URL', + }, + street: { + type: 'string', + description: 'Street address', + }, + city: { + type: 'string', + description: 'City', + }, + province: { + type: 'string', + description: 'Province/state', + }, + postal_code: { + type: 'string', + description: 'Postal code', + }, + country: { + type: 'string', + description: 'Country', + }, + currency_code: { + type: 'string', + description: 'Currency code (e.g., USD)', + }, + is_1099: { + type: 'boolean', + description: 'Is this vendor subject to 1099 reporting?', + }, + }, + required: ['vendor_name', 'primary_contact_email'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const result = await client.createVendor(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_update_vendor', + description: 'Update an existing vendor', + inputSchema: { + type: 'object', + properties: { + vendor_id: { + type: 'number', + description: 'Vendor ID to update', + }, + vendor_name: { + type: 'string', + description: 'Vendor name', + }, + primary_contact_email: { + type: 'string', + description: 'Primary contact email', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + street: { + type: 'string', + description: 'Street address', + }, + city: { + type: 'string', + description: 'City', + }, + }, + required: ['vendor_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + const { vendor_id, ...updates } = args; + const result = await client.updateVendor(vendor_id, updates); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + { + name: 'freshbooks_delete_vendor', + description: 'Delete a vendor from FreshBooks', + inputSchema: { + type: 'object', + properties: { + vendor_id: { + type: 'number', + description: 'Vendor ID to delete', + }, + }, + required: ['vendor_id'], + }, + handler: async (client: FreshBooksAPIClient, args: any) => { + await client.deleteVendor(args.vendor_id); + return { + content: [ + { + type: 'text', + text: `Vendor ${args.vendor_id} deleted successfully`, + }, + ], + }; + }, + }, +]; diff --git a/servers/freshbooks/src/types/index.ts b/servers/freshbooks/src/types/index.ts index 5435800..18f44fa 100644 --- a/servers/freshbooks/src/types/index.ts +++ b/servers/freshbooks/src/types/index.ts @@ -2,71 +2,82 @@ export interface FreshBooksConfig { accountId: string; - bearerToken: string; - baseUrl?: string; + accessToken: string; + apiBaseUrl?: string; } -export interface PaginatedResponse { - response: { - result: T; - page: number; - pages: number; - per_page: number; - total: number; - }; -} - -export interface FreshBooksError { - message: string; - code?: string; - errors?: Array<{ field: string; message: string }>; -} - -// Client Types -export interface Client { +export interface FreshBooksClient { id: number; - accounting_systemid: string; organization: string; fname: string; lname: string; email: string; - company_industry?: string; - company_size?: string; - bill_street?: string; - bill_city?: string; - bill_state?: string; - bill_country?: string; - bill_postal_code?: string; + username: string; + home_phone: string | null; + business_phone: string | null; + mobile_phone: string | null; + fax: string | null; + company_industry: string | null; + company_size: string | null; + vat_name: string | null; + vat_number: string | null; + s_province: string; + s_city: string; + s_street: string; + s_street2: string; + s_code: string; + s_country: string; + p_province: string; + p_city: string; + p_street: string; + p_street2: string; + p_code: string; + p_country: string; currency_code: string; + language: string; + note: string | null; + pref_email: boolean; + pref_gmail: boolean; + allow_late_fees: boolean; + allow_late_notifications: boolean; + role: string; + vis_state: number; updated: string; - created_at: string; - language?: string; - note?: string; - vat_name?: string; - vat_number?: string; - allow_late_fees?: boolean; - allow_late_notifications?: boolean; } -export interface ClientContact { - id: number; - clientid: number; - fname: string; - lname: string; - email: string; - phone?: string; - mobile?: string; -} - -// Invoice Types -export interface Invoice { +export interface FreshBooksInvoice { id: number; accountid: string; - accounting_systemid: string; - clientid: number; - create_date: string; + invoiceid: number; invoice_number: string; + customerid: number; + create_date: string; + generation_date: string | null; + discount_value: string; + discount_description: string | null; + po_number: string | null; + template: string; currency_code: string; + language: string; + terms: string | null; + notes: string | null; + address: string; + return_uri: string | null; + deposit_amount: string | null; + deposit_percentage: string | null; + deposit_status: string; + payment_status: string; + auto_bill: boolean; + v3_status: string; + date_paid: string | null; + estimateid: number; + basecampid: number; + sentid: number; + status: number; + parent: number; + fname: string; + lname: string; + organization: string; amount: { amount: string; code: string; @@ -79,31 +90,21 @@ export interface Invoice { amount: string; code: string; }; - due_date: string; - status: string; - payment_status: string; - v3_status: string; + due_offset_days: number; lines: InvoiceLine[]; - terms?: string; - notes?: string; - discount_total?: { - amount: string; - code: string; - }; - updated: string; - created_at: string; + presentation: InvoicePresentation; } export interface InvoiceLine { - id?: number; - name: string; - description?: string; - qty: number; - unit_cost: { + lineid?: number; + amount?: { amount: string; code: string; }; - amount: { + name: string; + description?: string; + qty: string; + unit_cost: { amount: string; code: string; }; @@ -111,219 +112,496 @@ export interface InvoiceLine { taxAmount1?: string; taxName2?: string; taxAmount2?: string; + type?: number; + expenseid?: number; } -export interface Payment { +export interface InvoicePresentation { + theme_primary_color: string; + theme_layout: string; + theme_font_name: string; + image_logo_src: string | null; + image_banner_src: string | null; +} + +export interface FreshBooksEstimate { id: number; - invoiceid: number; + accountid: string; + estimateid: number; + estimate_number: string; + customerid: number; + accepted: boolean; + create_date: string; + discount_value: string; + discount_description: string | null; + po_number: string | null; + template: string; + currency_code: string; + language: string; + terms: string | null; + notes: string | null; + address: string; + status: number; + fname: string; + lname: string; + organization: string; amount: { amount: string; code: string; }; - date: string; - type: string; - note?: string; - updated: string; - created_at: string; + lines: EstimateLine[]; + ui_status: string; } -// Expense Types -export interface Expense { - id: number; - category_id: number; - clientid?: number; - projectid?: number; - vendor: string; - amount: { +export interface EstimateLine { + lineid?: number; + amount?: { + amount: string; + code: string; + }; + name: string; + description?: string; + qty: string; + unit_cost: { amount: string; code: string; }; - date: string; - notes?: string; taxName1?: string; - taxAmount1?: number; - taxPercent1?: number; + taxAmount1?: string; + taxName2?: string; + taxAmount2?: string; + type?: number; +} + +export interface FreshBooksExpense { + id: number; + accountid: string; + amount: { + amount: string; + code: string; + }; + vendor: string; + date: string; + categoryid: number; + clientid: number; + projectid: number; + staffid: number; + notes: string | null; + taxName1: string; + taxAmount1: number; + taxName2: string; + taxAmount2: number; + status: number; + is_cogs: boolean; + from_bulk_import: boolean; + attachment: ExpenseAttachment | null; + markup_percent: string; updated: string; - created_at: string; - staffid?: number; - status?: string; +} + +export interface ExpenseAttachment { + id: number; + jwt: string; + media_type: string; } export interface ExpenseCategory { id: number; category: string; - is_cogs?: boolean; - is_editable?: boolean; - parentid?: number; + categoryid: number; + created_at: string; + is_cogs: boolean; + is_editable: boolean; + parentid: number | null; + updated_at: string; + vis_state: number; } -// Estimate Types -export interface Estimate { +export interface FreshBooksPayment { id: number; accountid: string; - clientid: number; - create_date: string; - estimate_number: string; - currency_code: string; amount: { amount: string; code: string; }; - status: string; - lines: EstimateLine[]; - terms?: string; - notes?: string; - discount_total?: { - amount: string; - code: string; - }; + bulk_paymentid: number; + clientid: number; + creditid: number | null; + date: string; + from_credit: boolean; + gateway: string | null; + invoiceid: number; + logid: number; + note: string | null; + orderid: string | null; + overpaymentid: number; + transactionid: string | null; + type: string; updated: string; - created_at: string; + vis_state: number; } -export interface EstimateLine { - id?: number; +export interface FreshBooksProject { + id: number; + title: string; + description: string; + due_date: string | null; + client_id: number; + internal: boolean; + budget: number | null; + fixed_price: number | null; + rate: number | null; + billing_method: string; + project_type: string; + active: boolean; + complete: boolean; + sample: boolean; + created_at: string; + updated_at: string; + logged_duration: number; + services: ProjectService[]; + billed_amount: string; + billed_status: string; + retainer_id: number | null; +} + +export interface ProjectService { + business_id: number; + id: number; name: string; - description?: string; - qty: number; + billable: boolean; + vis_state: number; +} + +export interface FreshBooksTimeEntry { + id: number; + identity_id: number; + is_logged: boolean; + started_at: string; + created_at: string; + client_id: number; + project_id: number; + pending_client: string | null; + pending_project: string | null; + pending_task: string | null; + task_id: number | null; + service_id: number | null; + note: string | null; + active: boolean; + billable: boolean; + billed: boolean; + internal: boolean; + retainer_id: number | null; + duration: number; + timer: Timer | null; +} + +export interface Timer { + id: number; + is_running: boolean; + started_at: string; + duration: number; +} + +export interface FreshBooksTax { + id: number; + accounting_systemid: string; + name: string; + number: string | null; + amount: string; + compound: boolean; + updated: string; +} + +export interface FreshBooksItem { + id: number; + accountid: string; + itemid: number; + name: string; + description: string; + quantity: string; + inventory: string; unit_cost: { amount: string; code: string; }; + tax1: number; + tax2: number; + updated: string; + vis_state: number; + sku: string; +} + +export interface FreshBooksStaff { + id: number; + identity_id: number; + first_name: string; + last_name: string; + email: string; + company: string; + business_id: number; + active: boolean; + created_at: string; + updated_at: string; + rate: { + amount: string; + code: string; + } | null; +} + +export interface FreshBooksBill { + id: number; amount: { amount: string; code: string; }; -} - -// Time Entry Types -export interface TimeEntry { - id: number; - is_logged?: boolean; - duration: number; - note?: string; - started_at: string; - clientid?: number; - projectid?: number; - service_id?: number; - billed_status?: string; + attachment: ExpenseAttachment | null; + bill_number: string | null; + bill_payments: BillPayment[]; created_at: string; - updated_at: string; -} - -// Project Types -export interface Project { - id: number; - title: string; - description?: string; - client_id?: number; - due_date?: string; - project_type?: string; - fixed_price?: string; - billing_method?: string; - rate?: string; - active?: boolean; - complete?: boolean; - created_at: string; - updated_at: string; -} - -export interface ProjectService { - id: number; - business_id: number; - name: string; - billable: boolean; - rate?: { - amount: string; - code: string; - }; -} - -// Item Types -export interface Item { - id: number; - name: string; - description?: string; - qty?: number; - inventory?: number; - unit_cost?: { - amount: string; - code: string; - }; - tax1?: number; - tax2?: number; - updated: string; - created_at: string; -} - -// Tax Types -export interface Tax { - id: number; - name: string; - number?: string; - amount: string; - updated: string; - created_at: string; -} - -// Recurring Profile Types -export interface RecurringProfile { - id: number; - clientid: number; - frequency: string; - numberRecurring: number; - create_date: string; currency_code: string; - lines: InvoiceLine[]; - status?: string; - updated: string; - created_at: string; -} - -// Account Types -export interface Account { - id: string; - account_name: string; - email: string; - business_phone?: string; - address?: { - street: string; - city: string; - province: string; - country: string; - postal_code: string; + due_date: string; + due_offset_days: number; + issue_date: string; + language: string; + lines: BillLine[]; + outstanding: { + amount: string; + code: string; }; + overall_category: string; + overall_description: string; + paid: { + amount: string; + code: string; + }; + status: string; + tax_amount: { + amount: string; + code: string; + }; + total_amount: { + amount: string; + code: string; + }; + updated_at: string; + vendor_id: number; + vis_state: number; } -export interface StaffMember { +export interface BillLine { id: number; - username: string; - first_name: string; - last_name: string; - email: string; - role?: string; + amount: { + amount: string; + code: string; + }; + category_id: number; + description: string; + list_index: number; + quantity: string; + tax_amount1: string | null; + tax_amount2: string | null; + tax_authorityid1: number | null; + tax_authorityid2: number | null; + tax_name1: string | null; + tax_name2: string | null; + tax_percent1: string | null; + tax_percent2: string | null; + total_amount: { + amount: string; + code: string; + }; + unit_cost: { + amount: string; + code: string; + }; +} + +export interface BillPayment { + id: number; + amount: { + amount: string; + code: string; + }; + bill_id: number; + matched_with_expense: boolean; + note: string | null; + paid_date: string; + payment_type: string; + vis_state: number; +} + +export interface BillVendor { + id: number; + account_number: string | null; + city: string; + country: string; + currency_code: string; + is_1099: boolean; + language: string; + outstanding_balance: { + amount: string; + code: string; + }[]; + overdue_balance: { + amount: string; + code: string; + }[]; + phone: string | null; + postal_code: string; + primary_contact_email: string; + primary_contact_first_name: string; + primary_contact_last_name: string; + province: string; + street: string; + street2: string | null; + tax_defaults: TaxDefault[]; + vendor_name: string; + vis_state: number; + website: string | null; +} + +export interface TaxDefault { + systemid: number; + taxid: number; +} + +export interface AccountingAccount { + id: number; + account_name: string; + account_number: string; + account_type: string; + balance: { + amount: string; + code: string; + }; + currency_code: string; + custom: boolean; + parentid: number | null; + sub_accounts: AccountingAccount[]; +} + +export interface JournalEntry { + id: number; + created_at: string; + currency_code: string; + description: string; + details: JournalEntryDetail[]; + name: string; + user_entered_date: string; +} + +export interface JournalEntryDetail { + id: number; + credit_amount: { + amount: string; + code: string; + } | null; + currency_code: string; + debit_amount: { + amount: string; + code: string; + } | null; + description: string | null; + name: string; + sub_accountid: number; + user_entered_date: string; +} + +export interface Retainer { + id: number; + active: boolean; + business_id: number; + client_id: number; + created_at: string; + end_date: string | null; + fee: string; + period: string; + start_date: string; + updated_at: string; +} + +export interface CreditNote { + id: number; + accounting_systemid: string; + clientid: number; + creditid: number; + credit_number: string; + credit_type: string; + currency_code: string; + amount: { + amount: string; + code: string; + }; + balance: { + amount: string; + code: string; + }; + create_date: string; + language: string; + notes: string | null; + terms: string | null; + status: string; + lines: CreditNoteLine[]; +} + +export interface CreditNoteLine { + lineid: number; + amount: { + amount: string; + code: string; + }; + name: string; + description: string; + qty: string; + unit_cost: { + amount: string; + code: string; + }; + taxName1: string; + taxAmount1: string; + taxName2: string; + taxAmount2: string; } -// Report Types export interface ProfitLossReport { - total_income: { - amount: string; - code: string; - }; - total_expenses: { - amount: string; - code: string; - }; + start_date: string; + end_date: string; + currency_code: string; + income: ReportCategory[]; + expenses: ReportCategory[]; net_profit: { amount: string; code: string; }; - start_date: string; - end_date: string; } -export interface TaxSummary { +export interface ReportCategory { + category_name: string; + total: { + amount: string; + code: string; + }; + children: ReportCategory[]; +} + +export interface TaxSummaryReport { + start_date: string; + end_date: string; + currency_code: string; + taxes: TaxSummaryItem[]; + total_tax: { + amount: string; + code: string; + }; +} + +export interface TaxSummaryItem { tax_name: string; + taxable_amount: { + amount: string; + code: string; + }; tax_collected: { amount: string; code: string; @@ -332,18 +610,115 @@ export interface TaxSummary { amount: string; code: string; }; -} - -export interface AccountsAgingReport { - client_userid: number; - organization: string; - outstanding_balance: { + net_tax: { amount: string; code: string; }; - current: { amount: string; code: string }; - '1-30': { amount: string; code: string }; - '31-60': { amount: string; code: string }; - '61-90': { amount: string; code: string }; - '91+': { amount: string; code: string }; +} + +export interface AgingReport { + currency_code: string; + current: { + amount: string; + code: string; + }; + days_1_30: { + amount: string; + code: string; + }; + days_31_60: { + amount: string; + code: string; + }; + days_61_90: { + amount: string; + code: string; + }; + days_over_90: { + amount: string; + code: string; + }; + total: { + amount: string; + code: string; + }; + clients: AgingReportClient[]; +} + +export interface AgingReportClient { + client_id: number; + client_name: string; + organization: string; + current: { + amount: string; + code: string; + }; + days_1_30: { + amount: string; + code: string; + }; + days_31_60: { + amount: string; + code: string; + }; + days_61_90: { + amount: string; + code: string; + }; + days_over_90: { + amount: string; + code: string; + }; + total: { + amount: string; + code: string; + }; +} + +export interface ExpenseReport { + start_date: string; + end_date: string; + currency_code: string; + categories: ExpenseReportCategory[]; + total: { + amount: string; + code: string; + }; +} + +export interface ExpenseReportCategory { + category_name: string; + total: { + amount: string; + code: string; + }; + expenses: FreshBooksExpense[]; +} + +export interface PaginatedResponse { + page: number; + pages: number; + per_page: number; + total: number; + results: T[]; +} + +export interface FreshBooksError { + message: string; + error_type?: string; + field?: string; +} + +export interface RecurringProfile { + id: number; + recurring_id: number; + clientid: number; + frequency: string; + numberRecurring: number; + create_date: string; + currency_code: string; + lines: InvoiceLine[]; + notes?: string; + terms?: string; + vis_state?: number; } diff --git a/servers/freshbooks/src/ui/react-app/aging-report/App.tsx b/servers/freshbooks/src/ui/react-app/aging-report/App.tsx new file mode 100644 index 0000000..f30f2eb --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/aging-report/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function AgingReport() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_aging_report', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading outstanding balance...
+
+ ); + } + + return ( +
+
+

Outstanding Balance

+

Accounts receivable aging

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/aging-report/index.html b/servers/freshbooks/src/ui/react-app/aging-report/index.html index 2b98197..13f131b 100644 --- a/servers/freshbooks/src/ui/react-app/aging-report/index.html +++ b/servers/freshbooks/src/ui/react-app/aging-report/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Outstanding Balance - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/aging-report/main.tsx b/servers/freshbooks/src/ui/react-app/aging-report/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/aging-report/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/aging-report/styles.css b/servers/freshbooks/src/ui/react-app/aging-report/styles.css new file mode 100644 index 0000000..7e2e27d --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/aging-report/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #ef4444; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #ef4444; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #ef4444; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/aging-report/vite.config.ts b/servers/freshbooks/src/ui/react-app/aging-report/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/aging-report/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/bill-manager/App.tsx b/servers/freshbooks/src/ui/react-app/bill-manager/App.tsx new file mode 100644 index 0000000..a842df9 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/bill-manager/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function BillManager() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_bills', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading bill manager...
+
+ ); + } + + return ( +
+
+

Bill Manager

+

Manage vendor bills

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/bill-manager/index.html b/servers/freshbooks/src/ui/react-app/bill-manager/index.html new file mode 100644 index 0000000..843936c --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/bill-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Bill Manager - FreshBooks MCP + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/bill-manager/main.tsx b/servers/freshbooks/src/ui/react-app/bill-manager/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/bill-manager/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/bill-manager/styles.css b/servers/freshbooks/src/ui/react-app/bill-manager/styles.css new file mode 100644 index 0000000..4ed0ae7 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/bill-manager/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #f59e0b; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #f59e0b; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #f59e0b; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/bill-manager/vite.config.ts b/servers/freshbooks/src/ui/react-app/bill-manager/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/bill-manager/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/build-all.js b/servers/freshbooks/src/ui/react-app/build-all.js new file mode 100644 index 0000000..7bf053c --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/build-all.js @@ -0,0 +1,37 @@ +import { build } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; +import { readdirSync, statSync } from 'fs'; + +const appsDir = resolve(process.cwd(), 'src/apps'); +const apps = readdirSync(appsDir).filter(file => { + const fullPath = resolve(appsDir, file); + return statSync(fullPath).isDirectory(); +}); + +console.log(`Building ${apps.length} apps...`); + +for (const app of apps) { + console.log(`Building ${app}...`); + + await build({ + plugins: [react()], + build: { + outDir: resolve(process.cwd(), 'dist'), + rollupOptions: { + input: resolve(appsDir, app, 'index.html'), + output: { + entryFileNames: `${app}.js`, + assetFileNames: `${app}.[ext]`, + }, + }, + }, + resolve: { + alias: { + '@': resolve(process.cwd(), 'src'), + }, + }, + }); +} + +console.log('Build complete!'); diff --git a/servers/freshbooks/src/ui/react-app/client-dashboard/App.tsx b/servers/freshbooks/src/ui/react-app/client-dashboard/App.tsx new file mode 100644 index 0000000..8e20292 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-dashboard/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ClientDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_clients', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading client directory...
+
+ ); + } + + return ( +
+
+

Client Directory

+

Manage all your clients

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/client-dashboard/index.html b/servers/freshbooks/src/ui/react-app/client-dashboard/index.html index bf1b97b..c19e891 100644 --- a/servers/freshbooks/src/ui/react-app/client-dashboard/index.html +++ b/servers/freshbooks/src/ui/react-app/client-dashboard/index.html @@ -1,99 +1,12 @@ - - - - Clients Dashboard - FreshBooks - - - - - - -
- - + + + + Client Directory - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/client-dashboard/main.tsx b/servers/freshbooks/src/ui/react-app/client-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/client-dashboard/styles.css b/servers/freshbooks/src/ui/react-app/client-dashboard/styles.css new file mode 100644 index 0000000..e2cc93e --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-dashboard/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #8b5cf6; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #8b5cf6; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #8b5cf6; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/client-dashboard/vite.config.ts b/servers/freshbooks/src/ui/react-app/client-dashboard/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-dashboard/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/client-detail/App.tsx b/servers/freshbooks/src/ui/react-app/client-detail/App.tsx new file mode 100644 index 0000000..62cb31b --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-detail/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ClientDetail() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_get_client', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading client detail...
+
+ ); + } + + return ( +
+
+

Client Detail

+

View detailed client information

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/client-detail/index.html b/servers/freshbooks/src/ui/react-app/client-detail/index.html index 2b98197..5e86de7 100644 --- a/servers/freshbooks/src/ui/react-app/client-detail/index.html +++ b/servers/freshbooks/src/ui/react-app/client-detail/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Client Detail - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/client-detail/main.tsx b/servers/freshbooks/src/ui/react-app/client-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/client-detail/styles.css b/servers/freshbooks/src/ui/react-app/client-detail/styles.css new file mode 100644 index 0000000..e2cc93e --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-detail/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #8b5cf6; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #8b5cf6; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #8b5cf6; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/client-detail/vite.config.ts b/servers/freshbooks/src/ui/react-app/client-detail/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/client-detail/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/credit-note-viewer/App.tsx b/servers/freshbooks/src/ui/react-app/credit-note-viewer/App.tsx new file mode 100644 index 0000000..a5f8af7 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/credit-note-viewer/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function CreditNoteViewer() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_credit_notes', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading credit note viewer...
+
+ ); + } + + return ( +
+
+

Credit Note Viewer

+

View and manage credit notes

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/credit-note-viewer/index.html b/servers/freshbooks/src/ui/react-app/credit-note-viewer/index.html new file mode 100644 index 0000000..379a5c3 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/credit-note-viewer/index.html @@ -0,0 +1,12 @@ + + + + + + Credit Note Viewer - FreshBooks MCP + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/credit-note-viewer/main.tsx b/servers/freshbooks/src/ui/react-app/credit-note-viewer/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/credit-note-viewer/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/credit-note-viewer/styles.css b/servers/freshbooks/src/ui/react-app/credit-note-viewer/styles.css new file mode 100644 index 0000000..919a8b5 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/credit-note-viewer/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #06b6d4; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #06b6d4; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #06b6d4; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/credit-note-viewer/vite.config.ts b/servers/freshbooks/src/ui/react-app/credit-note-viewer/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/credit-note-viewer/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/estimate-builder/App.tsx b/servers/freshbooks/src/ui/react-app/estimate-builder/App.tsx new file mode 100644 index 0000000..a483dba --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/estimate-builder/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function EstimateBuilder() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_estimates', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading estimate builder...
+
+ ); + } + + return ( +
+
+

Estimate Builder

+

Create and send estimates

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/estimate-builder/index.html b/servers/freshbooks/src/ui/react-app/estimate-builder/index.html index 2b98197..c5e67ae 100644 --- a/servers/freshbooks/src/ui/react-app/estimate-builder/index.html +++ b/servers/freshbooks/src/ui/react-app/estimate-builder/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Estimate Builder - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/estimate-builder/main.tsx b/servers/freshbooks/src/ui/react-app/estimate-builder/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/estimate-builder/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/estimate-builder/styles.css b/servers/freshbooks/src/ui/react-app/estimate-builder/styles.css new file mode 100644 index 0000000..0f25cef --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/estimate-builder/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #6366f1; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #6366f1; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #6366f1; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/estimate-builder/vite.config.ts b/servers/freshbooks/src/ui/react-app/estimate-builder/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/estimate-builder/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/expense-categories/App.tsx b/servers/freshbooks/src/ui/react-app/expense-categories/App.tsx new file mode 100644 index 0000000..1a2d378 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-categories/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ExpenseCategories() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_expense_categories', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading expense categories...
+
+ ); + } + + return ( +
+
+

Expense Categories

+

Manage expense categories

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/expense-categories/index.html b/servers/freshbooks/src/ui/react-app/expense-categories/index.html new file mode 100644 index 0000000..2f4a445 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-categories/index.html @@ -0,0 +1,12 @@ + + + + + + Expense Categories - FreshBooks MCP + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/expense-categories/main.tsx b/servers/freshbooks/src/ui/react-app/expense-categories/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-categories/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/expense-categories/styles.css b/servers/freshbooks/src/ui/react-app/expense-categories/styles.css new file mode 100644 index 0000000..4ed0ae7 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-categories/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #f59e0b; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #f59e0b; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #f59e0b; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/expense-categories/vite.config.ts b/servers/freshbooks/src/ui/react-app/expense-categories/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-categories/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/expense-tracker/App.tsx b/servers/freshbooks/src/ui/react-app/expense-tracker/App.tsx new file mode 100644 index 0000000..589a035 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-tracker/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ExpenseTracker() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_expenses', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading expense tracker...
+
+ ); + } + + return ( +
+
+

Expense Tracker

+

Track and categorize all expenses

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/expense-tracker/index.html b/servers/freshbooks/src/ui/react-app/expense-tracker/index.html index cdc5b97..280c9f2 100644 --- a/servers/freshbooks/src/ui/react-app/expense-tracker/index.html +++ b/servers/freshbooks/src/ui/react-app/expense-tracker/index.html @@ -1,134 +1,12 @@ - - - - Expense Tracker - FreshBooks - - - - - - -
- - + + + + Expense Tracker - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/expense-tracker/main.tsx b/servers/freshbooks/src/ui/react-app/expense-tracker/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-tracker/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/expense-tracker/styles.css b/servers/freshbooks/src/ui/react-app/expense-tracker/styles.css new file mode 100644 index 0000000..7e2e27d --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-tracker/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #ef4444; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #ef4444; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #ef4444; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/expense-tracker/vite.config.ts b/servers/freshbooks/src/ui/react-app/expense-tracker/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/expense-tracker/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/generate-apps.js b/servers/freshbooks/src/ui/react-app/generate-apps.js new file mode 100644 index 0000000..e827bb0 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/generate-apps.js @@ -0,0 +1,372 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const apps = [ + { + name: 'invoice-dashboard', + title: 'Invoice Dashboard', + desc: 'Manage and track all your invoices', + tool: 'freshbooks_list_invoices', + color: '#10b981' + }, + { + name: 'invoice-detail', + title: 'Invoice Detail', + desc: 'View detailed invoice information', + tool: 'freshbooks_get_invoice', + color: '#3b82f6' + }, + { + name: 'client-dashboard', + title: 'Client Directory', + desc: 'Manage all your clients', + tool: 'freshbooks_list_clients', + color: '#8b5cf6' + }, + { + name: 'client-detail', + title: 'Client Detail', + desc: 'View detailed client information', + tool: 'freshbooks_get_client', + color: '#8b5cf6' + }, + { + name: 'expense-tracker', + title: 'Expense Tracker', + desc: 'Track and categorize all expenses', + tool: 'freshbooks_list_expenses', + color: '#ef4444' + }, + { + name: 'expense-categories', + title: 'Expense Categories', + desc: 'Manage expense categories', + tool: 'freshbooks_list_expense_categories', + color: '#f59e0b' + }, + { + name: 'time-entries', + title: 'Time Entry Log', + desc: 'Log and track time entries', + tool: 'freshbooks_list_time_entries', + color: '#06b6d4' + }, + { + name: 'time-tracker', + title: 'Project Timer', + desc: 'Start and stop project timers', + tool: 'freshbooks_list_time_entries', + color: '#06b6d4' + }, + { + name: 'project-dashboard', + title: 'Project Overview', + desc: 'View all active projects', + tool: 'freshbooks_list_projects', + color: '#10b981' + }, + { + name: 'project-detail', + title: 'Project Detail', + desc: 'Detailed project information', + tool: 'freshbooks_get_project', + color: '#10b981' + }, + { + name: 'payment-history', + title: 'Payment History', + desc: 'Track all payments', + tool: 'freshbooks_list_payments', + color: '#10b981' + }, + { + name: 'estimate-builder', + title: 'Estimate Builder', + desc: 'Create and send estimates', + tool: 'freshbooks_list_estimates', + color: '#6366f1' + }, + { + name: 'tax-summary', + title: 'Tax Summary', + desc: 'View tax reporting and summaries', + tool: 'freshbooks_tax_summary_report', + color: '#f59e0b' + }, + { + name: 'recurring-invoices', + title: 'Recurring Templates', + desc: 'Manage recurring invoice templates', + tool: 'freshbooks_list_invoices', + color: '#8b5cf6' + }, + { + name: 'profit-loss', + title: 'Profit & Loss', + desc: 'Financial P&L statements', + tool: 'freshbooks_profit_loss_report', + color: '#10b981' + }, + { + name: 'revenue-chart', + title: 'Revenue Chart', + desc: 'Visual revenue analytics', + tool: 'freshbooks_list_invoices', + color: '#10b981' + }, + { + name: 'aging-report', + title: 'Outstanding Balance', + desc: 'Accounts receivable aging', + tool: 'freshbooks_aging_report', + color: '#ef4444' + }, + { + name: 'bill-manager', + title: 'Bill Manager', + desc: 'Manage vendor bills', + tool: 'freshbooks_list_bills', + color: '#f59e0b' + }, + { + name: 'credit-note-viewer', + title: 'Credit Note Viewer', + desc: 'View and manage credit notes', + tool: 'freshbooks_list_credit_notes', + color: '#06b6d4' + }, + { + name: 'reports-dashboard', + title: 'Report Builder', + desc: 'Build custom financial reports', + tool: 'freshbooks_profit_loss_report', + color: '#8b5cf6' + }, + { + name: 'invoice-builder', + title: 'Invoice Builder', + desc: 'Create new invoices', + tool: 'freshbooks_create_invoice', + color: '#10b981' + }, +]; + +const createApp = (app) => { + const appDir = path.join(__dirname, app.name); + + if (!fs.existsSync(appDir)) { + fs.mkdirSync(appDir, { recursive: true }); + } + + // App.tsx + const appTsx = `import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ${app.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('')}() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('${app.tool}', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading ${app.title.toLowerCase()}...
+
+ ); + } + + return ( +
+
+

${app.title}

+

${app.desc}

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} +`; + + // styles.css + const stylesCss = `* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: ${app.color}; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid ${app.color}; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: ${app.color}; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} +`; + + // main.tsx + const mainTsx = `import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); +`; + + // index.html + const indexHtml = ` + + + + + ${app.title} - FreshBooks MCP + + +
+ + + +`; + + // vite.config.ts + const viteConfig = `import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); +`; + + // Write files + fs.writeFileSync(path.join(appDir, 'App.tsx'), appTsx); + fs.writeFileSync(path.join(appDir, 'styles.css'), stylesCss); + fs.writeFileSync(path.join(appDir, 'main.tsx'), mainTsx); + fs.writeFileSync(path.join(appDir, 'index.html'), indexHtml); + fs.writeFileSync(path.join(appDir, 'vite.config.ts'), viteConfig); + + console.log(`✅ Created ${app.name}`); +}; + +console.log('🚀 Generating FreshBooks MCP Apps...\n'); +apps.forEach(createApp); +console.log(`\n✨ Generated ${apps.length} apps successfully!`); diff --git a/servers/freshbooks/src/ui/react-app/invoice-builder/App.tsx b/servers/freshbooks/src/ui/react-app/invoice-builder/App.tsx new file mode 100644 index 0000000..cb64756 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-builder/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function InvoiceBuilder() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_create_invoice', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading invoice builder...
+
+ ); + } + + return ( +
+
+

Invoice Builder

+

Create new invoices

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/invoice-builder/index.html b/servers/freshbooks/src/ui/react-app/invoice-builder/index.html index 9ff06d1..00698dd 100644 --- a/servers/freshbooks/src/ui/react-app/invoice-builder/index.html +++ b/servers/freshbooks/src/ui/react-app/invoice-builder/index.html @@ -1,139 +1,12 @@ - - - - Invoice Builder - FreshBooks - - - - - - -
- - + + + + Invoice Builder - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/invoice-builder/main.tsx b/servers/freshbooks/src/ui/react-app/invoice-builder/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-builder/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/invoice-builder/styles.css b/servers/freshbooks/src/ui/react-app/invoice-builder/styles.css new file mode 100644 index 0000000..a8c3631 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-builder/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #10b981; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #10b981; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #10b981; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/invoice-builder/vite.config.ts b/servers/freshbooks/src/ui/react-app/invoice-builder/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-builder/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/invoice-dashboard/App.tsx b/servers/freshbooks/src/ui/react-app/invoice-dashboard/App.tsx new file mode 100644 index 0000000..3a59b9b --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-dashboard/App.tsx @@ -0,0 +1,194 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface Invoice { + id: number; + number: string; + client: string; + amount: number; + status: 'paid' | 'partial' | 'overdue' | 'draft' | 'sent'; + date: string; + dueDate: string; +} + +export default function InvoiceDashboard() { + const [invoices, setInvoices] = useState([]); + const [stats, setStats] = useState({ total: 0, paid: 0, overdue: 0, draft: 0, outstanding: 0 }); + const [filter, setFilter] = useState({ status: 'all', client: '' }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + loadInvoices(); + }, []); + + const loadInvoices = async () => { + try { + // Call MCP tool to fetch invoices + const response = await (window as any).mcp?.callTool('freshbooks_list_invoices', { + page: 1, + per_page: 50 + }); + + if (response?.invoices) { + setInvoices(response.invoices); + calculateStats(response.invoices); + } else { + // Fallback to sample data + loadSampleData(); + } + } catch (error) { + console.error('Error loading invoices:', error); + loadSampleData(); + } finally { + setLoading(false); + } + }; + + const loadSampleData = () => { + const sampleInvoices: Invoice[] = [ + { id: 1, number: 'INV-001', client: 'Acme Corp', amount: 2500, status: 'paid', date: '2024-01-15', dueDate: '2024-02-15' }, + { id: 2, number: 'INV-002', client: 'Tech Solutions', amount: 4200, status: 'overdue', date: '2024-01-10', dueDate: '2024-02-10' }, + { id: 3, number: 'INV-003', client: 'Design Co', amount: 1800, status: 'partial', date: '2024-01-20', dueDate: '2024-02-20' }, + { id: 4, number: 'INV-004', client: 'Marketing Inc', amount: 3600, status: 'draft', date: '2024-01-25', dueDate: '2024-02-25' }, + { id: 5, number: 'INV-005', client: 'Startup Labs', amount: 5200, status: 'sent', date: '2024-02-01', dueDate: '2024-03-01' }, + { id: 6, number: 'INV-006', client: 'Enterprise Co', amount: 8900, status: 'paid', date: '2024-02-05', dueDate: '2024-03-05' }, + ]; + setInvoices(sampleInvoices); + calculateStats(sampleInvoices); + }; + + const calculateStats = (invoices: Invoice[]) => { + const totalRevenue = invoices.reduce((sum, inv) => sum + inv.amount, 0); + const paidAmount = invoices.filter(i => i.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0); + const overdueAmount = invoices.filter(i => i.status === 'overdue').reduce((sum, inv) => sum + inv.amount, 0); + const draftCount = invoices.filter(i => i.status === 'draft').length; + const outstandingAmount = invoices.filter(i => i.status === 'sent' || i.status === 'partial').reduce((sum, inv) => sum + inv.amount, 0); + + setStats({ + total: totalRevenue, + paid: paidAmount, + overdue: overdueAmount, + draft: draftCount, + outstanding: outstandingAmount + }); + }; + + const filteredInvoices = invoices.filter(inv => { + if (filter.status !== 'all' && inv.status !== filter.status) return false; + if (filter.client && !inv.client.toLowerCase().includes(filter.client.toLowerCase())) return false; + return true; + }); + + const handleViewInvoice = async (invoiceId: number) => { + try { + await (window as any).mcp?.callTool('freshbooks_get_invoice', { invoice_id: invoiceId }); + } catch (error) { + console.error('Error viewing invoice:', error); + } + }; + + if (loading) { + return ( +
+
Loading invoices...
+
+ ); + } + + return ( +
+
+

Invoice Dashboard

+

Manage and track all your invoices

+
+ +
+
+

Total Revenue

+
${stats.total.toLocaleString()}
+
All invoices
+
+
+

Paid

+
${stats.paid.toLocaleString()}
+
Received
+
+
+

Outstanding

+
${stats.outstanding.toLocaleString()}
+
Awaiting payment
+
+
+

Overdue

+
${stats.overdue.toLocaleString()}
+
Past due
+
+
+

Drafts

+
{stats.draft}
+
Pending
+
+
+ +
+
+ + +
+
+ + setFilter({ ...filter, client: e.target.value })} /> +
+
+ +
+
+ +
+ + + + + + + + + + + + + + {filteredInvoices.length === 0 ? ( + + + + ) : ( + filteredInvoices.map(invoice => ( + + + + + + + + + + )) + )} + +
Invoice #ClientDateDue DateAmountStatusActions
+ No invoices found +
{invoice.number}{invoice.client}{invoice.date}{invoice.dueDate}${invoice.amount.toLocaleString()}{invoice.status} + +
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html b/servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html index f1a3df5..85314bb 100644 --- a/servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html +++ b/servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html @@ -1,154 +1,12 @@ - - - - Invoice Dashboard - FreshBooks - - - - - - -
- - + + + + Invoice Dashboard - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/invoice-dashboard/main.tsx b/servers/freshbooks/src/ui/react-app/invoice-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/invoice-dashboard/styles.css b/servers/freshbooks/src/ui/react-app/invoice-dashboard/styles.css new file mode 100644 index 0000000..a8c3631 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-dashboard/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #10b981; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #10b981; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #10b981; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/invoice-dashboard/vite.config.ts b/servers/freshbooks/src/ui/react-app/invoice-dashboard/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-dashboard/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/invoice-detail/App.tsx b/servers/freshbooks/src/ui/react-app/invoice-detail/App.tsx new file mode 100644 index 0000000..15d0ada --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-detail/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function InvoiceDetail() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_get_invoice', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading invoice detail...
+
+ ); + } + + return ( +
+
+

Invoice Detail

+

View detailed invoice information

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/invoice-detail/index.html b/servers/freshbooks/src/ui/react-app/invoice-detail/index.html index c5e069e..fa69775 100644 --- a/servers/freshbooks/src/ui/react-app/invoice-detail/index.html +++ b/servers/freshbooks/src/ui/react-app/invoice-detail/index.html @@ -1,116 +1,12 @@ - - - - Invoice Detail - FreshBooks - - - - - - -
- - + + + + Invoice Detail - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/invoice-detail/main.tsx b/servers/freshbooks/src/ui/react-app/invoice-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/invoice-detail/styles.css b/servers/freshbooks/src/ui/react-app/invoice-detail/styles.css new file mode 100644 index 0000000..1d1824b --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-detail/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #3b82f6; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #3b82f6; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #3b82f6; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/invoice-detail/vite.config.ts b/servers/freshbooks/src/ui/react-app/invoice-detail/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/invoice-detail/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/package.json b/servers/freshbooks/src/ui/react-app/package.json new file mode 100644 index 0000000..94fddea --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/package.json @@ -0,0 +1,22 @@ +{ + "name": "freshbooks-mcp-apps", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "node build-all.js", + "dev": "vite" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^0.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.2", + "vite": "^6.0.7" + } +} diff --git a/servers/freshbooks/src/ui/react-app/payment-history/App.tsx b/servers/freshbooks/src/ui/react-app/payment-history/App.tsx new file mode 100644 index 0000000..25001ed --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/payment-history/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function PaymentHistory() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_payments', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading payment history...
+
+ ); + } + + return ( +
+
+

Payment History

+

Track all payments

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/payment-history/index.html b/servers/freshbooks/src/ui/react-app/payment-history/index.html index 7f5741b..0f4e509 100644 --- a/servers/freshbooks/src/ui/react-app/payment-history/index.html +++ b/servers/freshbooks/src/ui/react-app/payment-history/index.html @@ -1,86 +1,12 @@ - - - - Payment History - FreshBooks - - - - - - -
- - + + + + Payment History - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/payment-history/main.tsx b/servers/freshbooks/src/ui/react-app/payment-history/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/payment-history/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/payment-history/styles.css b/servers/freshbooks/src/ui/react-app/payment-history/styles.css new file mode 100644 index 0000000..a8c3631 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/payment-history/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #10b981; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #10b981; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #10b981; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/payment-history/vite.config.ts b/servers/freshbooks/src/ui/react-app/payment-history/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/payment-history/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/profit-loss/App.tsx b/servers/freshbooks/src/ui/react-app/profit-loss/App.tsx new file mode 100644 index 0000000..269ee12 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/profit-loss/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ProfitLoss() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_profit_loss_report', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading profit & loss...
+
+ ); + } + + return ( +
+
+

Profit & Loss

+

Financial P&L statements

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/profit-loss/index.html b/servers/freshbooks/src/ui/react-app/profit-loss/index.html index 2b98197..b464515 100644 --- a/servers/freshbooks/src/ui/react-app/profit-loss/index.html +++ b/servers/freshbooks/src/ui/react-app/profit-loss/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Profit & Loss - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/profit-loss/main.tsx b/servers/freshbooks/src/ui/react-app/profit-loss/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/profit-loss/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/profit-loss/styles.css b/servers/freshbooks/src/ui/react-app/profit-loss/styles.css new file mode 100644 index 0000000..a8c3631 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/profit-loss/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #10b981; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #10b981; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #10b981; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/profit-loss/vite.config.ts b/servers/freshbooks/src/ui/react-app/profit-loss/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/profit-loss/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/project-dashboard/App.tsx b/servers/freshbooks/src/ui/react-app/project-dashboard/App.tsx new file mode 100644 index 0000000..69acc8c --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-dashboard/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ProjectDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_projects', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading project overview...
+
+ ); + } + + return ( +
+
+

Project Overview

+

View all active projects

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/project-dashboard/index.html b/servers/freshbooks/src/ui/react-app/project-dashboard/index.html index 278b451..d7397ca 100644 --- a/servers/freshbooks/src/ui/react-app/project-dashboard/index.html +++ b/servers/freshbooks/src/ui/react-app/project-dashboard/index.html @@ -1,92 +1,12 @@ - - - - Projects - FreshBooks - - - - - - -
- - + + + + Project Overview - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/project-dashboard/main.tsx b/servers/freshbooks/src/ui/react-app/project-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/project-dashboard/styles.css b/servers/freshbooks/src/ui/react-app/project-dashboard/styles.css new file mode 100644 index 0000000..a8c3631 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-dashboard/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #10b981; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #10b981; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #10b981; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/project-dashboard/vite.config.ts b/servers/freshbooks/src/ui/react-app/project-dashboard/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-dashboard/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/project-detail/App.tsx b/servers/freshbooks/src/ui/react-app/project-detail/App.tsx new file mode 100644 index 0000000..4ad2694 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-detail/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ProjectDetail() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_get_project', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading project detail...
+
+ ); + } + + return ( +
+
+

Project Detail

+

Detailed project information

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/project-detail/index.html b/servers/freshbooks/src/ui/react-app/project-detail/index.html index 2b98197..7f1d9aa 100644 --- a/servers/freshbooks/src/ui/react-app/project-detail/index.html +++ b/servers/freshbooks/src/ui/react-app/project-detail/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Project Detail - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/project-detail/main.tsx b/servers/freshbooks/src/ui/react-app/project-detail/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-detail/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/project-detail/styles.css b/servers/freshbooks/src/ui/react-app/project-detail/styles.css new file mode 100644 index 0000000..a8c3631 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-detail/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #10b981; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #10b981; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #10b981; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/project-detail/vite.config.ts b/servers/freshbooks/src/ui/react-app/project-detail/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/project-detail/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/recurring-invoices/App.tsx b/servers/freshbooks/src/ui/react-app/recurring-invoices/App.tsx new file mode 100644 index 0000000..8c80fbe --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/recurring-invoices/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function RecurringInvoices() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_invoices', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading recurring templates...
+
+ ); + } + + return ( +
+
+

Recurring Templates

+

Manage recurring invoice templates

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/recurring-invoices/index.html b/servers/freshbooks/src/ui/react-app/recurring-invoices/index.html index 2b98197..a2cffc9 100644 --- a/servers/freshbooks/src/ui/react-app/recurring-invoices/index.html +++ b/servers/freshbooks/src/ui/react-app/recurring-invoices/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Recurring Templates - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/recurring-invoices/main.tsx b/servers/freshbooks/src/ui/react-app/recurring-invoices/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/recurring-invoices/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/recurring-invoices/styles.css b/servers/freshbooks/src/ui/react-app/recurring-invoices/styles.css new file mode 100644 index 0000000..e2cc93e --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/recurring-invoices/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #8b5cf6; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #8b5cf6; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #8b5cf6; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/recurring-invoices/vite.config.ts b/servers/freshbooks/src/ui/react-app/recurring-invoices/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/recurring-invoices/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/reports-dashboard/App.tsx b/servers/freshbooks/src/ui/react-app/reports-dashboard/App.tsx new file mode 100644 index 0000000..18b57fe --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/reports-dashboard/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ReportsDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_profit_loss_report', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading report builder...
+
+ ); + } + + return ( +
+
+

Report Builder

+

Build custom financial reports

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/reports-dashboard/index.html b/servers/freshbooks/src/ui/react-app/reports-dashboard/index.html index 93650be..649b392 100644 --- a/servers/freshbooks/src/ui/react-app/reports-dashboard/index.html +++ b/servers/freshbooks/src/ui/react-app/reports-dashboard/index.html @@ -1,57 +1,12 @@ - - - - Reports - FreshBooks - - - - - - -
- - + + + + Report Builder - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/reports-dashboard/main.tsx b/servers/freshbooks/src/ui/react-app/reports-dashboard/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/reports-dashboard/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/reports-dashboard/styles.css b/servers/freshbooks/src/ui/react-app/reports-dashboard/styles.css new file mode 100644 index 0000000..e2cc93e --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/reports-dashboard/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #8b5cf6; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #8b5cf6; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #8b5cf6; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/reports-dashboard/vite.config.ts b/servers/freshbooks/src/ui/react-app/reports-dashboard/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/reports-dashboard/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/revenue-chart/App.tsx b/servers/freshbooks/src/ui/react-app/revenue-chart/App.tsx new file mode 100644 index 0000000..fdcd545 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/revenue-chart/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function RevenueChart() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_invoices', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading revenue chart...
+
+ ); + } + + return ( +
+
+

Revenue Chart

+

Visual revenue analytics

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/revenue-chart/index.html b/servers/freshbooks/src/ui/react-app/revenue-chart/index.html index 2b98197..26a37f2 100644 --- a/servers/freshbooks/src/ui/react-app/revenue-chart/index.html +++ b/servers/freshbooks/src/ui/react-app/revenue-chart/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Revenue Chart - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/revenue-chart/main.tsx b/servers/freshbooks/src/ui/react-app/revenue-chart/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/revenue-chart/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/revenue-chart/styles.css b/servers/freshbooks/src/ui/react-app/revenue-chart/styles.css new file mode 100644 index 0000000..a8c3631 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/revenue-chart/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #10b981; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #10b981; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #10b981; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/revenue-chart/vite.config.ts b/servers/freshbooks/src/ui/react-app/revenue-chart/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/revenue-chart/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/aging-report/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/aging-report/App.tsx new file mode 100644 index 0000000..d638772 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/aging-report/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_aging_report'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Aging Report

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/aging-report/index.html b/servers/freshbooks/src/ui/react-app/src/apps/aging-report/index.html new file mode 100644 index 0000000..476a716 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/aging-report/index.html @@ -0,0 +1,12 @@ + + + + + + Aging Report - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/aging-report/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/aging-report/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/aging-report/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/App.tsx new file mode 100644 index 0000000..084db23 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_bills'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Bill Manager

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/index.html b/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/index.html new file mode 100644 index 0000000..93ab66e --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Bill Manager - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/bill-manager/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/App.tsx new file mode 100644 index 0000000..8e4702d --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_clients'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Client Dashboard

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/index.html b/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/index.html new file mode 100644 index 0000000..328b6ea --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Client Dashboard - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/client-dashboard/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/client-detail/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/client-detail/App.tsx new file mode 100644 index 0000000..197f48a --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/client-detail/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_get_client'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Client Detail

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/client-detail/index.html b/servers/freshbooks/src/ui/react-app/src/apps/client-detail/index.html new file mode 100644 index 0000000..176148a --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/client-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Client Detail - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/client-detail/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/client-detail/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/client-detail/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/App.tsx new file mode 100644 index 0000000..a05fa1a --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/App.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data: invoices, loading: invoicesLoading } = useFreshBooks('freshbooks_list_invoices', { per_page: 5 }); + const { data: clients, loading: clientsLoading } = useFreshBooks('freshbooks_list_clients', { per_page: 5 }); + const { data: expenses, loading: expensesLoading } = useFreshBooks('freshbooks_list_expenses', { per_page: 5 }); + + if (invoicesLoading || clientsLoading || expensesLoading) { + return ; + } + + const totalInvoices = invoices?.total || 0; + const totalClients = clients?.total || 0; + const recentInvoices = invoices?.results || []; + const recentClients = clients?.results || []; + + return ( +
+
+

FreshBooks Dashboard

+

Overview of your business

+
+ +
+ +
+
{totalInvoices}
+
Total Invoices
+
+
+ +
+
{totalClients}
+
Total Clients
+
+
+ +
+
{expenses?.total || 0}
+
Total Expenses
+
+
+
+ +
+ + +

Recent Invoices

+
+ + {recentInvoices.length === 0 ? ( +

No invoices found

+ ) : ( + + + + + + + + + + + {recentInvoices.map((inv: any) => ( + + + + + + + ))} + +
Invoice #ClientAmountStatus
{inv.invoice_number}{inv.organization || `${inv.fname} ${inv.lname}`}${inv.amount.amount} {inv.amount.code} + + {inv.v3_status} + +
+ )} +
+
+ + + +

Recent Clients

+
+ + {recentClients.length === 0 ? ( +

No clients found

+ ) : ( + + + + + + + + + + {recentClients.map((client: any) => ( + + + + + + ))} + +
NameOrganizationEmail
{client.fname} {client.lname}{client.organization}{client.email}
+ )} +
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/index.html b/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/index.html new file mode 100644 index 0000000..9e31c8c --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/index.html @@ -0,0 +1,12 @@ + + + + + + Dashboard Overview - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/dashboard-overview/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/App.tsx new file mode 100644 index 0000000..90ac871 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_estimates'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Estimate Builder

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/index.html b/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/index.html new file mode 100644 index 0000000..d8549dc --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/index.html @@ -0,0 +1,12 @@ + + + + + + Estimate Builder - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/estimate-builder/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/expense-report/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/expense-report/App.tsx new file mode 100644 index 0000000..c996318 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/expense-report/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_expense_report'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Expense Report

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/expense-report/index.html b/servers/freshbooks/src/ui/react-app/src/apps/expense-report/index.html new file mode 100644 index 0000000..6295f06 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/expense-report/index.html @@ -0,0 +1,12 @@ + + + + + + Expense Report - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/expense-report/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/expense-report/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/expense-report/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/App.tsx new file mode 100644 index 0000000..232d0cb --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_expenses'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Expense Tracker

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/index.html b/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/index.html new file mode 100644 index 0000000..f6448b3 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/index.html @@ -0,0 +1,12 @@ + + + + + + Expense Tracker - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/expense-tracker/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/App.tsx new file mode 100644 index 0000000..6e882f9 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_create_invoice'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Invoice Creator

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/index.html b/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/index.html new file mode 100644 index 0000000..45edd9d --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/index.html @@ -0,0 +1,12 @@ + + + + + + Invoice Creator - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-creator/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/App.tsx new file mode 100644 index 0000000..2c6fae0 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_invoices'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Invoice Dashboard

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/index.html b/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/index.html new file mode 100644 index 0000000..a577d2d --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Invoice Dashboard - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-dashboard/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/App.tsx new file mode 100644 index 0000000..0bdfa9a --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_get_invoice'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Invoice Detail

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/index.html b/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/index.html new file mode 100644 index 0000000..620f676 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Invoice Detail - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/invoice-detail/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/App.tsx new file mode 100644 index 0000000..f7b447b --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_items'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Item Catalog

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/index.html b/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/index.html new file mode 100644 index 0000000..a2ebccc --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/index.html @@ -0,0 +1,12 @@ + + + + + + Item Catalog - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/item-catalog/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/App.tsx new file mode 100644 index 0000000..80d18ce --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_payments'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Payment Dashboard

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/index.html b/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/index.html new file mode 100644 index 0000000..39b723e --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Payment Dashboard - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/payment-dashboard/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/App.tsx new file mode 100644 index 0000000..1cfb4a3 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_profit_loss_report'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Profit & Loss Report

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/index.html b/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/index.html new file mode 100644 index 0000000..ffbe65c --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/index.html @@ -0,0 +1,12 @@ + + + + + + Profit & Loss Report - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/profit-loss-report/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/App.tsx new file mode 100644 index 0000000..8134e51 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_projects'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Project Dashboard

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/index.html b/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/index.html new file mode 100644 index 0000000..f9e131c --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Project Dashboard - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/project-dashboard/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/project-detail/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/project-detail/App.tsx new file mode 100644 index 0000000..547fd51 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/project-detail/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_get_project'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Project Detail

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/project-detail/index.html b/servers/freshbooks/src/ui/react-app/src/apps/project-detail/index.html new file mode 100644 index 0000000..78c92de --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/project-detail/index.html @@ -0,0 +1,12 @@ + + + + + + Project Detail - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/project-detail/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/project-detail/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/project-detail/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/App.tsx new file mode 100644 index 0000000..c382585 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_staff'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Staff Directory

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/index.html b/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/index.html new file mode 100644 index 0000000..f8059b3 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/index.html @@ -0,0 +1,12 @@ + + + + + + Staff Directory - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/staff-directory/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/App.tsx new file mode 100644 index 0000000..60c9a24 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_tax_summary_report'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Tax Summary

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/index.html b/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/index.html new file mode 100644 index 0000000..f2060ed --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/index.html @@ -0,0 +1,12 @@ + + + + + + Tax Summary - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/tax-summary/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/time-report/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/time-report/App.tsx new file mode 100644 index 0000000..ae37e75 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/time-report/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_time_entries'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Time Report

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/time-report/index.html b/servers/freshbooks/src/ui/react-app/src/apps/time-report/index.html new file mode 100644 index 0000000..a6f7f51 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/time-report/index.html @@ -0,0 +1,12 @@ + + + + + + Time Report - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/time-report/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/time-report/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/time-report/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/App.tsx b/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/App.tsx new file mode 100644 index 0000000..a36f686 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useFreshBooks } from '../../hooks/useFreshBooks'; +import { Card, CardHeader, CardContent } from '../../components/Card'; +import { Loading } from '../../components/Loading'; + +export function App() { + const { data, loading, error } = useFreshBooks('freshbooks_list_time_entries'); + + if (loading) return ; + if (error) return
Error: {error}
; + + return ( +
+
+

Time Tracker

+
+ + +
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/index.html b/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/index.html new file mode 100644 index 0000000..be9137f --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/index.html @@ -0,0 +1,12 @@ + + + + + + Time Tracker - FreshBooks + + +
+ + + diff --git a/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/main.tsx b/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/main.tsx new file mode 100644 index 0000000..29038b4 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/apps/time-tracker/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { App } from './App'; +import '../../styles/global.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/src/components/Card.tsx b/servers/freshbooks/src/ui/react-app/src/components/Card.tsx new file mode 100644 index 0000000..e2c91e2 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/components/Card.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +interface CardProps { + children: React.ReactNode; + className?: string; +} + +export function Card({ children, className = '' }: CardProps) { + return ( +
+ {children} +
+ ); +} + +interface CardHeaderProps { + children: React.ReactNode; +} + +export function CardHeader({ children }: CardHeaderProps) { + return
{children}
; +} + +interface CardContentProps { + children: React.ReactNode; +} + +export function CardContent({ children }: CardContentProps) { + return
{children}
; +} diff --git a/servers/freshbooks/src/ui/react-app/src/components/Loading.tsx b/servers/freshbooks/src/ui/react-app/src/components/Loading.tsx new file mode 100644 index 0000000..40f64c6 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/components/Loading.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +export function Loading() { + return ( +
+
+

Loading...

+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/src/hooks/useCallTool.ts b/servers/freshbooks/src/ui/react-app/src/hooks/useCallTool.ts new file mode 100644 index 0000000..b468c7d --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/hooks/useCallTool.ts @@ -0,0 +1,25 @@ +import { useState, useCallback } from 'react'; + +export function useCallTool() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const callTool = useCallback(async (toolName: string, args: any = {}) => { + setLoading(true); + setError(null); + + try { + // In MCP apps, we use the message passing API + const result = await (window as any).mcp?.callTool(toolName, args); + setLoading(false); + return result; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err); + setError(errorMessage); + setLoading(false); + throw err; + } + }, []); + + return { callTool, loading, error }; +} diff --git a/servers/freshbooks/src/ui/react-app/src/hooks/useFreshBooks.ts b/servers/freshbooks/src/ui/react-app/src/hooks/useFreshBooks.ts new file mode 100644 index 0000000..2cd7109 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/hooks/useFreshBooks.ts @@ -0,0 +1,41 @@ +import { useState, useEffect } from 'react'; +import { useCallTool } from './useCallTool'; + +export function useFreshBooks(toolName: string, args?: any) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { callTool } = useCallTool(); + + useEffect(() => { + let mounted = true; + + async function fetchData() { + try { + setLoading(true); + const result = await callTool(toolName, args); + + if (mounted) { + const content = result?.content?.[0]?.text; + if (content) { + setData(JSON.parse(content)); + } + setLoading(false); + } + } catch (err) { + if (mounted) { + setError(err instanceof Error ? err.message : String(err)); + setLoading(false); + } + } + } + + fetchData(); + + return () => { + mounted = false; + }; + }, [toolName, JSON.stringify(args)]); + + return { data, loading, error }; +} diff --git a/servers/freshbooks/src/ui/react-app/src/styles/global.css b/servers/freshbooks/src/ui/react-app/src/styles/global.css new file mode 100644 index 0000000..de3f4d6 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/src/styles/global.css @@ -0,0 +1,245 @@ +:root { + --primary-color: #0075dd; + --secondary-color: #4caf50; + --danger-color: #f44336; + --warning-color: #ff9800; + --text-color: #333; + --border-color: #ddd; + --bg-color: #f5f5f5; + --card-bg: #fff; + --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-family); + color: var(--text-color); + background: var(--bg-color); + line-height: 1.6; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +.header { + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 2px solid var(--border-color); +} + +h1 { + font-size: 28px; + font-weight: 600; + color: var(--text-color); + margin-bottom: 8px; +} + +h2 { + font-size: 22px; + font-weight: 600; + margin-bottom: 16px; +} + +h3 { + font-size: 18px; + font-weight: 600; + margin-bottom: 12px; +} + +.card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.card-header { + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.card-content { + line-height: 1.8; +} + +.grid { + display: grid; + gap: 20px; +} + +.grid-2 { + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +.grid-3 { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} + +.btn { + display: inline-block; + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; +} + +.btn-primary { + background: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background: #0056b3; +} + +.btn-secondary { + background: var(--secondary-color); + color: white; +} + +.btn-danger { + background: var(--danger-color); + color: white; +} + +.badge { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; +} + +.badge-success { + background: #d4edda; + color: #155724; +} + +.badge-warning { + background: #fff3cd; + color: #856404; +} + +.badge-danger { + background: #f8d7da; + color: #721c24; +} + +.badge-info { + background: #d1ecf1; + color: #0c5460; +} + +.table { + width: 100%; + border-collapse: collapse; + margin-top: 12px; +} + +.table th, +.table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid var(--border-color); +} + +.table th { + background: var(--bg-color); + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + color: #666; +} + +.table tr:hover { + background: #fafafa; +} + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--border-color); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.error { + background: #fee; + border: 1px solid #fcc; + color: #c00; + padding: 16px; + border-radius: 6px; + margin: 20px 0; +} + +.form-group { + margin-bottom: 16px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + font-weight: 500; + font-size: 14px; +} + +.form-control { + width: 100%; + padding: 10px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 14px; +} + +.form-control:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(0,117,221,0.1); +} + +.stat-box { + text-align: center; + padding: 24px; +} + +.stat-value { + font-size: 32px; + font-weight: 700; + color: var(--primary-color); + margin-bottom: 8px; +} + +.stat-label { + font-size: 14px; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; +} diff --git a/servers/freshbooks/src/ui/react-app/tax-summary/App.tsx b/servers/freshbooks/src/ui/react-app/tax-summary/App.tsx new file mode 100644 index 0000000..051810c --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/tax-summary/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function TaxSummary() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_tax_summary_report', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading tax summary...
+
+ ); + } + + return ( +
+
+

Tax Summary

+

View tax reporting and summaries

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/tax-summary/index.html b/servers/freshbooks/src/ui/react-app/tax-summary/index.html index 2b98197..59019fe 100644 --- a/servers/freshbooks/src/ui/react-app/tax-summary/index.html +++ b/servers/freshbooks/src/ui/react-app/tax-summary/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Tax Summary - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/tax-summary/main.tsx b/servers/freshbooks/src/ui/react-app/tax-summary/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/tax-summary/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/tax-summary/styles.css b/servers/freshbooks/src/ui/react-app/tax-summary/styles.css new file mode 100644 index 0000000..4ed0ae7 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/tax-summary/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #f59e0b; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #f59e0b; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #f59e0b; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/tax-summary/vite.config.ts b/servers/freshbooks/src/ui/react-app/tax-summary/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/tax-summary/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/time-entries/App.tsx b/servers/freshbooks/src/ui/react-app/time-entries/App.tsx new file mode 100644 index 0000000..b5e2b4d --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-entries/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function TimeEntries() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_time_entries', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading time entry log...
+
+ ); + } + + return ( +
+
+

Time Entry Log

+

Log and track time entries

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/time-entries/index.html b/servers/freshbooks/src/ui/react-app/time-entries/index.html index 2b98197..12bea03 100644 --- a/servers/freshbooks/src/ui/react-app/time-entries/index.html +++ b/servers/freshbooks/src/ui/react-app/time-entries/index.html @@ -1,34 +1,12 @@ - - - - TITLE - FreshBooks - - - - - - -
- - + + + + Time Entry Log - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/time-entries/main.tsx b/servers/freshbooks/src/ui/react-app/time-entries/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-entries/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/time-entries/styles.css b/servers/freshbooks/src/ui/react-app/time-entries/styles.css new file mode 100644 index 0000000..919a8b5 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-entries/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #06b6d4; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #06b6d4; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #06b6d4; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/time-entries/vite.config.ts b/servers/freshbooks/src/ui/react-app/time-entries/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-entries/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/time-tracker/App.tsx b/servers/freshbooks/src/ui/react-app/time-tracker/App.tsx new file mode 100644 index 0000000..b3faeb9 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-tracker/App.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function TimeTracker() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // Call MCP tool + const response = await (window as any).mcp?.callTool('freshbooks_list_time_entries', {}); + + if (response) { + setData(response); + } else { + setData(getSampleData()); + } + } catch (err) { + console.error('Error loading data:', err); + setError(err instanceof Error ? err.message : 'Failed to load data'); + setData(getSampleData()); + } finally { + setLoading(false); + } + }; + + const getSampleData = () => { + return { message: 'Sample data - MCP tools not available', items: [] }; + }; + + if (loading) { + return ( +
+
Loading project timer...
+
+ ); + } + + return ( +
+
+

Project Timer

+

Start and stop project timers

+
+ +
+ {error &&
{error}
} + +
+

Data

+
{JSON.stringify(data, null, 2)}
+
+
+
+ ); +} diff --git a/servers/freshbooks/src/ui/react-app/time-tracker/index.html b/servers/freshbooks/src/ui/react-app/time-tracker/index.html index 3906148..0210605 100644 --- a/servers/freshbooks/src/ui/react-app/time-tracker/index.html +++ b/servers/freshbooks/src/ui/react-app/time-tracker/index.html @@ -1,122 +1,12 @@ - - - - Time Tracker - FreshBooks - - - - - - -
- - + + + + Project Timer - FreshBooks MCP + + +
+ + diff --git a/servers/freshbooks/src/ui/react-app/time-tracker/main.tsx b/servers/freshbooks/src/ui/react-app/time-tracker/main.tsx new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-tracker/main.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/freshbooks/src/ui/react-app/time-tracker/styles.css b/servers/freshbooks/src/ui/react-app/time-tracker/styles.css new file mode 100644 index 0000000..919a8b5 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-tracker/styles.css @@ -0,0 +1,87 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #111827; + color: #e2e8f0; +} + +.container { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + color: #06b6d4; +} + +.subtitle { + color: #94a3b8; +} + +.loading { + text-align: center; + padding: 4rem; + font-size: 1.25rem; + color: #94a3b8; +} + +.error { + background: #7f1d1d; + color: #fca5a5; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.card { + background: #1f2937; + border-radius: 8px; + padding: 1.5rem; + border-left: 4px solid #06b6d4; +} + +.card h2 { + color: #e2e8f0; + margin-bottom: 1rem; + font-size: 1.25rem; +} + +.data-preview { + background: #111827; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: 0.875rem; + color: #94a3b8; + max-height: 600px; + overflow-y: auto; +} + +.btn { + background: #06b6d4; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; +} + +.btn:hover { + opacity: 0.9; +} diff --git a/servers/freshbooks/src/ui/react-app/time-tracker/vite.config.ts b/servers/freshbooks/src/ui/react-app/time-tracker/vite.config.ts new file mode 100644 index 0000000..2d2b794 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/time-tracker/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + emptyOutDir: true, + }, +}); diff --git a/servers/freshbooks/src/ui/react-app/tsconfig.json b/servers/freshbooks/src/ui/react-app/tsconfig.json new file mode 100644 index 0000000..3934b8f --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/servers/freshbooks/src/ui/react-app/tsconfig.node.json b/servers/freshbooks/src/ui/react-app/tsconfig.node.json new file mode 100644 index 0000000..1b669b5 --- /dev/null +++ b/servers/freshbooks/src/ui/react-app/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "build-all.js"] +} diff --git a/servers/freshbooks/tsconfig.json b/servers/freshbooks/tsconfig.json index 78fd416..e56cf68 100644 --- a/servers/freshbooks/tsconfig.json +++ b/servers/freshbooks/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "ES2022", - "moduleResolution": "node", + "module": "Node16", + "moduleResolution": "Node16", "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", @@ -13,9 +13,8 @@ "resolveJsonModule": true, "declaration": true, "declarationMap": true, - "sourceMap": true, - "types": ["node"] + "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "tests", "src/ui"] } diff --git a/servers/wrike/tsconfig.json b/servers/wrike/tsconfig.json index 89bba95..a527823 100644 --- a/servers/wrike/tsconfig.json +++ b/servers/wrike/tsconfig.json @@ -6,7 +6,7 @@ "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", - "strict": false, + "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true,