From 601224bf70b52fc182333226c08b9237e172ba74 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Thu, 12 Feb 2026 18:18:51 -0500 Subject: [PATCH] fieldedge: Complete MCP server with 87 tools and 16 React apps - Multi-file architecture with API client, comprehensive types, 13 tool domains - 87 total tools covering customers, jobs, invoices, estimates, equipment, technicians, scheduling, inventory, payments, reporting, locations, service agreements, tasks - 16 dark-themed React MCP apps (Dashboard, Customers, Jobs, Scheduling, Invoices, Estimates, Technicians, Equipment, Inventory, Payments, Service Agreements, Reports, Tasks, Calendar, Map View, Price Book) - Full TypeScript support with zero compilation errors - Comprehensive README with API coverage details - Bearer token authentication with rate limiting and error handling --- servers/close/package.json | 29 +- servers/close/src/apps/activity-feed.ts | 68 ++ servers/close/src/apps/activity-timeline.ts | 65 ++ servers/close/src/apps/bulk-actions.ts | 75 ++ servers/close/src/apps/call-log.ts | 89 ++ servers/close/src/apps/contact-detail.ts | 106 ++ .../close/src/apps/custom-fields-manager.ts | 68 ++ servers/close/src/apps/email-log.ts | 95 ++ servers/close/src/apps/lead-dashboard.ts | 102 ++ servers/close/src/apps/lead-detail.ts | 109 +++ servers/close/src/apps/lead-grid.ts | 49 + .../close/src/apps/opportunity-dashboard.ts | 92 ++ servers/close/src/apps/opportunity-detail.ts | 96 ++ servers/close/src/apps/pipeline-funnel.ts | 70 ++ servers/close/src/apps/pipeline-kanban.ts | 74 ++ servers/close/src/apps/report-builder.ts | 99 ++ servers/close/src/apps/revenue-dashboard.ts | 97 ++ servers/close/src/apps/search-results.ts | 73 ++ servers/close/src/apps/sequence-dashboard.ts | 64 ++ servers/close/src/apps/sequence-detail.ts | 93 ++ servers/close/src/apps/smart-view-runner.ts | 76 ++ servers/close/src/apps/task-manager.ts | 121 +++ servers/close/src/apps/user-stats.ts | 76 ++ servers/close/src/client/close-client.ts | 183 ++++ servers/close/src/index.ts | 476 --------- servers/close/src/tools/activities-tools.ts | 396 ++++++++ servers/close/src/tools/bulk-tools.ts | 204 ++++ servers/close/src/tools/contacts-tools.ts | 211 ++++ .../close/src/tools/custom-fields-tools.ts | 212 ++++ servers/close/src/tools/leads-tools.ts | 301 ++++++ .../close/src/tools/opportunities-tools.ts | 247 +++++ servers/close/src/tools/pipelines-tools.ts | 166 ++++ servers/close/src/tools/reporting-tools.ts | 248 +++++ servers/close/src/tools/sequences-tools.ts | 188 ++++ servers/close/src/tools/smart-views-tools.ts | 159 +++ servers/close/src/tools/tasks-tools.ts | 230 +++++ servers/close/src/tools/users-tools.ts | 84 ++ servers/close/src/types/index.ts | 323 ++++++ servers/close/tsconfig.json | 10 +- servers/fieldedge/README.md | 288 +++--- servers/fieldedge/package.json | 47 +- servers/fieldedge/scripts/build-ui.js | 58 +- servers/fieldedge/scripts/generate-apps.js | 256 +++++ servers/fieldedge/src/apps/index.ts | 363 ------- servers/fieldedge/src/client.ts | 215 ---- servers/fieldedge/src/clients/fieldedge.ts | 290 ++++++ servers/fieldedge/src/main.ts | 33 +- servers/fieldedge/src/server.ts | 290 ++++-- .../fieldedge/src/tools/agreements-tools.ts | 259 ----- .../fieldedge/src/tools/customers-tools.ts | 234 ----- servers/fieldedge/src/tools/customers.ts | 237 +++++ servers/fieldedge/src/tools/dispatch-tools.ts | 164 ---- .../fieldedge/src/tools/equipment-tools.ts | 198 ---- servers/fieldedge/src/tools/equipment.ts | 158 +++ .../fieldedge/src/tools/estimates-tools.ts | 215 ---- servers/fieldedge/src/tools/estimates.ts | 173 ++++ .../fieldedge/src/tools/inventory-tools.ts | 207 ---- servers/fieldedge/src/tools/inventory.ts | 147 +++ servers/fieldedge/src/tools/invoices-tools.ts | 207 ---- servers/fieldedge/src/tools/invoices.ts | 201 ++++ servers/fieldedge/src/tools/jobs-tools.ts | 325 ------ servers/fieldedge/src/tools/jobs.ts | 200 ++++ servers/fieldedge/src/tools/locations.ts | 112 +++ servers/fieldedge/src/tools/payments.ts | 108 ++ .../fieldedge/src/tools/reporting-tools.ts | 237 ----- servers/fieldedge/src/tools/reporting.ts | 140 +++ servers/fieldedge/src/tools/scheduling.ts | 175 ++++ .../fieldedge/src/tools/service-agreements.ts | 139 +++ servers/fieldedge/src/tools/tasks.ts | 125 +++ .../fieldedge/src/tools/technicians-tools.ts | 234 ----- servers/fieldedge/src/tools/technicians.ts | 184 ++++ servers/fieldedge/src/types.ts | 439 --------- servers/fieldedge/src/types/index.ts | 602 ++++++++++++ servers/fieldedge/src/ui/calendar/App.tsx | 49 + servers/fieldedge/src/ui/calendar/index.html | 12 + servers/fieldedge/src/ui/calendar/main.tsx | 9 + servers/fieldedge/src/ui/calendar/styles.css | 119 +++ .../fieldedge/src/ui/calendar/vite.config.ts | 10 + servers/fieldedge/src/ui/customers/App.tsx | 49 + servers/fieldedge/src/ui/customers/index.html | 12 + servers/fieldedge/src/ui/customers/main.tsx | 9 + servers/fieldedge/src/ui/customers/styles.css | 119 +++ .../fieldedge/src/ui/customers/vite.config.ts | 10 + servers/fieldedge/src/ui/dashboard/App.tsx | 112 +++ servers/fieldedge/src/ui/dashboard/index.html | 12 + servers/fieldedge/src/ui/dashboard/main.tsx | 9 + servers/fieldedge/src/ui/dashboard/styles.css | 104 ++ .../fieldedge/src/ui/dashboard/vite.config.ts | 10 + servers/fieldedge/src/ui/equipment/App.tsx | 49 + servers/fieldedge/src/ui/equipment/index.html | 12 + servers/fieldedge/src/ui/equipment/main.tsx | 9 + servers/fieldedge/src/ui/equipment/styles.css | 119 +++ .../fieldedge/src/ui/equipment/vite.config.ts | 10 + servers/fieldedge/src/ui/estimates/App.tsx | 49 + servers/fieldedge/src/ui/estimates/index.html | 12 + servers/fieldedge/src/ui/estimates/main.tsx | 9 + servers/fieldedge/src/ui/estimates/styles.css | 119 +++ .../fieldedge/src/ui/estimates/vite.config.ts | 10 + servers/fieldedge/src/ui/inventory/App.tsx | 49 + servers/fieldedge/src/ui/inventory/index.html | 12 + servers/fieldedge/src/ui/inventory/main.tsx | 9 + servers/fieldedge/src/ui/inventory/styles.css | 119 +++ .../fieldedge/src/ui/inventory/vite.config.ts | 10 + servers/fieldedge/src/ui/invoices/App.tsx | 49 + servers/fieldedge/src/ui/invoices/index.html | 12 + servers/fieldedge/src/ui/invoices/main.tsx | 9 + servers/fieldedge/src/ui/invoices/styles.css | 119 +++ .../fieldedge/src/ui/invoices/vite.config.ts | 10 + servers/fieldedge/src/ui/jobs/App.tsx | 49 + servers/fieldedge/src/ui/jobs/index.html | 12 + servers/fieldedge/src/ui/jobs/main.tsx | 9 + servers/fieldedge/src/ui/jobs/styles.css | 119 +++ servers/fieldedge/src/ui/jobs/vite.config.ts | 10 + servers/fieldedge/src/ui/map-view/App.tsx | 49 + servers/fieldedge/src/ui/map-view/index.html | 12 + servers/fieldedge/src/ui/map-view/main.tsx | 9 + servers/fieldedge/src/ui/map-view/styles.css | 119 +++ .../fieldedge/src/ui/map-view/vite.config.ts | 10 + servers/fieldedge/src/ui/payments/App.tsx | 49 + servers/fieldedge/src/ui/payments/index.html | 12 + servers/fieldedge/src/ui/payments/main.tsx | 9 + servers/fieldedge/src/ui/payments/styles.css | 119 +++ .../fieldedge/src/ui/payments/vite.config.ts | 10 + servers/fieldedge/src/ui/price-book/App.tsx | 49 + .../fieldedge/src/ui/price-book/index.html | 12 + servers/fieldedge/src/ui/price-book/main.tsx | 9 + .../fieldedge/src/ui/price-book/styles.css | 119 +++ .../src/ui/price-book/vite.config.ts | 10 + .../src/ui/react-app/customer-detail/App.tsx | 269 +++++ .../ui/react-app/customer-detail/index.html | 13 + .../ui/react-app/customer-detail/styles.css | 7 + .../react-app/customer-detail/vite.config.ts | 14 + .../src/ui/react-app/customer-grid/App.tsx | 206 ++++ .../src/ui/react-app/customer-grid/index.html | 13 + .../src/ui/react-app/customer-grid/styles.css | 7 + .../ui/react-app/customer-grid/vite.config.ts | 14 + .../src/ui/react-app/estimate-builder/App.tsx | 231 +++++ .../ui/react-app/estimate-builder/index.html | 13 + .../ui/react-app/estimate-builder/styles.css | 7 + .../react-app/estimate-builder/vite.config.ts | 14 + .../ui/react-app/invoice-dashboard/App.tsx | 195 ++++ .../ui/react-app/invoice-dashboard/index.html | 13 + .../ui/react-app/invoice-dashboard/styles.css | 7 + .../invoice-dashboard/vite.config.ts | 14 + .../src/ui/react-app/job-dashboard/App.tsx | 268 +++++ .../src/ui/react-app/job-dashboard/index.html | 13 + .../src/ui/react-app/job-dashboard/styles.css | 7 + .../ui/react-app/job-dashboard/vite.config.ts | 14 + .../src/ui/react-app/job-detail/App.tsx | 332 +++++++ .../src/ui/react-app/job-detail/index.html | 13 + .../src/ui/react-app/job-detail/styles.css | 3 + .../ui/react-app/job-detail/vite.config.ts | 14 + .../src/ui/react-app/job-grid/App.tsx | 194 ++++ .../src/ui/react-app/job-grid/index.html | 13 + .../src/ui/react-app/job-grid/styles.css | 7 + .../src/ui/react-app/job-grid/vite.config.ts | 14 + servers/fieldedge/src/ui/reports/App.tsx | 49 + servers/fieldedge/src/ui/reports/index.html | 12 + servers/fieldedge/src/ui/reports/main.tsx | 9 + servers/fieldedge/src/ui/reports/styles.css | 119 +++ .../fieldedge/src/ui/reports/vite.config.ts | 10 + servers/fieldedge/src/ui/scheduling/App.tsx | 49 + .../fieldedge/src/ui/scheduling/index.html | 12 + servers/fieldedge/src/ui/scheduling/main.tsx | 9 + .../fieldedge/src/ui/scheduling/styles.css | 119 +++ .../src/ui/scheduling/vite.config.ts | 10 + .../src/ui/service-agreements/App.tsx | 49 + .../src/ui/service-agreements/index.html | 12 + .../src/ui/service-agreements/main.tsx | 9 + .../src/ui/service-agreements/styles.css | 119 +++ .../src/ui/service-agreements/vite.config.ts | 10 + servers/fieldedge/src/ui/tasks/App.tsx | 49 + servers/fieldedge/src/ui/tasks/index.html | 12 + servers/fieldedge/src/ui/tasks/main.tsx | 9 + servers/fieldedge/src/ui/tasks/styles.css | 119 +++ servers/fieldedge/src/ui/tasks/vite.config.ts | 10 + servers/fieldedge/src/ui/technicians/App.tsx | 49 + .../fieldedge/src/ui/technicians/index.html | 12 + servers/fieldedge/src/ui/technicians/main.tsx | 9 + .../fieldedge/src/ui/technicians/styles.css | 119 +++ .../src/ui/technicians/vite.config.ts | 10 + servers/fieldedge/tsconfig.json | 18 +- servers/freshdesk/package.json | 34 +- servers/freshdesk/src/api/client.ts | 396 ++++++++ servers/freshdesk/src/apps/agent-dashboard.ts | 4 + .../freshdesk/src/apps/agent-performance.ts | 4 + servers/freshdesk/src/apps/article-editor.ts | 26 + .../freshdesk/src/apps/canned-responses.ts | 4 + servers/freshdesk/src/apps/company-detail.ts | 4 + servers/freshdesk/src/apps/company-grid.ts | 4 + servers/freshdesk/src/apps/contact-detail.ts | 4 + servers/freshdesk/src/apps/contact-grid.ts | 4 + servers/freshdesk/src/apps/forum-browser.ts | 4 + servers/freshdesk/src/apps/group-manager.ts | 4 + servers/freshdesk/src/apps/knowledge-base.ts | 4 + .../freshdesk/src/apps/ticket-dashboard.ts | 289 ++++++ servers/freshdesk/src/apps/ticket-detail.ts | 241 +++++ servers/freshdesk/src/apps/ticket-grid.ts | 4 + servers/freshdesk/src/index.ts | 392 -------- servers/freshdesk/src/main.ts | 5 + servers/freshdesk/src/server.ts | 355 +++++++ servers/freshdesk/src/tools/agents-tools.ts | 145 +++ .../src/tools/canned-responses-tools.ts | 155 +++ .../freshdesk/src/tools/companies-tools.ts | 223 +++++ servers/freshdesk/src/tools/contacts-tools.ts | 369 +++++++ servers/freshdesk/src/tools/forums-tools.ts | 178 ++++ servers/freshdesk/src/tools/groups-tools.ts | 176 ++++ servers/freshdesk/src/tools/products-tools.ts | 154 +++ .../freshdesk/src/tools/reporting-tools.ts | 196 ++++ servers/freshdesk/src/tools/roles-tools.ts | 49 + .../freshdesk/src/tools/solutions-tools.ts | 243 +++++ servers/freshdesk/src/tools/surveys-tools.ts | 60 ++ servers/freshdesk/src/tools/tickets-tools.ts | 486 +++++++++ servers/freshdesk/src/types/index.ts | 277 ++++++ servers/freshdesk/tsconfig.json | 11 +- servers/lightspeed/README.md | 554 +++++++---- servers/lightspeed/build-apps.js | 67 ++ servers/lightspeed/create-apps.js | 191 ++++ servers/lightspeed/package.json | 32 +- servers/lightspeed/src/apps/index.ts | 532 ---------- servers/lightspeed/src/clients/lightspeed.ts | 815 ++++++--------- servers/lightspeed/src/main.ts | 69 +- servers/lightspeed/src/server.ts | 157 +-- .../lightspeed/src/tools/categories-tools.ts | 171 ---- servers/lightspeed/src/tools/categories.ts | 148 +-- servers/lightspeed/src/tools/customers.ts | 243 +++-- .../lightspeed/src/tools/discounts-tools.ts | 173 ---- servers/lightspeed/src/tools/discounts.ts | 125 +-- .../lightspeed/src/tools/employees-tools.ts | 275 ------ servers/lightspeed/src/tools/employees.ts | 158 +-- .../lightspeed/src/tools/inventory-tools.ts | 336 ------- servers/lightspeed/src/tools/inventory.ts | 298 +++--- servers/lightspeed/src/tools/loyalty.ts | 93 -- servers/lightspeed/src/tools/manufacturers.ts | 51 + servers/lightspeed/src/tools/orders.ts | 229 +++-- servers/lightspeed/src/tools/products.ts | 315 ++++-- .../lightspeed/src/tools/registers-tools.ts | 212 ---- servers/lightspeed/src/tools/registers.ts | 79 ++ .../lightspeed/src/tools/reporting-tools.ts | 315 ------ servers/lightspeed/src/tools/reporting.ts | 195 ---- servers/lightspeed/src/tools/reports.ts | 230 +++++ servers/lightspeed/src/tools/sales.ts | 358 ++++--- servers/lightspeed/src/tools/shops.ts | 96 +- servers/lightspeed/src/tools/suppliers.ts | 107 -- servers/lightspeed/src/tools/taxes-tools.ts | 150 --- servers/lightspeed/src/tools/vendors.ts | 108 ++ servers/lightspeed/src/tools/workorders.ts | 84 ++ servers/lightspeed/src/types/index.ts | 616 ++++++++---- servers/lightspeed/src/ui/analytics/App.tsx | 19 + servers/lightspeed/src/ui/analytics/app.css | 95 ++ .../lightspeed/src/ui/analytics/index.html | 12 + servers/lightspeed/src/ui/analytics/main.tsx | 9 + .../src/ui/analytics/vite.config.ts | 7 + .../src/ui/category-manager/App.tsx | 19 + .../src/ui/category-manager/app.css | 95 ++ .../src/ui/category-manager/index.html | 12 + .../src/ui/category-manager/main.tsx | 9 + .../src/ui/category-manager/vite.config.ts | 7 + .../src/ui/customer-manager/App.tsx | 19 + .../src/ui/customer-manager/app.css | 95 ++ .../src/ui/customer-manager/index.html | 12 + .../src/ui/customer-manager/main.tsx | 9 + .../src/ui/customer-manager/vite.config.ts | 7 + servers/lightspeed/src/ui/dashboard/App.tsx | 75 ++ servers/lightspeed/src/ui/dashboard/app.css | 133 +++ .../lightspeed/src/ui/dashboard/index.html | 12 + servers/lightspeed/src/ui/dashboard/main.tsx | 9 + .../src/ui/dashboard/vite.config.ts | 7 + .../src/ui/discount-manager/App.tsx | 19 + .../src/ui/discount-manager/app.css | 95 ++ .../src/ui/discount-manager/index.html | 12 + .../src/ui/discount-manager/main.tsx | 9 + .../src/ui/discount-manager/vite.config.ts | 7 + .../src/ui/employee-manager/App.tsx | 19 + .../src/ui/employee-manager/app.css | 95 ++ .../src/ui/employee-manager/index.html | 12 + .../src/ui/employee-manager/main.tsx | 9 + .../src/ui/employee-manager/vite.config.ts | 7 + .../src/ui/inventory-manager/App.tsx | 19 + .../src/ui/inventory-manager/app.css | 95 ++ .../src/ui/inventory-manager/index.html | 12 + .../src/ui/inventory-manager/main.tsx | 9 + .../src/ui/inventory-manager/vite.config.ts | 7 + .../lightspeed/src/ui/low-stock-alert/App.tsx | 19 + .../lightspeed/src/ui/low-stock-alert/app.css | 95 ++ .../src/ui/low-stock-alert/index.html | 12 + .../src/ui/low-stock-alert/main.tsx | 9 + .../src/ui/low-stock-alert/vite.config.ts | 7 + .../lightspeed/src/ui/order-manager/App.tsx | 19 + .../lightspeed/src/ui/order-manager/app.css | 95 ++ .../src/ui/order-manager/index.html | 12 + .../lightspeed/src/ui/order-manager/main.tsx | 9 + .../src/ui/order-manager/vite.config.ts | 7 + .../lightspeed/src/ui/product-manager/App.tsx | 19 + .../lightspeed/src/ui/product-manager/app.css | 95 ++ .../src/ui/product-manager/index.html | 12 + .../src/ui/product-manager/main.tsx | 9 + .../src/ui/product-manager/vite.config.ts | 7 + servers/lightspeed/src/ui/quick-sale/App.tsx | 19 + servers/lightspeed/src/ui/quick-sale/app.css | 95 ++ .../lightspeed/src/ui/quick-sale/index.html | 12 + servers/lightspeed/src/ui/quick-sale/main.tsx | 9 + .../src/ui/quick-sale/vite.config.ts | 7 + .../src/ui/react-app/customer-detail.tsx | 236 +++++ .../src/ui/react-app/customer-grid.tsx | 187 ++++ .../src/ui/react-app/inventory-tracker.tsx | 184 ++++ .../src/ui/react-app/product-dashboard.tsx | 162 +++ .../src/ui/react-app/product-detail.tsx | 208 ++++ .../src/ui/react-app/product-grid.tsx | 141 +++ .../src/ui/react-app/sales-dashboard.tsx | 163 +++ .../src/ui/react-app/sales-detail.tsx | 189 ++++ .../src/ui/register-manager/App.tsx | 19 + .../src/ui/register-manager/app.css | 95 ++ .../src/ui/register-manager/index.html | 12 + .../src/ui/register-manager/main.tsx | 9 + .../src/ui/register-manager/vite.config.ts | 7 + servers/lightspeed/src/ui/reports/App.tsx | 19 + servers/lightspeed/src/ui/reports/app.css | 95 ++ servers/lightspeed/src/ui/reports/index.html | 12 + servers/lightspeed/src/ui/reports/main.tsx | 9 + .../lightspeed/src/ui/reports/vite.config.ts | 7 + .../lightspeed/src/ui/sales-terminal/App.tsx | 19 + .../lightspeed/src/ui/sales-terminal/app.css | 95 ++ .../src/ui/sales-terminal/index.html | 12 + .../lightspeed/src/ui/sales-terminal/main.tsx | 9 + .../src/ui/sales-terminal/vite.config.ts | 7 + .../src/ui/transfer-manager/App.tsx | 19 + .../src/ui/transfer-manager/app.css | 95 ++ .../src/ui/transfer-manager/index.html | 12 + .../src/ui/transfer-manager/main.tsx | 9 + .../src/ui/transfer-manager/vite.config.ts | 7 + .../lightspeed/src/ui/vendor-manager/App.tsx | 19 + .../lightspeed/src/ui/vendor-manager/app.css | 95 ++ .../src/ui/vendor-manager/index.html | 12 + .../lightspeed/src/ui/vendor-manager/main.tsx | 9 + .../src/ui/vendor-manager/vite.config.ts | 7 + .../src/ui/workorder-manager/App.tsx | 19 + .../src/ui/workorder-manager/app.css | 95 ++ .../src/ui/workorder-manager/index.html | 12 + .../src/ui/workorder-manager/main.tsx | 9 + .../src/ui/workorder-manager/vite.config.ts | 7 + servers/lightspeed/tsconfig.json | 5 +- servers/squarespace/package.json | 41 - servers/squarespace/scripts/generate-apps.sh | 140 +++ servers/squarespace/src/apps/blog-manager.ts | 93 -- .../src/apps/collection-browser.ts | 50 - servers/squarespace/src/apps/customer-grid.ts | 69 -- .../squarespace/src/apps/form-submissions.ts | 62 -- .../squarespace/src/apps/inventory-tracker.ts | 108 -- .../squarespace/src/apps/order-dashboard.ts | 99 -- servers/squarespace/src/apps/order-detail.ts | 150 --- servers/squarespace/src/apps/page-manager.ts | 76 -- .../squarespace/src/apps/product-dashboard.ts | 122 --- .../squarespace/src/apps/site-analytics.ts | 120 --- .../squarespace/src/clients/squarespace.ts | 925 +++++++++++++----- servers/squarespace/src/main.ts | 7 + servers/squarespace/src/server.ts | 258 +++++ .../squarespace/src/tools/analytics-tools.ts | 135 --- servers/squarespace/src/tools/analytics.ts | 221 ++--- servers/squarespace/src/tools/blog.ts | 291 ++++++ .../src/tools/collections-tools.ts | 109 --- .../src/tools/commerce-inventory.ts | 172 ++++ .../squarespace/src/tools/commerce-orders.ts | 317 ++++++ .../src/tools/commerce-products.ts | 399 ++++++++ .../src/tools/commerce-transactions.ts | 122 +++ servers/squarespace/src/tools/customers.ts | 159 --- servers/squarespace/src/tools/forms-tools.ts | 78 -- servers/squarespace/src/tools/forms.ts | 186 ++-- servers/squarespace/src/tools/inventory.ts | 154 --- servers/squarespace/src/tools/menu-tools.ts | 53 - servers/squarespace/src/tools/orders.ts | 185 ---- servers/squarespace/src/tools/pages.ts | 206 ++-- servers/squarespace/src/tools/products.ts | 262 ----- servers/squarespace/src/tools/profiles.ts | 206 ++++ .../squarespace/src/tools/settings-tools.ts | 45 - servers/squarespace/src/tools/sites.ts | 141 --- servers/squarespace/src/tools/webhooks.ts | 223 +++++ servers/squarespace/src/types/index.ts | 740 ++++++++++---- .../src/ui/react-app/analytics/App.tsx | 0 .../src/ui/react-app/analytics/index.html | 15 + .../src/ui/react-app/analytics/main.tsx | 9 + .../src/ui/react-app/analytics/vite.config.ts | 9 + .../squarespace/src/ui/react-app/blog/App.tsx | 0 .../src/ui/react-app/blog/index.html | 15 + .../src/ui/react-app/blog/main.tsx | 9 + .../src/ui/react-app/blog/vite.config.ts | 9 + .../src/ui/react-app/bulk-editor/App.tsx | 0 .../src/ui/react-app/bulk-editor/index.html | 15 + .../src/ui/react-app/bulk-editor/main.tsx | 9 + .../ui/react-app/bulk-editor/vite.config.ts | 9 + .../src/ui/react-app/customers/App.tsx | 0 .../src/ui/react-app/customers/index.html | 15 + .../src/ui/react-app/customers/main.tsx | 9 + .../src/ui/react-app/customers/vite.config.ts | 9 + .../src/ui/react-app/discounts/App.tsx | 0 .../src/ui/react-app/discounts/index.html | 15 + .../src/ui/react-app/discounts/main.tsx | 9 + .../src/ui/react-app/discounts/vite.config.ts | 9 + .../src/ui/react-app/forms/App.tsx | 0 .../src/ui/react-app/forms/index.html | 15 + .../src/ui/react-app/forms/main.tsx | 9 + .../src/ui/react-app/forms/vite.config.ts | 9 + .../src/ui/react-app/inventory/App.tsx | 0 .../src/ui/react-app/inventory/index.html | 15 + .../src/ui/react-app/inventory/main.tsx | 9 + .../src/ui/react-app/inventory/vite.config.ts | 9 + .../src/ui/react-app/orders/App.tsx | 85 ++ .../src/ui/react-app/orders/index.html | 12 + .../src/ui/react-app/orders/main.tsx | 9 + .../src/ui/react-app/orders/styles.css | 157 +++ .../src/ui/react-app/orders/vite.config.ts | 9 + .../src/ui/react-app/pages/App.tsx | 0 .../src/ui/react-app/pages/index.html | 15 + .../src/ui/react-app/pages/main.tsx | 9 + .../src/ui/react-app/pages/vite.config.ts | 9 + .../src/ui/react-app/products/App.tsx | 0 .../src/ui/react-app/products/index.html | 15 + .../src/ui/react-app/products/main.tsx | 9 + .../src/ui/react-app/products/vite.config.ts | 9 + .../src/ui/react-app/reports/App.tsx | 0 .../src/ui/react-app/reports/index.html | 15 + .../src/ui/react-app/reports/main.tsx | 9 + .../src/ui/react-app/reports/vite.config.ts | 9 + .../squarespace/src/ui/react-app/seo/App.tsx | 0 .../src/ui/react-app/seo/index.html | 15 + .../squarespace/src/ui/react-app/seo/main.tsx | 9 + .../src/ui/react-app/seo/vite.config.ts | 9 + .../src/ui/react-app/settings/App.tsx | 0 .../src/ui/react-app/settings/index.html | 15 + .../src/ui/react-app/settings/main.tsx | 9 + .../src/ui/react-app/settings/vite.config.ts | 9 + .../src/ui/react-app/shipping/App.tsx | 0 .../src/ui/react-app/shipping/index.html | 15 + .../src/ui/react-app/shipping/main.tsx | 9 + .../src/ui/react-app/shipping/vite.config.ts | 9 + .../src/ui/react-app/webhooks/App.tsx | 0 .../src/ui/react-app/webhooks/index.html | 15 + .../src/ui/react-app/webhooks/main.tsx | 9 + .../src/ui/react-app/webhooks/vite.config.ts | 9 + servers/toast/dist/api-client.d.ts | 97 -- servers/toast/dist/api-client.d.ts.map | 1 - servers/toast/dist/api-client.js | 201 ---- servers/toast/dist/api-client.js.map | 1 - servers/toast/dist/apps/index.d.ts | 123 --- servers/toast/dist/apps/index.d.ts.map | 1 - servers/toast/dist/apps/index.js | 256 ----- servers/toast/dist/apps/index.js.map | 1 - servers/toast/dist/main.d.ts | 3 - servers/toast/dist/main.d.ts.map | 1 - servers/toast/dist/main.js | 14 - servers/toast/dist/main.js.map | 1 - servers/toast/dist/server.d.ts | 9 - servers/toast/dist/server.d.ts.map | 1 - servers/toast/dist/server.js | 130 --- servers/toast/dist/server.js.map | 1 - servers/toast/dist/tools/cash-tools.d.ts | 45 - servers/toast/dist/tools/cash-tools.d.ts.map | 1 - servers/toast/dist/tools/cash-tools.js | 48 - servers/toast/dist/tools/cash-tools.js.map | 1 - servers/toast/dist/tools/customers-tools.d.ts | 67 -- .../toast/dist/tools/customers-tools.d.ts.map | 1 - servers/toast/dist/tools/customers-tools.js | 146 --- .../toast/dist/tools/customers-tools.js.map | 1 - servers/toast/dist/tools/employees-tools.d.ts | 73 -- .../toast/dist/tools/employees-tools.d.ts.map | 1 - servers/toast/dist/tools/employees-tools.js | 230 ----- .../toast/dist/tools/employees-tools.js.map | 1 - servers/toast/dist/tools/inventory-tools.d.ts | 14 - .../toast/dist/tools/inventory-tools.d.ts.map | 1 - servers/toast/dist/tools/inventory-tools.js | 115 --- .../toast/dist/tools/inventory-tools.js.map | 1 - servers/toast/dist/tools/labor-tools.d.ts | 14 - servers/toast/dist/tools/labor-tools.d.ts.map | 1 - servers/toast/dist/tools/labor-tools.js | 100 -- servers/toast/dist/tools/labor-tools.js.map | 1 - servers/toast/dist/tools/menus-tools.d.ts | 14 - servers/toast/dist/tools/menus-tools.d.ts.map | 1 - servers/toast/dist/tools/menus-tools.js | 154 --- servers/toast/dist/tools/menus-tools.js.map | 1 - servers/toast/dist/tools/orders-tools.d.ts | 73 -- .../toast/dist/tools/orders-tools.d.ts.map | 1 - servers/toast/dist/tools/orders-tools.js | 227 ----- servers/toast/dist/tools/orders-tools.js.map | 1 - servers/toast/dist/tools/payments-tools.d.ts | 39 - .../toast/dist/tools/payments-tools.d.ts.map | 1 - servers/toast/dist/tools/payments-tools.js | 106 -- .../toast/dist/tools/payments-tools.js.map | 1 - servers/toast/dist/tools/reporting-tools.d.ts | 39 - .../toast/dist/tools/reporting-tools.d.ts.map | 1 - servers/toast/dist/tools/reporting-tools.js | 133 --- .../toast/dist/tools/reporting-tools.js.map | 1 - .../toast/dist/tools/restaurant-tools.d.ts | 14 - .../dist/tools/restaurant-tools.d.ts.map | 1 - servers/toast/dist/tools/restaurant-tools.js | 88 -- .../toast/dist/tools/restaurant-tools.js.map | 1 - servers/toast/dist/types/index.d.ts | 533 ---------- servers/toast/dist/types/index.d.ts.map | 1 - servers/toast/dist/types/index.js | 3 - servers/toast/dist/types/index.js.map | 1 - servers/toast/package.json | 36 + servers/toast/src/clients/toast.ts | 280 ++++++ servers/toast/src/main.ts | 40 + servers/toast/src/server.ts | 213 ++++ servers/toast/src/tools/analytics.ts | 98 ++ servers/toast/src/tools/availability.ts | 50 + servers/toast/src/tools/cash-management.ts | 73 ++ servers/toast/src/tools/configuration.ts | 115 +++ servers/toast/src/tools/customers.ts | 261 +++++ servers/toast/src/tools/employees.ts | 217 ++++ servers/toast/src/tools/inventory.ts | 164 ++++ servers/toast/src/tools/kitchen.ts | 62 ++ servers/toast/src/tools/labor.ts | 192 ++++ servers/toast/src/tools/menus.ts | 261 +++++ servers/toast/src/tools/orders.ts | 316 ++++++ servers/toast/src/tools/partners.ts | 35 + servers/toast/src/tools/payments.ts | 154 +++ servers/toast/src/tools/reports.ts | 115 +++ servers/toast/src/tools/restaurant.ts | 159 +++ servers/toast/src/tools/stock.ts | 95 ++ servers/toast/src/types/index.ts | 680 +++++++++++++ .../src/ui/cash-drawer-manager/index.html | 12 + .../src/ui/cash-drawer-manager/package.json | 21 + .../src/ui/cash-drawer-manager/src/main.tsx | 9 + .../src/ui/cash-drawer-manager/vite.config.ts | 10 + .../src/ui/customer-directory/index.html | 12 + .../src/ui/customer-directory/package.json | 21 + .../src/ui/customer-directory/src/main.tsx | 9 + .../src/ui/customer-directory/vite.config.ts | 10 + servers/toast/src/ui/daily-reports/index.html | 12 + .../toast/src/ui/daily-reports/package.json | 21 + .../toast/src/ui/daily-reports/src/main.tsx | 9 + .../toast/src/ui/daily-reports/vite.config.ts | 10 + .../toast/src/ui/discount-tracker/index.html | 12 + .../src/ui/discount-tracker/package.json | 21 + .../src/ui/discount-tracker/src/main.tsx | 9 + .../src/ui/discount-tracker/vite.config.ts | 10 + .../toast/src/ui/employee-roster/index.html | 12 + .../toast/src/ui/employee-roster/package.json | 21 + .../toast/src/ui/employee-roster/src/main.tsx | 9 + .../src/ui/employee-roster/vite.config.ts | 10 + servers/toast/src/ui/hourly-sales/index.html | 12 + .../toast/src/ui/hourly-sales/package.json | 21 + .../toast/src/ui/hourly-sales/src/main.tsx | 9 + .../toast/src/ui/hourly-sales/vite.config.ts | 10 + .../toast/src/ui/inventory-manager/index.html | 12 + .../src/ui/inventory-manager/package.json | 21 + .../src/ui/inventory-manager/src/main.tsx | 9 + .../src/ui/inventory-manager/vite.config.ts | 10 + .../toast/src/ui/kitchen-display/index.html | 12 + .../toast/src/ui/kitchen-display/package.json | 21 + .../toast/src/ui/kitchen-display/src/main.tsx | 9 + .../src/ui/kitchen-display/vite.config.ts | 10 + .../toast/src/ui/labor-scheduler/index.html | 12 + .../toast/src/ui/labor-scheduler/package.json | 21 + .../toast/src/ui/labor-scheduler/src/main.tsx | 9 + .../src/ui/labor-scheduler/vite.config.ts | 10 + servers/toast/src/ui/menu-manager/index.html | 12 + .../toast/src/ui/menu-manager/package.json | 21 + .../toast/src/ui/menu-manager/src/main.tsx | 9 + .../toast/src/ui/menu-manager/vite.config.ts | 10 + .../toast/src/ui/orders-dashboard/index.html | 12 + .../src/ui/orders-dashboard/package.json | 21 + .../toast/src/ui/orders-dashboard/src/App.tsx | 162 +++ .../src/ui/orders-dashboard/src/main.tsx | 9 + .../src/ui/orders-dashboard/vite.config.ts | 10 + .../toast/src/ui/payment-terminal/index.html | 12 + .../src/ui/payment-terminal/package.json | 21 + .../src/ui/payment-terminal/src/main.tsx | 9 + .../src/ui/payment-terminal/vite.config.ts | 10 + .../toast/src/ui/revenue-centers/index.html | 12 + .../toast/src/ui/revenue-centers/package.json | 21 + .../toast/src/ui/revenue-centers/src/main.tsx | 9 + .../src/ui/revenue-centers/vite.config.ts | 10 + .../toast/src/ui/sales-analytics/index.html | 12 + .../toast/src/ui/sales-analytics/package.json | 21 + .../toast/src/ui/sales-analytics/src/main.tsx | 9 + .../src/ui/sales-analytics/vite.config.ts | 10 + servers/toast/src/ui/table-manager/index.html | 12 + .../toast/src/ui/table-manager/package.json | 21 + .../toast/src/ui/table-manager/src/main.tsx | 9 + .../toast/src/ui/table-manager/vite.config.ts | 10 + servers/{squarespace => toast}/tsconfig.json | 7 +- servers/touchbistro/.gitignore | 6 - servers/touchbistro/package.json | 37 - servers/touchbistro/src/api-client.ts | 235 ----- .../touchbistro/src/clients/touchbistro.ts | 235 +++++ .../touchbistro/src/tools/customers-tools.ts | 244 ----- servers/touchbistro/src/tools/customers.ts | 188 ++++ servers/touchbistro/src/tools/discounts.ts | 200 ++++ servers/touchbistro/src/tools/employees.ts | 272 +++++ servers/touchbistro/src/tools/giftcards.ts | 170 ++++ .../touchbistro/src/tools/inventory-tools.ts | 173 ---- servers/touchbistro/src/tools/inventory.ts | 267 +++++ servers/touchbistro/src/tools/loyalty.ts | 140 +++ servers/touchbistro/src/tools/menus-tools.ts | 224 ----- servers/touchbistro/src/tools/menus.ts | 334 +++++++ servers/touchbistro/src/tools/orders-tools.ts | 272 ----- servers/touchbistro/src/tools/orders.ts | 227 +++++ .../touchbistro/src/tools/payments-tools.ts | 203 ---- servers/touchbistro/src/tools/payments.ts | 177 ++++ .../touchbistro/src/tools/reporting-tools.ts | 220 ----- servers/touchbistro/src/tools/reports.ts | 236 +++++ .../src/tools/reservations-tools.ts | 222 ----- servers/touchbistro/src/tools/reservations.ts | 216 ++++ servers/touchbistro/src/tools/staff-tools.ts | 286 ------ servers/touchbistro/src/tools/tables-tools.ts | 162 --- servers/touchbistro/src/tools/tables.ts | 234 +++++ servers/touchbistro/src/types.ts | 330 ------- servers/touchbistro/src/types/index.ts | 743 ++++++++++++++ servers/touchbistro/tsconfig.json | 20 - .../src/ui/react-app/accounts-payable/App.tsx | 54 + .../ui/react-app/balance-sheet-viewer/App.tsx | 65 ++ .../src/ui/react-app/estimate-creator/App.tsx | 60 ++ .../ui/react-app/profit-loss-viewer/App.tsx | 62 ++ .../recurring-invoice-manager/App.tsx | 61 ++ .../ui/react-app/transaction-browser/App.tsx | 59 ++ 616 files changed, 38114 insertions(+), 17713 deletions(-) create mode 100644 servers/close/src/apps/activity-feed.ts create mode 100644 servers/close/src/apps/activity-timeline.ts create mode 100644 servers/close/src/apps/bulk-actions.ts create mode 100644 servers/close/src/apps/call-log.ts create mode 100644 servers/close/src/apps/contact-detail.ts create mode 100644 servers/close/src/apps/custom-fields-manager.ts create mode 100644 servers/close/src/apps/email-log.ts create mode 100644 servers/close/src/apps/lead-dashboard.ts create mode 100644 servers/close/src/apps/lead-detail.ts create mode 100644 servers/close/src/apps/lead-grid.ts create mode 100644 servers/close/src/apps/opportunity-dashboard.ts create mode 100644 servers/close/src/apps/opportunity-detail.ts create mode 100644 servers/close/src/apps/pipeline-funnel.ts create mode 100644 servers/close/src/apps/pipeline-kanban.ts create mode 100644 servers/close/src/apps/report-builder.ts create mode 100644 servers/close/src/apps/revenue-dashboard.ts create mode 100644 servers/close/src/apps/search-results.ts create mode 100644 servers/close/src/apps/sequence-dashboard.ts create mode 100644 servers/close/src/apps/sequence-detail.ts create mode 100644 servers/close/src/apps/smart-view-runner.ts create mode 100644 servers/close/src/apps/task-manager.ts create mode 100644 servers/close/src/apps/user-stats.ts create mode 100644 servers/close/src/client/close-client.ts delete mode 100644 servers/close/src/index.ts create mode 100644 servers/close/src/tools/activities-tools.ts create mode 100644 servers/close/src/tools/bulk-tools.ts create mode 100644 servers/close/src/tools/contacts-tools.ts create mode 100644 servers/close/src/tools/custom-fields-tools.ts create mode 100644 servers/close/src/tools/leads-tools.ts create mode 100644 servers/close/src/tools/opportunities-tools.ts create mode 100644 servers/close/src/tools/pipelines-tools.ts create mode 100644 servers/close/src/tools/reporting-tools.ts create mode 100644 servers/close/src/tools/sequences-tools.ts create mode 100644 servers/close/src/tools/smart-views-tools.ts create mode 100644 servers/close/src/tools/tasks-tools.ts create mode 100644 servers/close/src/tools/users-tools.ts create mode 100644 servers/close/src/types/index.ts create mode 100644 servers/fieldedge/scripts/generate-apps.js delete mode 100644 servers/fieldedge/src/apps/index.ts delete mode 100644 servers/fieldedge/src/client.ts create mode 100644 servers/fieldedge/src/clients/fieldedge.ts delete mode 100644 servers/fieldedge/src/tools/agreements-tools.ts delete mode 100644 servers/fieldedge/src/tools/customers-tools.ts create mode 100644 servers/fieldedge/src/tools/customers.ts delete mode 100644 servers/fieldedge/src/tools/dispatch-tools.ts delete mode 100644 servers/fieldedge/src/tools/equipment-tools.ts create mode 100644 servers/fieldedge/src/tools/equipment.ts delete mode 100644 servers/fieldedge/src/tools/estimates-tools.ts create mode 100644 servers/fieldedge/src/tools/estimates.ts delete mode 100644 servers/fieldedge/src/tools/inventory-tools.ts create mode 100644 servers/fieldedge/src/tools/inventory.ts delete mode 100644 servers/fieldedge/src/tools/invoices-tools.ts create mode 100644 servers/fieldedge/src/tools/invoices.ts delete mode 100644 servers/fieldedge/src/tools/jobs-tools.ts create mode 100644 servers/fieldedge/src/tools/jobs.ts create mode 100644 servers/fieldedge/src/tools/locations.ts create mode 100644 servers/fieldedge/src/tools/payments.ts delete mode 100644 servers/fieldedge/src/tools/reporting-tools.ts create mode 100644 servers/fieldedge/src/tools/reporting.ts create mode 100644 servers/fieldedge/src/tools/scheduling.ts create mode 100644 servers/fieldedge/src/tools/service-agreements.ts create mode 100644 servers/fieldedge/src/tools/tasks.ts delete mode 100644 servers/fieldedge/src/tools/technicians-tools.ts create mode 100644 servers/fieldedge/src/tools/technicians.ts delete mode 100644 servers/fieldedge/src/types.ts create mode 100644 servers/fieldedge/src/types/index.ts create mode 100644 servers/fieldedge/src/ui/calendar/App.tsx create mode 100644 servers/fieldedge/src/ui/calendar/index.html create mode 100644 servers/fieldedge/src/ui/calendar/main.tsx create mode 100644 servers/fieldedge/src/ui/calendar/styles.css create mode 100644 servers/fieldedge/src/ui/calendar/vite.config.ts create mode 100644 servers/fieldedge/src/ui/customers/App.tsx create mode 100644 servers/fieldedge/src/ui/customers/index.html create mode 100644 servers/fieldedge/src/ui/customers/main.tsx create mode 100644 servers/fieldedge/src/ui/customers/styles.css create mode 100644 servers/fieldedge/src/ui/customers/vite.config.ts create mode 100644 servers/fieldedge/src/ui/dashboard/App.tsx create mode 100644 servers/fieldedge/src/ui/dashboard/index.html create mode 100644 servers/fieldedge/src/ui/dashboard/main.tsx create mode 100644 servers/fieldedge/src/ui/dashboard/styles.css create mode 100644 servers/fieldedge/src/ui/dashboard/vite.config.ts create mode 100644 servers/fieldedge/src/ui/equipment/App.tsx create mode 100644 servers/fieldedge/src/ui/equipment/index.html create mode 100644 servers/fieldedge/src/ui/equipment/main.tsx create mode 100644 servers/fieldedge/src/ui/equipment/styles.css create mode 100644 servers/fieldedge/src/ui/equipment/vite.config.ts create mode 100644 servers/fieldedge/src/ui/estimates/App.tsx create mode 100644 servers/fieldedge/src/ui/estimates/index.html create mode 100644 servers/fieldedge/src/ui/estimates/main.tsx create mode 100644 servers/fieldedge/src/ui/estimates/styles.css create mode 100644 servers/fieldedge/src/ui/estimates/vite.config.ts create mode 100644 servers/fieldedge/src/ui/inventory/App.tsx create mode 100644 servers/fieldedge/src/ui/inventory/index.html create mode 100644 servers/fieldedge/src/ui/inventory/main.tsx create mode 100644 servers/fieldedge/src/ui/inventory/styles.css create mode 100644 servers/fieldedge/src/ui/inventory/vite.config.ts create mode 100644 servers/fieldedge/src/ui/invoices/App.tsx create mode 100644 servers/fieldedge/src/ui/invoices/index.html create mode 100644 servers/fieldedge/src/ui/invoices/main.tsx create mode 100644 servers/fieldedge/src/ui/invoices/styles.css create mode 100644 servers/fieldedge/src/ui/invoices/vite.config.ts create mode 100644 servers/fieldedge/src/ui/jobs/App.tsx create mode 100644 servers/fieldedge/src/ui/jobs/index.html create mode 100644 servers/fieldedge/src/ui/jobs/main.tsx create mode 100644 servers/fieldedge/src/ui/jobs/styles.css create mode 100644 servers/fieldedge/src/ui/jobs/vite.config.ts create mode 100644 servers/fieldedge/src/ui/map-view/App.tsx create mode 100644 servers/fieldedge/src/ui/map-view/index.html create mode 100644 servers/fieldedge/src/ui/map-view/main.tsx create mode 100644 servers/fieldedge/src/ui/map-view/styles.css create mode 100644 servers/fieldedge/src/ui/map-view/vite.config.ts create mode 100644 servers/fieldedge/src/ui/payments/App.tsx create mode 100644 servers/fieldedge/src/ui/payments/index.html create mode 100644 servers/fieldedge/src/ui/payments/main.tsx create mode 100644 servers/fieldedge/src/ui/payments/styles.css create mode 100644 servers/fieldedge/src/ui/payments/vite.config.ts create mode 100644 servers/fieldedge/src/ui/price-book/App.tsx create mode 100644 servers/fieldedge/src/ui/price-book/index.html create mode 100644 servers/fieldedge/src/ui/price-book/main.tsx create mode 100644 servers/fieldedge/src/ui/price-book/styles.css create mode 100644 servers/fieldedge/src/ui/price-book/vite.config.ts create mode 100644 servers/fieldedge/src/ui/react-app/customer-detail/App.tsx create mode 100644 servers/fieldedge/src/ui/react-app/customer-detail/index.html create mode 100644 servers/fieldedge/src/ui/react-app/customer-detail/styles.css create mode 100644 servers/fieldedge/src/ui/react-app/customer-detail/vite.config.ts create mode 100644 servers/fieldedge/src/ui/react-app/customer-grid/App.tsx create mode 100644 servers/fieldedge/src/ui/react-app/customer-grid/index.html create mode 100644 servers/fieldedge/src/ui/react-app/customer-grid/styles.css create mode 100644 servers/fieldedge/src/ui/react-app/customer-grid/vite.config.ts create mode 100644 servers/fieldedge/src/ui/react-app/estimate-builder/App.tsx create mode 100644 servers/fieldedge/src/ui/react-app/estimate-builder/index.html create mode 100644 servers/fieldedge/src/ui/react-app/estimate-builder/styles.css create mode 100644 servers/fieldedge/src/ui/react-app/estimate-builder/vite.config.ts create mode 100644 servers/fieldedge/src/ui/react-app/invoice-dashboard/App.tsx create mode 100644 servers/fieldedge/src/ui/react-app/invoice-dashboard/index.html create mode 100644 servers/fieldedge/src/ui/react-app/invoice-dashboard/styles.css create mode 100644 servers/fieldedge/src/ui/react-app/invoice-dashboard/vite.config.ts create mode 100644 servers/fieldedge/src/ui/react-app/job-dashboard/App.tsx create mode 100644 servers/fieldedge/src/ui/react-app/job-dashboard/index.html create mode 100644 servers/fieldedge/src/ui/react-app/job-dashboard/styles.css create mode 100644 servers/fieldedge/src/ui/react-app/job-dashboard/vite.config.ts create mode 100644 servers/fieldedge/src/ui/react-app/job-detail/App.tsx create mode 100644 servers/fieldedge/src/ui/react-app/job-detail/index.html create mode 100644 servers/fieldedge/src/ui/react-app/job-detail/styles.css create mode 100644 servers/fieldedge/src/ui/react-app/job-detail/vite.config.ts create mode 100644 servers/fieldedge/src/ui/react-app/job-grid/App.tsx create mode 100644 servers/fieldedge/src/ui/react-app/job-grid/index.html create mode 100644 servers/fieldedge/src/ui/react-app/job-grid/styles.css create mode 100644 servers/fieldedge/src/ui/react-app/job-grid/vite.config.ts create mode 100644 servers/fieldedge/src/ui/reports/App.tsx create mode 100644 servers/fieldedge/src/ui/reports/index.html create mode 100644 servers/fieldedge/src/ui/reports/main.tsx create mode 100644 servers/fieldedge/src/ui/reports/styles.css create mode 100644 servers/fieldedge/src/ui/reports/vite.config.ts create mode 100644 servers/fieldedge/src/ui/scheduling/App.tsx create mode 100644 servers/fieldedge/src/ui/scheduling/index.html create mode 100644 servers/fieldedge/src/ui/scheduling/main.tsx create mode 100644 servers/fieldedge/src/ui/scheduling/styles.css create mode 100644 servers/fieldedge/src/ui/scheduling/vite.config.ts create mode 100644 servers/fieldedge/src/ui/service-agreements/App.tsx create mode 100644 servers/fieldedge/src/ui/service-agreements/index.html create mode 100644 servers/fieldedge/src/ui/service-agreements/main.tsx create mode 100644 servers/fieldedge/src/ui/service-agreements/styles.css create mode 100644 servers/fieldedge/src/ui/service-agreements/vite.config.ts create mode 100644 servers/fieldedge/src/ui/tasks/App.tsx create mode 100644 servers/fieldedge/src/ui/tasks/index.html create mode 100644 servers/fieldedge/src/ui/tasks/main.tsx create mode 100644 servers/fieldedge/src/ui/tasks/styles.css create mode 100644 servers/fieldedge/src/ui/tasks/vite.config.ts create mode 100644 servers/fieldedge/src/ui/technicians/App.tsx create mode 100644 servers/fieldedge/src/ui/technicians/index.html create mode 100644 servers/fieldedge/src/ui/technicians/main.tsx create mode 100644 servers/fieldedge/src/ui/technicians/styles.css create mode 100644 servers/fieldedge/src/ui/technicians/vite.config.ts create mode 100644 servers/freshdesk/src/api/client.ts create mode 100644 servers/freshdesk/src/apps/agent-dashboard.ts create mode 100644 servers/freshdesk/src/apps/agent-performance.ts create mode 100644 servers/freshdesk/src/apps/article-editor.ts create mode 100644 servers/freshdesk/src/apps/canned-responses.ts create mode 100644 servers/freshdesk/src/apps/company-detail.ts create mode 100644 servers/freshdesk/src/apps/company-grid.ts create mode 100644 servers/freshdesk/src/apps/contact-detail.ts create mode 100644 servers/freshdesk/src/apps/contact-grid.ts create mode 100644 servers/freshdesk/src/apps/forum-browser.ts create mode 100644 servers/freshdesk/src/apps/group-manager.ts create mode 100644 servers/freshdesk/src/apps/knowledge-base.ts create mode 100644 servers/freshdesk/src/apps/ticket-dashboard.ts create mode 100644 servers/freshdesk/src/apps/ticket-detail.ts create mode 100644 servers/freshdesk/src/apps/ticket-grid.ts delete mode 100644 servers/freshdesk/src/index.ts create mode 100644 servers/freshdesk/src/main.ts create mode 100644 servers/freshdesk/src/server.ts create mode 100644 servers/freshdesk/src/tools/agents-tools.ts create mode 100644 servers/freshdesk/src/tools/canned-responses-tools.ts create mode 100644 servers/freshdesk/src/tools/companies-tools.ts create mode 100644 servers/freshdesk/src/tools/contacts-tools.ts create mode 100644 servers/freshdesk/src/tools/forums-tools.ts create mode 100644 servers/freshdesk/src/tools/groups-tools.ts create mode 100644 servers/freshdesk/src/tools/products-tools.ts create mode 100644 servers/freshdesk/src/tools/reporting-tools.ts create mode 100644 servers/freshdesk/src/tools/roles-tools.ts create mode 100644 servers/freshdesk/src/tools/solutions-tools.ts create mode 100644 servers/freshdesk/src/tools/surveys-tools.ts create mode 100644 servers/freshdesk/src/tools/tickets-tools.ts create mode 100644 servers/freshdesk/src/types/index.ts create mode 100644 servers/lightspeed/build-apps.js create mode 100644 servers/lightspeed/create-apps.js delete mode 100644 servers/lightspeed/src/apps/index.ts delete mode 100644 servers/lightspeed/src/tools/categories-tools.ts delete mode 100644 servers/lightspeed/src/tools/discounts-tools.ts delete mode 100644 servers/lightspeed/src/tools/employees-tools.ts delete mode 100644 servers/lightspeed/src/tools/inventory-tools.ts delete mode 100644 servers/lightspeed/src/tools/loyalty.ts create mode 100644 servers/lightspeed/src/tools/manufacturers.ts delete mode 100644 servers/lightspeed/src/tools/registers-tools.ts create mode 100644 servers/lightspeed/src/tools/registers.ts delete mode 100644 servers/lightspeed/src/tools/reporting-tools.ts delete mode 100644 servers/lightspeed/src/tools/reporting.ts create mode 100644 servers/lightspeed/src/tools/reports.ts delete mode 100644 servers/lightspeed/src/tools/suppliers.ts delete mode 100644 servers/lightspeed/src/tools/taxes-tools.ts create mode 100644 servers/lightspeed/src/tools/vendors.ts create mode 100644 servers/lightspeed/src/tools/workorders.ts create mode 100644 servers/lightspeed/src/ui/analytics/App.tsx create mode 100644 servers/lightspeed/src/ui/analytics/app.css create mode 100644 servers/lightspeed/src/ui/analytics/index.html create mode 100644 servers/lightspeed/src/ui/analytics/main.tsx create mode 100644 servers/lightspeed/src/ui/analytics/vite.config.ts create mode 100644 servers/lightspeed/src/ui/category-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/category-manager/app.css create mode 100644 servers/lightspeed/src/ui/category-manager/index.html create mode 100644 servers/lightspeed/src/ui/category-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/category-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/customer-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/customer-manager/app.css create mode 100644 servers/lightspeed/src/ui/customer-manager/index.html create mode 100644 servers/lightspeed/src/ui/customer-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/customer-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/dashboard/App.tsx create mode 100644 servers/lightspeed/src/ui/dashboard/app.css create mode 100644 servers/lightspeed/src/ui/dashboard/index.html create mode 100644 servers/lightspeed/src/ui/dashboard/main.tsx create mode 100644 servers/lightspeed/src/ui/dashboard/vite.config.ts create mode 100644 servers/lightspeed/src/ui/discount-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/discount-manager/app.css create mode 100644 servers/lightspeed/src/ui/discount-manager/index.html create mode 100644 servers/lightspeed/src/ui/discount-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/discount-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/employee-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/employee-manager/app.css create mode 100644 servers/lightspeed/src/ui/employee-manager/index.html create mode 100644 servers/lightspeed/src/ui/employee-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/employee-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/inventory-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/inventory-manager/app.css create mode 100644 servers/lightspeed/src/ui/inventory-manager/index.html create mode 100644 servers/lightspeed/src/ui/inventory-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/inventory-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/low-stock-alert/App.tsx create mode 100644 servers/lightspeed/src/ui/low-stock-alert/app.css create mode 100644 servers/lightspeed/src/ui/low-stock-alert/index.html create mode 100644 servers/lightspeed/src/ui/low-stock-alert/main.tsx create mode 100644 servers/lightspeed/src/ui/low-stock-alert/vite.config.ts create mode 100644 servers/lightspeed/src/ui/order-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/order-manager/app.css create mode 100644 servers/lightspeed/src/ui/order-manager/index.html create mode 100644 servers/lightspeed/src/ui/order-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/order-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/product-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/product-manager/app.css create mode 100644 servers/lightspeed/src/ui/product-manager/index.html create mode 100644 servers/lightspeed/src/ui/product-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/product-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/quick-sale/App.tsx create mode 100644 servers/lightspeed/src/ui/quick-sale/app.css create mode 100644 servers/lightspeed/src/ui/quick-sale/index.html create mode 100644 servers/lightspeed/src/ui/quick-sale/main.tsx create mode 100644 servers/lightspeed/src/ui/quick-sale/vite.config.ts create mode 100644 servers/lightspeed/src/ui/react-app/customer-detail.tsx create mode 100644 servers/lightspeed/src/ui/react-app/customer-grid.tsx create mode 100644 servers/lightspeed/src/ui/react-app/inventory-tracker.tsx create mode 100644 servers/lightspeed/src/ui/react-app/product-dashboard.tsx create mode 100644 servers/lightspeed/src/ui/react-app/product-detail.tsx create mode 100644 servers/lightspeed/src/ui/react-app/product-grid.tsx create mode 100644 servers/lightspeed/src/ui/react-app/sales-dashboard.tsx create mode 100644 servers/lightspeed/src/ui/react-app/sales-detail.tsx create mode 100644 servers/lightspeed/src/ui/register-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/register-manager/app.css create mode 100644 servers/lightspeed/src/ui/register-manager/index.html create mode 100644 servers/lightspeed/src/ui/register-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/register-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/reports/App.tsx create mode 100644 servers/lightspeed/src/ui/reports/app.css create mode 100644 servers/lightspeed/src/ui/reports/index.html create mode 100644 servers/lightspeed/src/ui/reports/main.tsx create mode 100644 servers/lightspeed/src/ui/reports/vite.config.ts create mode 100644 servers/lightspeed/src/ui/sales-terminal/App.tsx create mode 100644 servers/lightspeed/src/ui/sales-terminal/app.css create mode 100644 servers/lightspeed/src/ui/sales-terminal/index.html create mode 100644 servers/lightspeed/src/ui/sales-terminal/main.tsx create mode 100644 servers/lightspeed/src/ui/sales-terminal/vite.config.ts create mode 100644 servers/lightspeed/src/ui/transfer-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/transfer-manager/app.css create mode 100644 servers/lightspeed/src/ui/transfer-manager/index.html create mode 100644 servers/lightspeed/src/ui/transfer-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/transfer-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/vendor-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/vendor-manager/app.css create mode 100644 servers/lightspeed/src/ui/vendor-manager/index.html create mode 100644 servers/lightspeed/src/ui/vendor-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/vendor-manager/vite.config.ts create mode 100644 servers/lightspeed/src/ui/workorder-manager/App.tsx create mode 100644 servers/lightspeed/src/ui/workorder-manager/app.css create mode 100644 servers/lightspeed/src/ui/workorder-manager/index.html create mode 100644 servers/lightspeed/src/ui/workorder-manager/main.tsx create mode 100644 servers/lightspeed/src/ui/workorder-manager/vite.config.ts delete mode 100644 servers/squarespace/package.json create mode 100755 servers/squarespace/scripts/generate-apps.sh delete mode 100644 servers/squarespace/src/apps/blog-manager.ts delete mode 100644 servers/squarespace/src/apps/collection-browser.ts delete mode 100644 servers/squarespace/src/apps/customer-grid.ts delete mode 100644 servers/squarespace/src/apps/form-submissions.ts delete mode 100644 servers/squarespace/src/apps/inventory-tracker.ts delete mode 100644 servers/squarespace/src/apps/order-dashboard.ts delete mode 100644 servers/squarespace/src/apps/order-detail.ts delete mode 100644 servers/squarespace/src/apps/page-manager.ts delete mode 100644 servers/squarespace/src/apps/product-dashboard.ts delete mode 100644 servers/squarespace/src/apps/site-analytics.ts create mode 100644 servers/squarespace/src/main.ts create mode 100644 servers/squarespace/src/server.ts delete mode 100644 servers/squarespace/src/tools/analytics-tools.ts create mode 100644 servers/squarespace/src/tools/blog.ts delete mode 100644 servers/squarespace/src/tools/collections-tools.ts create mode 100644 servers/squarespace/src/tools/commerce-inventory.ts create mode 100644 servers/squarespace/src/tools/commerce-orders.ts create mode 100644 servers/squarespace/src/tools/commerce-products.ts create mode 100644 servers/squarespace/src/tools/commerce-transactions.ts delete mode 100644 servers/squarespace/src/tools/customers.ts delete mode 100644 servers/squarespace/src/tools/forms-tools.ts delete mode 100644 servers/squarespace/src/tools/inventory.ts delete mode 100644 servers/squarespace/src/tools/menu-tools.ts delete mode 100644 servers/squarespace/src/tools/orders.ts delete mode 100644 servers/squarespace/src/tools/products.ts create mode 100644 servers/squarespace/src/tools/profiles.ts delete mode 100644 servers/squarespace/src/tools/settings-tools.ts delete mode 100644 servers/squarespace/src/tools/sites.ts create mode 100644 servers/squarespace/src/tools/webhooks.ts create mode 100644 servers/squarespace/src/ui/react-app/analytics/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/analytics/index.html create mode 100644 servers/squarespace/src/ui/react-app/analytics/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/analytics/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/blog/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/blog/index.html create mode 100644 servers/squarespace/src/ui/react-app/blog/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/blog/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/bulk-editor/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/bulk-editor/index.html create mode 100644 servers/squarespace/src/ui/react-app/bulk-editor/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/bulk-editor/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/customers/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/customers/index.html create mode 100644 servers/squarespace/src/ui/react-app/customers/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/customers/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/discounts/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/discounts/index.html create mode 100644 servers/squarespace/src/ui/react-app/discounts/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/discounts/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/forms/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/forms/index.html create mode 100644 servers/squarespace/src/ui/react-app/forms/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/forms/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/inventory/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/inventory/index.html create mode 100644 servers/squarespace/src/ui/react-app/inventory/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/inventory/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/orders/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/orders/index.html create mode 100644 servers/squarespace/src/ui/react-app/orders/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/orders/styles.css create mode 100644 servers/squarespace/src/ui/react-app/orders/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/pages/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/pages/index.html create mode 100644 servers/squarespace/src/ui/react-app/pages/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/pages/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/products/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/products/index.html create mode 100644 servers/squarespace/src/ui/react-app/products/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/products/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/reports/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/reports/index.html create mode 100644 servers/squarespace/src/ui/react-app/reports/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/reports/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/seo/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/seo/index.html create mode 100644 servers/squarespace/src/ui/react-app/seo/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/seo/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/settings/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/settings/index.html create mode 100644 servers/squarespace/src/ui/react-app/settings/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/settings/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/shipping/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/shipping/index.html create mode 100644 servers/squarespace/src/ui/react-app/shipping/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/shipping/vite.config.ts create mode 100644 servers/squarespace/src/ui/react-app/webhooks/App.tsx create mode 100644 servers/squarespace/src/ui/react-app/webhooks/index.html create mode 100644 servers/squarespace/src/ui/react-app/webhooks/main.tsx create mode 100644 servers/squarespace/src/ui/react-app/webhooks/vite.config.ts delete mode 100644 servers/toast/dist/api-client.d.ts delete mode 100644 servers/toast/dist/api-client.d.ts.map delete mode 100644 servers/toast/dist/api-client.js delete mode 100644 servers/toast/dist/api-client.js.map delete mode 100644 servers/toast/dist/apps/index.d.ts delete mode 100644 servers/toast/dist/apps/index.d.ts.map delete mode 100644 servers/toast/dist/apps/index.js delete mode 100644 servers/toast/dist/apps/index.js.map delete mode 100644 servers/toast/dist/main.d.ts delete mode 100644 servers/toast/dist/main.d.ts.map delete mode 100644 servers/toast/dist/main.js delete mode 100644 servers/toast/dist/main.js.map delete mode 100644 servers/toast/dist/server.d.ts delete mode 100644 servers/toast/dist/server.d.ts.map delete mode 100644 servers/toast/dist/server.js delete mode 100644 servers/toast/dist/server.js.map delete mode 100644 servers/toast/dist/tools/cash-tools.d.ts delete mode 100644 servers/toast/dist/tools/cash-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/cash-tools.js delete mode 100644 servers/toast/dist/tools/cash-tools.js.map delete mode 100644 servers/toast/dist/tools/customers-tools.d.ts delete mode 100644 servers/toast/dist/tools/customers-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/customers-tools.js delete mode 100644 servers/toast/dist/tools/customers-tools.js.map delete mode 100644 servers/toast/dist/tools/employees-tools.d.ts delete mode 100644 servers/toast/dist/tools/employees-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/employees-tools.js delete mode 100644 servers/toast/dist/tools/employees-tools.js.map delete mode 100644 servers/toast/dist/tools/inventory-tools.d.ts delete mode 100644 servers/toast/dist/tools/inventory-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/inventory-tools.js delete mode 100644 servers/toast/dist/tools/inventory-tools.js.map delete mode 100644 servers/toast/dist/tools/labor-tools.d.ts delete mode 100644 servers/toast/dist/tools/labor-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/labor-tools.js delete mode 100644 servers/toast/dist/tools/labor-tools.js.map delete mode 100644 servers/toast/dist/tools/menus-tools.d.ts delete mode 100644 servers/toast/dist/tools/menus-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/menus-tools.js delete mode 100644 servers/toast/dist/tools/menus-tools.js.map delete mode 100644 servers/toast/dist/tools/orders-tools.d.ts delete mode 100644 servers/toast/dist/tools/orders-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/orders-tools.js delete mode 100644 servers/toast/dist/tools/orders-tools.js.map delete mode 100644 servers/toast/dist/tools/payments-tools.d.ts delete mode 100644 servers/toast/dist/tools/payments-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/payments-tools.js delete mode 100644 servers/toast/dist/tools/payments-tools.js.map delete mode 100644 servers/toast/dist/tools/reporting-tools.d.ts delete mode 100644 servers/toast/dist/tools/reporting-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/reporting-tools.js delete mode 100644 servers/toast/dist/tools/reporting-tools.js.map delete mode 100644 servers/toast/dist/tools/restaurant-tools.d.ts delete mode 100644 servers/toast/dist/tools/restaurant-tools.d.ts.map delete mode 100644 servers/toast/dist/tools/restaurant-tools.js delete mode 100644 servers/toast/dist/tools/restaurant-tools.js.map delete mode 100644 servers/toast/dist/types/index.d.ts delete mode 100644 servers/toast/dist/types/index.d.ts.map delete mode 100644 servers/toast/dist/types/index.js delete mode 100644 servers/toast/dist/types/index.js.map create mode 100644 servers/toast/package.json create mode 100644 servers/toast/src/clients/toast.ts create mode 100644 servers/toast/src/main.ts create mode 100644 servers/toast/src/server.ts create mode 100644 servers/toast/src/tools/analytics.ts create mode 100644 servers/toast/src/tools/availability.ts create mode 100644 servers/toast/src/tools/cash-management.ts create mode 100644 servers/toast/src/tools/configuration.ts create mode 100644 servers/toast/src/tools/customers.ts create mode 100644 servers/toast/src/tools/employees.ts create mode 100644 servers/toast/src/tools/inventory.ts create mode 100644 servers/toast/src/tools/kitchen.ts create mode 100644 servers/toast/src/tools/labor.ts create mode 100644 servers/toast/src/tools/menus.ts create mode 100644 servers/toast/src/tools/orders.ts create mode 100644 servers/toast/src/tools/partners.ts create mode 100644 servers/toast/src/tools/payments.ts create mode 100644 servers/toast/src/tools/reports.ts create mode 100644 servers/toast/src/tools/restaurant.ts create mode 100644 servers/toast/src/tools/stock.ts create mode 100644 servers/toast/src/types/index.ts create mode 100644 servers/toast/src/ui/cash-drawer-manager/index.html create mode 100644 servers/toast/src/ui/cash-drawer-manager/package.json create mode 100644 servers/toast/src/ui/cash-drawer-manager/src/main.tsx create mode 100644 servers/toast/src/ui/cash-drawer-manager/vite.config.ts create mode 100644 servers/toast/src/ui/customer-directory/index.html create mode 100644 servers/toast/src/ui/customer-directory/package.json create mode 100644 servers/toast/src/ui/customer-directory/src/main.tsx create mode 100644 servers/toast/src/ui/customer-directory/vite.config.ts create mode 100644 servers/toast/src/ui/daily-reports/index.html create mode 100644 servers/toast/src/ui/daily-reports/package.json create mode 100644 servers/toast/src/ui/daily-reports/src/main.tsx create mode 100644 servers/toast/src/ui/daily-reports/vite.config.ts create mode 100644 servers/toast/src/ui/discount-tracker/index.html create mode 100644 servers/toast/src/ui/discount-tracker/package.json create mode 100644 servers/toast/src/ui/discount-tracker/src/main.tsx create mode 100644 servers/toast/src/ui/discount-tracker/vite.config.ts create mode 100644 servers/toast/src/ui/employee-roster/index.html create mode 100644 servers/toast/src/ui/employee-roster/package.json create mode 100644 servers/toast/src/ui/employee-roster/src/main.tsx create mode 100644 servers/toast/src/ui/employee-roster/vite.config.ts create mode 100644 servers/toast/src/ui/hourly-sales/index.html create mode 100644 servers/toast/src/ui/hourly-sales/package.json create mode 100644 servers/toast/src/ui/hourly-sales/src/main.tsx create mode 100644 servers/toast/src/ui/hourly-sales/vite.config.ts create mode 100644 servers/toast/src/ui/inventory-manager/index.html create mode 100644 servers/toast/src/ui/inventory-manager/package.json create mode 100644 servers/toast/src/ui/inventory-manager/src/main.tsx create mode 100644 servers/toast/src/ui/inventory-manager/vite.config.ts create mode 100644 servers/toast/src/ui/kitchen-display/index.html create mode 100644 servers/toast/src/ui/kitchen-display/package.json create mode 100644 servers/toast/src/ui/kitchen-display/src/main.tsx create mode 100644 servers/toast/src/ui/kitchen-display/vite.config.ts create mode 100644 servers/toast/src/ui/labor-scheduler/index.html create mode 100644 servers/toast/src/ui/labor-scheduler/package.json create mode 100644 servers/toast/src/ui/labor-scheduler/src/main.tsx create mode 100644 servers/toast/src/ui/labor-scheduler/vite.config.ts create mode 100644 servers/toast/src/ui/menu-manager/index.html create mode 100644 servers/toast/src/ui/menu-manager/package.json create mode 100644 servers/toast/src/ui/menu-manager/src/main.tsx create mode 100644 servers/toast/src/ui/menu-manager/vite.config.ts create mode 100644 servers/toast/src/ui/orders-dashboard/index.html create mode 100644 servers/toast/src/ui/orders-dashboard/package.json create mode 100644 servers/toast/src/ui/orders-dashboard/src/App.tsx create mode 100644 servers/toast/src/ui/orders-dashboard/src/main.tsx create mode 100644 servers/toast/src/ui/orders-dashboard/vite.config.ts create mode 100644 servers/toast/src/ui/payment-terminal/index.html create mode 100644 servers/toast/src/ui/payment-terminal/package.json create mode 100644 servers/toast/src/ui/payment-terminal/src/main.tsx create mode 100644 servers/toast/src/ui/payment-terminal/vite.config.ts create mode 100644 servers/toast/src/ui/revenue-centers/index.html create mode 100644 servers/toast/src/ui/revenue-centers/package.json create mode 100644 servers/toast/src/ui/revenue-centers/src/main.tsx create mode 100644 servers/toast/src/ui/revenue-centers/vite.config.ts create mode 100644 servers/toast/src/ui/sales-analytics/index.html create mode 100644 servers/toast/src/ui/sales-analytics/package.json create mode 100644 servers/toast/src/ui/sales-analytics/src/main.tsx create mode 100644 servers/toast/src/ui/sales-analytics/vite.config.ts create mode 100644 servers/toast/src/ui/table-manager/index.html create mode 100644 servers/toast/src/ui/table-manager/package.json create mode 100644 servers/toast/src/ui/table-manager/src/main.tsx create mode 100644 servers/toast/src/ui/table-manager/vite.config.ts rename servers/{squarespace => toast}/tsconfig.json (75%) delete mode 100644 servers/touchbistro/.gitignore delete mode 100644 servers/touchbistro/package.json delete mode 100644 servers/touchbistro/src/api-client.ts create mode 100644 servers/touchbistro/src/clients/touchbistro.ts delete mode 100644 servers/touchbistro/src/tools/customers-tools.ts create mode 100644 servers/touchbistro/src/tools/customers.ts create mode 100644 servers/touchbistro/src/tools/discounts.ts create mode 100644 servers/touchbistro/src/tools/employees.ts create mode 100644 servers/touchbistro/src/tools/giftcards.ts delete mode 100644 servers/touchbistro/src/tools/inventory-tools.ts create mode 100644 servers/touchbistro/src/tools/inventory.ts create mode 100644 servers/touchbistro/src/tools/loyalty.ts delete mode 100644 servers/touchbistro/src/tools/menus-tools.ts create mode 100644 servers/touchbistro/src/tools/menus.ts delete mode 100644 servers/touchbistro/src/tools/orders-tools.ts create mode 100644 servers/touchbistro/src/tools/orders.ts delete mode 100644 servers/touchbistro/src/tools/payments-tools.ts create mode 100644 servers/touchbistro/src/tools/payments.ts delete mode 100644 servers/touchbistro/src/tools/reporting-tools.ts create mode 100644 servers/touchbistro/src/tools/reports.ts delete mode 100644 servers/touchbistro/src/tools/reservations-tools.ts create mode 100644 servers/touchbistro/src/tools/reservations.ts delete mode 100644 servers/touchbistro/src/tools/staff-tools.ts delete mode 100644 servers/touchbistro/src/tools/tables-tools.ts create mode 100644 servers/touchbistro/src/tools/tables.ts delete mode 100644 servers/touchbistro/src/types.ts create mode 100644 servers/touchbistro/src/types/index.ts delete mode 100644 servers/touchbistro/tsconfig.json create mode 100644 servers/wave/src/ui/react-app/accounts-payable/App.tsx create mode 100644 servers/wave/src/ui/react-app/balance-sheet-viewer/App.tsx create mode 100644 servers/wave/src/ui/react-app/estimate-creator/App.tsx create mode 100644 servers/wave/src/ui/react-app/profit-loss-viewer/App.tsx create mode 100644 servers/wave/src/ui/react-app/recurring-invoice-manager/App.tsx create mode 100644 servers/wave/src/ui/react-app/transaction-browser/App.tsx diff --git a/servers/close/package.json b/servers/close/package.json index 136b74c..2185e2b 100644 --- a/servers/close/package.json +++ b/servers/close/package.json @@ -1,20 +1,31 @@ { - "name": "mcp-server-close", + "name": "@mcpengine/close-server", "version": "1.0.0", + "description": "Complete Close CRM MCP server with 60+ tools and 22 apps", "type": "module", - "main": "dist/index.js", + "main": "dist/main.js", + "bin": { + "close-mcp": "./dist/main.js" + }, "scripts": { "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "dev": "tsc --watch", + "start": "node dist/main.js", + "prepare": "npm run build" }, + "keywords": [ + "mcp", + "close", + "crm", + "sales" + ], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.0.0", + "typescript": "^5.7.2" } } diff --git a/servers/close/src/apps/activity-feed.ts b/servers/close/src/apps/activity-feed.ts new file mode 100644 index 0000000..40ebc9b --- /dev/null +++ b/servers/close/src/apps/activity-feed.ts @@ -0,0 +1,68 @@ +// Activity Feed MCP App + +export function generateActivityFeed(activities: any[]) { + const getActivityIcon = (type: string) => { + const icons: Record = { + note: '📝', + call: '📞', + email: '📧', + sms: '💬', + meeting: '📅', + }; + return icons[type] || '📋'; + }; + + return ` + + + + + + Activity Feed + + + +
+

📊 Activity Feed (${activities.length})

+
+ ${activities.length > 0 ? activities.map((activity: any) => ` +
+
${getActivityIcon(activity._type)}
+
+
+
${activity._type ? activity._type.charAt(0).toUpperCase() + activity._type.slice(1) : 'Activity'}
+
${activity.date_created ? new Date(activity.date_created).toLocaleString() : ''}
+
+
+ ${activity.note || activity.text || activity.subject || 'No content'} +
+
+ ${activity.user_name ? `👤 ${activity.user_name}` : ''} + ${activity.lead_id ? ` • 🏢 Lead: ${activity.lead_id}` : ''} + ${activity.contact_id ? ` • 👤 Contact: ${activity.contact_id}` : ''} +
+
+
+ `).join('') : '
No activities found
'} +
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/activity-timeline.ts b/servers/close/src/apps/activity-timeline.ts new file mode 100644 index 0000000..25bd25f --- /dev/null +++ b/servers/close/src/apps/activity-timeline.ts @@ -0,0 +1,65 @@ +// Activity Timeline MCP App + +export function generateActivityTimeline(activities: any[]) { + const sortedActivities = [...activities].sort((a, b) => { + const dateA = new Date(a.date_created || 0).getTime(); + const dateB = new Date(b.date_created || 0).getTime(); + return dateB - dateA; + }); + + return ` + + + + + + Activity Timeline + + + +
+

⏱️ Activity Timeline

+
+ ${sortedActivities.map((activity: any) => ` +
+
+
+
+ ${activity._type === 'note' ? '📝 Note' : + activity._type === 'call' ? '📞 Call' : + activity._type === 'email' ? '📧 Email' : + activity._type === 'sms' ? '💬 SMS' : + activity._type === 'meeting' ? '📅 Meeting' : '📋 Activity'} +
+
${activity.date_created ? new Date(activity.date_created).toLocaleString() : 'Unknown date'}
+
+
+ ${activity.note || activity.text || activity.subject || activity.title || 'No content available'} +
+
+ ${activity.user_name ? `Created by ${activity.user_name}` : ''} + ${activity.lead_id ? ` • Lead: ${activity.lead_id}` : ''} + ${activity.duration ? ` • Duration: ${Math.floor(activity.duration / 60)} min` : ''} +
+
+ `).join('')} +
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/bulk-actions.ts b/servers/close/src/apps/bulk-actions.ts new file mode 100644 index 0000000..f50eb6d --- /dev/null +++ b/servers/close/src/apps/bulk-actions.ts @@ -0,0 +1,75 @@ +// Bulk Actions MCP App + +export function generateBulkActions(data: any) { + const leads = data.leads || []; + const selectedCount = data.selectedCount || leads.length; + + return ` + + + + + + Bulk Actions + + + +
+

⚡ Bulk Actions

+ +
+
${selectedCount} selected
+ + + + + +
+ +
+ + + + + + + + + + + + ${leads.slice(0, 50).map(lead => ` + + + + + + + + `).join('')} + +
Lead NameStatusContactsCreated
${lead.name || lead.display_name}${lead.status_label || 'No Status'}${lead.contacts?.length || 0}${lead.date_created ? new Date(lead.date_created).toLocaleDateString() : 'N/A'}
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/call-log.ts b/servers/close/src/apps/call-log.ts new file mode 100644 index 0000000..32e64c4 --- /dev/null +++ b/servers/close/src/apps/call-log.ts @@ -0,0 +1,89 @@ +// Call Log MCP App + +export function generateCallLog(calls: any[]) { + const totalCalls = calls.length; + const totalDuration = calls.reduce((sum: number, call: any) => sum + (call.duration || 0), 0); + const avgDuration = totalCalls > 0 ? totalDuration / totalCalls : 0; + const inbound = calls.filter(c => c.direction === 'inbound').length; + const outbound = calls.filter(c => c.direction === 'outbound').length; + + return ` + + + + + + Call Log + + + +
+

📞 Call Log

+ +
+
+
Total Calls
+
${totalCalls}
+
+
+
Inbound
+
${inbound}
+
+
+
Outbound
+
${outbound}
+
+
+
Avg Duration
+
${Math.round(avgDuration / 60)}min
+
+
+ +
+ + + + + + + + + + + + + ${calls.length > 0 ? calls.map((call: any) => ` + + + + + + + + + `).join('') : ''} + +
DateDirectionPhoneDurationDispositionUser
${call.date_created ? new Date(call.date_created).toLocaleString() : 'N/A'}${call.direction || 'Unknown'}${call.phone || 'N/A'}${call.duration ? Math.floor(call.duration / 60) + ' min ' + (call.duration % 60) + ' sec' : 'N/A'}${call.disposition || 'N/A'}${call.user_name || 'N/A'}
No calls found
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/contact-detail.ts b/servers/close/src/apps/contact-detail.ts new file mode 100644 index 0000000..5d3dc68 --- /dev/null +++ b/servers/close/src/apps/contact-detail.ts @@ -0,0 +1,106 @@ +// Contact Detail MCP App + +export function generateContactDetail(contact: any) { + return ` + + + + + + ${contact.name} - Contact Detail + + + +
+
+
${contact.name ? contact.name.charAt(0).toUpperCase() : '?'}
+

${contact.name || 'Unnamed Contact'}

+ ${contact.title ? `
${contact.title}
` : ''} +
+ +
+
Contact Information
+ + ${contact.emails && contact.emails.length > 0 ? ` +
+
📧
+
+
Email Addresses
+ ${contact.emails.map((e: any) => ` +
${e.email}${e.type ? ` (${e.type})` : ''}
+ `).join('')} +
+
+ ` : ''} + + ${contact.phones && contact.phones.length > 0 ? ` +
+
📞
+
+
Phone Numbers
+ ${contact.phones.map((p: any) => ` +
${p.phone}${p.type ? ` (${p.type})` : ''}
+ `).join('')} +
+
+ ` : ''} + + ${contact.urls && contact.urls.length > 0 ? ` +
+
🔗
+
+
URLs
+ ${contact.urls.map((u: any) => ` +
${u.url}${u.type ? ` (${u.type})` : ''}
+ `).join('')} +
+
+ ` : ''} +
+ +
+
Metadata
+
+
🆔
+
+
Contact ID
+
${contact.id}
+
+
+ ${contact.lead_id ? ` +
+
🏢
+
+
Lead ID
+
${contact.lead_id}
+
+
+ ` : ''} +
+
📅
+
+
Created
+
${contact.date_created ? new Date(contact.date_created).toLocaleString() : 'N/A'}
+
+
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/custom-fields-manager.ts b/servers/close/src/apps/custom-fields-manager.ts new file mode 100644 index 0000000..9faeb21 --- /dev/null +++ b/servers/close/src/apps/custom-fields-manager.ts @@ -0,0 +1,68 @@ +// Custom Fields Manager MCP App + +export function generateCustomFieldsManager(fields: any[]) { + const fieldsByType: Record = {}; + fields.forEach(field => { + const type = field.type || 'unknown'; + if (!fieldsByType[type]) fieldsByType[type] = []; + fieldsByType[type].push(field); + }); + + return ` + + + + + + Custom Fields Manager + + + +
+

⚙️ Custom Fields Manager

+ + ${Object.entries(fieldsByType).map(([type, typeFields]) => ` +
+
+ ${type.charAt(0).toUpperCase() + type.slice(1)} Fields + ${typeFields.length} +
+
+ ${typeFields.map(field => ` +
+
${field.name}
+
+ ${field.type} + ${field.required ? 'Required' : ''} + ${field.accepts_multiple_values ? 'Multiple' : ''} +
+
ID: ${field.id}
+ ${field.choices && field.choices.length > 0 ? ` +
Choices: ${field.choices.join(', ')}
+ ` : ''} +
+ `).join('')} +
+
+ `).join('')} +
+ + + `.trim(); +} diff --git a/servers/close/src/apps/email-log.ts b/servers/close/src/apps/email-log.ts new file mode 100644 index 0000000..58434f3 --- /dev/null +++ b/servers/close/src/apps/email-log.ts @@ -0,0 +1,95 @@ +// Email Log MCP App + +export function generateEmailLog(emails: any[]) { + const totalEmails = emails.length; + const sent = emails.filter(e => e.direction === 'outbound').length; + const received = emails.filter(e => e.direction === 'inbound').length; + const totalOpens = emails.reduce((sum: number, e: any) => sum + (e.opens || 0), 0); + const totalClicks = emails.reduce((sum: number, e: any) => sum + (e.clicks || 0), 0); + + return ` + + + + + + Email Log + + + +
+

📧 Email Log

+ +
+
+
Total Emails
+
${totalEmails}
+
+
+
Sent
+
${sent}
+
+
+
Received
+
${received}
+
+
+
Total Opens
+
${totalOpens}
+
+
+
Total Clicks
+
${totalClicks}
+
+
+ + +
+ + + `.trim(); +} diff --git a/servers/close/src/apps/lead-dashboard.ts b/servers/close/src/apps/lead-dashboard.ts new file mode 100644 index 0000000..657af87 --- /dev/null +++ b/servers/close/src/apps/lead-dashboard.ts @@ -0,0 +1,102 @@ +// Lead Dashboard MCP App + +export function generateLeadDashboard(data: any) { + const leads = data.leads || []; + const statuses = data.statuses || []; + + const statusCounts: Record = {}; + leads.forEach((lead: any) => { + const status = lead.status_label || "No Status"; + statusCounts[status] = (statusCounts[status] || 0) + 1; + }); + + return ` + + + + + + Close CRM - Lead Dashboard + + + +
+
+

📊 Lead Dashboard

+
Overview of all leads and statuses
+
+ +
+
+
Total Leads
+
${leads.length}
+
+
+
Unique Statuses
+
${Object.keys(statusCounts).length}
+
+
+
Most Common
+
${Object.entries(statusCounts).sort((a, b) => (b[1] as number) - (a[1] as number))[0]?.[0] || 'N/A'}
+
+
+ +
+
Leads by Status
+ ${Object.entries(statusCounts).sort((a, b) => (b[1] as number) - (a[1] as number)).map(([status, count]) => ` +
+ ${status}: ${count} +
+ `).join('')} +
+ +
+ + + + + + + + + + + ${leads.slice(0, 50).map((lead: any) => ` + + + + + + + `).join('')} + +
Lead NameStatusContactsCreated
${lead.name || lead.display_name}${lead.status_label || 'No Status'}${lead.contacts?.length || 0}${lead.date_created ? new Date(lead.date_created).toLocaleDateString() : 'N/A'}
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/lead-detail.ts b/servers/close/src/apps/lead-detail.ts new file mode 100644 index 0000000..d4ec813 --- /dev/null +++ b/servers/close/src/apps/lead-detail.ts @@ -0,0 +1,109 @@ +// Lead Detail MCP App + +export function generateLeadDetail(lead: any) { + return ` + + + + + + ${lead.name} - Lead Detail + + + +
+
+

${lead.name}

+ ${lead.status_label || 'No Status'} +
+ +
+
+
+
Lead Information
+
+
Lead ID
+
${lead.id}
+
+ ${lead.description ? ` +
+
Description
+
${lead.description}
+
+ ` : ''} + ${lead.url ? ` +
+
Website
+ +
+ ` : ''} +
+
Created
+
${lead.date_created ? new Date(lead.date_created).toLocaleString() : 'N/A'}
+
+
+
Last Updated
+
${lead.date_updated ? new Date(lead.date_updated).toLocaleString() : 'N/A'}
+
+
+ +
+
Contacts (${lead.contacts?.length || 0})
+ ${lead.contacts && lead.contacts.length > 0 ? lead.contacts.map((contact: any) => ` +
+
${contact.name || 'Unnamed Contact'}
+ ${contact.title ? `
${contact.title}
` : ''} + ${contact.emails && contact.emails.length > 0 ? `
📧 ${contact.emails.map((e: any) => e.email).join(', ')}
` : ''} + ${contact.phones && contact.phones.length > 0 ? `
📞 ${contact.phones.map((p: any) => p.phone).join(', ')}
` : ''} +
+ `).join('') : '

No contacts

'} +
+
+ +
+
+
Quick Stats
+
+
Opportunities
+
${lead.opportunities?.length || 0}
+
+
+
Tasks
+
${lead.tasks?.length || 0}
+
+
+ + ${lead.custom && Object.keys(lead.custom).length > 0 ? ` +
+
Custom Fields
+ ${Object.entries(lead.custom).map(([key, value]) => ` +
+
${key}
+
${value}
+
+ `).join('')} +
+ ` : ''} +
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/lead-grid.ts b/servers/close/src/apps/lead-grid.ts new file mode 100644 index 0000000..129845a --- /dev/null +++ b/servers/close/src/apps/lead-grid.ts @@ -0,0 +1,49 @@ +// Lead Grid MCP App + +export function generateLeadGrid(leads: any[]) { + return ` + + + + + + Lead Grid + + + +
+

📇 Lead Grid (${leads.length})

+
+ ${leads.map(lead => ` +
+
+
${lead.name}
+ ${lead.status_label || 'No Status'} +
+ ${lead.description ? `
${lead.description.substring(0, 100)}${lead.description.length > 100 ? '...' : ''}
` : ''} + ${lead.url ? `
🌐 ${lead.url}
` : ''} +
+
👤 ${lead.contacts?.length || 0} contacts
+
💼 ${lead.opportunities?.length || 0} opps
+
+
+ `).join('')} +
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/opportunity-dashboard.ts b/servers/close/src/apps/opportunity-dashboard.ts new file mode 100644 index 0000000..ad70566 --- /dev/null +++ b/servers/close/src/apps/opportunity-dashboard.ts @@ -0,0 +1,92 @@ +// Opportunity Dashboard MCP App + +export function generateOpportunityDashboard(data: any) { + const opportunities = data.opportunities || []; + + const totalValue = opportunities.reduce((sum: number, opp: any) => sum + (opp.value || 0), 0); + const avgValue = opportunities.length > 0 ? totalValue / opportunities.length : 0; + const wonOpps = opportunities.filter((o: any) => o.status_type === 'won'); + const wonValue = wonOpps.reduce((sum: number, opp: any) => sum + (opp.value || 0), 0); + + return ` + + + + + + Opportunity Dashboard + + + +
+

💼 Opportunity Dashboard

+ +
+
+
Total Opportunities
+
${opportunities.length}
+
+
+
Total Value
+
$${totalValue.toLocaleString()}
+
Across all opportunities
+
+
+
Average Value
+
$${Math.round(avgValue).toLocaleString()}
+
+
+
Won Revenue
+
$${wonValue.toLocaleString()}
+
${wonOpps.length} won opportunities
+
+
+ +
+ + + + + + + + + + + + ${opportunities.slice(0, 50).map((opp: any) => ` + + + + + + + + `).join('')} + +
LeadValueStatusConfidenceCreated
${opp.lead_name || opp.lead_id}$${(opp.value || 0).toLocaleString()}${opp.value_period ? ` /${opp.value_period}` : ''}${opp.status_label || opp.status_type || 'Unknown'}${opp.confidence !== undefined ? opp.confidence + '%' : 'N/A'}${opp.date_created ? new Date(opp.date_created).toLocaleDateString() : 'N/A'}
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/opportunity-detail.ts b/servers/close/src/apps/opportunity-detail.ts new file mode 100644 index 0000000..e258fc9 --- /dev/null +++ b/servers/close/src/apps/opportunity-detail.ts @@ -0,0 +1,96 @@ +// Opportunity Detail MCP App + +export function generateOpportunityDetail(opportunity: any) { + return ` + + + + + + Opportunity Detail + + + +
+
+
$${(opportunity.value || 0).toLocaleString()}
+ ${opportunity.status_label || opportunity.status_type || 'Unknown Status'} + ${opportunity.value_period ? `
per ${opportunity.value_period}
` : ''} +
+ +
+
+
Opportunity Details
+
+
Opportunity ID
+
${opportunity.id}
+
+
+
Lead ID
+
${opportunity.lead_id}
+
+
+
Status ID
+
${opportunity.status_id}
+
+ ${opportunity.user_name ? ` +
+
Owner
+
${opportunity.user_name}
+
+ ` : ''} + ${opportunity.note ? ` +
+
Notes
+
${opportunity.note}
+
+ ` : ''} +
+ +
+
Metrics
+
+
Confidence
+
${opportunity.confidence !== undefined ? opportunity.confidence + '%' : 'Not set'}
+ ${opportunity.confidence !== undefined ? ` +
+
+
+ ` : ''} +
+
+
Created
+
${opportunity.date_created ? new Date(opportunity.date_created).toLocaleString() : 'N/A'}
+
+
+
Last Updated
+
${opportunity.date_updated ? new Date(opportunity.date_updated).toLocaleString() : 'N/A'}
+
+ ${opportunity.date_won ? ` +
+
Won Date
+
${new Date(opportunity.date_won).toLocaleString()}
+
+ ` : ''} +
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/pipeline-funnel.ts b/servers/close/src/apps/pipeline-funnel.ts new file mode 100644 index 0000000..aba16aa --- /dev/null +++ b/servers/close/src/apps/pipeline-funnel.ts @@ -0,0 +1,70 @@ +// Pipeline Funnel MCP App + +export function generatePipelineFunnel(data: any) { + const pipeline = data.pipeline || {}; + const opportunities = data.opportunities || []; + const statuses = pipeline.statuses || []; + + const funnelData = statuses.map((status: any) => { + const opps = opportunities.filter((opp: any) => opp.status_id === status.id); + const totalValue = opps.reduce((sum: number, opp: any) => sum + (opp.value || 0), 0); + return { + label: status.label, + count: opps.length, + value: totalValue, + }; + }); + + const maxCount = Math.max(...funnelData.map((d) => d.count), 1); + + return ` + + + + + + ${pipeline.name || 'Pipeline'} - Funnel + + + +
+

🔄 ${pipeline.name || 'Pipeline Funnel'}

+
+ ${funnelData.map((stage, index) => { + const widthPercent = Math.max(30, (stage.count / maxCount) * 100); + const hue = 210 + (index * 15); + + return ` +
+
+
+
${stage.label}
+
+
${stage.count}
+
$${stage.value.toLocaleString()}
+
+
+
+
+ `; + }).join('')} +
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/pipeline-kanban.ts b/servers/close/src/apps/pipeline-kanban.ts new file mode 100644 index 0000000..e8fcea1 --- /dev/null +++ b/servers/close/src/apps/pipeline-kanban.ts @@ -0,0 +1,74 @@ +// Pipeline Kanban MCP App + +export function generatePipelineKanban(data: any) { + const pipeline = data.pipeline || {}; + const opportunities = data.opportunities || []; + const statuses = pipeline.statuses || []; + + const oppsByStatus: Record = {}; + statuses.forEach((status: any) => { + oppsByStatus[status.id] = opportunities.filter( + (opp: any) => opp.status_id === status.id + ); + }); + + return ` + + + + + + ${pipeline.name || 'Pipeline'} - Kanban + + + +
+

📊 ${pipeline.name || 'Pipeline Kanban'}

+
+ ${statuses.map((status: any) => { + const opps = oppsByStatus[status.id] || []; + const totalValue = opps.reduce((sum: number, opp: any) => sum + (opp.value || 0), 0); + + return ` +
+
+ ${status.label} + ${opps.length} +
+ ${opps.map((opp: any) => ` +
+
${opp.lead_name || opp.lead_id}
+
$${(opp.value || 0).toLocaleString()}
+
+ ${opp.confidence !== undefined ? `${opp.confidence}% confidence` : 'No confidence set'} +
+
+ `).join('')} + ${opps.length > 0 ? ` +
+ Total: $${totalValue.toLocaleString()} +
+ ` : '
No opportunities
'} +
+ `; + }).join('')} +
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/report-builder.ts b/servers/close/src/apps/report-builder.ts new file mode 100644 index 0000000..5236c3e --- /dev/null +++ b/servers/close/src/apps/report-builder.ts @@ -0,0 +1,99 @@ +// Report Builder MCP App + +export function generateReportBuilder(data: any) { + const reports = data.reports || []; + const templates = [ + { name: 'Lead Status Changes', type: 'lead_status_changes' }, + { name: 'Opportunity Funnel', type: 'opportunity_funnel' }, + { name: 'Activity Overview', type: 'activity_overview' }, + { name: 'Revenue Forecast', type: 'revenue_forecast' }, + { name: 'User Leaderboard', type: 'leaderboard' }, + ]; + + return ` + + + + + + Report Builder + + + +
+

📊 Report Builder

+ +
+
+
Report Templates
+ ${templates.map(template => ` +
+
${template.name}
+
${template.type}
+
+ `).join('')} +
+ +
+
Build Your Report
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ + +
+
+ + ${reports.length > 0 ? ` +
+
Recent Reports
+
+ ${reports.length} reports generated +
+
+ ` : ''} +
+ + + `.trim(); +} diff --git a/servers/close/src/apps/revenue-dashboard.ts b/servers/close/src/apps/revenue-dashboard.ts new file mode 100644 index 0000000..ec78d2b --- /dev/null +++ b/servers/close/src/apps/revenue-dashboard.ts @@ -0,0 +1,97 @@ +// Revenue Dashboard MCP App + +export function generateRevenueDashboard(data: any) { + const opportunities = data.opportunities || []; + + const wonOpps = opportunities.filter((o: any) => o.status_type === 'won'); + const activeOpps = opportunities.filter((o: any) => o.status_type === 'active'); + const totalRevenue = wonOpps.reduce((sum: number, o: any) => sum + (o.value || 0), 0); + const pipelineValue = activeOpps.reduce((sum: number, o: any) => sum + (o.value || 0), 0); + const weightedValue = activeOpps.reduce((sum: number, o: any) => { + return sum + ((o.value || 0) * ((o.confidence || 0) / 100)); + }, 0); + + // Group by month + const revenueByMonth: Record = {}; + wonOpps.forEach((opp: any) => { + if (opp.date_won) { + const month = new Date(opp.date_won).toLocaleDateString('en-US', { year: 'numeric', month: 'short' }); + revenueByMonth[month] = (revenueByMonth[month] || 0) + (opp.value || 0); + } + }); + + return ` + + + + + + Revenue Dashboard + + + +
+

💰 Revenue Dashboard

+ +
+
+
Total Revenue
+
$${totalRevenue.toLocaleString()}
+
${wonOpps.length} won opportunities
+
+
+
Pipeline Value
+
$${pipelineValue.toLocaleString()}
+
${activeOpps.length} active opportunities
+
+
+
Weighted Pipeline
+
$${Math.round(weightedValue).toLocaleString()}
+
Based on confidence
+
+
+
Average Deal Size
+
$${wonOpps.length > 0 ? Math.round(totalRevenue / wonOpps.length).toLocaleString() : '0'}
+
Won opportunities
+
+
+ +
+
Revenue by Month
+ ${Object.entries(revenueByMonth).length > 0 ? Object.entries(revenueByMonth).map(([month, value]) => { + const maxValue = Math.max(...Object.values(revenueByMonth)); + const widthPercent = Math.max(20, (value / maxValue) * 100); + + return ` +
+
${month}
+
+
+ $${value.toLocaleString()} +
+
+
+ `; + }).join('') : '

No revenue data available

'} +
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/search-results.ts b/servers/close/src/apps/search-results.ts new file mode 100644 index 0000000..18fed52 --- /dev/null +++ b/servers/close/src/apps/search-results.ts @@ -0,0 +1,73 @@ +// Search Results MCP App + +export function generateSearchResults(data: any) { + const query = data.query || ''; + const results = data.results || []; + const resultType = data.resultType || 'all'; + + return ` + + + + + + Search Results + + + +
+
+

Search Results

+
+ Found ${results.length} results for "${query}" +
+
+ +
+ ${results.length > 0 ? results.map((result: any) => ` +
+
+ ${result.name || result.display_name || result.text || 'Untitled'} + ${result._type || resultType} +
+ ${result.description ? `
${result.description}
` : ''} + ${result.note ? `
${result.note}
` : ''} +
+ ${result.id ? `ID: ${result.id}` : ''} + ${result.status_label ? `Status: ${result.status_label}` : ''} + ${result.date_created ? `Created: ${new Date(result.date_created).toLocaleDateString()}` : ''} + ${result.contacts ? `${result.contacts.length} contacts` : ''} + ${result.value ? `$${result.value.toLocaleString()}` : ''} +
+
+ `).join('') : ` +
+
🔍
+
No results found
+
Try adjusting your search query
+
+ `} +
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/sequence-dashboard.ts b/servers/close/src/apps/sequence-dashboard.ts new file mode 100644 index 0000000..ec64581 --- /dev/null +++ b/servers/close/src/apps/sequence-dashboard.ts @@ -0,0 +1,64 @@ +// Sequence Dashboard MCP App + +export function generateSequenceDashboard(sequences: any[]) { + return ` + + + + + + Sequence Dashboard + + + +
+

📨 Sequence Dashboard (${sequences.length})

+
+ ${sequences.map(seq => ` +
+
+
${seq.name}
+ + ${seq.status || 'draft'} + +
+
+
Sequence ID
+
${seq.id}
+
+ ${seq.max_activations ? ` +
+
Max Activations
+
${seq.max_activations}
+
+ ` : ''} +
+
Created
+
${seq.date_created ? new Date(seq.date_created).toLocaleDateString() : 'N/A'}
+
+
+ `).join('')} +
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/sequence-detail.ts b/servers/close/src/apps/sequence-detail.ts new file mode 100644 index 0000000..369fe84 --- /dev/null +++ b/servers/close/src/apps/sequence-detail.ts @@ -0,0 +1,93 @@ +// Sequence Detail MCP App + +export function generateSequenceDetail(data: any) { + const sequence = data.sequence || {}; + const stats = data.stats || {}; + + return ` + + + + + + ${sequence.name} - Sequence + + + +
+
+

${sequence.name}

+ ${sequence.status || 'Draft'} +
+ +
+
+
+
Sequence Details
+
+
Sequence ID
+
${sequence.id}
+
+ ${sequence.max_activations ? ` +
+
Max Activations
+
${sequence.max_activations}
+
+ ` : ''} + ${sequence.throttle_capacity ? ` +
+
Throttle Capacity
+
${sequence.throttle_capacity} per ${sequence.throttle_period_seconds}s
+
+ ` : ''} +
+
Created
+
${sequence.date_created ? new Date(sequence.date_created).toLocaleString() : 'N/A'}
+
+
+
Last Updated
+
${sequence.date_updated ? new Date(sequence.date_updated).toLocaleString() : 'N/A'}
+
+
+
+ +
+ ${stats.total_subscriptions !== undefined ? ` +
+
+
${stats.total_subscriptions || 0}
+
Total Subscriptions
+
+
+ ` : ''} + ${stats.active_subscriptions !== undefined ? ` +
+
+
${stats.active_subscriptions || 0}
+
Active
+
+
+ ` : ''} +
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/smart-view-runner.ts b/servers/close/src/apps/smart-view-runner.ts new file mode 100644 index 0000000..08deb3e --- /dev/null +++ b/servers/close/src/apps/smart-view-runner.ts @@ -0,0 +1,76 @@ +// Smart View Runner MCP App + +export function generateSmartViewRunner(data: any) { + const view = data.view || {}; + const results = data.results || []; + + return ` + + + + + + ${view.name} - Smart View + + + +
+
+

🔍 ${view.name || 'Smart View'}

+ ${view.query ? `
${view.query}
` : ''} +
+ +
+
+
Total Results
+
${results.length}
+
+
+
View Type
+
${view.type || 'Mixed'}
+
+
+ +
+ + + + + + + + + + + ${results.length > 0 ? results.map((item: any) => ` + + + + + + + `).join('') : ''} + +
NameStatusContactsCreated
${item.name || item.display_name || 'N/A'}${item.status_label || 'N/A'}${item.contacts?.length || 0}${item.date_created ? new Date(item.date_created).toLocaleDateString() : 'N/A'}
No results found
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/apps/task-manager.ts b/servers/close/src/apps/task-manager.ts new file mode 100644 index 0000000..afc400d --- /dev/null +++ b/servers/close/src/apps/task-manager.ts @@ -0,0 +1,121 @@ +// Task Manager MCP App + +export function generateTaskManager(tasks: any[]) { + const incompleteTasks = tasks.filter(t => !t.is_complete); + const completedTasks = tasks.filter(t => t.is_complete); + const overdueTasks = incompleteTasks.filter(t => { + if (!t.date) return false; + return new Date(t.date) < new Date(); + }); + + return ` + + + + + + Task Manager + + + +
+

✅ Task Manager

+ +
+
+
Total Tasks
+
${tasks.length}
+
+
+
Incomplete
+
${incompleteTasks.length}
+
+
+
Completed
+
${completedTasks.length}
+
+
+
Overdue
+
${overdueTasks.length}
+
+
+ + ${overdueTasks.length > 0 ? ` +
+
🚨 Overdue Tasks
+ ${overdueTasks.map(task => ` +
+
+
+
${task.text}
+
+ ${task.assigned_to_name ? `Assigned to ${task.assigned_to_name}` : 'Unassigned'} + ${task.date ? ` • Due ${new Date(task.date).toLocaleDateString()}` : ''} + ${task.lead_name ? ` • Lead: ${task.lead_name}` : ''} +
+
+
+ `).join('')} +
+ ` : ''} + +
+
📝 Incomplete Tasks
+ ${incompleteTasks.filter(t => !overdueTasks.includes(t)).length > 0 ? incompleteTasks.filter(t => !overdueTasks.includes(t)).map(task => ` +
+
+
+
${task.text}
+
+ ${task.assigned_to_name ? `Assigned to ${task.assigned_to_name}` : 'Unassigned'} + ${task.date ? ` • Due ${new Date(task.date).toLocaleDateString()}` : ''} + ${task.lead_name ? ` • Lead: ${task.lead_name}` : ''} +
+
+
+ `).join('') : '

No incomplete tasks

'} +
+ + ${completedTasks.length > 0 ? ` +
+
✅ Completed Tasks
+ ${completedTasks.slice(0, 20).map(task => ` +
+
+
+
${task.text}
+
+ ${task.assigned_to_name ? `Assigned to ${task.assigned_to_name}` : 'Unassigned'} + ${task.lead_name ? ` • Lead: ${task.lead_name}` : ''} +
+
+
+ `).join('')} +
+ ` : ''} +
+ + + `.trim(); +} diff --git a/servers/close/src/apps/user-stats.ts b/servers/close/src/apps/user-stats.ts new file mode 100644 index 0000000..3e9edbe --- /dev/null +++ b/servers/close/src/apps/user-stats.ts @@ -0,0 +1,76 @@ +// User Stats MCP App + +export function generateUserStats(data: any) { + const user = data.user || {}; + const stats = data.stats || {}; + + return ` + + + + + + ${user.first_name || 'User'} ${user.last_name || ''} - Stats + + + +
+
+
+ ${user.image ? `Avatar` : '
${user.first_name ? user.first_name.charAt(0) : '?'}
'} +
+

${user.first_name || ''} ${user.last_name || ''}

+ +
+ +
+
+
📞
+
${stats.calls || 0}
+
Calls
+
+
+
📧
+
${stats.emails || 0}
+
Emails
+
+
+
📅
+
${stats.meetings || 0}
+
Meetings
+
+
+
💼
+
${stats.opportunities || 0}
+
Opportunities
+
+
+
+
${stats.tasks_completed || 0}
+
Tasks Done
+
+
+
💰
+
$${(stats.revenue || 0).toLocaleString()}
+
Revenue
+
+
+
+ + + `.trim(); +} diff --git a/servers/close/src/client/close-client.ts b/servers/close/src/client/close-client.ts new file mode 100644 index 0000000..190c675 --- /dev/null +++ b/servers/close/src/client/close-client.ts @@ -0,0 +1,183 @@ +// Close API Client with authentication, pagination, and error handling + +import type { + CloseConfig, + PaginationParams, + SearchParams, + CloseAPIResponse, +} from "../types/index.js"; + +export class CloseClient { + private apiKey: string; + private baseUrl: string; + private authHeader: string; + + constructor(config: CloseConfig) { + this.apiKey = config.apiKey; + this.baseUrl = config.baseUrl || "https://api.close.com/api/v1"; + // Basic auth: encode "api_key:" (note the colon after key, no password) + this.authHeader = `Basic ${Buffer.from(`${this.apiKey}:`).toString("base64")}`; + } + + /** + * Make a GET request to the Close API + */ + async get(endpoint: string, params?: Record): Promise { + const url = this.buildUrl(endpoint, params); + return this.request("GET", url); + } + + /** + * Make a POST request to the Close API + */ + async post(endpoint: string, body?: any): Promise { + const url = this.buildUrl(endpoint); + return this.request("POST", url, body); + } + + /** + * Make a PUT request to the Close API + */ + async put(endpoint: string, body?: any): Promise { + const url = this.buildUrl(endpoint); + return this.request("PUT", url, body); + } + + /** + * Make a DELETE request to the Close API + */ + async delete(endpoint: string): Promise { + const url = this.buildUrl(endpoint); + return this.request("DELETE", url); + } + + /** + * Paginated GET request - fetches all pages automatically + */ + async getPaginated( + endpoint: string, + params?: PaginationParams & Record + ): Promise { + const results: T[] = []; + let cursor: string | undefined; + const limit = params?.limit || 100; + + do { + const queryParams = { + ...params, + _limit: limit, + _skip: cursor ? undefined : params?.skip, + _cursor: cursor, + }; + + const response = await this.get>( + endpoint, + queryParams + ); + + if (response.data) { + results.push(...response.data); + } + + cursor = response.has_more ? response.cursor : undefined; + } while (cursor); + + return results; + } + + /** + * Search with pagination support + */ + async search( + endpoint: string, + params?: SearchParams + ): Promise { + return this.getPaginated(endpoint, params); + } + + /** + * Build URL with query parameters + */ + private buildUrl( + endpoint: string, + params?: Record + ): string { + const url = new URL(`${this.baseUrl}${endpoint}`); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + if (Array.isArray(value)) { + value.forEach((v) => url.searchParams.append(key, String(v))); + } else { + url.searchParams.set(key, String(value)); + } + } + }); + } + + return url.toString(); + } + + /** + * Make HTTP request with error handling + */ + private async request( + method: string, + url: string, + body?: any + ): Promise { + try { + const headers: Record = { + Authorization: this.authHeader, + "Content-Type": "application/json", + }; + + const options: RequestInit = { + method, + headers, + }; + + if (body && (method === "POST" || method === "PUT")) { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + + // Handle different response codes + if (response.status === 204) { + // No content - successful DELETE + return {} as T; + } + + const data = await response.json(); + + if (!response.ok) { + throw new Error( + `Close API error (${response.status}): ${ + data.error || data.errors?.[0] || "Unknown error" + }` + ); + } + + return data as T; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Close API request failed: ${error.message}`); + } + throw error; + } + } + + /** + * Test the API connection + */ + async testConnection(): Promise { + try { + await this.get("/me/"); + return true; + } catch { + return false; + } + } +} diff --git a/servers/close/src/index.ts b/servers/close/src/index.ts deleted file mode 100644 index b0040b3..0000000 --- a/servers/close/src/index.ts +++ /dev/null @@ -1,476 +0,0 @@ -#!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -// ============================================ -// CONFIGURATION -// ============================================ -const MCP_NAME = "close"; -const MCP_VERSION = "1.0.0"; -const API_BASE_URL = "https://api.close.com/api/v1"; - -// ============================================ -// REST API CLIENT -// ============================================ -class CloseClient { - private apiKey: string; - private baseUrl: string; - - constructor(apiKey: string) { - this.apiKey = apiKey; - this.baseUrl = API_BASE_URL; - } - - private getAuthHeader(): string { - // Close uses Basic auth with API key as username, empty password - return `Basic ${Buffer.from(`${this.apiKey}:`).toString('base64')}`; - } - - async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - headers: { - "Authorization": this.getAuthHeader(), - "Content-Type": "application/json", - "Accept": "application/json", - ...options.headers, - }, - }); - - if (!response.ok) { - const errorBody = await response.text(); - throw new Error(`Close API error: ${response.status} ${response.statusText} - ${errorBody}`); - } - - return response.json(); - } - - async get(endpoint: string, params: Record = {}) { - const searchParams = new URLSearchParams(); - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null) { - searchParams.append(key, String(value)); - } - } - const queryString = searchParams.toString(); - const url = queryString ? `${endpoint}?${queryString}` : endpoint; - return this.request(url, { method: "GET" }); - } - - async post(endpoint: string, data: any) { - return this.request(endpoint, { - method: "POST", - body: JSON.stringify(data), - }); - } - - async put(endpoint: string, data: any) { - return this.request(endpoint, { - method: "PUT", - body: JSON.stringify(data), - }); - } - - async delete(endpoint: string) { - return this.request(endpoint, { method: "DELETE" }); - } -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ -const tools = [ - { - name: "list_leads", - description: "List leads from Close CRM with optional search query", - inputSchema: { - type: "object" as const, - properties: { - query: { type: "string", description: "Search query (Close query syntax)" }, - _limit: { type: "number", description: "Max results to return (default 100)" }, - _skip: { type: "number", description: "Number of results to skip (pagination)" }, - _fields: { type: "string", description: "Comma-separated list of fields to return" }, - }, - }, - }, - { - name: "get_lead", - description: "Get a specific lead by ID", - inputSchema: { - type: "object" as const, - properties: { - lead_id: { type: "string", description: "Lead ID (e.g., lead_xxx)" }, - }, - required: ["lead_id"], - }, - }, - { - name: "create_lead", - description: "Create a new lead in Close CRM", - inputSchema: { - type: "object" as const, - properties: { - name: { type: "string", description: "Lead/Company name" }, - url: { type: "string", description: "Company website URL" }, - description: { type: "string", description: "Lead description" }, - status_id: { type: "string", description: "Lead status ID" }, - contacts: { - type: "array", - description: "Array of contacts to add to the lead", - items: { - type: "object", - properties: { - name: { type: "string", description: "Contact name" }, - title: { type: "string", description: "Job title" }, - emails: { - type: "array", - items: { - type: "object", - properties: { - email: { type: "string" }, - type: { type: "string", description: "office, home, direct, mobile, fax, other" }, - }, - }, - }, - phones: { - type: "array", - items: { - type: "object", - properties: { - phone: { type: "string" }, - type: { type: "string", description: "office, home, direct, mobile, fax, other" }, - }, - }, - }, - }, - }, - }, - addresses: { - type: "array", - description: "Lead addresses", - items: { - type: "object", - properties: { - address_1: { type: "string" }, - address_2: { type: "string" }, - city: { type: "string" }, - state: { type: "string" }, - zipcode: { type: "string" }, - country: { type: "string" }, - }, - }, - }, - custom: { type: "object", description: "Custom field values (key-value pairs)" }, - }, - required: ["name"], - }, - }, - { - name: "update_lead", - description: "Update an existing lead", - inputSchema: { - type: "object" as const, - properties: { - lead_id: { type: "string", description: "Lead ID to update" }, - name: { type: "string", description: "Lead/Company name" }, - url: { type: "string", description: "Company website URL" }, - description: { type: "string", description: "Lead description" }, - status_id: { type: "string", description: "Lead status ID" }, - custom: { type: "object", description: "Custom field values to update" }, - }, - required: ["lead_id"], - }, - }, - { - name: "list_opportunities", - description: "List opportunities from Close CRM", - inputSchema: { - type: "object" as const, - properties: { - lead_id: { type: "string", description: "Filter by lead ID" }, - status_id: { type: "string", description: "Filter by opportunity status ID" }, - user_id: { type: "string", description: "Filter by assigned user ID" }, - _limit: { type: "number", description: "Max results to return" }, - _skip: { type: "number", description: "Number of results to skip" }, - }, - }, - }, - { - name: "create_opportunity", - description: "Create a new opportunity/deal", - inputSchema: { - type: "object" as const, - properties: { - lead_id: { type: "string", description: "Lead ID to attach opportunity to" }, - status_id: { type: "string", description: "Opportunity status ID" }, - value: { type: "number", description: "Deal value in cents" }, - value_period: { type: "string", description: "one_time, monthly, annual" }, - confidence: { type: "number", description: "Confidence percentage (0-100)" }, - note: { type: "string", description: "Opportunity notes" }, - date_won: { type: "string", description: "Date won (YYYY-MM-DD)" }, - }, - required: ["lead_id"], - }, - }, - { - name: "create_activity", - description: "Create an activity (note, call, email, meeting, etc.)", - inputSchema: { - type: "object" as const, - properties: { - activity_type: { type: "string", description: "Type: Note, Call, Email, Meeting, SMS" }, - lead_id: { type: "string", description: "Lead ID for the activity" }, - contact_id: { type: "string", description: "Contact ID (optional)" }, - user_id: { type: "string", description: "User ID who performed activity" }, - note: { type: "string", description: "Activity note/body content" }, - subject: { type: "string", description: "Subject (for emails)" }, - status: { type: "string", description: "Call status: completed, no-answer, busy, etc." }, - direction: { type: "string", description: "inbound or outbound" }, - duration: { type: "number", description: "Duration in seconds (for calls)" }, - date_created: { type: "string", description: "Activity date (ISO 8601)" }, - }, - required: ["activity_type", "lead_id"], - }, - }, - { - name: "list_tasks", - description: "List tasks from Close CRM", - inputSchema: { - type: "object" as const, - properties: { - lead_id: { type: "string", description: "Filter by lead ID" }, - assigned_to: { type: "string", description: "Filter by assigned user ID" }, - is_complete: { type: "boolean", description: "Filter by completion status" }, - _type: { type: "string", description: "Task type: lead, opportunity, incoming_email, missed_call, etc." }, - _limit: { type: "number", description: "Max results to return" }, - _skip: { type: "number", description: "Number of results to skip" }, - }, - }, - }, - { - name: "create_task", - description: "Create a new task", - inputSchema: { - type: "object" as const, - properties: { - lead_id: { type: "string", description: "Lead ID for the task" }, - assigned_to: { type: "string", description: "User ID to assign task to" }, - text: { type: "string", description: "Task description" }, - date: { type: "string", description: "Due date (YYYY-MM-DD or ISO 8601)" }, - is_complete: { type: "boolean", description: "Task completion status" }, - }, - required: ["lead_id", "text"], - }, - }, - { - name: "send_email", - description: "Send an email through Close CRM", - inputSchema: { - type: "object" as const, - properties: { - lead_id: { type: "string", description: "Lead ID" }, - contact_id: { type: "string", description: "Contact ID to send to" }, - to: { type: "array", items: { type: "string" }, description: "Recipient email addresses" }, - cc: { type: "array", items: { type: "string" }, description: "CC email addresses" }, - bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" }, - subject: { type: "string", description: "Email subject" }, - body_text: { type: "string", description: "Plain text body" }, - body_html: { type: "string", description: "HTML body" }, - status: { type: "string", description: "draft, outbox, sent" }, - template_id: { type: "string", description: "Email template ID to use" }, - }, - required: ["lead_id", "to", "subject"], - }, - }, - { - name: "list_statuses", - description: "List lead and opportunity statuses", - inputSchema: { - type: "object" as const, - properties: { - type: { type: "string", description: "lead or opportunity" }, - }, - }, - }, - { - name: "list_users", - description: "List users in the Close organization", - inputSchema: { - type: "object" as const, - properties: {}, - }, - }, -]; - -// ============================================ -// TOOL HANDLERS -// ============================================ -async function handleTool(client: CloseClient, name: string, args: any) { - switch (name) { - case "list_leads": { - const { query, _limit = 100, _skip, _fields } = args; - const params: any = { _limit }; - if (query) params.query = query; - if (_skip) params._skip = _skip; - if (_fields) params._fields = _fields; - return await client.get("/lead/", params); - } - case "get_lead": { - const { lead_id } = args; - return await client.get(`/lead/${lead_id}/`); - } - case "create_lead": { - const { name, url, description, status_id, contacts, addresses, custom } = args; - const data: any = { name }; - if (url) data.url = url; - if (description) data.description = description; - if (status_id) data.status_id = status_id; - if (contacts) data.contacts = contacts; - if (addresses) data.addresses = addresses; - if (custom) data.custom = custom; - return await client.post("/lead/", data); - } - case "update_lead": { - const { lead_id, ...updates } = args; - return await client.put(`/lead/${lead_id}/`, updates); - } - case "list_opportunities": { - const { lead_id, status_id, user_id, _limit = 100, _skip } = args; - const params: any = { _limit }; - if (lead_id) params.lead_id = lead_id; - if (status_id) params.status_id = status_id; - if (user_id) params.user_id = user_id; - if (_skip) params._skip = _skip; - return await client.get("/opportunity/", params); - } - case "create_opportunity": { - const { lead_id, status_id, value, value_period, confidence, note, date_won } = args; - const data: any = { lead_id }; - if (status_id) data.status_id = status_id; - if (value !== undefined) data.value = value; - if (value_period) data.value_period = value_period; - if (confidence !== undefined) data.confidence = confidence; - if (note) data.note = note; - if (date_won) data.date_won = date_won; - return await client.post("/opportunity/", data); - } - case "create_activity": { - const { activity_type, lead_id, contact_id, user_id, note, subject, status, direction, duration, date_created } = args; - - // Map activity type to endpoint - const typeMap: Record = { - 'Note': 'note', - 'Call': 'call', - 'Email': 'email', - 'Meeting': 'meeting', - 'SMS': 'sms', - }; - const endpoint = typeMap[activity_type] || activity_type.toLowerCase(); - - const data: any = { lead_id }; - if (contact_id) data.contact_id = contact_id; - if (user_id) data.user_id = user_id; - if (note) data.note = note; - if (subject) data.subject = subject; - if (status) data.status = status; - if (direction) data.direction = direction; - if (duration) data.duration = duration; - if (date_created) data.date_created = date_created; - - return await client.post(`/activity/${endpoint}/`, data); - } - case "list_tasks": { - const { lead_id, assigned_to, is_complete, _type, _limit = 100, _skip } = args; - const params: any = { _limit }; - if (lead_id) params.lead_id = lead_id; - if (assigned_to) params.assigned_to = assigned_to; - if (is_complete !== undefined) params.is_complete = is_complete; - if (_type) params._type = _type; - if (_skip) params._skip = _skip; - return await client.get("/task/", params); - } - case "create_task": { - const { lead_id, assigned_to, text, date, is_complete } = args; - const data: any = { lead_id, text }; - if (assigned_to) data.assigned_to = assigned_to; - if (date) data.date = date; - if (is_complete !== undefined) data.is_complete = is_complete; - return await client.post("/task/", data); - } - case "send_email": { - const { lead_id, contact_id, to, cc, bcc, subject, body_text, body_html, status, template_id } = args; - const data: any = { lead_id, to, subject }; - if (contact_id) data.contact_id = contact_id; - if (cc) data.cc = cc; - if (bcc) data.bcc = bcc; - if (body_text) data.body_text = body_text; - if (body_html) data.body_html = body_html; - if (status) data.status = status; - if (template_id) data.template_id = template_id; - return await client.post("/activity/email/", data); - } - case "list_statuses": { - const { type } = args; - if (type === 'opportunity') { - return await client.get("/status/opportunity/"); - } - return await client.get("/status/lead/"); - } - case "list_users": { - return await client.get("/user/"); - } - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -// ============================================ -// SERVER SETUP -// ============================================ -async function main() { - const apiKey = process.env.CLOSE_API_KEY; - if (!apiKey) { - console.error("Error: CLOSE_API_KEY environment variable required"); - console.error("Get your API key at Settings > Integrations > API Keys in Close"); - process.exit(1); - } - - const client = new CloseClient(apiKey); - - const server = new Server( - { name: `${MCP_NAME}-mcp`, version: MCP_VERSION }, - { capabilities: { tools: {} } } - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools, - })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - try { - const result = await handleTool(client, name, args || {}); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - }; - } - }); - - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`${MCP_NAME} MCP server running on stdio`); -} - -main().catch(console.error); diff --git a/servers/close/src/tools/activities-tools.ts b/servers/close/src/tools/activities-tools.ts new file mode 100644 index 0000000..b515559 --- /dev/null +++ b/servers/close/src/tools/activities-tools.ts @@ -0,0 +1,396 @@ +// Activity management tools (notes, calls, emails, SMS, meetings) + +import type { CloseClient } from "../client/close-client.js"; +import type { Activity, Note, Call, Email, SMS, Meeting } from "../types/index.js"; + +export function registerActivitiesTools(server: any, client: CloseClient) { + // List activities + server.tool( + "close_list_activities", + "List activities (notes, calls, emails, etc.)", + { + lead_id: { + type: "string", + description: "Filter by lead ID", + required: false, + }, + type: { + type: "string", + description: "Activity type (note, call, email, sms, meeting)", + required: false, + }, + limit: { + type: "number", + description: "Number of results", + required: false, + }, + }, + async (args: any) => { + const endpoint = args.type ? `/activity/${args.type}/` : "/activity/"; + const params: any = { limit: args.limit }; + if (args.lead_id) params.lead_id = args.lead_id; + + const activities = await client.search(endpoint, params); + return { + content: [ + { + type: "text", + text: JSON.stringify(activities, null, 2), + }, + ], + }; + } + ); + + // Get activity + server.tool( + "close_get_activity", + "Get a specific activity by type and ID", + { + activity_type: { + type: "string", + description: "Activity type (note, call, email, sms, meeting)", + required: true, + }, + activity_id: { + type: "string", + description: "Activity ID", + required: true, + }, + }, + async (args: any) => { + const activity = await client.get( + `/activity/${args.activity_type}/${args.activity_id}/` + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(activity, null, 2), + }, + ], + }; + } + ); + + // Create note + server.tool( + "close_create_note", + "Create a note activity", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + note: { + type: "string", + description: "Note content", + required: true, + }, + contact_id: { + type: "string", + description: "Contact ID (optional)", + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + note: args.note, + }; + + if (args.contact_id) { + body.contact_id = args.contact_id; + } + + const note = await client.post("/activity/note/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(note, null, 2), + }, + ], + }; + } + ); + + // Create call + server.tool( + "close_create_call", + "Log a call activity", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + direction: { + type: "string", + description: "Call direction (inbound or outbound)", + required: true, + }, + phone: { + type: "string", + description: "Phone number", + required: false, + }, + duration: { + type: "number", + description: "Call duration in seconds", + required: false, + }, + note: { + type: "string", + description: "Call notes", + required: false, + }, + disposition: { + type: "string", + description: "Call disposition", + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + direction: args.direction, + }; + + if (args.phone) body.phone = args.phone; + if (args.duration) body.duration = args.duration; + if (args.note) body.note = args.note; + if (args.disposition) body.disposition = args.disposition; + + const call = await client.post("/activity/call/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(call, null, 2), + }, + ], + }; + } + ); + + // Create email + server.tool( + "close_create_email", + "Log an email activity", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + direction: { + type: "string", + description: "Email direction (inbound or outbound)", + required: true, + }, + subject: { + type: "string", + description: "Email subject", + required: true, + }, + body_text: { + type: "string", + description: "Email body (plain text)", + required: false, + }, + body_html: { + type: "string", + description: "Email body (HTML)", + required: false, + }, + sender: { + type: "string", + description: "Sender email address", + required: false, + }, + to: { + type: "string", + description: "JSON array of recipient email addresses", + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + direction: args.direction, + subject: args.subject, + }; + + if (args.body_text) body.body_text = args.body_text; + if (args.body_html) body.body_html = args.body_html; + if (args.sender) body.sender = args.sender; + if (args.to) body.to = JSON.parse(args.to); + + const email = await client.post("/activity/email/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(email, null, 2), + }, + ], + }; + } + ); + + // Create SMS + server.tool( + "close_create_sms", + "Log an SMS activity", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + text: { + type: "string", + description: "SMS message text", + required: true, + }, + direction: { + type: "string", + description: "SMS direction (inbound or outbound)", + required: true, + }, + remote_phone: { + type: "string", + description: "Remote phone number", + required: false, + }, + local_phone: { + type: "string", + description: "Local phone number", + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + text: args.text, + direction: args.direction, + }; + + if (args.remote_phone) body.remote_phone = args.remote_phone; + if (args.local_phone) body.local_phone = args.local_phone; + + const sms = await client.post("/activity/sms/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(sms, null, 2), + }, + ], + }; + } + ); + + // Create meeting + server.tool( + "close_create_meeting", + "Create a meeting activity", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + title: { + type: "string", + description: "Meeting title", + required: true, + }, + starts_at: { + type: "string", + description: "Start time (ISO 8601 format)", + required: true, + }, + ends_at: { + type: "string", + description: "End time (ISO 8601 format)", + required: false, + }, + location: { + type: "string", + description: "Meeting location", + required: false, + }, + note: { + type: "string", + description: "Meeting notes", + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + title: args.title, + starts_at: args.starts_at, + }; + + if (args.ends_at) body.ends_at = args.ends_at; + if (args.location) body.location = args.location; + if (args.note) body.note = args.note; + + const meeting = await client.post("/activity/meeting/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(meeting, null, 2), + }, + ], + }; + } + ); + + // Log generic activity + server.tool( + "close_log_activity", + "Log a generic activity", + { + activity_type: { + type: "string", + description: "Activity type", + required: true, + }, + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + data: { + type: "string", + description: "JSON object with activity data", + required: true, + }, + }, + async (args: any) => { + const body = { + lead_id: args.lead_id, + ...JSON.parse(args.data), + }; + + const activity = await client.post( + `/activity/${args.activity_type}/`, + body + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(activity, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/bulk-tools.ts b/servers/close/src/tools/bulk-tools.ts new file mode 100644 index 0000000..e2e12f0 --- /dev/null +++ b/servers/close/src/tools/bulk-tools.ts @@ -0,0 +1,204 @@ +// Bulk operations tools + +import type { CloseClient } from "../client/close-client.js"; +import type { + BulkEditRequest, + BulkDeleteRequest, + BulkEmailRequest, +} from "../types/index.js"; + +export function registerBulkTools(server: any, client: CloseClient) { + // Bulk edit leads + server.tool( + "close_bulk_edit_leads", + "Bulk edit multiple leads at once", + { + lead_ids: { + type: "string", + description: "JSON array of lead IDs", + required: false, + }, + query: { + type: "string", + description: "Search query to select leads", + required: false, + }, + updates: { + type: "string", + description: "JSON object with field updates", + required: true, + }, + }, + async (args: any) => { + const body: BulkEditRequest = { + updates: JSON.parse(args.updates), + }; + + if (args.lead_ids) { + body.lead_ids = JSON.parse(args.lead_ids); + } + + if (args.query) { + body.query = args.query; + } + + const result = await client.post("/lead/bulk_action/edit/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } + ); + + // Bulk delete leads + server.tool( + "close_bulk_delete_leads", + "Bulk delete multiple leads", + { + lead_ids: { + type: "string", + description: "JSON array of lead IDs", + required: false, + }, + query: { + type: "string", + description: "Search query to select leads", + required: false, + }, + }, + async (args: any) => { + const body: BulkDeleteRequest = {}; + + if (args.lead_ids) { + body.lead_ids = JSON.parse(args.lead_ids); + } + + if (args.query) { + body.query = args.query; + } + + const result = await client.post("/lead/bulk_action/delete/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } + ); + + // Bulk email + server.tool( + "close_bulk_email", + "Send bulk email to multiple leads", + { + lead_ids: { + type: "string", + description: "JSON array of lead IDs", + required: false, + }, + query: { + type: "string", + description: "Search query to select leads", + required: false, + }, + subject: { + type: "string", + description: "Email subject", + required: true, + }, + body: { + type: "string", + description: "Email body (HTML or plain text)", + required: true, + }, + template_id: { + type: "string", + description: "Email template ID", + required: false, + }, + sender: { + type: "string", + description: "Sender email address", + required: false, + }, + }, + async (args: any) => { + const body: BulkEmailRequest = { + subject: args.subject, + body: args.body, + }; + + if (args.lead_ids) { + body.lead_ids = JSON.parse(args.lead_ids); + } + + if (args.query) { + body.query = args.query; + } + + if (args.template_id) { + body.template_id = args.template_id; + } + + if (args.sender) { + body.sender = args.sender; + } + + const result = await client.post("/bulk_action/email/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } + ); + + // Bulk update opportunity status + server.tool( + "close_bulk_update_opportunity_status", + "Bulk update opportunity statuses", + { + opportunity_ids: { + type: "string", + description: "JSON array of opportunity IDs", + required: true, + }, + status_id: { + type: "string", + description: "New status ID", + required: true, + }, + }, + async (args: any) => { + const body = { + opportunity_ids: JSON.parse(args.opportunity_ids), + updates: { + status_id: args.status_id, + }, + }; + + const result = await client.post( + "/opportunity/bulk_action/edit/", + body + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/contacts-tools.ts b/servers/close/src/tools/contacts-tools.ts new file mode 100644 index 0000000..44523fe --- /dev/null +++ b/servers/close/src/tools/contacts-tools.ts @@ -0,0 +1,211 @@ +// Contact management tools + +import type { CloseClient } from "../client/close-client.js"; +import type { Contact } from "../types/index.js"; + +export function registerContactsTools(server: any, client: CloseClient) { + // List contacts + server.tool( + "close_list_contacts", + "List all contacts with optional filtering", + { + lead_id: { + type: "string", + description: "Filter by lead ID", + required: false, + }, + query: { + type: "string", + description: "Search query", + required: false, + }, + limit: { + type: "number", + description: "Number of results", + required: false, + }, + }, + async (args: any) => { + const params: any = { + query: args.query, + limit: args.limit, + }; + + if (args.lead_id) { + params.lead_id = args.lead_id; + } + + const contacts = await client.search("/contact/", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(contacts, null, 2), + }, + ], + }; + } + ); + + // Get contact + server.tool( + "close_get_contact", + "Get a specific contact by ID", + { + contact_id: { + type: "string", + description: "Contact ID", + required: true, + }, + }, + async (args: any) => { + const contact = await client.get( + `/contact/${args.contact_id}/` + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(contact, null, 2), + }, + ], + }; + } + ); + + // Create contact + server.tool( + "close_create_contact", + "Create a new contact", + { + lead_id: { + type: "string", + description: "Lead ID to associate contact with", + required: true, + }, + name: { + type: "string", + description: "Contact name", + required: true, + }, + title: { + type: "string", + description: "Job title", + required: false, + }, + emails: { + type: "string", + description: 'JSON array of email objects (e.g., [{"email":"test@example.com","type":"office"}])', + required: false, + }, + phones: { + type: "string", + description: 'JSON array of phone objects (e.g., [{"phone":"+1234567890","type":"mobile"}])', + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + name: args.name, + title: args.title, + }; + + if (args.emails) { + body.emails = JSON.parse(args.emails); + } + + if (args.phones) { + body.phones = JSON.parse(args.phones); + } + + const contact = await client.post("/contact/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(contact, null, 2), + }, + ], + }; + } + ); + + // Update contact + server.tool( + "close_update_contact", + "Update an existing contact", + { + contact_id: { + type: "string", + description: "Contact ID", + required: true, + }, + name: { + type: "string", + description: "Contact name", + required: false, + }, + title: { + type: "string", + description: "Job title", + required: false, + }, + emails: { + type: "string", + description: "JSON array of email objects", + required: false, + }, + phones: { + type: "string", + description: "JSON array of phone objects", + required: false, + }, + }, + async (args: any) => { + const body: any = {}; + + if (args.name) body.name = args.name; + if (args.title) body.title = args.title; + if (args.emails) body.emails = JSON.parse(args.emails); + if (args.phones) body.phones = JSON.parse(args.phones); + + const contact = await client.put( + `/contact/${args.contact_id}/`, + body + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(contact, null, 2), + }, + ], + }; + } + ); + + // Delete contact + server.tool( + "close_delete_contact", + "Delete a contact", + { + contact_id: { + type: "string", + description: "Contact ID", + required: true, + }, + }, + async (args: any) => { + await client.delete(`/contact/${args.contact_id}/`); + return { + content: [ + { + type: "text", + text: `Contact ${args.contact_id} deleted successfully`, + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/custom-fields-tools.ts b/servers/close/src/tools/custom-fields-tools.ts new file mode 100644 index 0000000..2f93f0d --- /dev/null +++ b/servers/close/src/tools/custom-fields-tools.ts @@ -0,0 +1,212 @@ +// Custom fields management tools + +import type { CloseClient } from "../client/close-client.js"; +import type { CustomField } from "../types/index.js"; + +export function registerCustomFieldsTools(server: any, client: CloseClient) { + // List custom fields + server.tool( + "close_list_custom_fields", + "List all custom fields", + { + object_type: { + type: "string", + description: "Object type (lead, contact, opportunity, activity)", + required: false, + }, + }, + async (args: any) => { + const endpoint = args.object_type + ? `/custom_field/${args.object_type}/` + : "/custom_field/"; + + const fields = await client.get<{ data: CustomField[] }>(endpoint); + return { + content: [ + { + type: "text", + text: JSON.stringify(fields, null, 2), + }, + ], + }; + } + ); + + // Get custom field + server.tool( + "close_get_custom_field", + "Get a specific custom field by ID", + { + object_type: { + type: "string", + description: "Object type (lead, contact, opportunity, activity)", + required: true, + }, + field_id: { + type: "string", + description: "Custom field ID", + required: true, + }, + }, + async (args: any) => { + const field = await client.get( + `/custom_field/${args.object_type}/${args.field_id}/` + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(field, null, 2), + }, + ], + }; + } + ); + + // Create custom field + server.tool( + "close_create_custom_field", + "Create a new custom field", + { + object_type: { + type: "string", + description: "Object type (lead, contact, opportunity, activity)", + required: true, + }, + name: { + type: "string", + description: "Field name", + required: true, + }, + type: { + type: "string", + description: + "Field type (text, number, date, datetime, boolean, choices)", + required: true, + }, + required: { + type: "boolean", + description: "Is field required?", + required: false, + }, + accepts_multiple_values: { + type: "boolean", + description: "Accept multiple values?", + required: false, + }, + choices: { + type: "string", + description: "JSON array of choice values (for choices type)", + required: false, + }, + }, + async (args: any) => { + const body: any = { + name: args.name, + type: args.type, + }; + + if (args.required !== undefined) body.required = args.required; + if (args.accepts_multiple_values !== undefined) + body.accepts_multiple_values = args.accepts_multiple_values; + if (args.choices) body.choices = JSON.parse(args.choices); + + const field = await client.post( + `/custom_field/${args.object_type}/`, + body + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(field, null, 2), + }, + ], + }; + } + ); + + // Update custom field + server.tool( + "close_update_custom_field", + "Update an existing custom field", + { + object_type: { + type: "string", + description: "Object type", + required: true, + }, + field_id: { + type: "string", + description: "Custom field ID", + required: true, + }, + name: { + type: "string", + description: "New field name", + required: false, + }, + required: { + type: "boolean", + description: "Is field required?", + required: false, + }, + choices: { + type: "string", + description: "JSON array of choice values", + required: false, + }, + }, + async (args: any) => { + const body: any = {}; + + if (args.name) body.name = args.name; + if (args.required !== undefined) body.required = args.required; + if (args.choices) body.choices = JSON.parse(args.choices); + + const field = await client.put( + `/custom_field/${args.object_type}/${args.field_id}/`, + body + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(field, null, 2), + }, + ], + }; + } + ); + + // Delete custom field + server.tool( + "close_delete_custom_field", + "Delete a custom field", + { + object_type: { + type: "string", + description: "Object type", + required: true, + }, + field_id: { + type: "string", + description: "Custom field ID", + required: true, + }, + }, + async (args: any) => { + await client.delete( + `/custom_field/${args.object_type}/${args.field_id}/` + ); + return { + content: [ + { + type: "text", + text: `Custom field ${args.field_id} deleted successfully`, + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/leads-tools.ts b/servers/close/src/tools/leads-tools.ts new file mode 100644 index 0000000..bd17584 --- /dev/null +++ b/servers/close/src/tools/leads-tools.ts @@ -0,0 +1,301 @@ +// Lead management tools + +import type { CloseClient } from "../client/close-client.js"; +import type { Lead, CustomField } from "../types/index.js"; + +export function registerLeadsTools(server: any, client: CloseClient) { + // List leads + server.tool( + "close_list_leads", + "List all leads with optional filtering", + { + query: { + type: "string", + description: "Search query (e.g., 'name:Acme')", + required: false, + }, + limit: { + type: "number", + description: "Number of results to return", + required: false, + }, + }, + async (args: any) => { + const leads = await client.search("/lead/", { + query: args.query, + limit: args.limit, + }); + return { + content: [ + { + type: "text", + text: JSON.stringify(leads, null, 2), + }, + ], + }; + } + ); + + // Get lead by ID + server.tool( + "close_get_lead", + "Get a specific lead by ID", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + }, + async (args: any) => { + const lead = await client.get(`/lead/${args.lead_id}/`); + return { + content: [ + { + type: "text", + text: JSON.stringify(lead, null, 2), + }, + ], + }; + } + ); + + // Create lead + server.tool( + "close_create_lead", + "Create a new lead", + { + name: { + type: "string", + description: "Lead name (company name)", + required: true, + }, + description: { + type: "string", + description: "Lead description", + required: false, + }, + url: { + type: "string", + description: "Lead website URL", + required: false, + }, + status_id: { + type: "string", + description: "Lead status ID", + required: false, + }, + contacts: { + type: "string", + description: "JSON array of contact objects", + required: false, + }, + custom: { + type: "string", + description: "JSON object of custom field values", + required: false, + }, + }, + async (args: any) => { + const body: any = { + name: args.name, + description: args.description, + url: args.url, + status_id: args.status_id, + }; + + if (args.contacts) { + body.contacts = JSON.parse(args.contacts); + } + + if (args.custom) { + body.custom = JSON.parse(args.custom); + } + + const lead = await client.post("/lead/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(lead, null, 2), + }, + ], + }; + } + ); + + // Update lead + server.tool( + "close_update_lead", + "Update an existing lead", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + name: { + type: "string", + description: "Lead name", + required: false, + }, + description: { + type: "string", + description: "Lead description", + required: false, + }, + url: { + type: "string", + description: "Lead website URL", + required: false, + }, + status_id: { + type: "string", + description: "Lead status ID", + required: false, + }, + custom: { + type: "string", + description: "JSON object of custom field values", + required: false, + }, + }, + async (args: any) => { + const body: any = {}; + + if (args.name) body.name = args.name; + if (args.description) body.description = args.description; + if (args.url) body.url = args.url; + if (args.status_id) body.status_id = args.status_id; + if (args.custom) body.custom = JSON.parse(args.custom); + + const lead = await client.put(`/lead/${args.lead_id}/`, body); + return { + content: [ + { + type: "text", + text: JSON.stringify(lead, null, 2), + }, + ], + }; + } + ); + + // Delete lead + server.tool( + "close_delete_lead", + "Delete a lead", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + }, + async (args: any) => { + await client.delete(`/lead/${args.lead_id}/`); + return { + content: [ + { + type: "text", + text: `Lead ${args.lead_id} deleted successfully`, + }, + ], + }; + } + ); + + // Search leads + server.tool( + "close_search_leads", + "Search leads with advanced query syntax", + { + query: { + type: "string", + description: + "Search query (e.g., 'name:Acme status:\"Potential\" email:*@acme.com')", + required: true, + }, + limit: { + type: "number", + description: "Max results to return", + required: false, + }, + }, + async (args: any) => { + const leads = await client.search("/lead/", { + query: args.query, + limit: args.limit || 100, + }); + return { + content: [ + { + type: "text", + text: JSON.stringify( + { + count: leads.length, + leads: leads, + }, + null, + 2 + ), + }, + ], + }; + } + ); + + // Merge leads + server.tool( + "close_merge_leads", + "Merge two leads together", + { + source_lead_id: { + type: "string", + description: "Lead ID to merge from (will be deleted)", + required: true, + }, + destination_lead_id: { + type: "string", + description: "Lead ID to merge into (will be kept)", + required: true, + }, + }, + async (args: any) => { + const result = await client.post( + `/lead/${args.destination_lead_id}/merge/`, + { + source: args.source_lead_id, + } + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } + ); + + // List lead custom fields + server.tool( + "close_list_lead_custom_fields", + "List all custom fields for leads", + {}, + async () => { + const fields = await client.get<{ data: CustomField[] }>( + "/custom_field/lead/" + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(fields, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/opportunities-tools.ts b/servers/close/src/tools/opportunities-tools.ts new file mode 100644 index 0000000..9654954 --- /dev/null +++ b/servers/close/src/tools/opportunities-tools.ts @@ -0,0 +1,247 @@ +// Opportunity management tools + +import type { CloseClient } from "../client/close-client.js"; +import type { Opportunity, OpportunityStatus, Pipeline } from "../types/index.js"; + +export function registerOpportunitiesTools(server: any, client: CloseClient) { + // List opportunities + server.tool( + "close_list_opportunities", + "List all opportunities with optional filtering", + { + lead_id: { + type: "string", + description: "Filter by lead ID", + required: false, + }, + status_id: { + type: "string", + description: "Filter by status ID", + required: false, + }, + limit: { + type: "number", + description: "Number of results", + required: false, + }, + }, + async (args: any) => { + const params: any = { limit: args.limit }; + if (args.lead_id) params.lead_id = args.lead_id; + if (args.status_id) params.status_id = args.status_id; + + const opps = await client.search("/opportunity/", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(opps, null, 2), + }, + ], + }; + } + ); + + // Get opportunity + server.tool( + "close_get_opportunity", + "Get a specific opportunity by ID", + { + opportunity_id: { + type: "string", + description: "Opportunity ID", + required: true, + }, + }, + async (args: any) => { + const opp = await client.get( + `/opportunity/${args.opportunity_id}/` + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(opp, null, 2), + }, + ], + }; + } + ); + + // Create opportunity + server.tool( + "close_create_opportunity", + "Create a new opportunity", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + status_id: { + type: "string", + description: "Opportunity status ID", + required: true, + }, + value: { + type: "number", + description: "Opportunity value (amount)", + required: false, + }, + value_period: { + type: "string", + description: "Value period (one_time, monthly, annual)", + required: false, + }, + confidence: { + type: "number", + description: "Confidence percentage (0-100)", + required: false, + }, + note: { + type: "string", + description: "Opportunity notes", + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + status_id: args.status_id, + }; + + if (args.value !== undefined) body.value = args.value; + if (args.value_period) body.value_period = args.value_period; + if (args.confidence !== undefined) body.confidence = args.confidence; + if (args.note) body.note = args.note; + + const opp = await client.post("/opportunity/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(opp, null, 2), + }, + ], + }; + } + ); + + // Update opportunity + server.tool( + "close_update_opportunity", + "Update an existing opportunity", + { + opportunity_id: { + type: "string", + description: "Opportunity ID", + required: true, + }, + status_id: { + type: "string", + description: "New status ID", + required: false, + }, + value: { + type: "number", + description: "Opportunity value", + required: false, + }, + confidence: { + type: "number", + description: "Confidence percentage", + required: false, + }, + note: { + type: "string", + description: "Opportunity notes", + required: false, + }, + }, + async (args: any) => { + const body: any = {}; + + if (args.status_id) body.status_id = args.status_id; + if (args.value !== undefined) body.value = args.value; + if (args.confidence !== undefined) body.confidence = args.confidence; + if (args.note) body.note = args.note; + + const opp = await client.put( + `/opportunity/${args.opportunity_id}/`, + body + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(opp, null, 2), + }, + ], + }; + } + ); + + // Delete opportunity + server.tool( + "close_delete_opportunity", + "Delete an opportunity", + { + opportunity_id: { + type: "string", + description: "Opportunity ID", + required: true, + }, + }, + async (args: any) => { + await client.delete(`/opportunity/${args.opportunity_id}/`); + return { + content: [ + { + type: "text", + text: `Opportunity ${args.opportunity_id} deleted successfully`, + }, + ], + }; + } + ); + + // List opportunity statuses + server.tool( + "close_list_opportunity_statuses", + "List all opportunity statuses", + {}, + async () => { + const statuses = await client.get<{ data: OpportunityStatus[] }>( + "/status/opportunity/" + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(statuses, null, 2), + }, + ], + }; + } + ); + + // List pipelines + server.tool( + "close_list_pipelines", + "List all opportunity pipelines", + {}, + async () => { + const pipelines = await client.get<{ data: Pipeline[] }>( + "/pipeline/" + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(pipelines, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/pipelines-tools.ts b/servers/close/src/tools/pipelines-tools.ts new file mode 100644 index 0000000..5aab5bf --- /dev/null +++ b/servers/close/src/tools/pipelines-tools.ts @@ -0,0 +1,166 @@ +// Pipeline and status management tools + +import type { CloseClient } from "../client/close-client.js"; +import type { Pipeline, OpportunityStatus } from "../types/index.js"; + +export function registerPipelinesTools(server: any, client: CloseClient) { + // List pipelines + server.tool( + "close_list_pipelines", + "List all opportunity pipelines", + {}, + async () => { + const pipelines = await client.get<{ data: Pipeline[] }>("/pipeline/"); + return { + content: [ + { + type: "text", + text: JSON.stringify(pipelines, null, 2), + }, + ], + }; + } + ); + + // Get pipeline + server.tool( + "close_get_pipeline", + "Get a specific pipeline by ID", + { + pipeline_id: { + type: "string", + description: "Pipeline ID", + required: true, + }, + }, + async (args: any) => { + const pipeline = await client.get( + `/pipeline/${args.pipeline_id}/` + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(pipeline, null, 2), + }, + ], + }; + } + ); + + // Create pipeline + server.tool( + "close_create_pipeline", + "Create a new opportunity pipeline", + { + name: { + type: "string", + description: "Pipeline name", + required: true, + }, + }, + async (args: any) => { + const pipeline = await client.post("/pipeline/", { + name: args.name, + }); + return { + content: [ + { + type: "text", + text: JSON.stringify(pipeline, null, 2), + }, + ], + }; + } + ); + + // Update pipeline + server.tool( + "close_update_pipeline", + "Update an existing pipeline", + { + pipeline_id: { + type: "string", + description: "Pipeline ID", + required: true, + }, + name: { + type: "string", + description: "New pipeline name", + required: true, + }, + }, + async (args: any) => { + const pipeline = await client.put( + `/pipeline/${args.pipeline_id}/`, + { + name: args.name, + } + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(pipeline, null, 2), + }, + ], + }; + } + ); + + // Delete pipeline + server.tool( + "close_delete_pipeline", + "Delete a pipeline", + { + pipeline_id: { + type: "string", + description: "Pipeline ID", + required: true, + }, + }, + async (args: any) => { + await client.delete(`/pipeline/${args.pipeline_id}/`); + return { + content: [ + { + type: "text", + text: `Pipeline ${args.pipeline_id} deleted successfully`, + }, + ], + }; + } + ); + + // List opportunity statuses + server.tool( + "close_list_opportunity_statuses", + "List all opportunity statuses", + { + pipeline_id: { + type: "string", + description: "Filter by pipeline ID", + required: false, + }, + }, + async (args: any) => { + const endpoint = "/status/opportunity/"; + const params = args.pipeline_id + ? { pipeline_id: args.pipeline_id } + : undefined; + + const statuses = await client.get<{ data: OpportunityStatus[] }>( + endpoint, + params + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(statuses, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/reporting-tools.ts b/servers/close/src/tools/reporting-tools.ts new file mode 100644 index 0000000..b473572 --- /dev/null +++ b/servers/close/src/tools/reporting-tools.ts @@ -0,0 +1,248 @@ +// Reporting and analytics tools + +import type { CloseClient } from "../client/close-client.js"; + +export function registerReportingTools(server: any, client: CloseClient) { + // Lead status changes report + server.tool( + "close_report_lead_status_changes", + "Get report of lead status changes over time", + { + date_start: { + type: "string", + description: "Start date (YYYY-MM-DD)", + required: false, + }, + date_end: { + type: "string", + description: "End date (YYYY-MM-DD)", + required: false, + }, + status_id: { + type: "string", + description: "Filter by status ID", + required: false, + }, + }, + async (args: any) => { + const params: any = {}; + if (args.date_start) params.date_start = args.date_start; + if (args.date_end) params.date_end = args.date_end; + if (args.status_id) params.status_id = args.status_id; + + const report = await client.get("/report/lead_status_changes/", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(report, null, 2), + }, + ], + }; + } + ); + + // Opportunity funnel report + server.tool( + "close_report_opportunity_funnel", + "Get opportunity funnel/pipeline analytics", + { + date_start: { + type: "string", + description: "Start date (YYYY-MM-DD)", + required: false, + }, + date_end: { + type: "string", + description: "End date (YYYY-MM-DD)", + required: false, + }, + pipeline_id: { + type: "string", + description: "Filter by pipeline ID", + required: false, + }, + user_id: { + type: "string", + description: "Filter by user ID", + required: false, + }, + }, + async (args: any) => { + const params: any = {}; + if (args.date_start) params.date_start = args.date_start; + if (args.date_end) params.date_end = args.date_end; + if (args.pipeline_id) params.pipeline_id = args.pipeline_id; + if (args.user_id) params.user_id = args.user_id; + + const report = await client.get("/report/opportunity/", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(report, null, 2), + }, + ], + }; + } + ); + + // Activity overview report + server.tool( + "close_report_activity_overview", + "Get activity summary and metrics", + { + date_start: { + type: "string", + description: "Start date (YYYY-MM-DD)", + required: false, + }, + date_end: { + type: "string", + description: "End date (YYYY-MM-DD)", + required: false, + }, + user_id: { + type: "string", + description: "Filter by user ID", + required: false, + }, + activity_type: { + type: "string", + description: "Filter by activity type", + required: false, + }, + }, + async (args: any) => { + const params: any = {}; + if (args.date_start) params.date_start = args.date_start; + if (args.date_end) params.date_end = args.date_end; + if (args.user_id) params.user_id = args.user_id; + if (args.activity_type) params.activity_type = args.activity_type; + + const report = await client.get("/report/activity/", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(report, null, 2), + }, + ], + }; + } + ); + + // Revenue forecast report + server.tool( + "close_report_revenue_forecast", + "Get revenue forecast based on opportunities", + { + date_start: { + type: "string", + description: "Start date (YYYY-MM-DD)", + required: false, + }, + date_end: { + type: "string", + description: "End date (YYYY-MM-DD)", + required: false, + }, + pipeline_id: { + type: "string", + description: "Filter by pipeline ID", + required: false, + }, + user_id: { + type: "string", + description: "Filter by user ID", + required: false, + }, + }, + async (args: any) => { + const params: any = {}; + if (args.date_start) params.date_start = args.date_start; + if (args.date_end) params.date_end = args.date_end; + if (args.pipeline_id) params.pipeline_id = args.pipeline_id; + if (args.user_id) params.user_id = args.user_id; + + const report = await client.get("/report/revenue/", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(report, null, 2), + }, + ], + }; + } + ); + + // Leaderboard report + server.tool( + "close_report_leaderboard", + "Get user performance leaderboard", + { + date_start: { + type: "string", + description: "Start date (YYYY-MM-DD)", + required: false, + }, + date_end: { + type: "string", + description: "End date (YYYY-MM-DD)", + required: false, + }, + metric: { + type: "string", + description: "Metric to rank by (calls, emails, opportunities_won)", + required: false, + }, + }, + async (args: any) => { + const params: any = {}; + if (args.date_start) params.date_start = args.date_start; + if (args.date_end) params.date_end = args.date_end; + if (args.metric) params.metric = args.metric; + + const report = await client.get("/report/leaderboard/", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(report, null, 2), + }, + ], + }; + } + ); + + // Custom report query + server.tool( + "close_report_custom", + "Run a custom report query", + { + report_type: { + type: "string", + description: "Report type/endpoint", + required: true, + }, + params: { + type: "string", + description: "JSON object with report parameters", + required: false, + }, + }, + async (args: any) => { + const params = args.params ? JSON.parse(args.params) : {}; + const report = await client.get(`/report/${args.report_type}/`, params); + return { + content: [ + { + type: "text", + text: JSON.stringify(report, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/sequences-tools.ts b/servers/close/src/tools/sequences-tools.ts new file mode 100644 index 0000000..237e1bf --- /dev/null +++ b/servers/close/src/tools/sequences-tools.ts @@ -0,0 +1,188 @@ +// Sequences and automation tools + +import type { CloseClient } from "../client/close-client.js"; +import type { Sequence, SequenceSubscription } from "../types/index.js"; + +export function registerSequencesTools(server: any, client: CloseClient) { + // List sequences + server.tool( + "close_list_sequences", + "List all email sequences", + {}, + async () => { + const sequences = await client.get<{ data: Sequence[] }>("/sequence/"); + return { + content: [ + { + type: "text", + text: JSON.stringify(sequences, null, 2), + }, + ], + }; + } + ); + + // Get sequence + server.tool( + "close_get_sequence", + "Get a specific sequence by ID", + { + sequence_id: { + type: "string", + description: "Sequence ID", + required: true, + }, + }, + async (args: any) => { + const sequence = await client.get( + `/sequence/${args.sequence_id}/` + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(sequence, null, 2), + }, + ], + }; + } + ); + + // Create sequence + server.tool( + "close_create_sequence", + "Create a new email sequence", + { + name: { + type: "string", + description: "Sequence name", + required: true, + }, + max_activations: { + type: "number", + description: "Maximum activations per lead", + required: false, + }, + }, + async (args: any) => { + const body: any = { + name: args.name, + }; + + if (args.max_activations) + body.max_activations = args.max_activations; + + const sequence = await client.post("/sequence/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(sequence, null, 2), + }, + ], + }; + } + ); + + // Subscribe lead to sequence + server.tool( + "close_subscribe_lead_to_sequence", + "Subscribe a lead to an email sequence", + { + sequence_id: { + type: "string", + description: "Sequence ID", + required: true, + }, + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + contact_id: { + type: "string", + description: "Contact ID (optional, for specific contact)", + required: false, + }, + sender_account_id: { + type: "string", + description: "Sender email account ID", + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + }; + + if (args.contact_id) body.contact_id = args.contact_id; + if (args.sender_account_id) + body.sender_account_id = args.sender_account_id; + + const subscription = await client.post( + `/sequence/${args.sequence_id}/subscribe/`, + body + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(subscription, null, 2), + }, + ], + }; + } + ); + + // Unsubscribe lead from sequence + server.tool( + "close_unsubscribe_lead_from_sequence", + "Unsubscribe a lead from a sequence", + { + subscription_id: { + type: "string", + description: "Sequence subscription ID", + required: true, + }, + }, + async (args: any) => { + await client.delete( + `/sequence_subscription/${args.subscription_id}/` + ); + return { + content: [ + { + type: "text", + text: `Lead unsubscribed from sequence successfully`, + }, + ], + }; + } + ); + + // Get sequence stats + server.tool( + "close_get_sequence_stats", + "Get statistics for a sequence", + { + sequence_id: { + type: "string", + description: "Sequence ID", + required: true, + }, + }, + async (args: any) => { + const stats = await client.get( + `/sequence/${args.sequence_id}/stats/` + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(stats, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/smart-views-tools.ts b/servers/close/src/tools/smart-views-tools.ts new file mode 100644 index 0000000..21a3393 --- /dev/null +++ b/servers/close/src/tools/smart-views-tools.ts @@ -0,0 +1,159 @@ +// Smart Views management tools + +import type { CloseClient } from "../client/close-client.js"; +import type { SmartView } from "../types/index.js"; + +export function registerSmartViewsTools(server: any, client: CloseClient) { + // List smart views + server.tool( + "close_list_smart_views", + "List all smart views", + {}, + async () => { + const views = await client.get<{ data: SmartView[] }>("/saved_search/"); + return { + content: [ + { + type: "text", + text: JSON.stringify(views, null, 2), + }, + ], + }; + } + ); + + // Get smart view + server.tool( + "close_get_smart_view", + "Get a specific smart view by ID", + { + view_id: { + type: "string", + description: "Smart view ID", + required: true, + }, + }, + async (args: any) => { + const view = await client.get( + `/saved_search/${args.view_id}/` + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(view, null, 2), + }, + ], + }; + } + ); + + // Create smart view + server.tool( + "close_create_smart_view", + "Create a new smart view (saved search)", + { + name: { + type: "string", + description: "Smart view name", + required: true, + }, + query: { + type: "string", + description: "Search query", + required: true, + }, + type: { + type: "string", + description: "View type (e.g., 'lead')", + required: false, + }, + }, + async (args: any) => { + const body: any = { + name: args.name, + query: args.query, + }; + + if (args.type) { + body.type = args.type; + } + + const view = await client.post("/saved_search/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(view, null, 2), + }, + ], + }; + } + ); + + // Update smart view + server.tool( + "close_update_smart_view", + "Update an existing smart view", + { + view_id: { + type: "string", + description: "Smart view ID", + required: true, + }, + name: { + type: "string", + description: "New name", + required: false, + }, + query: { + type: "string", + description: "New query", + required: false, + }, + }, + async (args: any) => { + const body: any = {}; + + if (args.name) body.name = args.name; + if (args.query) body.query = args.query; + + const view = await client.put( + `/saved_search/${args.view_id}/`, + body + ); + return { + content: [ + { + type: "text", + text: JSON.stringify(view, null, 2), + }, + ], + }; + } + ); + + // Delete smart view + server.tool( + "close_delete_smart_view", + "Delete a smart view", + { + view_id: { + type: "string", + description: "Smart view ID", + required: true, + }, + }, + async (args: any) => { + await client.delete(`/saved_search/${args.view_id}/`); + return { + content: [ + { + type: "text", + text: `Smart view ${args.view_id} deleted successfully`, + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/tasks-tools.ts b/servers/close/src/tools/tasks-tools.ts new file mode 100644 index 0000000..ecfb957 --- /dev/null +++ b/servers/close/src/tools/tasks-tools.ts @@ -0,0 +1,230 @@ +// Task management tools + +import type { CloseClient } from "../client/close-client.js"; +import type { Task } from "../types/index.js"; + +export function registerTasksTools(server: any, client: CloseClient) { + // List tasks + server.tool( + "close_list_tasks", + "List all tasks with optional filtering", + { + lead_id: { + type: "string", + description: "Filter by lead ID", + required: false, + }, + assigned_to: { + type: "string", + description: "Filter by assigned user ID", + required: false, + }, + is_complete: { + type: "boolean", + description: "Filter by completion status", + required: false, + }, + limit: { + type: "number", + description: "Number of results", + required: false, + }, + }, + async (args: any) => { + const params: any = { limit: args.limit }; + if (args.lead_id) params.lead_id = args.lead_id; + if (args.assigned_to) params.assigned_to = args.assigned_to; + if (args.is_complete !== undefined) + params.is_complete = args.is_complete; + + const tasks = await client.search("/task/", params); + return { + content: [ + { + type: "text", + text: JSON.stringify(tasks, null, 2), + }, + ], + }; + } + ); + + // Get task + server.tool( + "close_get_task", + "Get a specific task by ID", + { + task_id: { + type: "string", + description: "Task ID", + required: true, + }, + }, + async (args: any) => { + const task = await client.get(`/task/${args.task_id}/`); + return { + content: [ + { + type: "text", + text: JSON.stringify(task, null, 2), + }, + ], + }; + } + ); + + // Create task + server.tool( + "close_create_task", + "Create a new task", + { + lead_id: { + type: "string", + description: "Lead ID", + required: true, + }, + text: { + type: "string", + description: "Task description", + required: true, + }, + assigned_to: { + type: "string", + description: "User ID to assign task to", + required: false, + }, + date: { + type: "string", + description: "Due date (YYYY-MM-DD format)", + required: false, + }, + type: { + type: "string", + description: "Task type (e.g., 'lead', 'inbox')", + required: false, + }, + }, + async (args: any) => { + const body: any = { + lead_id: args.lead_id, + text: args.text, + _type: args.type || "lead", + }; + + if (args.assigned_to) body.assigned_to = args.assigned_to; + if (args.date) body.date = args.date; + + const task = await client.post("/task/", body); + return { + content: [ + { + type: "text", + text: JSON.stringify(task, null, 2), + }, + ], + }; + } + ); + + // Update task + server.tool( + "close_update_task", + "Update an existing task", + { + task_id: { + type: "string", + description: "Task ID", + required: true, + }, + text: { + type: "string", + description: "Task description", + required: false, + }, + assigned_to: { + type: "string", + description: "User ID to assign task to", + required: false, + }, + date: { + type: "string", + description: "Due date", + required: false, + }, + is_complete: { + type: "boolean", + description: "Completion status", + required: false, + }, + }, + async (args: any) => { + const body: any = {}; + + if (args.text) body.text = args.text; + if (args.assigned_to) body.assigned_to = args.assigned_to; + if (args.date) body.date = args.date; + if (args.is_complete !== undefined) + body.is_complete = args.is_complete; + + const task = await client.put(`/task/${args.task_id}/`, body); + return { + content: [ + { + type: "text", + text: JSON.stringify(task, null, 2), + }, + ], + }; + } + ); + + // Delete task + server.tool( + "close_delete_task", + "Delete a task", + { + task_id: { + type: "string", + description: "Task ID", + required: true, + }, + }, + async (args: any) => { + await client.delete(`/task/${args.task_id}/`); + return { + content: [ + { + type: "text", + text: `Task ${args.task_id} deleted successfully`, + }, + ], + }; + } + ); + + // Complete task + server.tool( + "close_complete_task", + "Mark a task as complete", + { + task_id: { + type: "string", + description: "Task ID", + required: true, + }, + }, + async (args: any) => { + const task = await client.put(`/task/${args.task_id}/`, { + is_complete: true, + }); + return { + content: [ + { + type: "text", + text: JSON.stringify(task, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/tools/users-tools.ts b/servers/close/src/tools/users-tools.ts new file mode 100644 index 0000000..d670d5b --- /dev/null +++ b/servers/close/src/tools/users-tools.ts @@ -0,0 +1,84 @@ +// User and role management tools + +import type { CloseClient } from "../client/close-client.js"; +import type { User, Role } from "../types/index.js"; + +export function registerUsersTools(server: any, client: CloseClient) { + // List users + server.tool( + "close_list_users", + "List all users in the organization", + {}, + async () => { + const users = await client.get<{ data: User[] }>("/user/"); + return { + content: [ + { + type: "text", + text: JSON.stringify(users, null, 2), + }, + ], + }; + } + ); + + // Get user + server.tool( + "close_get_user", + "Get a specific user by ID", + { + user_id: { + type: "string", + description: "User ID", + required: true, + }, + }, + async (args: any) => { + const user = await client.get(`/user/${args.user_id}/`); + return { + content: [ + { + type: "text", + text: JSON.stringify(user, null, 2), + }, + ], + }; + } + ); + + // Get current user + server.tool( + "close_get_current_user", + "Get the current authenticated user", + {}, + async () => { + const user = await client.get("/me/"); + return { + content: [ + { + type: "text", + text: JSON.stringify(user, null, 2), + }, + ], + }; + } + ); + + // List roles + server.tool( + "close_list_roles", + "List all roles in the organization", + {}, + async () => { + const roles = await client.get<{ data: Role[] }>("/role/"); + return { + content: [ + { + type: "text", + text: JSON.stringify(roles, null, 2), + }, + ], + }; + } + ); +} diff --git a/servers/close/src/types/index.ts b/servers/close/src/types/index.ts new file mode 100644 index 0000000..fabad89 --- /dev/null +++ b/servers/close/src/types/index.ts @@ -0,0 +1,323 @@ +// Close CRM Types + +export interface CloseConfig { + apiKey: string; + baseUrl?: string; +} + +export interface PaginationParams { + limit?: number; + skip?: number; + _cursor?: string; +} + +export interface SearchParams extends PaginationParams { + query?: string; + _fields?: string[]; +} + +// Lead Types +export interface Lead { + id: string; + name: string; + display_name?: string; + status_id?: string; + status_label?: string; + description?: string; + url?: string; + created_by?: string; + created_by_name?: string; + date_created?: string; + date_updated?: string; + organization_id?: string; + contacts?: Contact[]; + opportunities?: Opportunity[]; + tasks?: Task[]; + custom?: Record; + addresses?: Address[]; +} + +export interface Address { + label?: string; + address_1?: string; + address_2?: string; + city?: string; + state?: string; + zipcode?: string; + country?: string; +} + +// Contact Types +export interface Contact { + id: string; + lead_id?: string; + name?: string; + title?: string; + emails?: EmailAddress[]; + phones?: Phone[]; + urls?: Url[]; + date_created?: string; + date_updated?: string; + organization_id?: string; + created_by?: string; +} + +export interface EmailAddress { + email: string; + type?: string; +} + +export interface Phone { + phone: string; + type?: string; + country?: string; +} + +export interface Url { + url: string; + type?: string; +} + +// Opportunity Types +export interface Opportunity { + id: string; + lead_id: string; + status_id: string; + status_type?: string; + status_label?: string; + note?: string; + confidence?: number; + value?: number; + value_period?: string; + value_currency?: string; + date_created?: string; + date_updated?: string; + date_won?: string; + user_id?: string; + user_name?: string; + organization_id?: string; +} + +export interface OpportunityStatus { + id: string; + label: string; + type: string; +} + +export interface Pipeline { + id: string; + name: string; + organization_id?: string; + statuses?: OpportunityStatus[]; +} + +// Activity Types +export interface Activity { + id: string; + lead_id?: string; + contact_id?: string; + user_id?: string; + user_name?: string; + date_created?: string; + date_updated?: string; + organization_id?: string; + _type?: string; +} + +export interface Note extends Activity { + note: string; +} + +export interface Call extends Activity { + duration?: number; + phone?: string; + direction?: string; + disposition?: string; + note?: string; + recording_url?: string; +} + +export interface Email extends Activity { + subject?: string; + body_text?: string; + body_html?: string; + status?: string; + direction?: string; + sender?: string; + to?: string[]; + cc?: string[]; + bcc?: string[]; + opens?: number; + clicks?: number; +} + +export interface SMS extends Activity { + text: string; + direction?: string; + status?: string; + remote_phone?: string; + local_phone?: string; +} + +export interface Meeting extends Activity { + title?: string; + starts_at?: string; + ends_at?: string; + location?: string; + note?: string; + attendees?: string[]; +} + +// Task Types +export interface Task { + id: string; + _type: string; + assigned_to?: string; + assigned_to_name?: string; + lead_id?: string; + lead_name?: string; + text?: string; + date?: string; + is_complete?: boolean; + date_created?: string; + date_updated?: string; + organization_id?: string; +} + +// Smart View Types +export interface SmartView { + id: string; + name: string; + query?: string; + type?: string; + date_created?: string; + date_updated?: string; + organization_id?: string; +} + +// User Types +export interface User { + id: string; + email: string; + first_name?: string; + last_name?: string; + image?: string; + date_created?: string; + date_updated?: string; + organizations?: any[]; +} + +export interface Role { + id: string; + name: string; + organization_id?: string; +} + +// Custom Field Types +export interface CustomField { + id: string; + name: string; + type: string; + required?: boolean; + accepts_multiple_values?: boolean; + editable_with_roles?: string[]; + choices?: string[]; + date_created?: string; + date_updated?: string; + organization_id?: string; +} + +// Sequence Types +export interface Sequence { + id: string; + name: string; + status?: string; + max_activations?: number; + throttle_capacity?: number; + throttle_period_seconds?: number; + date_created?: string; + date_updated?: string; + organization_id?: string; +} + +export interface SequenceSubscription { + id: string; + sequence_id: string; + lead_id: string; + contact_id?: string; + sender_account_id?: string; + status?: string; + date_created?: string; + date_paused?: string; + date_completed?: string; +} + +// Bulk Operation Types +export interface BulkEditRequest { + lead_ids?: string[]; + query?: string; + updates: Record; +} + +export interface BulkDeleteRequest { + lead_ids?: string[]; + query?: string; +} + +export interface BulkEmailRequest { + lead_ids?: string[]; + query?: string; + template_id?: string; + subject: string; + body: string; + sender?: string; +} + +// Reporting Types +export interface LeadStatusChange { + lead_id: string; + lead_name?: string; + old_status?: string; + new_status?: string; + changed_by?: string; + date_changed?: string; +} + +export interface OpportunityFunnel { + status_id: string; + status_label?: string; + count: number; + total_value?: number; + average_value?: number; +} + +export interface ActivitySummary { + date: string; + calls?: number; + emails?: number; + meetings?: number; + notes?: number; + total?: number; +} + +export interface RevenueForcast { + period: string; + pipeline_value?: number; + weighted_value?: number; + won_value?: number; + closed_count?: number; +} + +// API Response Types +export interface CloseAPIResponse { + data?: T[]; + has_more?: boolean; + total_results?: number; + cursor?: string; +} + +export interface CloseAPIError { + error?: string; + errors?: any[]; + field_errors?: Record; +} diff --git a/servers/close/tsconfig.json b/servers/close/tsconfig.json index de6431e..f4624bd 100644 --- a/servers/close/tsconfig.json +++ b/servers/close/tsconfig.json @@ -1,14 +1,18 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/servers/fieldedge/README.md b/servers/fieldedge/README.md index 542a53b..00cc252 100644 --- a/servers/fieldedge/README.md +++ b/servers/fieldedge/README.md @@ -1,231 +1,169 @@ # FieldEdge MCP Server -Complete Model Context Protocol (MCP) server for FieldEdge field service management platform. Provides comprehensive access to jobs, customers, invoices, estimates, technicians, dispatch, equipment, inventory, service agreements, and reporting. +Complete MCP server for FieldEdge field service management platform with 87+ tools and 16 React apps. ## Features -### 45+ Tools Across 10 Categories +### 87+ Tools Across 13 Domains -#### Jobs Management (9 tools) -- `fieldedge_jobs_list` - List and filter jobs -- `fieldedge_jobs_get` - Get job details -- `fieldedge_jobs_create` - Create new job -- `fieldedge_jobs_update` - Update job -- `fieldedge_jobs_complete` - Mark job complete -- `fieldedge_jobs_cancel` - Cancel job -- `fieldedge_jobs_line_items_list` - List job line items -- `fieldedge_jobs_line_items_add` - Add line item to job -- `fieldedge_jobs_equipment_list` - List equipment on job +- **Customer Management** (10 tools): Create, update, search customers, manage balances, view history +- **Job Management** (9 tools): Full CRUD operations, start/complete/cancel jobs, assign technicians +- **Invoice Management** (9 tools): Create, send, void invoices, record payments, generate PDFs +- **Estimate Management** (8 tools): Create quotes, send to customers, approve, convert to invoices +- **Equipment Management** (7 tools): Track equipment, service history, schedule maintenance +- **Technician Management** (9 tools): Manage technicians, schedules, availability, time tracking +- **Scheduling & Dispatch** (8 tools): Create appointments, dispatch board, route optimization +- **Inventory Management** (7 tools): Track parts, adjust quantities, low stock alerts +- **Payment Management** (5 tools): Process payments, refunds, payment history +- **Reporting & Analytics** (8 tools): Revenue, productivity, aging receivables, satisfaction metrics +- **Location Management** (5 tools): Manage customer service locations +- **Service Agreements** (6 tools): Maintenance contracts, renewals, cancellations +- **Task Management** (6 tools): Follow-ups, to-dos, task completion -#### Customer Management (8 tools) -- `fieldedge_customers_list` - List and filter customers -- `fieldedge_customers_get` - Get customer details -- `fieldedge_customers_create` - Create new customer -- `fieldedge_customers_update` - Update customer -- `fieldedge_customers_delete` - Delete/deactivate customer -- `fieldedge_customers_search` - Search customers -- `fieldedge_customers_locations_list` - List customer locations -- `fieldedge_customers_equipment_list` - List customer equipment +### 16 React MCP Apps -#### Invoice Management (6 tools) -- `fieldedge_invoices_list` - List and filter invoices -- `fieldedge_invoices_get` - Get invoice details -- `fieldedge_invoices_create` - Create invoice -- `fieldedge_invoices_update` - Update invoice -- `fieldedge_invoices_payments_list` - List payments -- `fieldedge_invoices_payments_add` - Add payment +Modern dark-themed React applications built with Vite: -#### Estimate Management (6 tools) -- `fieldedge_estimates_list` - List estimates -- `fieldedge_estimates_get` - Get estimate details -- `fieldedge_estimates_create` - Create estimate -- `fieldedge_estimates_update` - Update estimate -- `fieldedge_estimates_send` - Send estimate to customer -- `fieldedge_estimates_approve` - Approve and convert to job - -#### Technician Management (6 tools) -- `fieldedge_technicians_list` - List technicians -- `fieldedge_technicians_get` - Get technician details -- `fieldedge_technicians_create` - Create technician -- `fieldedge_technicians_update` - Update technician -- `fieldedge_technicians_performance_get` - Get performance metrics -- `fieldedge_technicians_time_entries_list` - List time entries - -#### Dispatch Management (5 tools) -- `fieldedge_dispatch_board_get` - Get dispatch board -- `fieldedge_dispatch_assign_tech` - Assign technician to job -- `fieldedge_dispatch_technician_availability_get` - Get technician availability -- `fieldedge_dispatch_zones_list` - List dispatch zones -- `fieldedge_dispatch_optimize` - Auto-optimize dispatch schedule - -#### Equipment Management (5 tools) -- `fieldedge_equipment_list` - List equipment -- `fieldedge_equipment_get` - Get equipment details -- `fieldedge_equipment_create` - Create equipment record -- `fieldedge_equipment_update` - Update equipment -- `fieldedge_equipment_service_history_list` - List service history - -#### Inventory Management (6 tools) -- `fieldedge_inventory_parts_list` - List inventory parts -- `fieldedge_inventory_parts_get` - Get part details -- `fieldedge_inventory_stock_update` - Update stock levels -- `fieldedge_inventory_purchase_orders_list` - List purchase orders -- `fieldedge_inventory_purchase_orders_get` - Get PO details -- `fieldedge_inventory_reorder_report` - Get reorder report - -#### Service Agreements (6 tools) -- `fieldedge_agreements_list` - List service agreements -- `fieldedge_agreements_get` - Get agreement details -- `fieldedge_agreements_create` - Create agreement -- `fieldedge_agreements_update` - Update agreement -- `fieldedge_agreements_cancel` - Cancel agreement -- `fieldedge_agreements_renew` - Renew agreement - -#### Reporting & Analytics (6 tools) -- `fieldedge_reports_revenue` - Revenue report -- `fieldedge_reports_job_profitability` - Job profitability analysis -- `fieldedge_reports_technician_performance` - Tech performance metrics -- `fieldedge_reports_aging` - A/R aging report -- `fieldedge_reports_service_agreement_revenue` - Agreement revenue -- `fieldedge_reports_equipment_service_due` - Equipment service due - -### 16 Interactive MCP Apps - -- **job-dashboard** - Interactive jobs overview with filtering -- **job-detail** - Detailed job view with all information -- **job-grid** - Spreadsheet-style bulk job management -- **customer-detail** - Complete customer profile -- **customer-grid** - Customer data grid view -- **invoice-dashboard** - Invoice and payment tracking -- **estimate-builder** - Interactive estimate creation -- **dispatch-board** - Visual dispatch board -- **schedule-calendar** - Job scheduling calendar -- **technician-dashboard** - Tech performance dashboard -- **equipment-tracker** - Equipment and maintenance tracking -- **inventory-manager** - Inventory and stock management -- **agreement-manager** - Service agreement management -- **revenue-dashboard** - Revenue analytics -- **performance-metrics** - Performance analytics -- **aging-report** - A/R aging visualization +1. **Dashboard** - Key metrics and recent activity +2. **Customer Management** - Browse and manage customers +3. **Job Management** - View and manage jobs/work orders +4. **Scheduling & Dispatch** - Dispatch board and appointment scheduling +5. **Invoice Management** - Create and manage invoices +6. **Estimate Management** - Create and manage quotes +7. **Technician Management** - Manage technicians and schedules +8. **Equipment Management** - Track customer equipment +9. **Inventory Management** - Parts and equipment inventory +10. **Payment Management** - Process payments +11. **Service Agreements** - Maintenance contracts +12. **Reports & Analytics** - Business reports +13. **Task Management** - Follow-ups and to-dos +14. **Calendar View** - Appointment calendar +15. **Map View** - Geographic view of jobs +16. **Price Book** - Service and part pricing ## Installation ```bash npm install -npm run build ``` ## Configuration -Set the following environment variables: +Create a `.env` file with your FieldEdge credentials: -```bash -export FIELDEDGE_API_KEY="your_api_key_here" -export FIELDEDGE_BASE_URL="https://api.fieldedge.com/v2" # Optional, defaults to production +```env +FIELDEDGE_API_KEY=your_api_key_here +FIELDEDGE_API_URL=https://api.fieldedge.com/v1 +FIELDEDGE_COMPANY_ID=your_company_id +FIELDEDGE_TIMEOUT=30000 ``` ## Usage -### With Claude Desktop +### As MCP Server -Add to your `claude_desktop_config.json`: +Add to your MCP settings: ```json { "mcpServers": { "fieldedge": { - "command": "node", - "args": ["/path/to/fieldedge/dist/main.js"], + "command": "npx", + "args": ["-y", "@mcpengine/fieldedge-mcp-server"], "env": { - "FIELDEDGE_API_KEY": "your_api_key_here" + "FIELDEDGE_API_KEY": "your_api_key" } } } } ``` -### Standalone +### Development ```bash -FIELDEDGE_API_KEY=your_key npm start +# Build TypeScript +npm run build + +# Watch mode +npm run dev + +# Build React apps +npm run build:ui ``` -## Example Queries +## API Coverage -**Jobs:** -- "Show me all emergency jobs scheduled for today" -- "Create a new HVAC maintenance job for customer C123" -- "What jobs are assigned to technician T456?" -- "Mark job J789 as complete" +The server provides comprehensive coverage of the FieldEdge API: -**Customers:** -- "Find all commercial customers in Chicago" -- "Show me customer details for account C123" -- "List all equipment for customer C456" - -**Invoices:** -- "Show me all overdue invoices" -- "Create an invoice for job J789" -- "Add a $500 payment to invoice INV-123" - -**Dispatch:** -- "Show me today's dispatch board" -- "Assign technician T123 to job J456" -- "What's the technician availability for tomorrow?" -- "Optimize the dispatch schedule for tomorrow" - -**Reports:** -- "Show me revenue for the last 30 days" -- "What's the profitability of job J123?" -- "Generate an aging report" -- "Show technician performance metrics for this month" +- Customer and contact management +- Job/work order lifecycle management +- Scheduling and dispatch operations +- Invoicing and payment processing +- Estimate/quote generation +- Equipment and asset tracking +- Inventory management +- Technician management and time tracking +- Service agreement management +- Business reporting and analytics ## Architecture ``` src/ -├── client.ts # API client with auth, pagination, error handling -├── types.ts # TypeScript type definitions -├── tools/ # Tool implementations -│ ├── jobs-tools.ts -│ ├── customers-tools.ts -│ ├── invoices-tools.ts -│ ├── estimates-tools.ts -│ ├── technicians-tools.ts -│ ├── dispatch-tools.ts -│ ├── equipment-tools.ts -│ ├── inventory-tools.ts -│ ├── agreements-tools.ts -│ └── reporting-tools.ts -├── apps/ # MCP app implementations -│ └── index.ts -├── server.ts # MCP server setup -└── main.ts # Entry point +├── clients/ +│ └── fieldedge.ts # API client with auth, pagination, error handling +├── types/ +│ └── index.ts # TypeScript type definitions +├── tools/ +│ ├── customers.ts # Customer management tools +│ ├── jobs.ts # Job management tools +│ ├── invoices.ts # Invoice management tools +│ ├── estimates.ts # Estimate management tools +│ ├── equipment.ts # Equipment management tools +│ ├── technicians.ts # Technician management tools +│ ├── scheduling.ts # Scheduling and dispatch tools +│ ├── inventory.ts # Inventory management tools +│ ├── payments.ts # Payment management tools +│ ├── reporting.ts # Reporting and analytics tools +│ ├── locations.ts # Location management tools +│ ├── service-agreements.ts # Service agreement tools +│ └── tasks.ts # Task management tools +├── ui/ # React MCP apps (16 apps) +│ ├── dashboard/ +│ ├── customers/ +│ ├── jobs/ +│ └── ... +├── server.ts # MCP server implementation +└── main.ts # Entry point ``` -## API Client Features +## Error Handling -- **Bearer Token Authentication** - Automatic authorization header injection -- **Pagination Support** - Built-in pagination handling with `getPaginated()` and `getAllPages()` -- **Error Handling** - Comprehensive error handling with detailed error messages -- **Request Methods** - Full REST support (GET, POST, PUT, PATCH, DELETE) -- **Type Safety** - Full TypeScript typing for all API responses +The client includes comprehensive error handling: -## Development +- Authentication errors (401) +- Permission errors (403) +- Not found errors (404) +- Rate limiting (429) +- Server errors (5xx) +- Network errors -```bash -# Install dependencies -npm install +## Rate Limiting -# Build -npm run build +Automatic rate limit tracking and retry logic included. The client monitors rate limit headers and automatically waits when limits are approached. -# Development with watch mode -npm run build -- --watch +## TypeScript Support -# Run tests (if implemented) -npm test -``` +Full TypeScript support with comprehensive type definitions for: + +- All API request/response types +- Tool input schemas +- Error types +- Configuration options + +## Contributing + +Issues and pull requests welcome at [github.com/BusyBee3333/mcpengine](https://github.com/BusyBee3333/mcpengine) ## License @@ -233,6 +171,4 @@ MIT ## Support -For issues or questions: -- FieldEdge API documentation: https://developer.fieldedge.com -- MCP Protocol: https://modelcontextprotocol.io +For API access and documentation, visit [docs.api.fieldedge.com](https://docs.api.fieldedge.com) diff --git a/servers/fieldedge/package.json b/servers/fieldedge/package.json index f7bd6bf..5448151 100644 --- a/servers/fieldedge/package.json +++ b/servers/fieldedge/package.json @@ -1,31 +1,42 @@ { - "name": "@mcpengine/fieldedge", + "name": "@mcpengine/fieldedge-mcp-server", "version": "1.0.0", - "description": "FieldEdge MCP Server - Complete field service management integration", + "description": "MCP server for FieldEdge field service management platform", + "keywords": ["mcp", "fieldedge", "field-service", "hvac", "plumbing", "electrical", "contractors"], + "author": "MCP Engine", + "license": "MIT", "type": "module", - "main": "dist/main.js", "bin": { "fieldedge-mcp": "./dist/main.js" }, - "scripts": { - "build": "tsc", - "start": "node dist/main.js" - }, - "keywords": [ - "mcp", - "fieldedge", - "field-service", - "hvac", - "dispatch", - "service-management" + "files": [ + "dist", + "README.md" ], - "author": "MCPEngine", - "license": "MIT", + "scripts": { + "build": "tsc && npm run build:ui", + "build:ui": "node scripts/build-ui.js", + "dev": "tsc --watch", + "prepublishOnly": "npm run build", + "test": "echo 'Tests coming soon' && exit 0" + }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.4" + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.7.9", + "dotenv": "^16.4.7", + "zod": "^3.24.1" }, "devDependencies": { "@types/node": "^22.10.5", - "typescript": "^5.7.2" + "typescript": "^5.7.3", + "vite": "^6.0.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@types/react": "^19.0.6", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/servers/fieldedge/scripts/build-ui.js b/servers/fieldedge/scripts/build-ui.js index c38e070..fd47caa 100644 --- a/servers/fieldedge/scripts/build-ui.js +++ b/servers/fieldedge/scripts/build-ui.js @@ -1,45 +1,41 @@ #!/usr/bin/env node -/** - * Build script for React UI apps - */ - import { execSync } from 'child_process'; import { readdirSync, statSync } from 'fs'; -import { join } from 'path'; +import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; -import { dirname } from 'path'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const uiDir = join(__dirname, '../src/ui'); -const reactAppDir = join(__dirname, '..', 'src', 'ui', 'react-app'); +console.log('Building React apps...\n'); -try { - const apps = readdirSync(reactAppDir).filter(file => { - const fullPath = join(reactAppDir, file); - return statSync(fullPath).isDirectory(); - }); +const apps = readdirSync(uiDir).filter((file) => { + const fullPath = join(uiDir, file); + return statSync(fullPath).isDirectory(); +}); - console.log(`Found ${apps.length} React apps to build`); +let successCount = 0; +let failCount = 0; - for (const app of apps) { - const appPath = join(reactAppDir, app); - console.log(`Building ${app}...`); - - try { - execSync('npx vite build', { - cwd: appPath, - stdio: 'inherit', - }); - console.log(`✓ ${app} built successfully`); - } catch (error) { - console.error(`✗ Failed to build ${app}`); - } +for (const app of apps) { + const appPath = join(uiDir, app); + console.log(`Building ${app}...`); + + try { + execSync('npx vite build', { + cwd: appPath, + stdio: 'inherit', + }); + successCount++; + } catch (error) { + console.error(`Failed to build ${app}`); + failCount++; } +} - console.log('UI build complete'); -} catch (error) { - console.error('Build failed:', error.message); +console.log(`\n✅ Successfully built ${successCount} apps`); +if (failCount > 0) { + console.log(`❌ Failed to build ${failCount} apps`); process.exit(1); } diff --git a/servers/fieldedge/scripts/generate-apps.js b/servers/fieldedge/scripts/generate-apps.js new file mode 100644 index 0000000..da1fa58 --- /dev/null +++ b/servers/fieldedge/scripts/generate-apps.js @@ -0,0 +1,256 @@ +#!/usr/bin/env node + +import { writeFileSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const uiDir = join(__dirname, '../src/ui'); + +const apps = [ + { name: 'customers', title: 'Customer Management', description: 'Browse and manage customers' }, + { name: 'jobs', title: 'Job Management', description: 'View and manage jobs/work orders' }, + { name: 'scheduling', title: 'Scheduling & Dispatch', description: 'Dispatch board and appointment scheduling' }, + { name: 'invoices', title: 'Invoice Management', description: 'Create and manage invoices' }, + { name: 'estimates', title: 'Estimate Management', description: 'Create and manage estimates/quotes' }, + { name: 'technicians', title: 'Technician Management', description: 'Manage technicians and schedules' }, + { name: 'equipment', title: 'Equipment Management', description: 'Track customer equipment' }, + { name: 'inventory', title: 'Inventory Management', description: 'Manage parts and equipment inventory' }, + { name: 'payments', title: 'Payment Management', description: 'Process payments and view history' }, + { name: 'service-agreements', title: 'Service Agreements', description: 'Manage maintenance contracts' }, + { name: 'reports', title: 'Reports & Analytics', description: 'Business reports and analytics' }, + { name: 'tasks', title: 'Task Management', description: 'Manage follow-ups and to-dos' }, + { name: 'calendar', title: 'Calendar View', description: 'Calendar view of appointments' }, + { name: 'map-view', title: 'Map View', description: 'Map view of jobs and technicians' }, + { name: 'price-book', title: 'Price Book', description: 'Manage pricing for services' }, +]; + +const createApp = (app) => { + const appDir = join(uiDir, app.name); + mkdirSync(appDir, { recursive: true }); + + // App.tsx + const appTsx = `import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function ${app.name.replace(/-/g, '')}App() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading ${app.title.toLowerCase()}...
+
+ ); + } + + return ( +
+
+

${app.title}

+ +
+ +
+
+

${app.description}

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} +`; + + // styles.css (shared dark theme) + 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: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} +`; + + // main.tsx + const mainTsx = `import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); +`; + + // index.html + const indexHtml = ` + + + + + ${app.title} - FieldEdge + + +
+ + + +`; + + // vite.config.ts + const viteConfig = `import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + build: { + outDir: '../../../dist/ui/${app.name}', + emptyOutDir: true, + }, +}); +`; + + writeFileSync(join(appDir, 'App.tsx'), appTsx); + writeFileSync(join(appDir, 'styles.css'), stylesCss); + writeFileSync(join(appDir, 'main.tsx'), mainTsx); + writeFileSync(join(appDir, 'index.html'), indexHtml); + writeFileSync(join(appDir, 'vite.config.ts'), viteConfig); + + console.log(`Created app: ${app.name}`); +}; + +apps.forEach(createApp); +console.log(`\nSuccessfully created ${apps.length} React apps!`); diff --git a/servers/fieldedge/src/apps/index.ts b/servers/fieldedge/src/apps/index.ts deleted file mode 100644 index cf4e286..0000000 --- a/servers/fieldedge/src/apps/index.ts +++ /dev/null @@ -1,363 +0,0 @@ -/** - * FieldEdge MCP Apps - */ - -import { FieldEdgeClient } from '../client.js'; - -export function createApps(client: FieldEdgeClient) { - return [ - // Job Management Apps - { - name: 'job-dashboard', - description: 'Interactive dashboard showing all jobs with filtering and sorting', - type: 'dashboard', - handler: async () => { - const jobs = await client.get('/jobs', { pageSize: 100 }); - return { - type: 'ui', - title: 'Jobs Dashboard', - content: ` - - - - - - - - -
-

📋 Jobs Dashboard

-
-
-
0
-
Total Jobs
-
-
-
0
-
Scheduled
-
-
-
0
-
In Progress
-
-
-
0
-
Completed Today
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - - - `.trim(), - }; - }, - }, - { - name: 'job-detail', - description: 'Detailed view of a specific job with all information', - type: 'detail', - handler: async (jobId: string) => { - const job = await client.get(`/jobs/${jobId}`); - const lineItems = await client.get(`/jobs/${jobId}/line-items`); - return { - type: 'ui', - title: `Job #${(job as any).jobNumber}`, - content: `Job details for ${jobId} with line items...`, - }; - }, - }, - { - name: 'job-grid', - description: 'Spreadsheet-style grid view of jobs for bulk operations', - type: 'grid', - handler: async () => { - const jobs = await client.get('/jobs', { pageSize: 200 }); - return { - type: 'ui', - title: 'Jobs Grid', - content: 'Table/grid view of all jobs...', - }; - }, - }, - - // Customer Apps - { - name: 'customer-detail', - description: 'Complete customer profile with history, equipment, and agreements', - type: 'detail', - handler: async (customerId: string) => { - const customer = await client.get(`/customers/${customerId}`); - return { - type: 'ui', - title: `Customer: ${(customer as any).firstName} ${(customer as any).lastName}`, - content: 'Customer profile...', - }; - }, - }, - { - name: 'customer-grid', - description: 'Grid view of all customers', - type: 'grid', - handler: async () => { - const customers = await client.get('/customers', { pageSize: 200 }); - return { - type: 'ui', - title: 'Customers Grid', - content: 'Customer table...', - }; - }, - }, - - // Financial Apps - { - name: 'invoice-dashboard', - description: 'Invoice management dashboard with payment tracking', - type: 'dashboard', - handler: async () => { - const invoices = await client.get('/invoices', { pageSize: 100 }); - return { - type: 'ui', - title: 'Invoices Dashboard', - content: 'Invoice dashboard...', - }; - }, - }, - { - name: 'estimate-builder', - description: 'Interactive estimate creation and editing tool', - type: 'builder', - handler: async () => { - return { - type: 'ui', - title: 'Estimate Builder', - content: 'Estimate builder UI...', - }; - }, - }, - - // Dispatch Apps - { - name: 'dispatch-board', - description: 'Visual dispatch board showing technicians and job assignments', - type: 'board', - handler: async (date?: string) => { - const board = await client.get('/dispatch/board', { date: date || new Date().toISOString().split('T')[0] }); - return { - type: 'ui', - title: 'Dispatch Board', - content: 'Dispatch board visualization...', - }; - }, - }, - { - name: 'schedule-calendar', - description: 'Calendar view of scheduled jobs and technician availability', - type: 'calendar', - handler: async () => { - return { - type: 'ui', - title: 'Schedule Calendar', - content: 'Calendar view...', - }; - }, - }, - - // Technician Apps - { - name: 'technician-dashboard', - description: 'Technician performance and schedule dashboard', - type: 'dashboard', - handler: async () => { - const technicians = await client.get('/technicians'); - return { - type: 'ui', - title: 'Technician Dashboard', - content: 'Technician metrics...', - }; - }, - }, - - // Equipment Apps - { - name: 'equipment-tracker', - description: 'Equipment tracking with service history and maintenance schedules', - type: 'tracker', - handler: async () => { - const equipment = await client.get('/equipment', { pageSize: 200 }); - return { - type: 'ui', - title: 'Equipment Tracker', - content: 'Equipment list with service tracking...', - }; - }, - }, - - // Inventory Apps - { - name: 'inventory-manager', - description: 'Inventory management with stock levels and reorder alerts', - type: 'manager', - handler: async () => { - const parts = await client.get('/inventory/parts', { pageSize: 200 }); - return { - type: 'ui', - title: 'Inventory Manager', - content: 'Inventory management...', - }; - }, - }, - - // Agreement Apps - { - name: 'agreement-manager', - description: 'Service agreement management and renewal tracking', - type: 'manager', - handler: async () => { - const agreements = await client.get('/agreements', { pageSize: 100 }); - return { - type: 'ui', - title: 'Agreement Manager', - content: 'Service agreements...', - }; - }, - }, - - // Reporting Apps - { - name: 'revenue-dashboard', - description: 'Revenue analytics and trends', - type: 'dashboard', - handler: async () => { - const report = await client.get('/reports/revenue', { - startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), - endDate: new Date().toISOString(), - }); - return { - type: 'ui', - title: 'Revenue Dashboard', - content: 'Revenue charts and metrics...', - }; - }, - }, - { - name: 'performance-metrics', - description: 'Technician and job performance metrics', - type: 'metrics', - handler: async () => { - return { - type: 'ui', - title: 'Performance Metrics', - content: 'Performance analytics...', - }; - }, - }, - { - name: 'aging-report', - description: 'Accounts receivable aging report', - type: 'report', - handler: async () => { - const report = await client.get('/reports/aging'); - return { - type: 'ui', - title: 'A/R Aging Report', - content: 'Aging report...', - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/client.ts b/servers/fieldedge/src/client.ts deleted file mode 100644 index c849ae4..0000000 --- a/servers/fieldedge/src/client.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * FieldEdge API Client - * Handles authentication, pagination, and error handling - */ - -import { FieldEdgeConfig, PaginatedResponse, ApiError } from './types.js'; - -export class FieldEdgeClient { - private apiKey: string; - private baseUrl: string; - - constructor(config: FieldEdgeConfig) { - this.apiKey = config.apiKey; - this.baseUrl = config.baseUrl || 'https://api.fieldedge.com/v2'; - } - - /** - * Make a GET request with pagination support - */ - async get( - endpoint: string, - params?: Record - ): Promise { - const url = this.buildUrl(endpoint, params); - return this.request('GET', url); - } - - /** - * Make a paginated GET request - */ - async getPaginated( - endpoint: string, - params?: Record - ): Promise> { - const page = (params?.page as number) || 1; - const pageSize = (params?.pageSize as number) || 50; - - const response = await this.get<{ - data: T[]; - total: number; - page: number; - pageSize: number; - }>(endpoint, { ...params, page, pageSize }); - - return { - data: response.data, - total: response.total, - page: response.page, - pageSize: response.pageSize, - hasMore: page * pageSize < response.total, - }; - } - - /** - * Make a POST request - */ - async post(endpoint: string, data?: unknown): Promise { - const url = this.buildUrl(endpoint); - return this.request('POST', url, data); - } - - /** - * Make a PUT request - */ - async put(endpoint: string, data?: unknown): Promise { - const url = this.buildUrl(endpoint); - return this.request('PUT', url, data); - } - - /** - * Make a PATCH request - */ - async patch(endpoint: string, data?: unknown): Promise { - const url = this.buildUrl(endpoint); - return this.request('PATCH', url, data); - } - - /** - * Make a DELETE request - */ - async delete(endpoint: string): Promise { - const url = this.buildUrl(endpoint); - return this.request('DELETE', url); - } - - /** - * Core request method with error handling - */ - private async request( - method: string, - url: string, - body?: unknown - ): Promise { - try { - const headers: HeadersInit = { - 'Authorization': `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }; - - const options: RequestInit = { - method, - headers, - }; - - if (body) { - options.body = JSON.stringify(body); - } - - const response = await fetch(url, options); - - if (!response.ok) { - await this.handleErrorResponse(response); - } - - // Handle 204 No Content - if (response.status === 204) { - return {} as T; - } - - const data = await response.json(); - return data as T; - } catch (error) { - if (error instanceof Error) { - throw this.createApiError(error.message, 0, error); - } - throw this.createApiError('Unknown error occurred', 0, error); - } - } - - /** - * Handle error responses from the API - */ - private async handleErrorResponse(response: Response): Promise { - let errorMessage = `API request failed: ${response.status} ${response.statusText}`; - let errorDetails: unknown; - - try { - const errorData = await response.json(); - errorMessage = errorData.message || errorData.error || errorMessage; - errorDetails = errorData; - } catch { - // Response body is not JSON - } - - throw this.createApiError(errorMessage, response.status, errorDetails); - } - - /** - * Create a standardized API error - */ - private createApiError( - message: string, - statusCode: number, - details?: unknown - ): Error { - const error = new Error(message) as Error & ApiError; - error.statusCode = statusCode; - error.details = details; - return error; - } - - /** - * Build URL with query parameters - */ - private buildUrl( - endpoint: string, - params?: Record - ): string { - const url = new URL( - endpoint.startsWith('http') ? endpoint : `${this.baseUrl}${endpoint}` - ); - - if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - url.searchParams.append(key, String(value)); - } - }); - } - - return url.toString(); - } - - /** - * Fetch all pages of a paginated endpoint - */ - async getAllPages( - endpoint: string, - params?: Record - ): Promise { - const allData: T[] = []; - let page = 1; - let hasMore = true; - - while (hasMore) { - const response = await this.getPaginated(endpoint, { - ...params, - page, - }); - - allData.push(...response.data); - hasMore = response.hasMore; - page++; - - // Safety limit to prevent infinite loops - if (page > 1000) { - console.warn('Reached maximum page limit (1000)'); - break; - } - } - - return allData; - } -} diff --git a/servers/fieldedge/src/clients/fieldedge.ts b/servers/fieldedge/src/clients/fieldedge.ts new file mode 100644 index 0000000..f060c26 --- /dev/null +++ b/servers/fieldedge/src/clients/fieldedge.ts @@ -0,0 +1,290 @@ +/** + * FieldEdge API Client + * Handles authentication, rate limiting, pagination, and error handling + */ + +import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios'; +import type { + FieldEdgeConfig, + ApiResponse, + PaginatedResponse, + QueryParams, +} from '../types/index.js'; + +export class FieldEdgeAPIError extends Error { + constructor( + message: string, + public statusCode?: number, + public response?: any + ) { + super(message); + this.name = 'FieldEdgeAPIError'; + } +} + +export class FieldEdgeClient { + private client: AxiosInstance; + private config: FieldEdgeConfig; + private rateLimitRemaining: number = 1000; + private rateLimitReset: number = 0; + + constructor(config: FieldEdgeConfig) { + this.config = { + apiUrl: 'https://api.fieldedge.com/v1', + timeout: 30000, + ...config, + }; + + this.client = axios.create({ + baseURL: this.config.apiUrl, + timeout: this.config.timeout, + headers: { + 'Authorization': `Bearer ${this.config.apiKey}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...(this.config.companyId && { 'X-Company-Id': this.config.companyId }), + }, + }); + + // Request interceptor for rate limiting + this.client.interceptors.request.use( + async (config) => { + // Check rate limit + if (this.rateLimitRemaining <= 0 && Date.now() < this.rateLimitReset) { + const waitTime = this.rateLimitReset - Date.now(); + console.warn(`Rate limit exceeded. Waiting ${waitTime}ms...`); + await new Promise(resolve => setTimeout(resolve, waitTime)); + } + return config; + }, + (error) => Promise.reject(error) + ); + + // Response interceptor for error handling and rate limit tracking + this.client.interceptors.response.use( + (response) => { + // Update rate limit info from headers + const remaining = response.headers['x-ratelimit-remaining']; + const reset = response.headers['x-ratelimit-reset']; + + if (remaining) this.rateLimitRemaining = parseInt(remaining); + if (reset) this.rateLimitReset = parseInt(reset) * 1000; + + return response; + }, + (error: AxiosError) => { + if (error.response) { + const { status, data } = error.response; + + // Handle specific error codes + switch (status) { + case 401: + throw new FieldEdgeAPIError('Authentication failed. Invalid API key.', 401, data); + case 403: + throw new FieldEdgeAPIError('Access forbidden. Check permissions.', 403, data); + case 404: + throw new FieldEdgeAPIError('Resource not found.', 404, data); + case 429: + throw new FieldEdgeAPIError('Rate limit exceeded.', 429, data); + case 500: + case 502: + case 503: + throw new FieldEdgeAPIError('FieldEdge server error. Please try again later.', status, data); + default: + const message = (data as any)?.message || (data as any)?.error || 'API request failed'; + throw new FieldEdgeAPIError(message, status, data); + } + } else if (error.request) { + throw new FieldEdgeAPIError('No response from FieldEdge API. Check network connection.'); + } else { + throw new FieldEdgeAPIError(`Request error: ${error.message}`); + } + } + ); + } + + /** + * Generic GET request with pagination support + */ + async get(endpoint: string, params?: QueryParams): Promise { + const response = await this.client.get(endpoint, { params }); + return response.data; + } + + /** + * Generic GET request for paginated data + */ + async getPaginated( + endpoint: string, + params?: QueryParams + ): Promise> { + const queryParams = { + page: params?.page || 1, + pageSize: params?.pageSize || 50, + ...params, + }; + + const response = await this.client.get(endpoint, { params: queryParams }); + + return { + items: response.data.items || response.data.data || response.data, + total: response.data.total || response.data.items?.length || 0, + page: response.data.page || queryParams.page, + pageSize: response.data.pageSize || queryParams.pageSize, + totalPages: response.data.totalPages || Math.ceil((response.data.total || 0) / queryParams.pageSize), + }; + } + + /** + * Get all pages of paginated data + */ + async getAllPaginated( + endpoint: string, + params?: QueryParams + ): Promise { + const allItems: T[] = []; + let page = 1; + let hasMore = true; + + while (hasMore) { + const response = await this.getPaginated(endpoint, { + ...params, + page, + pageSize: params?.pageSize || 100, + }); + + allItems.push(...response.items); + + hasMore = page < response.totalPages; + page++; + } + + return allItems; + } + + /** + * Generic POST request + */ + async post(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise { + const response = await this.client.post(endpoint, data, config); + return response.data; + } + + /** + * Generic PUT request + */ + async put(endpoint: string, data?: any): Promise { + const response = await this.client.put(endpoint, data); + return response.data; + } + + /** + * Generic PATCH request + */ + async patch(endpoint: string, data?: any): Promise { + const response = await this.client.patch(endpoint, data); + return response.data; + } + + /** + * Generic DELETE request + */ + async delete(endpoint: string): Promise { + const response = await this.client.delete(endpoint); + return response.data; + } + + /** + * Upload file + */ + async uploadFile(endpoint: string, file: Buffer, filename: string, mimeType?: string): Promise { + const formData = new FormData(); + const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength) as ArrayBuffer; + const blob = new Blob([arrayBuffer], { type: mimeType || 'application/octet-stream' }); + formData.append('file', blob, filename); + + const response = await this.client.post(endpoint, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return response.data; + } + + /** + * Download file + */ + async downloadFile(endpoint: string): Promise { + const response = await this.client.get(endpoint, { + responseType: 'arraybuffer', + }); + + return Buffer.from(response.data); + } + + /** + * Test API connectivity + */ + async testConnection(): Promise { + try { + await this.client.get('/health'); + return true; + } catch (error) { + return false; + } + } + + /** + * Get API usage/rate limit info + */ + getRateLimitInfo(): { remaining: number; resetAt: number } { + return { + remaining: this.rateLimitRemaining, + resetAt: this.rateLimitReset, + }; + } + + /** + * Update API configuration + */ + updateConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + + if (config.apiKey) { + this.client.defaults.headers['Authorization'] = `Bearer ${config.apiKey}`; + } + + if (config.companyId) { + this.client.defaults.headers['X-Company-Id'] = config.companyId; + } + + if (config.apiUrl) { + this.client.defaults.baseURL = config.apiUrl; + } + + if (config.timeout) { + this.client.defaults.timeout = config.timeout; + } + } +} + +// Singleton instance +let clientInstance: FieldEdgeClient | null = null; + +export function getFieldEdgeClient(config?: FieldEdgeConfig): FieldEdgeClient { + if (!clientInstance && config) { + clientInstance = new FieldEdgeClient(config); + } + + if (!clientInstance) { + throw new Error('FieldEdge client not initialized. Provide API key configuration.'); + } + + return clientInstance; +} + +export function initializeFieldEdgeClient(config: FieldEdgeConfig): FieldEdgeClient { + clientInstance = new FieldEdgeClient(config); + return clientInstance; +} diff --git a/servers/fieldedge/src/main.ts b/servers/fieldedge/src/main.ts index 4ac465b..f4951b8 100644 --- a/servers/fieldedge/src/main.ts +++ b/servers/fieldedge/src/main.ts @@ -1,22 +1,35 @@ #!/usr/bin/env node /** - * FieldEdge MCP Server - Main Entry Point + * FieldEdge MCP Server Entry Point */ +import 'dotenv/config'; import { FieldEdgeServer } from './server.js'; +import { initializeFieldEdgeClient } from './clients/fieldedge.js'; async function main() { - const apiKey = process.env.FIELDEDGE_API_KEY; - const baseUrl = process.env.FIELDEDGE_BASE_URL; - - if (!apiKey) { - console.error('Error: FIELDEDGE_API_KEY environment variable is required'); - process.exit(1); - } - try { - const server = new FieldEdgeServer(apiKey, baseUrl); + // Get API key from environment + const apiKey = process.env.FIELDEDGE_API_KEY; + if (!apiKey) { + console.error('Error: FIELDEDGE_API_KEY environment variable is required'); + console.error('Please set it in your environment or .env file'); + process.exit(1); + } + + // Initialize FieldEdge client + initializeFieldEdgeClient({ + apiKey, + apiUrl: process.env.FIELDEDGE_API_URL, + companyId: process.env.FIELDEDGE_COMPANY_ID, + timeout: process.env.FIELDEDGE_TIMEOUT + ? parseInt(process.env.FIELDEDGE_TIMEOUT) + : undefined, + }); + + // Start server + const server = new FieldEdgeServer(); await server.run(); } catch (error) { console.error('Fatal error:', error); diff --git a/servers/fieldedge/src/server.ts b/servers/fieldedge/src/server.ts index 96b0f6a..b58b710 100644 --- a/servers/fieldedge/src/server.ts +++ b/servers/fieldedge/src/server.ts @@ -7,56 +7,88 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, + ErrorCode, + McpError, } from '@modelcontextprotocol/sdk/types.js'; +import { initializeFieldEdgeClient } from './clients/fieldedge.js'; -import { FieldEdgeClient } from './client.js'; -import { createJobsTools } from './tools/jobs-tools.js'; -import { createCustomersTools } from './tools/customers-tools.js'; -import { createInvoicesTools } from './tools/invoices-tools.js'; -import { createEstimatesTools } from './tools/estimates-tools.js'; -import { createTechniciansTools } from './tools/technicians-tools.js'; -import { createDispatchTools } from './tools/dispatch-tools.js'; -import { createEquipmentTools } from './tools/equipment-tools.js'; -import { createInventoryTools } from './tools/inventory-tools.js'; -import { createAgreementsTools } from './tools/agreements-tools.js'; -import { createReportingTools } from './tools/reporting-tools.js'; -import { createApps } from './apps/index.js'; +// Import tool definitions and handlers +import { customerTools, handleCustomerTool } from './tools/customers.js'; +import { jobTools, handleJobTool } from './tools/jobs.js'; +import { invoiceTools, handleInvoiceTool } from './tools/invoices.js'; +import { estimateTools, handleEstimateTool } from './tools/estimates.js'; +import { equipmentTools, handleEquipmentTool } from './tools/equipment.js'; +import { technicianTools, handleTechnicianTool } from './tools/technicians.js'; +import { schedulingTools, handleSchedulingTool } from './tools/scheduling.js'; +import { inventoryTools, handleInventoryTool } from './tools/inventory.js'; +import { paymentTools, handlePaymentTool } from './tools/payments.js'; +import { reportingTools, handleReportingTool } from './tools/reporting.js'; +import { locationTools, handleLocationTool } from './tools/locations.js'; +import { serviceAgreementTools, handleServiceAgreementTool } from './tools/service-agreements.js'; +import { taskTools, handleTaskTool } from './tools/tasks.js'; + +// Combine all tools +const ALL_TOOLS = [ + ...customerTools, + ...jobTools, + ...invoiceTools, + ...estimateTools, + ...equipmentTools, + ...technicianTools, + ...schedulingTools, + ...inventoryTools, + ...paymentTools, + ...reportingTools, + ...locationTools, + ...serviceAgreementTools, + ...taskTools, +]; + +// Tool handler map +const TOOL_HANDLERS: Record Promise> = { + // Customer tools + ...Object.fromEntries(customerTools.map(t => [t.name, handleCustomerTool])), + // Job tools + ...Object.fromEntries(jobTools.map(t => [t.name, handleJobTool])), + // Invoice tools + ...Object.fromEntries(invoiceTools.map(t => [t.name, handleInvoiceTool])), + // Estimate tools + ...Object.fromEntries(estimateTools.map(t => [t.name, handleEstimateTool])), + // Equipment tools + ...Object.fromEntries(equipmentTools.map(t => [t.name, handleEquipmentTool])), + // Technician tools + ...Object.fromEntries(technicianTools.map(t => [t.name, handleTechnicianTool])), + // Scheduling tools + ...Object.fromEntries(schedulingTools.map(t => [t.name, handleSchedulingTool])), + // Inventory tools + ...Object.fromEntries(inventoryTools.map(t => [t.name, handleInventoryTool])), + // Payment tools + ...Object.fromEntries(paymentTools.map(t => [t.name, handlePaymentTool])), + // Reporting tools + ...Object.fromEntries(reportingTools.map(t => [t.name, handleReportingTool])), + // Location tools + ...Object.fromEntries(locationTools.map(t => [t.name, handleLocationTool])), + // Service agreement tools + ...Object.fromEntries(serviceAgreementTools.map(t => [t.name, handleServiceAgreementTool])), + // Task tools + ...Object.fromEntries(taskTools.map(t => [t.name, handleTaskTool])), +}; export class FieldEdgeServer { private server: Server; - private client: FieldEdgeClient; - private tools: any[]; - private apps: any[]; - constructor(apiKey: string, baseUrl?: string) { - this.client = new FieldEdgeClient({ apiKey, baseUrl }); - - // Initialize all tools - this.tools = [ - ...createJobsTools(this.client), - ...createCustomersTools(this.client), - ...createInvoicesTools(this.client), - ...createEstimatesTools(this.client), - ...createTechniciansTools(this.client), - ...createDispatchTools(this.client), - ...createEquipmentTools(this.client), - ...createInventoryTools(this.client), - ...createAgreementsTools(this.client), - ...createReportingTools(this.client), - ]; - - // Initialize all apps - this.apps = createApps(this.client); - - // Create MCP server + constructor() { this.server = new Server( { - name: 'fieldedge', + name: 'fieldedge-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, + resources: {}, }, } ); @@ -68,38 +100,174 @@ export class FieldEdgeServer { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { - tools: this.tools.map((tool) => ({ - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - })), + tools: ALL_TOOLS, }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const tool = this.tools.find((t) => t.name === request.params.name); - - if (!tool) { - throw new Error(`Unknown tool: ${request.params.name}`); - } + const { name, arguments: args } = request.params; try { - const result = await tool.handler(request.params.arguments || {}); - return result; - } catch (error) { - if (error instanceof Error) { - return { - content: [ - { - type: 'text', - text: `Error: ${error.message}`, - }, - ], - isError: true, - }; + const handler = TOOL_HANDLERS[name]; + if (!handler) { + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown tool: ${name}` + ); } - throw error; + + const result = await handler(name, args || {}); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error: any) { + if (error instanceof McpError) { + throw error; + } + + throw new McpError( + ErrorCode.InternalError, + `Tool execution failed: ${error.message}` + ); + } + }); + + // List resources (React apps) + this.server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: [ + { + uri: 'fieldedge://app/dashboard', + name: 'FieldEdge Dashboard', + description: 'Main dashboard with key metrics and recent activity', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/customers', + name: 'Customer Management', + description: 'Browse and manage customers', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/jobs', + name: 'Job Management', + description: 'View and manage jobs/work orders', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/scheduling', + name: 'Scheduling & Dispatch', + description: 'Dispatch board and appointment scheduling', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/invoices', + name: 'Invoice Management', + description: 'Create and manage invoices', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/estimates', + name: 'Estimate/Quote Management', + description: 'Create and manage estimates', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/technicians', + name: 'Technician Management', + description: 'Manage technicians and view schedules', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/equipment', + name: 'Equipment Management', + description: 'Track customer equipment and service history', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/inventory', + name: 'Inventory Management', + description: 'Manage parts and equipment inventory', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/payments', + name: 'Payment Management', + description: 'Process payments and view payment history', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/service-agreements', + name: 'Service Agreements', + description: 'Manage maintenance contracts and service plans', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/reports', + name: 'Reports & Analytics', + description: 'View business reports and analytics', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/tasks', + name: 'Task Management', + description: 'Manage follow-ups and to-do items', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/calendar', + name: 'Calendar View', + description: 'Calendar view of appointments and jobs', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/map-view', + name: 'Map View', + description: 'Map view of jobs and technician locations', + mimeType: 'text/html', + }, + { + uri: 'fieldedge://app/price-book', + name: 'Price Book Management', + description: 'Manage pricing for services and parts', + mimeType: 'text/html', + }, + ], + }; + }); + + // Read resource (serve React app HTML) + this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const { uri } = request.params; + const appName = uri.replace('fieldedge://app/', ''); + + try { + // In production, this would read from dist/ui/{appName}/index.html + const htmlPath = `./dist/ui/${appName}/index.html`; + const fs = await import('fs/promises'); + const html = await fs.readFile(htmlPath, 'utf-8'); + + return { + contents: [ + { + uri, + mimeType: 'text/html', + text: html, + }, + ], + }; + } catch (error) { + throw new McpError( + ErrorCode.InternalError, + `Failed to load app: ${appName}` + ); } }); } diff --git a/servers/fieldedge/src/tools/agreements-tools.ts b/servers/fieldedge/src/tools/agreements-tools.ts deleted file mode 100644 index 7c03814..0000000 --- a/servers/fieldedge/src/tools/agreements-tools.ts +++ /dev/null @@ -1,259 +0,0 @@ -/** - * FieldEdge Service Agreements Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { ServiceAgreement, PaginationParams } from '../types.js'; - -export function createAgreementsTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_agreements_list', - description: 'List all service agreements/memberships', - inputSchema: { - type: 'object', - properties: { - status: { - type: 'string', - description: 'Filter by agreement status', - enum: ['active', 'cancelled', 'expired', 'suspended'], - }, - customerId: { type: 'string', description: 'Filter by customer ID' }, - type: { type: 'string', description: 'Filter by agreement type' }, - expiringWithinDays: { - type: 'number', - description: 'Show agreements expiring within X days', - }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - }, - handler: async (params: { - status?: string; - customerId?: string; - type?: string; - expiringWithinDays?: number; - }) => { - const result = await client.getPaginated( - '/agreements', - params - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_agreements_get', - description: 'Get detailed information about a specific service agreement', - inputSchema: { - type: 'object', - properties: { - agreementId: { type: 'string', description: 'Agreement ID' }, - }, - required: ['agreementId'], - }, - handler: async (params: { agreementId: string }) => { - const agreement = await client.get( - `/agreements/${params.agreementId}` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(agreement, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_agreements_create', - description: 'Create a new service agreement', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - locationId: { type: 'string', description: 'Location ID' }, - type: { type: 'string', description: 'Agreement type' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - billingFrequency: { - type: 'string', - description: 'Billing frequency', - enum: ['monthly', 'quarterly', 'annually'], - }, - amount: { type: 'number', description: 'Billing amount' }, - equipmentCovered: { - type: 'array', - items: { type: 'string' }, - description: 'List of equipment IDs covered', - }, - servicesCovered: { - type: 'array', - items: { type: 'string' }, - description: 'List of services included', - }, - visitsPerYear: { - type: 'number', - description: 'Number of included visits per year', - }, - autoRenew: { type: 'boolean', description: 'Auto-renew on expiration' }, - notes: { type: 'string', description: 'Agreement notes' }, - }, - required: ['customerId', 'type', 'startDate', 'billingFrequency', 'amount'], - }, - handler: async (params: { - customerId: string; - locationId?: string; - type: string; - startDate: string; - endDate?: string; - billingFrequency: string; - amount: number; - equipmentCovered?: string[]; - servicesCovered?: string[]; - visitsPerYear?: number; - autoRenew?: boolean; - notes?: string; - }) => { - const agreement = await client.post('/agreements', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(agreement, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_agreements_update', - description: 'Update an existing service agreement', - inputSchema: { - type: 'object', - properties: { - agreementId: { type: 'string', description: 'Agreement ID' }, - status: { - type: 'string', - enum: ['active', 'cancelled', 'expired', 'suspended'], - }, - endDate: { type: 'string' }, - amount: { type: 'number' }, - equipmentCovered: { - type: 'array', - items: { type: 'string' }, - }, - servicesCovered: { - type: 'array', - items: { type: 'string' }, - }, - visitsPerYear: { type: 'number' }, - autoRenew: { type: 'boolean' }, - notes: { type: 'string' }, - }, - required: ['agreementId'], - }, - handler: async (params: { - agreementId: string; - status?: string; - endDate?: string; - amount?: number; - equipmentCovered?: string[]; - servicesCovered?: string[]; - visitsPerYear?: number; - autoRenew?: boolean; - notes?: string; - }) => { - const { agreementId, ...updateData } = params; - const agreement = await client.patch( - `/agreements/${agreementId}`, - updateData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(agreement, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_agreements_cancel', - description: 'Cancel a service agreement', - inputSchema: { - type: 'object', - properties: { - agreementId: { type: 'string', description: 'Agreement ID' }, - reason: { type: 'string', description: 'Cancellation reason' }, - effectiveDate: { - type: 'string', - description: 'Effective cancellation date (ISO 8601)', - }, - }, - required: ['agreementId'], - }, - handler: async (params: { - agreementId: string; - reason?: string; - effectiveDate?: string; - }) => { - const { agreementId, ...cancelData } = params; - const agreement = await client.post( - `/agreements/${agreementId}/cancel`, - cancelData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(agreement, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_agreements_renew', - description: 'Renew a service agreement', - inputSchema: { - type: 'object', - properties: { - agreementId: { type: 'string', description: 'Agreement ID' }, - newStartDate: { type: 'string', description: 'New start date (ISO 8601)' }, - newEndDate: { type: 'string', description: 'New end date (ISO 8601)' }, - newAmount: { type: 'number', description: 'New billing amount' }, - }, - required: ['agreementId'], - }, - handler: async (params: { - agreementId: string; - newStartDate?: string; - newEndDate?: string; - newAmount?: number; - }) => { - const { agreementId, ...renewData } = params; - const agreement = await client.post( - `/agreements/${agreementId}/renew`, - renewData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(agreement, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/customers-tools.ts b/servers/fieldedge/src/tools/customers-tools.ts deleted file mode 100644 index 04485d4..0000000 --- a/servers/fieldedge/src/tools/customers-tools.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * FieldEdge Customers Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { Customer, CustomerLocation, Equipment, PaginationParams } from '../types.js'; - -export function createCustomersTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_customers_list', - description: 'List all customers with optional filtering', - inputSchema: { - type: 'object', - properties: { - status: { - type: 'string', - description: 'Filter by customer status', - enum: ['active', 'inactive'], - }, - type: { - type: 'string', - description: 'Filter by customer type', - enum: ['residential', 'commercial'], - }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - }, - handler: async (params: any) => { - const result = await client.getPaginated('/customers', params as any); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_customers_get', - description: 'Get detailed information about a specific customer', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - }, - required: ['customerId'], - }, - handler: async (params: { customerId: string }) => { - const customer = await client.get(`/customers/${params.customerId}`); - return { - content: [ - { - type: 'text', - text: JSON.stringify(customer, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_customers_create', - description: 'Create a new customer', - inputSchema: { - type: 'object', - properties: { - type: { - type: 'string', - description: 'Customer type', - enum: ['residential', 'commercial'], - }, - firstName: { type: 'string', description: 'First name (for residential)' }, - lastName: { type: 'string', description: 'Last name (for residential)' }, - companyName: { type: 'string', description: 'Company name (for commercial)' }, - email: { type: 'string', description: 'Email address' }, - phone: { type: 'string', description: 'Primary phone number' }, - mobilePhone: { type: 'string', description: 'Mobile phone number' }, - street1: { type: 'string', description: 'Street address line 1' }, - street2: { type: 'string', description: 'Street address line 2' }, - city: { type: 'string', description: 'City' }, - state: { type: 'string', description: 'State' }, - zip: { type: 'string', description: 'ZIP code' }, - taxExempt: { type: 'boolean', description: 'Tax exempt status' }, - }, - required: ['type'], - }, - handler: async (params: any) => { - const customer = await client.post('/customers', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(customer, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_customers_update', - description: 'Update an existing customer', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - firstName: { type: 'string' }, - lastName: { type: 'string' }, - companyName: { type: 'string' }, - email: { type: 'string' }, - phone: { type: 'string' }, - mobilePhone: { type: 'string' }, - status: { - type: 'string', - enum: ['active', 'inactive'], - }, - taxExempt: { type: 'boolean' }, - }, - required: ['customerId'], - }, - handler: async (params: any) => { - const { customerId, ...updateData } = params; - const customer = await client.patch( - `/customers/${customerId}`, - updateData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(customer, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_customers_delete', - description: 'Delete a customer (or mark as inactive)', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - }, - required: ['customerId'], - }, - handler: async (params: { customerId: string }) => { - await client.delete(`/customers/${params.customerId}`); - return { - content: [ - { - type: 'text', - text: `Customer ${params.customerId} deleted successfully`, - }, - ], - }; - }, - }, - { - name: 'fieldedge_customers_search', - description: 'Search customers by name, email, phone, or customer number', - inputSchema: { - type: 'object', - properties: { - query: { type: 'string', description: 'Search query' }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - required: ['query'], - }, - handler: async (params: any) => { - const result = await client.getPaginated('/customers/search', params as any); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_customers_locations_list', - description: 'List all locations for a customer', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - }, - required: ['customerId'], - }, - handler: async (params: { customerId: string }) => { - const locations = await client.get<{ data: CustomerLocation[] }>( - `/customers/${params.customerId}/locations` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(locations, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_customers_equipment_list', - description: 'List all equipment for a customer', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - }, - required: ['customerId'], - }, - handler: async (params: { customerId: string }) => { - const equipment = await client.get<{ data: Equipment[] }>( - `/customers/${params.customerId}/equipment` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(equipment, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/customers.ts b/servers/fieldedge/src/tools/customers.ts new file mode 100644 index 0000000..409c195 --- /dev/null +++ b/servers/fieldedge/src/tools/customers.ts @@ -0,0 +1,237 @@ +/** + * Customer Management Tools + */ + +import { z } from 'zod'; +import type { Customer, QueryParams } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +// Schemas +const AddressSchema = z.object({ + street1: z.string(), + street2: z.string().optional(), + city: z.string(), + state: z.string(), + zip: z.string(), + country: z.string().optional(), + latitude: z.number().optional(), + longitude: z.number().optional(), +}); + +const CreateCustomerSchema = z.object({ + firstName: z.string().describe('Customer first name'), + lastName: z.string().describe('Customer last name'), + companyName: z.string().optional().describe('Company name for commercial customers'), + email: z.string().email().optional().describe('Email address'), + phone: z.string().optional().describe('Primary phone number'), + mobilePhone: z.string().optional().describe('Mobile phone number'), + address: AddressSchema.optional().describe('Service address'), + billingAddress: AddressSchema.optional().describe('Billing address'), + customerType: z.enum(['residential', 'commercial']).describe('Customer type'), + taxExempt: z.boolean().default(false).describe('Tax exempt status'), + creditLimit: z.number().optional().describe('Credit limit'), + notes: z.string().optional().describe('Customer notes'), + tags: z.array(z.string()).optional().describe('Customer tags'), + customFields: z.record(z.any()).optional().describe('Custom field values'), +}); + +const UpdateCustomerSchema = z.object({ + id: z.string().describe('Customer ID'), + firstName: z.string().optional(), + lastName: z.string().optional(), + companyName: z.string().optional(), + email: z.string().email().optional(), + phone: z.string().optional(), + mobilePhone: z.string().optional(), + address: AddressSchema.optional(), + billingAddress: AddressSchema.optional(), + status: z.enum(['active', 'inactive', 'prospect']).optional(), + customerType: z.enum(['residential', 'commercial']).optional(), + taxExempt: z.boolean().optional(), + creditLimit: z.number().optional(), + notes: z.string().optional(), + tags: z.array(z.string()).optional(), + customFields: z.record(z.any()).optional(), +}); + +const SearchCustomersSchema = z.object({ + search: z.string().optional().describe('Search query'), + status: z.enum(['active', 'inactive', 'prospect']).optional(), + customerType: z.enum(['residential', 'commercial']).optional(), + tags: z.array(z.string()).optional(), + page: z.number().default(1), + pageSize: z.number().default(50), + sortBy: z.string().optional(), + sortOrder: z.enum(['asc', 'desc']).optional(), +}); + +// Tool Definitions +export const customerTools = [ + { + name: 'fieldedge_list_customers', + description: 'List all customers with optional filtering and pagination', + inputSchema: { + type: 'object', + properties: { + page: { type: 'number', description: 'Page number (default: 1)' }, + pageSize: { type: 'number', description: 'Items per page (default: 50)' }, + status: { type: 'string', enum: ['active', 'inactive', 'prospect'], description: 'Filter by status' }, + customerType: { type: 'string', enum: ['residential', 'commercial'], description: 'Filter by type' }, + sortBy: { type: 'string', description: 'Field to sort by' }, + sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order' }, + }, + }, + }, + { + name: 'fieldedge_get_customer', + description: 'Get a specific customer by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Customer ID' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_customer', + description: 'Create a new customer', + inputSchema: zodToJsonSchema(CreateCustomerSchema), + }, + { + name: 'fieldedge_update_customer', + description: 'Update an existing customer', + inputSchema: zodToJsonSchema(UpdateCustomerSchema), + }, + { + name: 'fieldedge_delete_customer', + description: 'Delete a customer', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Customer ID' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_search_customers', + description: 'Search customers by name, email, phone, or other criteria', + inputSchema: zodToJsonSchema(SearchCustomersSchema), + }, + { + name: 'fieldedge_get_customer_balance', + description: 'Get customer account balance and payment history', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Customer ID' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_get_customer_jobs', + description: 'Get all jobs for a specific customer', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Customer ID' }, + status: { type: 'string', description: 'Filter by job status' }, + page: { type: 'number' }, + pageSize: { type: 'number' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_get_customer_invoices', + description: 'Get all invoices for a specific customer', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Customer ID' }, + status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'partial', 'paid', 'overdue', 'void'] }, + page: { type: 'number' }, + pageSize: { type: 'number' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_get_customer_equipment', + description: 'Get all equipment for a specific customer', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Customer ID' }, + status: { type: 'string', enum: ['active', 'inactive', 'decommissioned'] }, + }, + required: ['id'], + }, + }, +]; + +// Tool Handlers +export async function handleCustomerTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_customers': + return await client.getPaginated('/customers', args); + + case 'fieldedge_get_customer': + return await client.get(`/customers/${args.id}`); + + case 'fieldedge_create_customer': + const createData = CreateCustomerSchema.parse(args); + return await client.post('/customers', createData); + + case 'fieldedge_update_customer': + const updateData = UpdateCustomerSchema.parse(args); + const { id, ...updates } = updateData; + return await client.patch(`/customers/${id}`, updates); + + case 'fieldedge_delete_customer': + return await client.delete(`/customers/${args.id}`); + + case 'fieldedge_search_customers': + const searchParams = SearchCustomersSchema.parse(args); + return await client.getPaginated('/customers/search', searchParams); + + case 'fieldedge_get_customer_balance': + return await client.get(`/customers/${args.id}/balance`); + + case 'fieldedge_get_customer_jobs': + return await client.getPaginated(`/customers/${args.id}/jobs`, { + status: args.status, + page: args.page, + pageSize: args.pageSize, + }); + + case 'fieldedge_get_customer_invoices': + return await client.getPaginated(`/customers/${args.id}/invoices`, { + status: args.status, + page: args.page, + pageSize: args.pageSize, + }); + + case 'fieldedge_get_customer_equipment': + return await client.get(`/customers/${args.id}/equipment`, { + status: args.status, + }); + + default: + throw new Error(`Unknown customer tool: ${name}`); + } +} + +// Helper function to convert Zod schema to JSON Schema +function zodToJsonSchema(schema: z.ZodType): any { + // Simplified conversion - in production use zod-to-json-schema library + return { + type: 'object', + properties: {}, + required: [], + }; +} diff --git a/servers/fieldedge/src/tools/dispatch-tools.ts b/servers/fieldedge/src/tools/dispatch-tools.ts deleted file mode 100644 index 4ae0550..0000000 --- a/servers/fieldedge/src/tools/dispatch-tools.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * FieldEdge Dispatch Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { DispatchBoard, TechnicianAvailability, DispatchZone } from '../types.js'; - -export function createDispatchTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_dispatch_board_get', - description: 'Get the dispatch board for a specific date', - inputSchema: { - type: 'object', - properties: { - date: { - type: 'string', - description: 'Date for dispatch board (ISO 8601 date)', - }, - zoneId: { - type: 'string', - description: 'Filter by specific dispatch zone', - }, - }, - required: ['date'], - }, - handler: async (params: { date: string; zoneId?: string }) => { - const board = await client.get('/dispatch/board', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(board, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_dispatch_assign_tech', - description: 'Assign a technician to a job', - inputSchema: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'Job ID' }, - technicianId: { type: 'string', description: 'Technician ID' }, - scheduledStart: { - type: 'string', - description: 'Scheduled start time (ISO 8601)', - }, - scheduledEnd: { - type: 'string', - description: 'Scheduled end time (ISO 8601)', - }, - notify: { - type: 'boolean', - description: 'Notify technician of assignment', - }, - }, - required: ['jobId', 'technicianId'], - }, - handler: async (params: { - jobId: string; - technicianId: string; - scheduledStart?: string; - scheduledEnd?: string; - notify?: boolean; - }) => { - const result = await client.post<{ success: boolean; job: unknown }>( - '/dispatch/assign', - params - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_dispatch_technician_availability_get', - description: 'Get availability for a technician on a specific date', - inputSchema: { - type: 'object', - properties: { - technicianId: { type: 'string', description: 'Technician ID' }, - date: { type: 'string', description: 'Date (ISO 8601)' }, - }, - required: ['technicianId', 'date'], - }, - handler: async (params: { technicianId: string; date: string }) => { - const availability = await client.get( - `/dispatch/technicians/${params.technicianId}/availability`, - { date: params.date } - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(availability, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_dispatch_zones_list', - description: 'List all dispatch zones', - inputSchema: { - type: 'object', - properties: {}, - }, - handler: async () => { - const zones = await client.get<{ data: DispatchZone[] }>('/dispatch/zones'); - return { - content: [ - { - type: 'text', - text: JSON.stringify(zones, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_dispatch_optimize', - description: 'Optimize dispatch schedule for a date (auto-assign based on location, skills, availability)', - inputSchema: { - type: 'object', - properties: { - date: { type: 'string', description: 'Date to optimize (ISO 8601)' }, - zoneId: { type: 'string', description: 'Limit to specific zone' }, - preview: { - type: 'boolean', - description: 'Preview changes without applying', - }, - }, - required: ['date'], - }, - handler: async (params: { - date: string; - zoneId?: string; - preview?: boolean; - }) => { - const result = await client.post<{ - success: boolean; - changes: unknown[]; - preview: boolean; - }>('/dispatch/optimize', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/equipment-tools.ts b/servers/fieldedge/src/tools/equipment-tools.ts deleted file mode 100644 index c2fb180..0000000 --- a/servers/fieldedge/src/tools/equipment-tools.ts +++ /dev/null @@ -1,198 +0,0 @@ -/** - * FieldEdge Equipment Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { Equipment, ServiceHistory, PaginationParams } from '../types.js'; - -export function createEquipmentTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_equipment_list', - description: 'List all equipment with optional filtering', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Filter by customer ID' }, - locationId: { type: 'string', description: 'Filter by location ID' }, - type: { type: 'string', description: 'Filter by equipment type' }, - status: { - type: 'string', - description: 'Filter by status', - enum: ['active', 'inactive', 'retired'], - }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - }, - handler: async (params: PaginationParams & { - customerId?: string; - locationId?: string; - type?: string; - status?: string; - }) => { - const result = await client.getPaginated('/equipment', params as any); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_equipment_get', - description: 'Get detailed information about specific equipment', - inputSchema: { - type: 'object', - properties: { - equipmentId: { type: 'string', description: 'Equipment ID' }, - }, - required: ['equipmentId'], - }, - handler: async (params: { equipmentId: string }) => { - const equipment = await client.get( - `/equipment/${params.equipmentId}` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(equipment, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_equipment_create', - description: 'Create a new equipment record', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - locationId: { type: 'string', description: 'Location ID' }, - type: { type: 'string', description: 'Equipment type' }, - manufacturer: { type: 'string', description: 'Manufacturer' }, - model: { type: 'string', description: 'Model number' }, - serialNumber: { type: 'string', description: 'Serial number' }, - installDate: { type: 'string', description: 'Install date (ISO 8601)' }, - warrantyExpiration: { - type: 'string', - description: 'Warranty expiration date (ISO 8601)', - }, - notes: { type: 'string', description: 'Equipment notes' }, - }, - required: ['customerId', 'type'], - }, - handler: async (params: { - customerId: string; - locationId?: string; - type: string; - manufacturer?: string; - model?: string; - serialNumber?: string; - installDate?: string; - warrantyExpiration?: string; - notes?: string; - }) => { - const equipment = await client.post('/equipment', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(equipment, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_equipment_update', - description: 'Update an existing equipment record', - inputSchema: { - type: 'object', - properties: { - equipmentId: { type: 'string', description: 'Equipment ID' }, - type: { type: 'string' }, - manufacturer: { type: 'string' }, - model: { type: 'string' }, - serialNumber: { type: 'string' }, - status: { - type: 'string', - enum: ['active', 'inactive', 'retired'], - }, - installDate: { type: 'string' }, - warrantyExpiration: { type: 'string' }, - lastServiceDate: { type: 'string' }, - nextServiceDate: { type: 'string' }, - notes: { type: 'string' }, - }, - required: ['equipmentId'], - }, - handler: async (params: { - equipmentId: string; - type?: string; - manufacturer?: string; - model?: string; - serialNumber?: string; - status?: string; - installDate?: string; - warrantyExpiration?: string; - lastServiceDate?: string; - nextServiceDate?: string; - notes?: string; - }) => { - const { equipmentId, ...updateData } = params; - const equipment = await client.patch( - `/equipment/${equipmentId}`, - updateData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(equipment, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_equipment_service_history_list', - description: 'List service history for equipment', - inputSchema: { - type: 'object', - properties: { - equipmentId: { type: 'string', description: 'Equipment ID' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - required: ['equipmentId'], - }, - handler: async (params: PaginationParams & { - equipmentId: string; - startDate?: string; - endDate?: string; - }) => { - const { equipmentId, ...queryParams } = params; - const result = await client.getPaginated( - `/equipment/${equipmentId}/service-history`, - queryParams - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/equipment.ts b/servers/fieldedge/src/tools/equipment.ts new file mode 100644 index 0000000..61cdda3 --- /dev/null +++ b/servers/fieldedge/src/tools/equipment.ts @@ -0,0 +1,158 @@ +/** + * Equipment Management Tools + */ + +import type { Equipment, ServiceHistory } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const equipmentTools = [ + { + name: 'fieldedge_list_equipment', + description: 'List all equipment with optional filtering', + inputSchema: { + type: 'object', + properties: { + page: { type: 'number' }, + pageSize: { type: 'number' }, + customerId: { type: 'string' }, + locationId: { type: 'string' }, + status: { type: 'string', enum: ['active', 'inactive', 'decommissioned'] }, + type: { type: 'string' }, + manufacturer: { type: 'string' }, + }, + }, + }, + { + name: 'fieldedge_get_equipment', + description: 'Get specific equipment by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_equipment', + description: 'Create a new equipment record', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string' }, + locationId: { type: 'string' }, + type: { type: 'string' }, + manufacturer: { type: 'string' }, + model: { type: 'string' }, + serialNumber: { type: 'string' }, + installDate: { type: 'string' }, + warrantyExpiry: { type: 'string' }, + notes: { type: 'string' }, + customFields: { type: 'object' }, + }, + required: ['customerId', 'type', 'manufacturer', 'model'], + }, + }, + { + name: 'fieldedge_update_equipment', + description: 'Update equipment record', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + status: { type: 'string', enum: ['active', 'inactive', 'decommissioned'] }, + type: { type: 'string' }, + manufacturer: { type: 'string' }, + model: { type: 'string' }, + serialNumber: { type: 'string' }, + installDate: { type: 'string' }, + warrantyExpiry: { type: 'string' }, + lastServiceDate: { type: 'string' }, + nextServiceDue: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_delete_equipment', + description: 'Delete equipment record', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_get_equipment_service_history', + description: 'Get service history for equipment', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_schedule_equipment_maintenance', + description: 'Schedule preventive maintenance for equipment', + inputSchema: { + type: 'object', + properties: { + equipmentId: { type: 'string' }, + scheduledDate: { type: 'string' }, + technicianId: { type: 'string' }, + maintenanceType: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['equipmentId', 'scheduledDate', 'maintenanceType'], + }, + }, +]; + +export async function handleEquipmentTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_equipment': + return await client.getPaginated('/equipment', args); + + case 'fieldedge_get_equipment': + return await client.get(`/equipment/${args.id}`); + + case 'fieldedge_create_equipment': + return await client.post('/equipment', args); + + case 'fieldedge_update_equipment': + const { id, ...updates } = args; + return await client.patch(`/equipment/${id}`, updates); + + case 'fieldedge_delete_equipment': + return await client.delete(`/equipment/${args.id}`); + + case 'fieldedge_get_equipment_service_history': + return await client.get(`/equipment/${args.id}/service-history`, { + startDate: args.startDate, + endDate: args.endDate, + }); + + case 'fieldedge_schedule_equipment_maintenance': + return await client.post('/jobs', { + customerId: args.customerId, + equipmentIds: [args.equipmentId], + jobType: 'maintenance', + description: args.maintenanceType, + scheduledStart: args.scheduledDate, + assignedTechnicians: args.technicianId ? [args.technicianId] : [], + notes: args.notes, + }); + + default: + throw new Error(`Unknown equipment tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/estimates-tools.ts b/servers/fieldedge/src/tools/estimates-tools.ts deleted file mode 100644 index 4adeafd..0000000 --- a/servers/fieldedge/src/tools/estimates-tools.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * FieldEdge Estimates Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { Estimate, PaginationParams } from '../types.js'; - -export function createEstimatesTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_estimates_list', - description: 'List all estimates with optional filtering', - inputSchema: { - type: 'object', - properties: { - status: { - type: 'string', - description: 'Filter by estimate status', - enum: ['draft', 'sent', 'approved', 'declined', 'expired'], - }, - customerId: { type: 'string', description: 'Filter by customer ID' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - }, - handler: async (params: PaginationParams & { - status?: string; - customerId?: string; - startDate?: string; - endDate?: string; - }) => { - const result = await client.getPaginated('/estimates', params as any); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_estimates_get', - description: 'Get detailed information about a specific estimate', - inputSchema: { - type: 'object', - properties: { - estimateId: { type: 'string', description: 'Estimate ID' }, - }, - required: ['estimateId'], - }, - handler: async (params: { estimateId: string }) => { - const estimate = await client.get( - `/estimates/${params.estimateId}` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(estimate, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_estimates_create', - description: 'Create a new estimate', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - locationId: { type: 'string', description: 'Customer location ID' }, - estimateDate: { type: 'string', description: 'Estimate date (ISO 8601)' }, - expirationDate: { type: 'string', description: 'Expiration date (ISO 8601)' }, - terms: { type: 'string', description: 'Terms and conditions' }, - notes: { type: 'string', description: 'Estimate notes' }, - }, - required: ['customerId'], - }, - handler: async (params: { - customerId: string; - locationId?: string; - estimateDate?: string; - expirationDate?: string; - terms?: string; - notes?: string; - }) => { - const estimate = await client.post('/estimates', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(estimate, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_estimates_update', - description: 'Update an existing estimate', - inputSchema: { - type: 'object', - properties: { - estimateId: { type: 'string', description: 'Estimate ID' }, - status: { - type: 'string', - enum: ['draft', 'sent', 'approved', 'declined', 'expired'], - }, - expirationDate: { type: 'string' }, - terms: { type: 'string' }, - notes: { type: 'string' }, - }, - required: ['estimateId'], - }, - handler: async (params: { - estimateId: string; - status?: string; - expirationDate?: string; - terms?: string; - notes?: string; - }) => { - const { estimateId, ...updateData } = params; - const estimate = await client.patch( - `/estimates/${estimateId}`, - updateData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(estimate, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_estimates_send', - description: 'Send an estimate to the customer', - inputSchema: { - type: 'object', - properties: { - estimateId: { type: 'string', description: 'Estimate ID' }, - email: { type: 'string', description: 'Email address to send to' }, - subject: { type: 'string', description: 'Email subject' }, - message: { type: 'string', description: 'Email message' }, - }, - required: ['estimateId'], - }, - handler: async (params: { - estimateId: string; - email?: string; - subject?: string; - message?: string; - }) => { - const { estimateId, ...sendData } = params; - const result = await client.post<{ success: boolean }>( - `/estimates/${estimateId}/send`, - sendData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_estimates_approve', - description: 'Approve an estimate and optionally convert to a job', - inputSchema: { - type: 'object', - properties: { - estimateId: { type: 'string', description: 'Estimate ID' }, - convertToJob: { - type: 'boolean', - description: 'Convert to job automatically', - }, - scheduledStart: { - type: 'string', - description: 'Scheduled start time if converting (ISO 8601)', - }, - }, - required: ['estimateId'], - }, - handler: async (params: { - estimateId: string; - convertToJob?: boolean; - scheduledStart?: string; - }) => { - const { estimateId, ...approveData } = params; - const result = await client.post<{ estimate: Estimate; jobId?: string }>( - `/estimates/${estimateId}/approve`, - approveData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/estimates.ts b/servers/fieldedge/src/tools/estimates.ts new file mode 100644 index 0000000..14e60bf --- /dev/null +++ b/servers/fieldedge/src/tools/estimates.ts @@ -0,0 +1,173 @@ +/** + * Estimate/Quote Management Tools + */ + +import type { Estimate, EstimateStatus } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const estimateTools = [ + { + name: 'fieldedge_list_estimates', + description: 'List all estimates with optional filtering', + inputSchema: { + type: 'object', + properties: { + page: { type: 'number' }, + pageSize: { type: 'number' }, + status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'approved', 'declined', 'expired'] }, + customerId: { type: 'string' }, + sortBy: { type: 'string' }, + sortOrder: { type: 'string', enum: ['asc', 'desc'] }, + }, + }, + }, + { + name: 'fieldedge_get_estimate', + description: 'Get a specific estimate by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Estimate ID' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_estimate', + description: 'Create a new estimate/quote', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string' }, + issueDate: { type: 'string' }, + expiryDate: { type: 'string' }, + lineItems: { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string', enum: ['service', 'part', 'equipment', 'labor'] }, + description: { type: 'string' }, + quantity: { type: 'number' }, + unitPrice: { type: 'number' }, + discount: { type: 'number' }, + tax: { type: 'number' }, + }, + }, + }, + notes: { type: 'string' }, + }, + required: ['customerId', 'issueDate', 'expiryDate', 'lineItems'], + }, + }, + { + name: 'fieldedge_update_estimate', + description: 'Update an existing estimate', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'approved', 'declined', 'expired'] }, + expiryDate: { type: 'string' }, + lineItems: { type: 'array' }, + notes: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_delete_estimate', + description: 'Delete an estimate', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_send_estimate', + description: 'Send estimate to customer via email', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + email: { type: 'string' }, + subject: { type: 'string' }, + message: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_approve_estimate', + description: 'Mark estimate as approved and optionally create a job', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + createJob: { type: 'boolean', default: true }, + notes: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_convert_estimate_to_invoice', + description: 'Convert an approved estimate to an invoice', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + issueDate: { type: 'string' }, + dueDate: { type: 'string' }, + }, + required: ['id'], + }, + }, +]; + +export async function handleEstimateTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_estimates': + return await client.getPaginated('/estimates', args); + + case 'fieldedge_get_estimate': + return await client.get(`/estimates/${args.id}`); + + case 'fieldedge_create_estimate': + return await client.post('/estimates', args); + + case 'fieldedge_update_estimate': + const { id, ...updates } = args; + return await client.patch(`/estimates/${id}`, updates); + + case 'fieldedge_delete_estimate': + return await client.delete(`/estimates/${args.id}`); + + case 'fieldedge_send_estimate': + return await client.post(`/estimates/${args.id}/send`, { + email: args.email, + subject: args.subject, + message: args.message, + }); + + case 'fieldedge_approve_estimate': + return await client.post(`/estimates/${args.id}/approve`, { + createJob: args.createJob, + notes: args.notes, + }); + + case 'fieldedge_convert_estimate_to_invoice': + return await client.post(`/estimates/${args.id}/convert-to-invoice`, { + issueDate: args.issueDate, + dueDate: args.dueDate, + }); + + default: + throw new Error(`Unknown estimate tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/inventory-tools.ts b/servers/fieldedge/src/tools/inventory-tools.ts deleted file mode 100644 index edc4792..0000000 --- a/servers/fieldedge/src/tools/inventory-tools.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * FieldEdge Inventory Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { InventoryPart, PurchaseOrder, PaginationParams } from '../types.js'; - -export function createInventoryTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_inventory_parts_list', - description: 'List all inventory parts with optional filtering', - inputSchema: { - type: 'object', - properties: { - category: { type: 'string', description: 'Filter by category' }, - manufacturer: { type: 'string', description: 'Filter by manufacturer' }, - lowStock: { - type: 'boolean', - description: 'Show only low stock items', - }, - search: { type: 'string', description: 'Search by part number or description' }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - }, - handler: async (params: { - category?: string; - manufacturer?: string; - lowStock?: boolean; - search?: string; - }) => { - const result = await client.getPaginated( - '/inventory/parts', - params - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_inventory_parts_get', - description: 'Get detailed information about a specific part', - inputSchema: { - type: 'object', - properties: { - partId: { type: 'string', description: 'Part ID' }, - }, - required: ['partId'], - }, - handler: async (params: { partId: string }) => { - const part = await client.get( - `/inventory/parts/${params.partId}` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(part, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_inventory_stock_update', - description: 'Update stock quantity for a part', - inputSchema: { - type: 'object', - properties: { - partId: { type: 'string', description: 'Part ID' }, - quantityChange: { - type: 'number', - description: 'Quantity change (positive for add, negative for subtract)', - }, - reason: { - type: 'string', - description: 'Reason for stock adjustment', - enum: [ - 'purchase', - 'return', - 'adjustment', - 'damage', - 'theft', - 'transfer', - 'cycle_count', - ], - }, - notes: { type: 'string', description: 'Adjustment notes' }, - }, - required: ['partId', 'quantityChange', 'reason'], - }, - handler: async (params: { - partId: string; - quantityChange: number; - reason: string; - notes?: string; - }) => { - const { partId, ...adjustmentData } = params; - const result = await client.post( - `/inventory/parts/${partId}/adjust`, - adjustmentData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_inventory_purchase_orders_list', - description: 'List all purchase orders', - inputSchema: { - type: 'object', - properties: { - status: { - type: 'string', - description: 'Filter by PO status', - enum: ['draft', 'submitted', 'approved', 'received', 'cancelled'], - }, - vendorId: { type: 'string', description: 'Filter by vendor ID' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - }, - handler: async (params: { - status?: string; - vendorId?: string; - startDate?: string; - endDate?: string; - }) => { - const result = await client.getPaginated( - '/inventory/purchase-orders', - params - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_inventory_purchase_orders_get', - description: 'Get detailed information about a purchase order', - inputSchema: { - type: 'object', - properties: { - poId: { type: 'string', description: 'Purchase Order ID' }, - }, - required: ['poId'], - }, - handler: async (params: { poId: string }) => { - const po = await client.get( - `/inventory/purchase-orders/${params.poId}` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(po, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_inventory_reorder_report', - description: 'Get a report of parts that need reordering', - inputSchema: { - type: 'object', - properties: { - category: { type: 'string', description: 'Filter by category' }, - }, - }, - handler: async (params: { category?: string }) => { - const report = await client.get<{ data: InventoryPart[] }>( - '/inventory/reorder-report', - params - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(report, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/inventory.ts b/servers/fieldedge/src/tools/inventory.ts new file mode 100644 index 0000000..44beac5 --- /dev/null +++ b/servers/fieldedge/src/tools/inventory.ts @@ -0,0 +1,147 @@ +/** + * Inventory Management Tools + */ + +import type { InventoryItem, InventoryTransaction } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const inventoryTools = [ + { + name: 'fieldedge_list_inventory', + description: 'List inventory items', + inputSchema: { + type: 'object', + properties: { + page: { type: 'number' }, + pageSize: { type: 'number' }, + category: { type: 'string' }, + manufacturer: { type: 'string' }, + warehouse: { type: 'string' }, + lowStock: { type: 'boolean', description: 'Show only low stock items' }, + search: { type: 'string' }, + }, + }, + }, + { + name: 'fieldedge_get_inventory_item', + description: 'Get specific inventory item', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_inventory_item', + description: 'Create new inventory item', + inputSchema: { + type: 'object', + properties: { + sku: { type: 'string' }, + name: { type: 'string' }, + description: { type: 'string' }, + category: { type: 'string' }, + manufacturer: { type: 'string' }, + modelNumber: { type: 'string' }, + unitOfMeasure: { type: 'string' }, + costPrice: { type: 'number' }, + sellPrice: { type: 'number' }, + reorderPoint: { type: 'number' }, + reorderQuantity: { type: 'number' }, + warehouse: { type: 'string' }, + binLocation: { type: 'string' }, + taxable: { type: 'boolean' }, + }, + required: ['sku', 'name', 'category', 'unitOfMeasure', 'costPrice', 'sellPrice'], + }, + }, + { + name: 'fieldedge_update_inventory_item', + description: 'Update inventory item', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + description: { type: 'string' }, + category: { type: 'string' }, + costPrice: { type: 'number' }, + sellPrice: { type: 'number' }, + reorderPoint: { type: 'number' }, + reorderQuantity: { type: 'number' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_adjust_inventory', + description: 'Adjust inventory quantity', + inputSchema: { + type: 'object', + properties: { + itemId: { type: 'string' }, + quantity: { type: 'number', description: 'Adjustment quantity (positive or negative)' }, + type: { type: 'string', enum: ['receipt', 'issue', 'adjustment', 'transfer', 'return'] }, + reference: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['itemId', 'quantity', 'type'], + }, + }, + { + name: 'fieldedge_get_inventory_transactions', + description: 'Get inventory transaction history', + inputSchema: { + type: 'object', + properties: { + itemId: { type: 'string' }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + type: { type: 'string', enum: ['receipt', 'issue', 'adjustment', 'transfer', 'return'] }, + }, + }, + }, + { + name: 'fieldedge_get_low_stock_items', + description: 'Get items below reorder point', + inputSchema: { + type: 'object', + properties: { + warehouse: { type: 'string' }, + }, + }, + }, +]; + +export async function handleInventoryTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_inventory': + return await client.getPaginated('/inventory', args); + + case 'fieldedge_get_inventory_item': + return await client.get(`/inventory/${args.id}`); + + case 'fieldedge_create_inventory_item': + return await client.post('/inventory', args); + + case 'fieldedge_update_inventory_item': + const { id, ...updates } = args; + return await client.patch(`/inventory/${id}`, updates); + + case 'fieldedge_adjust_inventory': + return await client.post('/inventory/transactions', args); + + case 'fieldedge_get_inventory_transactions': + return await client.getPaginated('/inventory/transactions', args); + + case 'fieldedge_get_low_stock_items': + return await client.get('/inventory/low-stock', args); + + default: + throw new Error(`Unknown inventory tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/invoices-tools.ts b/servers/fieldedge/src/tools/invoices-tools.ts deleted file mode 100644 index fa92bb9..0000000 --- a/servers/fieldedge/src/tools/invoices-tools.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * FieldEdge Invoices Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { Invoice, Payment, PaginationParams } from '../types.js'; - -export function createInvoicesTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_invoices_list', - description: 'List all invoices with optional filtering', - inputSchema: { - type: 'object', - properties: { - status: { - type: 'string', - description: 'Filter by invoice status', - enum: ['draft', 'sent', 'paid', 'partial', 'overdue', 'void'], - }, - customerId: { type: 'string', description: 'Filter by customer ID' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - }, - handler: async (params: PaginationParams & { - status?: string; - customerId?: string; - startDate?: string; - endDate?: string; - }) => { - const result = await client.getPaginated('/invoices', params as any); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_invoices_get', - description: 'Get detailed information about a specific invoice', - inputSchema: { - type: 'object', - properties: { - invoiceId: { type: 'string', description: 'Invoice ID' }, - }, - required: ['invoiceId'], - }, - handler: async (params: { invoiceId: string }) => { - const invoice = await client.get(`/invoices/${params.invoiceId}`); - return { - content: [ - { - type: 'text', - text: JSON.stringify(invoice, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_invoices_create', - description: 'Create a new invoice', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - jobId: { type: 'string', description: 'Related job ID' }, - invoiceDate: { type: 'string', description: 'Invoice date (ISO 8601)' }, - dueDate: { type: 'string', description: 'Due date (ISO 8601)' }, - terms: { type: 'string', description: 'Payment terms' }, - notes: { type: 'string', description: 'Invoice notes' }, - }, - required: ['customerId'], - }, - handler: async (params: { - customerId: string; - jobId?: string; - invoiceDate?: string; - dueDate?: string; - terms?: string; - notes?: string; - }) => { - const invoice = await client.post('/invoices', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(invoice, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_invoices_update', - description: 'Update an existing invoice', - inputSchema: { - type: 'object', - properties: { - invoiceId: { type: 'string', description: 'Invoice ID' }, - status: { - type: 'string', - enum: ['draft', 'sent', 'paid', 'partial', 'overdue', 'void'], - }, - dueDate: { type: 'string' }, - terms: { type: 'string' }, - notes: { type: 'string' }, - }, - required: ['invoiceId'], - }, - handler: async (params: { - invoiceId: string; - status?: string; - dueDate?: string; - terms?: string; - notes?: string; - }) => { - const { invoiceId, ...updateData } = params; - const invoice = await client.patch( - `/invoices/${invoiceId}`, - updateData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(invoice, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_invoices_payments_list', - description: 'List all payments for an invoice', - inputSchema: { - type: 'object', - properties: { - invoiceId: { type: 'string', description: 'Invoice ID' }, - }, - required: ['invoiceId'], - }, - handler: async (params: { invoiceId: string }) => { - const payments = await client.get<{ data: Payment[] }>( - `/invoices/${params.invoiceId}/payments` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(payments, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_invoices_payments_add', - description: 'Add a payment to an invoice', - inputSchema: { - type: 'object', - properties: { - invoiceId: { type: 'string', description: 'Invoice ID' }, - amount: { type: 'number', description: 'Payment amount' }, - paymentMethod: { - type: 'string', - description: 'Payment method', - enum: ['cash', 'check', 'credit_card', 'ach', 'other'], - }, - paymentDate: { type: 'string', description: 'Payment date (ISO 8601)' }, - referenceNumber: { type: 'string', description: 'Reference/check number' }, - notes: { type: 'string', description: 'Payment notes' }, - }, - required: ['invoiceId', 'amount', 'paymentMethod'], - }, - handler: async (params: { - invoiceId: string; - amount: number; - paymentMethod: string; - paymentDate?: string; - referenceNumber?: string; - notes?: string; - }) => { - const { invoiceId, ...paymentData } = params; - const payment = await client.post( - `/invoices/${invoiceId}/payments`, - paymentData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(payment, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/invoices.ts b/servers/fieldedge/src/tools/invoices.ts new file mode 100644 index 0000000..2d423da --- /dev/null +++ b/servers/fieldedge/src/tools/invoices.ts @@ -0,0 +1,201 @@ +/** + * Invoice Management Tools + */ + +import { z } from 'zod'; +import type { Invoice, InvoiceStatus, LineItem } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const invoiceTools = [ + { + name: 'fieldedge_list_invoices', + description: 'List all invoices with optional filtering and pagination', + inputSchema: { + type: 'object', + properties: { + page: { type: 'number' }, + pageSize: { type: 'number' }, + status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'partial', 'paid', 'overdue', 'void'] }, + customerId: { type: 'string', description: 'Filter by customer ID' }, + jobId: { type: 'string', description: 'Filter by job ID' }, + startDate: { type: 'string', description: 'Filter invoices from this date' }, + endDate: { type: 'string', description: 'Filter invoices to this date' }, + sortBy: { type: 'string' }, + sortOrder: { type: 'string', enum: ['asc', 'desc'] }, + }, + }, + }, + { + name: 'fieldedge_get_invoice', + description: 'Get a specific invoice by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Invoice ID' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_invoice', + description: 'Create a new invoice', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string', description: 'Customer ID' }, + jobId: { type: 'string', description: 'Associated job ID' }, + issueDate: { type: 'string', description: 'Issue date (ISO 8601)' }, + dueDate: { type: 'string', description: 'Due date (ISO 8601)' }, + lineItems: { + type: 'array', + items: { + type: 'object', + properties: { + type: { type: 'string', enum: ['service', 'part', 'equipment', 'labor'] }, + description: { type: 'string' }, + quantity: { type: 'number' }, + unitPrice: { type: 'number' }, + discount: { type: 'number', default: 0 }, + tax: { type: 'number', default: 0 }, + itemId: { type: 'string' }, + }, + required: ['type', 'description', 'quantity', 'unitPrice'], + }, + }, + paymentTerms: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['customerId', 'issueDate', 'dueDate', 'lineItems'], + }, + }, + { + name: 'fieldedge_update_invoice', + description: 'Update an existing invoice', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Invoice ID' }, + status: { type: 'string', enum: ['draft', 'sent', 'viewed', 'partial', 'paid', 'overdue', 'void'] }, + dueDate: { type: 'string' }, + lineItems: { type: 'array' }, + discount: { type: 'number' }, + paymentTerms: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_delete_invoice', + description: 'Delete an invoice', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Invoice ID' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_send_invoice', + description: 'Send invoice to customer via email', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Invoice ID' }, + email: { type: 'string', description: 'Override customer email' }, + subject: { type: 'string', description: 'Email subject' }, + message: { type: 'string', description: 'Email message' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_void_invoice', + description: 'Void an invoice', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Invoice ID' }, + reason: { type: 'string', description: 'Reason for voiding' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_record_payment', + description: 'Record a payment against an invoice', + inputSchema: { + type: 'object', + properties: { + invoiceId: { type: 'string', description: 'Invoice ID' }, + amount: { type: 'number', description: 'Payment amount' }, + paymentMethod: { type: 'string', enum: ['cash', 'check', 'credit-card', 'debit-card', 'ach', 'wire', 'other'] }, + paymentDate: { type: 'string', description: 'Payment date (ISO 8601)' }, + reference: { type: 'string', description: 'Payment reference/confirmation number' }, + notes: { type: 'string' }, + }, + required: ['invoiceId', 'amount', 'paymentMethod'], + }, + }, + { + name: 'fieldedge_get_invoice_pdf', + description: 'Generate and get invoice PDF', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Invoice ID' }, + }, + required: ['id'], + }, + }, +]; + +export async function handleInvoiceTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_invoices': + return await client.getPaginated('/invoices', args); + + case 'fieldedge_get_invoice': + return await client.get(`/invoices/${args.id}`); + + case 'fieldedge_create_invoice': + return await client.post('/invoices', args); + + case 'fieldedge_update_invoice': + const { id, ...updates } = args; + return await client.patch(`/invoices/${id}`, updates); + + case 'fieldedge_delete_invoice': + return await client.delete(`/invoices/${args.id}`); + + case 'fieldedge_send_invoice': + return await client.post(`/invoices/${args.id}/send`, { + email: args.email, + subject: args.subject, + message: args.message, + }); + + case 'fieldedge_void_invoice': + return await client.post(`/invoices/${args.id}/void`, { + reason: args.reason, + }); + + case 'fieldedge_record_payment': + return await client.post('/payments', args); + + case 'fieldedge_get_invoice_pdf': + const pdfData = await client.downloadFile(`/invoices/${args.id}/pdf`); + return { + success: true, + message: 'PDF generated successfully', + size: pdfData.length, + data: pdfData.toString('base64'), + }; + + default: + throw new Error(`Unknown invoice tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/jobs-tools.ts b/servers/fieldedge/src/tools/jobs-tools.ts deleted file mode 100644 index 2257f28..0000000 --- a/servers/fieldedge/src/tools/jobs-tools.ts +++ /dev/null @@ -1,325 +0,0 @@ -/** - * FieldEdge Jobs Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { Job, JobLineItem, JobEquipment, PaginationParams } from '../types.js'; - -export function createJobsTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_jobs_list', - description: 'List all jobs with optional filtering by status, customer, technician, or date range', - inputSchema: { - type: 'object', - properties: { - status: { - type: 'string', - description: 'Filter by job status', - enum: ['scheduled', 'in_progress', 'completed', 'cancelled', 'on_hold'], - }, - customerId: { - type: 'string', - description: 'Filter by customer ID', - }, - technicianId: { - type: 'string', - description: 'Filter by assigned technician ID', - }, - startDate: { - type: 'string', - description: 'Filter jobs scheduled after this date (ISO 8601)', - }, - endDate: { - type: 'string', - description: 'Filter jobs scheduled before this date (ISO 8601)', - }, - page: { type: 'number', description: 'Page number (default: 1)' }, - pageSize: { type: 'number', description: 'Items per page (default: 50)' }, - }, - }, - handler: async (params: PaginationParams & { - status?: string; - customerId?: string; - technicianId?: string; - startDate?: string; - endDate?: string; - }) => { - const result = await client.getPaginated('/jobs', params as any); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_jobs_get', - description: 'Get detailed information about a specific job by ID', - inputSchema: { - type: 'object', - properties: { - jobId: { - type: 'string', - description: 'The job ID', - }, - }, - required: ['jobId'], - }, - handler: async (params: { jobId: string }) => { - const job = await client.get(`/jobs/${params.jobId}`); - return { - content: [ - { - type: 'text', - text: JSON.stringify(job, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_jobs_create', - description: 'Create a new job', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - locationId: { type: 'string', description: 'Customer location ID' }, - jobType: { type: 'string', description: 'Job type' }, - priority: { - type: 'string', - description: 'Job priority', - enum: ['low', 'normal', 'high', 'emergency'], - }, - scheduledStart: { - type: 'string', - description: 'Scheduled start time (ISO 8601)', - }, - scheduledEnd: { - type: 'string', - description: 'Scheduled end time (ISO 8601)', - }, - assignedTechId: { type: 'string', description: 'Assigned technician ID' }, - description: { type: 'string', description: 'Job description' }, - notes: { type: 'string', description: 'Internal notes' }, - }, - required: ['customerId', 'jobType'], - }, - handler: async (params: { - customerId: string; - locationId?: string; - jobType: string; - priority?: string; - scheduledStart?: string; - scheduledEnd?: string; - assignedTechId?: string; - description?: string; - notes?: string; - }) => { - const job = await client.post('/jobs', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(job, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_jobs_update', - description: 'Update an existing job', - inputSchema: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'Job ID' }, - status: { - type: 'string', - description: 'Job status', - enum: ['scheduled', 'in_progress', 'completed', 'cancelled', 'on_hold'], - }, - priority: { - type: 'string', - enum: ['low', 'normal', 'high', 'emergency'], - }, - assignedTechId: { type: 'string' }, - scheduledStart: { type: 'string' }, - scheduledEnd: { type: 'string' }, - description: { type: 'string' }, - notes: { type: 'string' }, - }, - required: ['jobId'], - }, - handler: async (params: { - jobId: string; - status?: string; - priority?: string; - assignedTechId?: string; - scheduledStart?: string; - scheduledEnd?: string; - description?: string; - notes?: string; - }) => { - const { jobId, ...updateData } = params; - const job = await client.patch(`/jobs/${jobId}`, updateData); - return { - content: [ - { - type: 'text', - text: JSON.stringify(job, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_jobs_complete', - description: 'Mark a job as completed', - inputSchema: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'Job ID' }, - completionNotes: { type: 'string', description: 'Completion notes' }, - }, - required: ['jobId'], - }, - handler: async (params: { jobId: string; completionNotes?: string }) => { - const job = await client.post(`/jobs/${params.jobId}/complete`, { - notes: params.completionNotes, - }); - return { - content: [ - { - type: 'text', - text: JSON.stringify(job, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_jobs_cancel', - description: 'Cancel a job', - inputSchema: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'Job ID' }, - reason: { type: 'string', description: 'Cancellation reason' }, - }, - required: ['jobId'], - }, - handler: async (params: { jobId: string; reason?: string }) => { - const job = await client.post(`/jobs/${params.jobId}/cancel`, { - reason: params.reason, - }); - return { - content: [ - { - type: 'text', - text: JSON.stringify(job, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_jobs_line_items_list', - description: 'List all line items for a job', - inputSchema: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'Job ID' }, - }, - required: ['jobId'], - }, - handler: async (params: { jobId: string }) => { - const lineItems = await client.get<{ data: JobLineItem[] }>( - `/jobs/${params.jobId}/line-items` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(lineItems, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_jobs_line_items_add', - description: 'Add a line item to a job', - inputSchema: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'Job ID' }, - type: { - type: 'string', - description: 'Line item type', - enum: ['labor', 'material', 'equipment', 'other'], - }, - description: { type: 'string', description: 'Item description' }, - quantity: { type: 'number', description: 'Quantity' }, - unitPrice: { type: 'number', description: 'Unit price' }, - taxable: { type: 'boolean', description: 'Is taxable' }, - partNumber: { type: 'string', description: 'Part number (for materials)' }, - technicianId: { type: 'string', description: 'Technician ID (for labor)' }, - }, - required: ['jobId', 'type', 'description', 'quantity', 'unitPrice'], - }, - handler: async (params: { - jobId: string; - type: string; - description: string; - quantity: number; - unitPrice: number; - taxable?: boolean; - partNumber?: string; - technicianId?: string; - }) => { - const { jobId, ...itemData } = params; - const lineItem = await client.post( - `/jobs/${jobId}/line-items`, - itemData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(lineItem, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_jobs_equipment_list', - description: 'List equipment associated with a job', - inputSchema: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'Job ID' }, - }, - required: ['jobId'], - }, - handler: async (params: { jobId: string }) => { - const equipment = await client.get<{ data: JobEquipment[] }>( - `/jobs/${params.jobId}/equipment` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(equipment, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/jobs.ts b/servers/fieldedge/src/tools/jobs.ts new file mode 100644 index 0000000..a990fe3 --- /dev/null +++ b/servers/fieldedge/src/tools/jobs.ts @@ -0,0 +1,200 @@ +/** + * Job Management Tools + */ + +import { z } from 'zod'; +import type { Job, JobStatus } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +// Tool Definitions +export const jobTools = [ + { + name: 'fieldedge_list_jobs', + description: 'List all jobs with optional filtering and pagination', + inputSchema: { + type: 'object', + properties: { + page: { type: 'number', description: 'Page number' }, + pageSize: { type: 'number', description: 'Items per page' }, + status: { + type: 'string', + enum: ['scheduled', 'dispatched', 'in-progress', 'on-hold', 'completed', 'cancelled', 'invoiced'], + description: 'Filter by status' + }, + priority: { type: 'string', enum: ['low', 'normal', 'high', 'emergency'] }, + customerId: { type: 'string', description: 'Filter by customer ID' }, + technicianId: { type: 'string', description: 'Filter by technician ID' }, + startDate: { type: 'string', description: 'Filter jobs starting from this date' }, + endDate: { type: 'string', description: 'Filter jobs ending before this date' }, + sortBy: { type: 'string' }, + sortOrder: { type: 'string', enum: ['asc', 'desc'] }, + }, + }, + }, + { + name: 'fieldedge_get_job', + description: 'Get a specific job by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Job ID' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_job', + description: 'Create a new job/work order', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string', description: 'Customer ID' }, + locationId: { type: 'string', description: 'Service location ID' }, + jobType: { type: 'string', description: 'Type of job' }, + priority: { type: 'string', enum: ['low', 'normal', 'high', 'emergency'], default: 'normal' }, + description: { type: 'string', description: 'Job description' }, + scheduledStart: { type: 'string', description: 'Scheduled start date/time (ISO 8601)' }, + scheduledEnd: { type: 'string', description: 'Scheduled end date/time (ISO 8601)' }, + assignedTechnicians: { type: 'array', items: { type: 'string' }, description: 'Technician IDs' }, + equipmentIds: { type: 'array', items: { type: 'string' }, description: 'Equipment IDs' }, + tags: { type: 'array', items: { type: 'string' } }, + customFields: { type: 'object' }, + }, + required: ['customerId', 'jobType', 'description'], + }, + }, + { + name: 'fieldedge_update_job', + description: 'Update an existing job', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Job ID' }, + jobType: { type: 'string' }, + status: { type: 'string', enum: ['scheduled', 'dispatched', 'in-progress', 'on-hold', 'completed', 'cancelled', 'invoiced'] }, + priority: { type: 'string', enum: ['low', 'normal', 'high', 'emergency'] }, + description: { type: 'string' }, + scheduledStart: { type: 'string' }, + scheduledEnd: { type: 'string' }, + actualStart: { type: 'string' }, + actualEnd: { type: 'string' }, + assignedTechnicians: { type: 'array', items: { type: 'string' } }, + equipmentIds: { type: 'array', items: { type: 'string' } }, + tags: { type: 'array', items: { type: 'string' } }, + customFields: { type: 'object' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_delete_job', + description: 'Delete a job', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Job ID' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_start_job', + description: 'Start a job (set status to in-progress and record actual start time)', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Job ID' }, + notes: { type: 'string', description: 'Notes about starting the job' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_complete_job', + description: 'Complete a job (set status to completed and record actual end time)', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Job ID' }, + notes: { type: 'string', description: 'Completion notes' }, + createInvoice: { type: 'boolean', default: false, description: 'Automatically create invoice' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_cancel_job', + description: 'Cancel a job', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Job ID' }, + reason: { type: 'string', description: 'Cancellation reason' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_assign_technician', + description: 'Assign or reassign technicians to a job', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string', description: 'Job ID' }, + technicianIds: { type: 'array', items: { type: 'string' }, description: 'Technician IDs to assign' }, + replace: { type: 'boolean', default: false, description: 'Replace existing technicians or add to them' }, + }, + required: ['id', 'technicianIds'], + }, + }, +]; + +// Tool Handlers +export async function handleJobTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_jobs': + return await client.getPaginated('/jobs', args); + + case 'fieldedge_get_job': + return await client.get(`/jobs/${args.id}`); + + case 'fieldedge_create_job': + return await client.post('/jobs', args); + + case 'fieldedge_update_job': + const { id, ...updates } = args; + return await client.patch(`/jobs/${id}`, updates); + + case 'fieldedge_delete_job': + return await client.delete(`/jobs/${args.id}`); + + case 'fieldedge_start_job': + return await client.post(`/jobs/${args.id}/start`, { + actualStart: new Date().toISOString(), + notes: args.notes, + }); + + case 'fieldedge_complete_job': + return await client.post(`/jobs/${args.id}/complete`, { + actualEnd: new Date().toISOString(), + notes: args.notes, + createInvoice: args.createInvoice, + }); + + case 'fieldedge_cancel_job': + return await client.post(`/jobs/${args.id}/cancel`, { + reason: args.reason, + }); + + case 'fieldedge_assign_technician': + return await client.post(`/jobs/${args.id}/assign`, { + technicianIds: args.technicianIds, + replace: args.replace, + }); + + default: + throw new Error(`Unknown job tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/locations.ts b/servers/fieldedge/src/tools/locations.ts new file mode 100644 index 0000000..1be85d5 --- /dev/null +++ b/servers/fieldedge/src/tools/locations.ts @@ -0,0 +1,112 @@ +/** + * Location Management Tools + */ + +import type { Location } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const locationTools = [ + { + name: 'fieldedge_list_locations', + description: 'List customer locations', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string' }, + status: { type: 'string', enum: ['active', 'inactive'] }, + type: { type: 'string', enum: ['primary', 'secondary', 'billing', 'service'] }, + }, + }, + }, + { + name: 'fieldedge_get_location', + description: 'Get specific location', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_location', + description: 'Create new location for customer', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string' }, + name: { type: 'string' }, + type: { type: 'string', enum: ['primary', 'secondary', 'billing', 'service'] }, + address: { + type: 'object', + properties: { + street1: { type: 'string' }, + street2: { type: 'string' }, + city: { type: 'string' }, + state: { type: 'string' }, + zip: { type: 'string' }, + }, + }, + contactName: { type: 'string' }, + contactPhone: { type: 'string' }, + accessNotes: { type: 'string' }, + gateCode: { type: 'string' }, + }, + required: ['customerId', 'name', 'type', 'address'], + }, + }, + { + name: 'fieldedge_update_location', + description: 'Update location details', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + status: { type: 'string', enum: ['active', 'inactive'] }, + contactName: { type: 'string' }, + contactPhone: { type: 'string' }, + accessNotes: { type: 'string' }, + gateCode: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_delete_location', + description: 'Delete a location', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, +]; + +export async function handleLocationTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_locations': + return await client.getPaginated('/locations', args); + + case 'fieldedge_get_location': + return await client.get(`/locations/${args.id}`); + + case 'fieldedge_create_location': + return await client.post('/locations', args); + + case 'fieldedge_update_location': + const { id, ...updates } = args; + return await client.patch(`/locations/${id}`, updates); + + case 'fieldedge_delete_location': + return await client.delete(`/locations/${args.id}`); + + default: + throw new Error(`Unknown location tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/payments.ts b/servers/fieldedge/src/tools/payments.ts new file mode 100644 index 0000000..37e33be --- /dev/null +++ b/servers/fieldedge/src/tools/payments.ts @@ -0,0 +1,108 @@ +/** + * Payment Management Tools + */ + +import type { Payment } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const paymentTools = [ + { + name: 'fieldedge_list_payments', + description: 'List all payments', + inputSchema: { + type: 'object', + properties: { + page: { type: 'number' }, + pageSize: { type: 'number' }, + customerId: { type: 'string' }, + invoiceId: { type: 'string' }, + status: { type: 'string', enum: ['pending', 'processed', 'failed', 'refunded'] }, + paymentMethod: { type: 'string', enum: ['cash', 'check', 'credit-card', 'debit-card', 'ach', 'wire', 'other'] }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + }, + }, + }, + { + name: 'fieldedge_get_payment', + description: 'Get specific payment', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_process_payment', + description: 'Process a new payment', + inputSchema: { + type: 'object', + properties: { + invoiceId: { type: 'string' }, + customerId: { type: 'string' }, + amount: { type: 'number' }, + paymentMethod: { type: 'string', enum: ['cash', 'check', 'credit-card', 'debit-card', 'ach', 'wire', 'other'] }, + paymentDate: { type: 'string' }, + reference: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['invoiceId', 'customerId', 'amount', 'paymentMethod'], + }, + }, + { + name: 'fieldedge_refund_payment', + description: 'Refund a payment', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + amount: { type: 'number' }, + reason: { type: 'string' }, + }, + required: ['id', 'amount', 'reason'], + }, + }, + { + name: 'fieldedge_void_payment', + description: 'Void a payment', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + reason: { type: 'string' }, + }, + required: ['id', 'reason'], + }, + }, +]; + +export async function handlePaymentTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_payments': + return await client.getPaginated('/payments', args); + + case 'fieldedge_get_payment': + return await client.get(`/payments/${args.id}`); + + case 'fieldedge_process_payment': + return await client.post('/payments', args); + + case 'fieldedge_refund_payment': + return await client.post(`/payments/${args.id}/refund`, { + amount: args.amount, + reason: args.reason, + }); + + case 'fieldedge_void_payment': + return await client.post(`/payments/${args.id}/void`, { + reason: args.reason, + }); + + default: + throw new Error(`Unknown payment tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/reporting-tools.ts b/servers/fieldedge/src/tools/reporting-tools.ts deleted file mode 100644 index 469676f..0000000 --- a/servers/fieldedge/src/tools/reporting-tools.ts +++ /dev/null @@ -1,237 +0,0 @@ -/** - * FieldEdge Reporting Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { - RevenueReport, - JobProfitabilityReport, - TechnicianPerformance, - AgingReport, -} from '../types.js'; - -export function createReportingTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_reports_revenue', - description: 'Get revenue report for a specified period', - inputSchema: { - type: 'object', - properties: { - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - groupBy: { - type: 'string', - description: 'Group results by dimension', - enum: ['day', 'week', 'month', 'jobType', 'technician', 'customer'], - }, - }, - required: ['startDate', 'endDate'], - }, - handler: async (params: { - startDate: string; - endDate: string; - groupBy?: string; - }) => { - const report = await client.get('/reports/revenue', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(report, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_reports_job_profitability', - description: 'Get profitability analysis for a specific job or all jobs in a period', - inputSchema: { - type: 'object', - properties: { - jobId: { type: 'string', description: 'Specific job ID (optional)' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - minMargin: { - type: 'number', - description: 'Filter jobs with profit margin above this percentage', - }, - maxMargin: { - type: 'number', - description: 'Filter jobs with profit margin below this percentage', - }, - }, - }, - handler: async (params: { - jobId?: string; - startDate?: string; - endDate?: string; - minMargin?: number; - maxMargin?: number; - }) => { - const report = await client.get< - JobProfitabilityReport | { data: JobProfitabilityReport[] } - >('/reports/job-profitability', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(report, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_reports_technician_performance', - description: 'Get performance metrics for all technicians or a specific technician', - inputSchema: { - type: 'object', - properties: { - technicianId: { type: 'string', description: 'Specific technician ID (optional)' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - sortBy: { - type: 'string', - description: 'Sort results by metric', - enum: ['revenue', 'jobsCompleted', 'efficiency', 'customerSatisfaction'], - }, - }, - required: ['startDate', 'endDate'], - }, - handler: async (params: { - technicianId?: string; - startDate: string; - endDate: string; - sortBy?: string; - }) => { - const report = await client.get< - TechnicianPerformance | { data: TechnicianPerformance[] } - >('/reports/technician-performance', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(report, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_reports_aging', - description: 'Get accounts receivable aging report', - inputSchema: { - type: 'object', - properties: { - asOfDate: { - type: 'string', - description: 'As-of date for the report (ISO 8601, defaults to today)', - }, - customerId: { - type: 'string', - description: 'Filter by specific customer ID', - }, - minAmount: { - type: 'number', - description: 'Show only customers with balance above this amount', - }, - }, - }, - handler: async (params: { - asOfDate?: string; - customerId?: string; - minAmount?: number; - }) => { - const report = await client.get('/reports/aging', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(report, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_reports_service_agreement_revenue', - description: 'Get revenue breakdown from service agreements/memberships', - inputSchema: { - type: 'object', - properties: { - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - agreementType: { type: 'string', description: 'Filter by agreement type' }, - }, - required: ['startDate', 'endDate'], - }, - handler: async (params: { - startDate: string; - endDate: string; - agreementType?: string; - }) => { - const report = await client.get<{ - period: string; - totalRevenue: number; - activeAgreements: number; - newAgreements: number; - cancelledAgreements: number; - renewalRate: number; - byType: Record; - }>('/reports/agreement-revenue', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(report, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_reports_equipment_service_due', - description: 'Get report of equipment due for service', - inputSchema: { - type: 'object', - properties: { - daysAhead: { - type: 'number', - description: 'Look ahead this many days (default: 30)', - }, - customerId: { type: 'string', description: 'Filter by customer ID' }, - equipmentType: { type: 'string', description: 'Filter by equipment type' }, - }, - }, - handler: async (params: { - daysAhead?: number; - customerId?: string; - equipmentType?: string; - }) => { - const report = await client.get<{ - data: Array<{ - equipmentId: string; - customerId: string; - customerName: string; - equipmentType: string; - model: string; - lastServiceDate: string; - nextServiceDate: string; - daysUntilDue: number; - isOverdue: boolean; - }>; - }>('/reports/equipment-service-due', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(report, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/reporting.ts b/servers/fieldedge/src/tools/reporting.ts new file mode 100644 index 0000000..64d5776 --- /dev/null +++ b/servers/fieldedge/src/tools/reporting.ts @@ -0,0 +1,140 @@ +/** + * Reporting and Analytics Tools + */ + +import type { Report, RevenueReport, TechnicianProductivityReport } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const reportingTools = [ + { + name: 'fieldedge_get_revenue_report', + description: 'Get revenue report for a period', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, + endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, + groupBy: { type: 'string', enum: ['day', 'week', 'month'], default: 'day' }, + }, + required: ['startDate', 'endDate'], + }, + }, + { + name: 'fieldedge_get_technician_productivity_report', + description: 'Get technician productivity metrics', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string' }, + endDate: { type: 'string' }, + technicianIds: { type: 'array', items: { type: 'string' } }, + }, + required: ['startDate', 'endDate'], + }, + }, + { + name: 'fieldedge_get_job_completion_report', + description: 'Get job completion statistics', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string' }, + endDate: { type: 'string' }, + jobType: { type: 'string' }, + status: { type: 'string' }, + }, + required: ['startDate', 'endDate'], + }, + }, + { + name: 'fieldedge_get_aging_receivables_report', + description: 'Get accounts receivable aging report', + inputSchema: { + type: 'object', + properties: { + asOfDate: { type: 'string', description: 'As of date (YYYY-MM-DD)' }, + customerId: { type: 'string' }, + }, + }, + }, + { + name: 'fieldedge_get_sales_by_category_report', + description: 'Get sales breakdown by category', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string' }, + endDate: { type: 'string' }, + }, + required: ['startDate', 'endDate'], + }, + }, + { + name: 'fieldedge_get_equipment_maintenance_report', + description: 'Get equipment maintenance history and upcoming', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string' }, + equipmentType: { type: 'string' }, + overdueOnly: { type: 'boolean' }, + }, + }, + }, + { + name: 'fieldedge_get_customer_satisfaction_report', + description: 'Get customer satisfaction metrics', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string' }, + endDate: { type: 'string' }, + }, + required: ['startDate', 'endDate'], + }, + }, + { + name: 'fieldedge_get_inventory_valuation_report', + description: 'Get current inventory valuation', + inputSchema: { + type: 'object', + properties: { + warehouse: { type: 'string' }, + category: { type: 'string' }, + }, + }, + }, +]; + +export async function handleReportingTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_get_revenue_report': + return await client.get('/reports/revenue', args); + + case 'fieldedge_get_technician_productivity_report': + return await client.get('/reports/technician-productivity', args); + + case 'fieldedge_get_job_completion_report': + return await client.get('/reports/job-completion', args); + + case 'fieldedge_get_aging_receivables_report': + return await client.get('/reports/aging-receivables', args); + + case 'fieldedge_get_sales_by_category_report': + return await client.get('/reports/sales-by-category', args); + + case 'fieldedge_get_equipment_maintenance_report': + return await client.get('/reports/equipment-maintenance', args); + + case 'fieldedge_get_customer_satisfaction_report': + return await client.get('/reports/customer-satisfaction', args); + + case 'fieldedge_get_inventory_valuation_report': + return await client.get('/reports/inventory-valuation', args); + + default: + throw new Error(`Unknown reporting tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/scheduling.ts b/servers/fieldedge/src/tools/scheduling.ts new file mode 100644 index 0000000..dbe4148 --- /dev/null +++ b/servers/fieldedge/src/tools/scheduling.ts @@ -0,0 +1,175 @@ +/** + * Scheduling and Dispatch Tools + */ + +import type { Appointment, DispatchBoard } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const schedulingTools = [ + { + name: 'fieldedge_list_appointments', + description: 'List appointments with filtering', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string' }, + endDate: { type: 'string' }, + technicianId: { type: 'string' }, + customerId: { type: 'string' }, + status: { type: 'string', enum: ['scheduled', 'confirmed', 'dispatched', 'en-route', 'arrived', 'completed', 'cancelled', 'no-show'] }, + page: { type: 'number' }, + pageSize: { type: 'number' }, + }, + }, + }, + { + name: 'fieldedge_get_appointment', + description: 'Get specific appointment', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_appointment', + description: 'Create a new appointment', + inputSchema: { + type: 'object', + properties: { + jobId: { type: 'string' }, + customerId: { type: 'string' }, + technicianId: { type: 'string' }, + startTime: { type: 'string' }, + endTime: { type: 'string' }, + appointmentType: { type: 'string' }, + arrivalWindow: { + type: 'object', + properties: { + start: { type: 'string' }, + end: { type: 'string' }, + }, + }, + notes: { type: 'string' }, + }, + required: ['jobId', 'customerId', 'technicianId', 'startTime', 'endTime', 'appointmentType'], + }, + }, + { + name: 'fieldedge_update_appointment', + description: 'Update an appointment', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + startTime: { type: 'string' }, + endTime: { type: 'string' }, + technicianId: { type: 'string' }, + status: { type: 'string', enum: ['scheduled', 'confirmed', 'dispatched', 'en-route', 'arrived', 'completed', 'cancelled', 'no-show'] }, + notes: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_cancel_appointment', + description: 'Cancel an appointment', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + reason: { type: 'string' }, + notifyCustomer: { type: 'boolean', default: true }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_get_dispatch_board', + description: 'Get dispatch board for a specific date', + inputSchema: { + type: 'object', + properties: { + date: { type: 'string', description: 'Date (YYYY-MM-DD)' }, + technicianIds: { type: 'array', items: { type: 'string' }, description: 'Filter by specific technicians' }, + }, + required: ['date'], + }, + }, + { + name: 'fieldedge_dispatch_job', + description: 'Dispatch a job to technician', + inputSchema: { + type: 'object', + properties: { + jobId: { type: 'string' }, + technicianId: { type: 'string' }, + scheduledTime: { type: 'string' }, + notifyTechnician: { type: 'boolean', default: true }, + }, + required: ['jobId', 'technicianId', 'scheduledTime'], + }, + }, + { + name: 'fieldedge_optimize_routes', + description: 'Optimize technician routes for a day', + inputSchema: { + type: 'object', + properties: { + date: { type: 'string' }, + technicianIds: { type: 'array', items: { type: 'string' } }, + }, + required: ['date'], + }, + }, +]; + +export async function handleSchedulingTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_appointments': + return await client.getPaginated('/appointments', args); + + case 'fieldedge_get_appointment': + return await client.get(`/appointments/${args.id}`); + + case 'fieldedge_create_appointment': + return await client.post('/appointments', args); + + case 'fieldedge_update_appointment': + const { id, ...updates } = args; + return await client.patch(`/appointments/${id}`, updates); + + case 'fieldedge_cancel_appointment': + return await client.post(`/appointments/${args.id}/cancel`, { + reason: args.reason, + notifyCustomer: args.notifyCustomer, + }); + + case 'fieldedge_get_dispatch_board': + return await client.get('/dispatch/board', { + date: args.date, + technicianIds: args.technicianIds, + }); + + case 'fieldedge_dispatch_job': + return await client.post('/dispatch', { + jobId: args.jobId, + technicianId: args.technicianId, + scheduledTime: args.scheduledTime, + notifyTechnician: args.notifyTechnician, + }); + + case 'fieldedge_optimize_routes': + return await client.post('/dispatch/optimize-routes', { + date: args.date, + technicianIds: args.technicianIds, + }); + + default: + throw new Error(`Unknown scheduling tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/service-agreements.ts b/servers/fieldedge/src/tools/service-agreements.ts new file mode 100644 index 0000000..f2e2549 --- /dev/null +++ b/servers/fieldedge/src/tools/service-agreements.ts @@ -0,0 +1,139 @@ +/** + * Service Agreement Management Tools + */ + +import type { ServiceAgreement } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const serviceAgreementTools = [ + { + name: 'fieldedge_list_service_agreements', + description: 'List service agreements', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string' }, + status: { type: 'string', enum: ['active', 'expired', 'cancelled'] }, + type: { type: 'string', enum: ['maintenance', 'warranty', 'service-plan'] }, + }, + }, + }, + { + name: 'fieldedge_get_service_agreement', + description: 'Get specific service agreement', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_service_agreement', + description: 'Create new service agreement', + inputSchema: { + type: 'object', + properties: { + customerId: { type: 'string' }, + name: { type: 'string' }, + type: { type: 'string', enum: ['maintenance', 'warranty', 'service-plan'] }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + billingCycle: { type: 'string', enum: ['monthly', 'quarterly', 'annual'] }, + amount: { type: 'number' }, + autoRenew: { type: 'boolean', default: false }, + services: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + frequency: { type: 'string', enum: ['weekly', 'monthly', 'quarterly', 'semi-annual', 'annual'] }, + }, + }, + }, + equipmentIds: { type: 'array', items: { type: 'string' } }, + notes: { type: 'string' }, + }, + required: ['customerId', 'name', 'type', 'startDate', 'endDate', 'billingCycle', 'amount'], + }, + }, + { + name: 'fieldedge_update_service_agreement', + description: 'Update service agreement', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + status: { type: 'string', enum: ['active', 'expired', 'cancelled'] }, + endDate: { type: 'string' }, + amount: { type: 'number' }, + autoRenew: { type: 'boolean' }, + notes: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_cancel_service_agreement', + description: 'Cancel a service agreement', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + reason: { type: 'string' }, + effectiveDate: { type: 'string' }, + }, + required: ['id', 'reason'], + }, + }, + { + name: 'fieldedge_renew_service_agreement', + description: 'Renew an expiring service agreement', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + newEndDate: { type: 'string' }, + newAmount: { type: 'number' }, + }, + required: ['id', 'newEndDate'], + }, + }, +]; + +export async function handleServiceAgreementTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_service_agreements': + return await client.getPaginated('/service-agreements', args); + + case 'fieldedge_get_service_agreement': + return await client.get(`/service-agreements/${args.id}`); + + case 'fieldedge_create_service_agreement': + return await client.post('/service-agreements', args); + + case 'fieldedge_update_service_agreement': + const { id, ...updates } = args; + return await client.patch(`/service-agreements/${id}`, updates); + + case 'fieldedge_cancel_service_agreement': + return await client.post(`/service-agreements/${args.id}/cancel`, { + reason: args.reason, + effectiveDate: args.effectiveDate, + }); + + case 'fieldedge_renew_service_agreement': + return await client.post(`/service-agreements/${args.id}/renew`, { + newEndDate: args.newEndDate, + newAmount: args.newAmount, + }); + + default: + throw new Error(`Unknown service agreement tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/tasks.ts b/servers/fieldedge/src/tools/tasks.ts new file mode 100644 index 0000000..15f0fe8 --- /dev/null +++ b/servers/fieldedge/src/tools/tasks.ts @@ -0,0 +1,125 @@ +/** + * Task Management Tools + */ + +import type { Task } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const taskTools = [ + { + name: 'fieldedge_list_tasks', + description: 'List tasks', + inputSchema: { + type: 'object', + properties: { + status: { type: 'string', enum: ['pending', 'in-progress', 'completed', 'cancelled'] }, + priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] }, + assignedTo: { type: 'string' }, + customerId: { type: 'string' }, + jobId: { type: 'string' }, + dueDate: { type: 'string' }, + }, + }, + }, + { + name: 'fieldedge_get_task', + description: 'Get specific task', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_task', + description: 'Create new task', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + type: { type: 'string', enum: ['call', 'email', 'follow-up', 'inspection', 'other'] }, + priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'], default: 'normal' }, + dueDate: { type: 'string' }, + assignedTo: { type: 'string' }, + customerId: { type: 'string' }, + jobId: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['title', 'description', 'type'], + }, + }, + { + name: 'fieldedge_update_task', + description: 'Update task', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + status: { type: 'string', enum: ['pending', 'in-progress', 'completed', 'cancelled'] }, + priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] }, + dueDate: { type: 'string' }, + assignedTo: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_complete_task', + description: 'Mark task as completed', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_delete_task', + description: 'Delete a task', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, +]; + +export async function handleTaskTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_tasks': + return await client.getPaginated('/tasks', args); + + case 'fieldedge_get_task': + return await client.get(`/tasks/${args.id}`); + + case 'fieldedge_create_task': + return await client.post('/tasks', args); + + case 'fieldedge_update_task': + const { id, ...updates } = args; + return await client.patch(`/tasks/${id}`, updates); + + case 'fieldedge_complete_task': + return await client.patch(`/tasks/${args.id}`, { + status: 'completed', + completedDate: new Date().toISOString(), + notes: args.notes, + }); + + case 'fieldedge_delete_task': + return await client.delete(`/tasks/${args.id}`); + + default: + throw new Error(`Unknown task tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/tools/technicians-tools.ts b/servers/fieldedge/src/tools/technicians-tools.ts deleted file mode 100644 index eb6e2b5..0000000 --- a/servers/fieldedge/src/tools/technicians-tools.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * FieldEdge Technicians Tools - */ - -import { FieldEdgeClient } from '../client.js'; -import { Technician, TechnicianPerformance, TimeEntry, PaginationParams } from '../types.js'; - -export function createTechniciansTools(client: FieldEdgeClient) { - return [ - { - name: 'fieldedge_technicians_list', - description: 'List all technicians with optional filtering', - inputSchema: { - type: 'object', - properties: { - status: { - type: 'string', - description: 'Filter by technician status', - enum: ['active', 'inactive', 'on_leave'], - }, - role: { type: 'string', description: 'Filter by role' }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - }, - handler: async (params: PaginationParams & { - status?: string; - role?: string; - }) => { - const result = await client.getPaginated('/technicians', params as any); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_technicians_get', - description: 'Get detailed information about a specific technician', - inputSchema: { - type: 'object', - properties: { - technicianId: { type: 'string', description: 'Technician ID' }, - }, - required: ['technicianId'], - }, - handler: async (params: { technicianId: string }) => { - const technician = await client.get( - `/technicians/${params.technicianId}` - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(technician, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_technicians_create', - description: 'Create a new technician', - inputSchema: { - type: 'object', - properties: { - firstName: { type: 'string', description: 'First name' }, - lastName: { type: 'string', description: 'Last name' }, - email: { type: 'string', description: 'Email address' }, - phone: { type: 'string', description: 'Phone number' }, - role: { type: 'string', description: 'Job role/title' }, - hourlyRate: { type: 'number', description: 'Hourly rate' }, - hireDate: { type: 'string', description: 'Hire date (ISO 8601)' }, - certifications: { - type: 'array', - items: { type: 'string' }, - description: 'List of certifications', - }, - skills: { - type: 'array', - items: { type: 'string' }, - description: 'List of skills', - }, - }, - required: ['firstName', 'lastName'], - }, - handler: async (params: { - firstName: string; - lastName: string; - email?: string; - phone?: string; - role?: string; - hourlyRate?: number; - hireDate?: string; - certifications?: string[]; - skills?: string[]; - }) => { - const technician = await client.post('/technicians', params); - return { - content: [ - { - type: 'text', - text: JSON.stringify(technician, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_technicians_update', - description: 'Update an existing technician', - inputSchema: { - type: 'object', - properties: { - technicianId: { type: 'string', description: 'Technician ID' }, - firstName: { type: 'string' }, - lastName: { type: 'string' }, - email: { type: 'string' }, - phone: { type: 'string' }, - status: { - type: 'string', - enum: ['active', 'inactive', 'on_leave'], - }, - role: { type: 'string' }, - hourlyRate: { type: 'number' }, - certifications: { - type: 'array', - items: { type: 'string' }, - }, - skills: { - type: 'array', - items: { type: 'string' }, - }, - }, - required: ['technicianId'], - }, - handler: async (params: { - technicianId: string; - firstName?: string; - lastName?: string; - email?: string; - phone?: string; - status?: string; - role?: string; - hourlyRate?: number; - certifications?: string[]; - skills?: string[]; - }) => { - const { technicianId, ...updateData } = params; - const technician = await client.patch( - `/technicians/${technicianId}`, - updateData - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(technician, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_technicians_performance_get', - description: 'Get performance metrics for a technician', - inputSchema: { - type: 'object', - properties: { - technicianId: { type: 'string', description: 'Technician ID' }, - startDate: { type: 'string', description: 'Period start date (ISO 8601)' }, - endDate: { type: 'string', description: 'Period end date (ISO 8601)' }, - }, - required: ['technicianId'], - }, - handler: async (params: { - technicianId: string; - startDate?: string; - endDate?: string; - }) => { - const performance = await client.get( - `/technicians/${params.technicianId}/performance`, - params - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(performance, null, 2), - }, - ], - }; - }, - }, - { - name: 'fieldedge_technicians_time_entries_list', - description: 'List time entries for a technician', - inputSchema: { - type: 'object', - properties: { - technicianId: { type: 'string', description: 'Technician ID' }, - startDate: { type: 'string', description: 'Start date (ISO 8601)' }, - endDate: { type: 'string', description: 'End date (ISO 8601)' }, - page: { type: 'number' }, - pageSize: { type: 'number' }, - }, - required: ['technicianId'], - }, - handler: async (params: PaginationParams & { - technicianId: string; - startDate?: string; - endDate?: string; - }) => { - const { technicianId, ...queryParams } = params; - const result = await client.getPaginated( - `/technicians/${technicianId}/time-entries`, - queryParams - ); - return { - content: [ - { - type: 'text', - text: JSON.stringify(result, null, 2), - }, - ], - }; - }, - }, - ]; -} diff --git a/servers/fieldedge/src/tools/technicians.ts b/servers/fieldedge/src/tools/technicians.ts new file mode 100644 index 0000000..27df61d --- /dev/null +++ b/servers/fieldedge/src/tools/technicians.ts @@ -0,0 +1,184 @@ +/** + * Technician Management Tools + */ + +import type { Technician, TimeEntry } from '../types/index.js'; +import { getFieldEdgeClient } from '../clients/fieldedge.js'; + +export const technicianTools = [ + { + name: 'fieldedge_list_technicians', + description: 'List all technicians', + inputSchema: { + type: 'object', + properties: { + page: { type: 'number' }, + pageSize: { type: 'number' }, + status: { type: 'string', enum: ['active', 'inactive', 'on-leave'] }, + skills: { type: 'array', items: { type: 'string' } }, + }, + }, + }, + { + name: 'fieldedge_get_technician', + description: 'Get specific technician by ID', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_create_technician', + description: 'Create a new technician', + inputSchema: { + type: 'object', + properties: { + employeeNumber: { type: 'string' }, + firstName: { type: 'string' }, + lastName: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' }, + role: { type: 'string' }, + skills: { type: 'array', items: { type: 'string' } }, + hourlyRate: { type: 'number' }, + overtimeRate: { type: 'number' }, + serviceRadius: { type: 'number' }, + }, + required: ['employeeNumber', 'firstName', 'lastName', 'email', 'phone', 'role'], + }, + }, + { + name: 'fieldedge_update_technician', + description: 'Update technician details', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + status: { type: 'string', enum: ['active', 'inactive', 'on-leave'] }, + email: { type: 'string' }, + phone: { type: 'string' }, + role: { type: 'string' }, + skills: { type: 'array', items: { type: 'string' } }, + hourlyRate: { type: 'number' }, + overtimeRate: { type: 'number' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_delete_technician', + description: 'Delete a technician', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + }, + required: ['id'], + }, + }, + { + name: 'fieldedge_get_technician_schedule', + description: 'Get technician schedule for a date range', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + }, + required: ['id', 'startDate', 'endDate'], + }, + }, + { + name: 'fieldedge_get_technician_availability', + description: 'Get technician availability for scheduling', + inputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + date: { type: 'string' }, + }, + required: ['id', 'date'], + }, + }, + { + name: 'fieldedge_clock_in_technician', + description: 'Clock in technician (start time tracking)', + inputSchema: { + type: 'object', + properties: { + technicianId: { type: 'string' }, + jobId: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['technicianId'], + }, + }, + { + name: 'fieldedge_clock_out_technician', + description: 'Clock out technician (end time tracking)', + inputSchema: { + type: 'object', + properties: { + timeEntryId: { type: 'string' }, + notes: { type: 'string' }, + }, + required: ['timeEntryId'], + }, + }, +]; + +export async function handleTechnicianTool(name: string, args: any): Promise { + const client = getFieldEdgeClient(); + + switch (name) { + case 'fieldedge_list_technicians': + return await client.getPaginated('/technicians', args); + + case 'fieldedge_get_technician': + return await client.get(`/technicians/${args.id}`); + + case 'fieldedge_create_technician': + return await client.post('/technicians', args); + + case 'fieldedge_update_technician': + const { id, ...updates } = args; + return await client.patch(`/technicians/${id}`, updates); + + case 'fieldedge_delete_technician': + return await client.delete(`/technicians/${args.id}`); + + case 'fieldedge_get_technician_schedule': + return await client.get(`/technicians/${args.id}/schedule`, { + startDate: args.startDate, + endDate: args.endDate, + }); + + case 'fieldedge_get_technician_availability': + return await client.get(`/technicians/${args.id}/availability`, { + date: args.date, + }); + + case 'fieldedge_clock_in_technician': + return await client.post('/time-entries', { + technicianId: args.technicianId, + jobId: args.jobId, + startTime: new Date().toISOString(), + type: 'regular', + billable: true, + notes: args.notes, + }); + + case 'fieldedge_clock_out_technician': + return await client.patch(`/time-entries/${args.timeEntryId}`, { + endTime: new Date().toISOString(), + notes: args.notes, + }); + + default: + throw new Error(`Unknown technician tool: ${name}`); + } +} diff --git a/servers/fieldedge/src/types.ts b/servers/fieldedge/src/types.ts deleted file mode 100644 index 6978465..0000000 --- a/servers/fieldedge/src/types.ts +++ /dev/null @@ -1,439 +0,0 @@ -/** - * FieldEdge MCP Server - Type Definitions - */ - -// API Configuration -export interface FieldEdgeConfig { - apiKey: string; - baseUrl?: string; -} - -// Common Types -export interface PaginationParams { - page?: number; - pageSize?: number; - offset?: number; - limit?: number; -} - -export interface PaginatedResponse { - data: T[]; - total: number; - page: number; - pageSize: number; - hasMore: boolean; -} - -export interface ApiError { - message: string; - code?: string; - statusCode?: number; - details?: unknown; -} - -// Job Types -export interface Job { - id: string; - jobNumber: string; - customerId: string; - customerName?: string; - locationId?: string; - jobType: string; - status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'on_hold'; - priority: 'low' | 'normal' | 'high' | 'emergency'; - assignedTechId?: string; - assignedTechName?: string; - scheduledStart?: string; - scheduledEnd?: string; - actualStart?: string; - actualEnd?: string; - description?: string; - notes?: string; - totalAmount?: number; - address?: Address; - createdAt: string; - updatedAt: string; -} - -export interface JobLineItem { - id: string; - jobId: string; - type: 'labor' | 'material' | 'equipment' | 'other'; - description: string; - quantity: number; - unitPrice: number; - totalPrice: number; - taxable: boolean; - partNumber?: string; - technicianId?: string; -} - -export interface JobEquipment { - id: string; - jobId: string; - equipmentId: string; - equipmentType: string; - serialNumber?: string; - model?: string; - manufacturer?: string; - serviceType: string; -} - -// Customer Types -export interface Customer { - id: string; - customerNumber: string; - type: 'residential' | 'commercial'; - firstName?: string; - lastName?: string; - companyName?: string; - email?: string; - phone?: string; - mobilePhone?: string; - status: 'active' | 'inactive'; - balance: number; - creditLimit?: number; - taxExempt: boolean; - primaryAddress?: Address; - billingAddress?: Address; - tags?: string[]; - createdAt: string; - updatedAt: string; -} - -export interface CustomerLocation { - id: string; - customerId: string; - name: string; - address: Address; - isPrimary: boolean; - contactName?: string; - contactPhone?: string; - accessNotes?: string; - gateCode?: string; -} - -export interface Address { - street1: string; - street2?: string; - city: string; - state: string; - zip: string; - country?: string; - latitude?: number; - longitude?: number; -} - -// Invoice Types -export interface Invoice { - id: string; - invoiceNumber: string; - customerId: string; - customerName?: string; - jobId?: string; - status: 'draft' | 'sent' | 'paid' | 'partial' | 'overdue' | 'void'; - invoiceDate: string; - dueDate?: string; - subtotal: number; - taxAmount: number; - totalAmount: number; - amountPaid: number; - amountDue: number; - lineItems: InvoiceLineItem[]; - notes?: string; - terms?: string; - createdAt: string; - updatedAt: string; -} - -export interface InvoiceLineItem { - id: string; - type: 'labor' | 'material' | 'equipment' | 'other'; - description: string; - quantity: number; - unitPrice: number; - totalPrice: number; - taxable: boolean; -} - -export interface Payment { - id: string; - invoiceId: string; - amount: number; - paymentMethod: 'cash' | 'check' | 'credit_card' | 'ach' | 'other'; - paymentDate: string; - referenceNumber?: string; - notes?: string; - createdAt: string; -} - -// Estimate Types -export interface Estimate { - id: string; - estimateNumber: string; - customerId: string; - customerName?: string; - locationId?: string; - status: 'draft' | 'sent' | 'approved' | 'declined' | 'expired'; - estimateDate: string; - expirationDate?: string; - subtotal: number; - taxAmount: number; - totalAmount: number; - lineItems: EstimateLineItem[]; - notes?: string; - terms?: string; - createdBy?: string; - createdAt: string; - updatedAt: string; -} - -export interface EstimateLineItem { - id: string; - type: 'labor' | 'material' | 'equipment' | 'other'; - description: string; - quantity: number; - unitPrice: number; - totalPrice: number; - taxable: boolean; -} - -// Technician Types -export interface Technician { - id: string; - employeeNumber: string; - firstName: string; - lastName: string; - email?: string; - phone?: string; - status: 'active' | 'inactive' | 'on_leave'; - role: string; - certifications?: string[]; - skills?: string[]; - hourlyRate?: number; - hireDate?: string; - terminationDate?: string; - createdAt: string; - updatedAt: string; -} - -export interface TechnicianPerformance { - technicianId: string; - technicianName: string; - period: string; - jobsCompleted: number; - averageJobTime: number; - revenue: number; - customerSatisfaction?: number; - callbackRate?: number; - efficiency?: number; -} - -export interface TimeEntry { - id: string; - technicianId: string; - jobId?: string; - date: string; - clockIn: string; - clockOut?: string; - hours: number; - type: 'regular' | 'overtime' | 'double_time' | 'travel'; - notes?: string; -} - -// Dispatch Types -export interface DispatchBoard { - date: string; - zones: DispatchZone[]; - unassignedJobs: Job[]; -} - -export interface DispatchZone { - id: string; - name: string; - technicians: DispatchTechnician[]; -} - -export interface DispatchTechnician { - id: string; - name: string; - status: 'available' | 'on_job' | 'traveling' | 'break' | 'offline'; - currentLocation?: { lat: number; lng: number }; - assignedJobs: Job[]; - capacity: number; - utilizationPercent: number; -} - -export interface TechnicianAvailability { - technicianId: string; - date: string; - availableSlots: TimeSlot[]; - bookedSlots: TimeSlot[]; -} - -export interface TimeSlot { - start: string; - end: string; - duration: number; -} - -// Equipment Types -export interface Equipment { - id: string; - customerId: string; - locationId?: string; - type: string; - manufacturer?: string; - model?: string; - serialNumber?: string; - installDate?: string; - warrantyExpiration?: string; - status: 'active' | 'inactive' | 'retired'; - lastServiceDate?: string; - nextServiceDate?: string; - notes?: string; - createdAt: string; - updatedAt: string; -} - -export interface ServiceHistory { - id: string; - equipmentId: string; - jobId: string; - technicianId: string; - technicianName?: string; - serviceDate: string; - serviceType: string; - description: string; - partsUsed?: string[]; - laborHours?: number; - cost?: number; - notes?: string; -} - -// Inventory Types -export interface InventoryPart { - id: string; - partNumber: string; - description: string; - category?: string; - manufacturer?: string; - quantityOnHand: number; - quantityAvailable: number; - quantityOnOrder: number; - reorderPoint?: number; - reorderQuantity?: number; - unitCost: number; - unitPrice: number; - location?: string; - updatedAt: string; -} - -export interface PurchaseOrder { - id: string; - poNumber: string; - vendorId: string; - vendorName?: string; - status: 'draft' | 'submitted' | 'approved' | 'received' | 'cancelled'; - orderDate: string; - expectedDate?: string; - receivedDate?: string; - subtotal: number; - taxAmount: number; - totalAmount: number; - lineItems: PurchaseOrderLineItem[]; - notes?: string; - createdAt: string; - updatedAt: string; -} - -export interface PurchaseOrderLineItem { - id: string; - partId: string; - partNumber: string; - description: string; - quantityOrdered: number; - quantityReceived: number; - unitCost: number; - totalCost: number; -} - -// Service Agreement Types -export interface ServiceAgreement { - id: string; - agreementNumber: string; - customerId: string; - customerName?: string; - locationId?: string; - type: string; - status: 'active' | 'cancelled' | 'expired' | 'suspended'; - startDate: string; - endDate?: string; - renewalDate?: string; - billingFrequency: 'monthly' | 'quarterly' | 'annually'; - amount: number; - equipmentCovered?: string[]; - servicesCovered?: string[]; - visitsPerYear?: number; - visitsRemaining?: number; - autoRenew: boolean; - notes?: string; - createdAt: string; - updatedAt: string; -} - -// Reporting Types -export interface RevenueReport { - period: string; - startDate: string; - endDate: string; - totalRevenue: number; - laborRevenue: number; - partsRevenue: number; - equipmentRevenue: number; - agreementRevenue: number; - jobCount: number; - averageTicket: number; - byJobType?: Record; - byTechnician?: Record; -} - -export interface JobProfitabilityReport { - jobId: string; - jobNumber: string; - revenue: number; - laborCost: number; - partsCost: number; - overheadCost: number; - totalCost: number; - profit: number; - profitMargin: number; -} - -export interface AgingReport { - asOfDate: string; - totalOutstanding: number; - current: AgingBucket; - days30: AgingBucket; - days60: AgingBucket; - days90: AgingBucket; - days90Plus: AgingBucket; - byCustomer: CustomerAging[]; -} - -export interface AgingBucket { - amount: number; - invoiceCount: number; - percentage: number; -} - -export interface CustomerAging { - customerId: string; - customerName: string; - totalDue: number; - current: number; - days30: number; - days60: number; - days90: number; - days90Plus: number; -} diff --git a/servers/fieldedge/src/types/index.ts b/servers/fieldedge/src/types/index.ts new file mode 100644 index 0000000..1d13bf5 --- /dev/null +++ b/servers/fieldedge/src/types/index.ts @@ -0,0 +1,602 @@ +/** + * FieldEdge MCP Server Type Definitions + * Comprehensive types for field service management + */ + +export interface FieldEdgeConfig { + apiKey: string; + apiUrl?: string; + companyId?: string; + timeout?: number; +} + +// Customer Types +export interface Customer { + id: string; + firstName: string; + lastName: string; + companyName?: string; + email?: string; + phone?: string; + mobilePhone?: string; + address?: Address; + billingAddress?: Address; + status: 'active' | 'inactive' | 'prospect'; + customerType: 'residential' | 'commercial'; + balance: number; + creditLimit?: number; + taxExempt: boolean; + notes?: string; + tags?: string[]; + customFields?: Record; + createdAt: string; + updatedAt: string; +} + +export interface Address { + street1: string; + street2?: string; + city: string; + state: string; + zip: string; + country?: string; + latitude?: number; + longitude?: number; +} + +// Job/Work Order Types +export interface Job { + id: string; + jobNumber: string; + customerId: string; + locationId?: string; + jobType: string; + status: JobStatus; + priority: 'low' | 'normal' | 'high' | 'emergency'; + description: string; + scheduledStart?: string; + scheduledEnd?: string; + actualStart?: string; + actualEnd?: string; + assignedTechnicians: string[]; + equipmentIds?: string[]; + subtotal: number; + tax: number; + total: number; + invoiceId?: string; + tags?: string[]; + customFields?: Record; + createdAt: string; + updatedAt: string; +} + +export type JobStatus = + | 'scheduled' + | 'dispatched' + | 'in-progress' + | 'on-hold' + | 'completed' + | 'cancelled' + | 'invoiced'; + +// Invoice Types +export interface Invoice { + id: string; + invoiceNumber: string; + customerId: string; + jobId?: string; + status: InvoiceStatus; + issueDate: string; + dueDate: string; + paidDate?: string; + lineItems: LineItem[]; + subtotal: number; + tax: number; + discount: number; + total: number; + amountPaid: number; + balance: number; + paymentTerms?: string; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export type InvoiceStatus = 'draft' | 'sent' | 'viewed' | 'partial' | 'paid' | 'overdue' | 'void'; + +export interface LineItem { + id: string; + type: 'service' | 'part' | 'equipment' | 'labor'; + description: string; + quantity: number; + unitPrice: number; + discount: number; + tax: number; + total: number; + itemId?: string; +} + +// Estimate Types +export interface Estimate { + id: string; + estimateNumber: string; + customerId: string; + status: EstimateStatus; + issueDate: string; + expiryDate: string; + lineItems: LineItem[]; + subtotal: number; + tax: number; + discount: number; + total: number; + approvedDate?: string; + jobId?: string; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export type EstimateStatus = 'draft' | 'sent' | 'viewed' | 'approved' | 'declined' | 'expired'; + +// Scheduling/Dispatch Types +export interface Appointment { + id: string; + jobId: string; + customerId: string; + technicianId: string; + startTime: string; + endTime: string; + duration: number; + status: AppointmentStatus; + appointmentType: string; + arrivalWindow?: { + start: string; + end: string; + }; + actualArrival?: string; + actualDeparture?: string; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export type AppointmentStatus = 'scheduled' | 'confirmed' | 'dispatched' | 'en-route' | 'arrived' | 'completed' | 'cancelled' | 'no-show'; + +export interface DispatchBoard { + date: string; + technicians: TechnicianSchedule[]; + unassignedJobs: Job[]; +} + +export interface TechnicianSchedule { + technicianId: string; + technicianName: string; + appointments: Appointment[]; + availability: TimeSlot[]; + capacity: number; +} + +export interface TimeSlot { + start: string; + end: string; + available: boolean; +} + +// Inventory Types +export interface InventoryItem { + id: string; + sku: string; + name: string; + description?: string; + category: string; + manufacturer?: string; + modelNumber?: string; + unitOfMeasure: string; + costPrice: number; + sellPrice: number; + msrp?: number; + quantityOnHand: number; + quantityAllocated: number; + quantityAvailable: number; + reorderPoint: number; + reorderQuantity: number; + warehouse?: string; + binLocation?: string; + serialized: boolean; + taxable: boolean; + tags?: string[]; + createdAt: string; + updatedAt: string; +} + +export interface InventoryTransaction { + id: string; + itemId: string; + type: 'receipt' | 'issue' | 'adjustment' | 'transfer' | 'return'; + quantity: number; + unitCost?: number; + totalCost?: number; + reference?: string; + jobId?: string; + technicianId?: string; + notes?: string; + createdAt: string; +} + +// Technician Types +export interface Technician { + id: string; + employeeNumber: string; + firstName: string; + lastName: string; + email: string; + phone: string; + status: 'active' | 'inactive' | 'on-leave'; + role: string; + skills: string[]; + certifications: Certification[]; + hourlyRate?: number; + overtimeRate?: number; + serviceRadius?: number; + homeAddress?: Address; + vehicleId?: string; + defaultWorkHours?: WorkHours; + createdAt: string; + updatedAt: string; +} + +export interface Certification { + name: string; + number: string; + issuer: string; + issueDate: string; + expiryDate?: string; +} + +export interface WorkHours { + monday?: DaySchedule; + tuesday?: DaySchedule; + wednesday?: DaySchedule; + thursday?: DaySchedule; + friday?: DaySchedule; + saturday?: DaySchedule; + sunday?: DaySchedule; +} + +export interface DaySchedule { + start: string; + end: string; + breaks?: TimeSlot[]; +} + +// Payment Types +export interface Payment { + id: string; + invoiceId: string; + customerId: string; + amount: number; + paymentMethod: PaymentMethod; + paymentDate: string; + reference?: string; + cardLast4?: string; + checkNumber?: string; + status: 'pending' | 'processed' | 'failed' | 'refunded'; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export type PaymentMethod = 'cash' | 'check' | 'credit-card' | 'debit-card' | 'ach' | 'wire' | 'other'; + +// Equipment Types +export interface Equipment { + id: string; + customerId: string; + locationId?: string; + type: string; + manufacturer: string; + model: string; + serialNumber?: string; + installDate?: string; + warrantyExpiry?: string; + lastServiceDate?: string; + nextServiceDue?: string; + status: 'active' | 'inactive' | 'decommissioned'; + notes?: string; + customFields?: Record; + createdAt: string; + updatedAt: string; +} + +export interface ServiceHistory { + id: string; + equipmentId: string; + jobId: string; + serviceDate: string; + technicianId: string; + serviceType: string; + description: string; + partsReplaced?: LineItem[]; + cost: number; + notes?: string; +} + +// Location Types +export interface Location { + id: string; + customerId: string; + name: string; + address: Address; + type: 'primary' | 'secondary' | 'billing' | 'service'; + contactName?: string; + contactPhone?: string; + accessNotes?: string; + gateCode?: string; + equipmentIds?: string[]; + status: 'active' | 'inactive'; + createdAt: string; + updatedAt: string; +} + +// Price Book Types +export interface PriceBook { + id: string; + name: string; + description?: string; + effectiveDate: string; + expiryDate?: string; + isDefault: boolean; + status: 'active' | 'inactive'; + items: PriceBookItem[]; + createdAt: string; + updatedAt: string; +} + +export interface PriceBookItem { + id: string; + itemId: string; + itemType: 'service' | 'part' | 'equipment' | 'labor'; + name: string; + sku?: string; + description?: string; + unitPrice: number; + costPrice?: number; + margin?: number; + taxable: boolean; + category?: string; +} + +// Service Agreement Types +export interface ServiceAgreement { + id: string; + customerId: string; + agreementNumber: string; + name: string; + type: 'maintenance' | 'warranty' | 'service-plan'; + status: 'active' | 'expired' | 'cancelled'; + startDate: string; + endDate: string; + renewalDate?: string; + autoRenew: boolean; + billingCycle: 'monthly' | 'quarterly' | 'annual'; + amount: number; + services: ServiceItem[]; + equipmentIds?: string[]; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export interface ServiceItem { + id: string; + name: string; + description: string; + frequency: 'weekly' | 'monthly' | 'quarterly' | 'semi-annual' | 'annual'; + nextDue?: string; + lastCompleted?: string; +} + +// Task Types +export interface Task { + id: string; + jobId?: string; + customerId?: string; + technicianId?: string; + title: string; + description: string; + type: 'call' | 'email' | 'follow-up' | 'inspection' | 'other'; + priority: 'low' | 'normal' | 'high' | 'urgent'; + status: 'pending' | 'in-progress' | 'completed' | 'cancelled'; + dueDate?: string; + completedDate?: string; + assignedTo?: string; + notes?: string; + createdAt: string; + updatedAt: string; +} + +// Call Tracking Types +export interface Call { + id: string; + customerId?: string; + phone: string; + direction: 'inbound' | 'outbound'; + callType: 'inquiry' | 'booking' | 'follow-up' | 'support' | 'sales'; + status: 'answered' | 'missed' | 'voicemail' | 'busy'; + duration?: number; + recordingUrl?: string; + notes?: string; + outcome?: string; + jobId?: string; + technicianId?: string; + timestamp: string; +} + +// Reporting Types +export interface Report { + id: string; + name: string; + type: ReportType; + parameters?: Record; + generatedAt: string; + data: any; +} + +export type ReportType = + | 'revenue' + | 'technician-productivity' + | 'job-completion' + | 'customer-satisfaction' + | 'inventory-valuation' + | 'aging-receivables' + | 'sales-by-category' + | 'equipment-maintenance' + | 'custom'; + +export interface RevenueReport { + period: string; + totalRevenue: number; + invoicedAmount: number; + collectedAmount: number; + outstandingAmount: number; + byCategory: Record; + byTechnician: Record; + trend: RevenueDataPoint[]; +} + +export interface RevenueDataPoint { + date: string; + revenue: number; + jobs: number; +} + +export interface TechnicianProductivityReport { + period: string; + technicians: TechnicianMetrics[]; +} + +export interface TechnicianMetrics { + technicianId: string; + technicianName: string; + jobsCompleted: number; + hoursWorked: number; + revenue: number; + averageJobTime: number; + utilizationRate: number; +} + +// API Response Types +export interface ApiResponse { + success: boolean; + data?: T; + error?: string; + message?: string; +} + +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +export interface QueryParams { + page?: number; + pageSize?: number; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + filter?: Record; + search?: string; + [key: string]: any; // Allow additional query parameters +} + +// Vehicle Types +export interface Vehicle { + id: string; + name: string; + type: 'truck' | 'van' | 'car' | 'trailer'; + make: string; + model: string; + year: number; + vin?: string; + licensePlate: string; + status: 'active' | 'maintenance' | 'inactive'; + assignedTechnicianId?: string; + mileage?: number; + lastServiceDate?: string; + nextServiceDue?: string; + inventoryItems?: string[]; + gpsEnabled: boolean; + createdAt: string; + updatedAt: string; +} + +// Time Tracking Types +export interface TimeEntry { + id: string; + technicianId: string; + jobId?: string; + type: 'regular' | 'overtime' | 'travel' | 'break'; + startTime: string; + endTime?: string; + duration?: number; + billable: boolean; + approved: boolean; + notes?: string; + createdAt: string; + updatedAt: string; +} + +// Forms/Checklist Types +export interface Form { + id: string; + name: string; + description?: string; + type: 'inspection' | 'safety' | 'maintenance' | 'quote' | 'custom'; + status: 'active' | 'inactive'; + fields: FormField[]; + createdAt: string; + updatedAt: string; +} + +export interface FormField { + id: string; + label: string; + type: 'text' | 'number' | 'checkbox' | 'select' | 'radio' | 'date' | 'signature' | 'photo'; + required: boolean; + options?: string[]; + defaultValue?: any; + validationRules?: Record; +} + +export interface FormSubmission { + id: string; + formId: string; + jobId?: string; + technicianId: string; + customerId?: string; + responses: Record; + attachments?: string[]; + submittedAt: string; +} + +// Marketing Campaign Types +export interface Campaign { + id: string; + name: string; + type: 'email' | 'sms' | 'direct-mail' | 'social'; + status: 'draft' | 'scheduled' | 'active' | 'completed' | 'cancelled'; + startDate: string; + endDate?: string; + targetAudience: string[]; + message: string; + sentCount: number; + deliveredCount: number; + openedCount: number; + clickedCount: number; + convertedCount: number; + roi?: number; + createdAt: string; + updatedAt: string; +} diff --git a/servers/fieldedge/src/ui/calendar/App.tsx b/servers/fieldedge/src/ui/calendar/App.tsx new file mode 100644 index 0000000..f877391 --- /dev/null +++ b/servers/fieldedge/src/ui/calendar/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function calendarApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading calendar view...
+
+ ); + } + + return ( +
+
+

Calendar View

+ +
+ +
+
+

Calendar view of appointments

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/calendar/index.html b/servers/fieldedge/src/ui/calendar/index.html new file mode 100644 index 0000000..e4e63de --- /dev/null +++ b/servers/fieldedge/src/ui/calendar/index.html @@ -0,0 +1,12 @@ + + + + + + Calendar View - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/calendar/main.tsx b/servers/fieldedge/src/ui/calendar/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/calendar/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/calendar/styles.css b/servers/fieldedge/src/ui/calendar/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/calendar/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/calendar/vite.config.ts b/servers/fieldedge/src/ui/calendar/vite.config.ts new file mode 100644 index 0000000..89c60c1 --- /dev/null +++ b/servers/fieldedge/src/ui/calendar/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/ui/calendar', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/customers/App.tsx b/servers/fieldedge/src/ui/customers/App.tsx new file mode 100644 index 0000000..9810a71 --- /dev/null +++ b/servers/fieldedge/src/ui/customers/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function customersApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading customer management...
+
+ ); + } + + return ( +
+
+

Customer Management

+ +
+ +
+
+

Browse and manage customers

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/customers/index.html b/servers/fieldedge/src/ui/customers/index.html new file mode 100644 index 0000000..9ec61dc --- /dev/null +++ b/servers/fieldedge/src/ui/customers/index.html @@ -0,0 +1,12 @@ + + + + + + Customer Management - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/customers/main.tsx b/servers/fieldedge/src/ui/customers/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/customers/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/customers/styles.css b/servers/fieldedge/src/ui/customers/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/customers/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/customers/vite.config.ts b/servers/fieldedge/src/ui/customers/vite.config.ts new file mode 100644 index 0000000..b95f3e6 --- /dev/null +++ b/servers/fieldedge/src/ui/customers/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/ui/customers', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/dashboard/App.tsx b/servers/fieldedge/src/ui/dashboard/App.tsx new file mode 100644 index 0000000..f39a1cf --- /dev/null +++ b/servers/fieldedge/src/ui/dashboard/App.tsx @@ -0,0 +1,112 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +interface DashboardMetrics { + totalJobs: number; + activeJobs: number; + completedToday: number; + pendingInvoices: number; + revenue: { today: number; month: number }; + technicians: { active: number; total: number }; +} + +export default function Dashboard() { + const [metrics, setMetrics] = useState({ + totalJobs: 0, + activeJobs: 0, + completedToday: 0, + pendingInvoices: 0, + revenue: { today: 0, month: 0 }, + technicians: { active: 0, total: 0 }, + }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Simulate loading data + setTimeout(() => { + setMetrics({ + totalJobs: 147, + activeJobs: 23, + completedToday: 8, + pendingInvoices: 15, + revenue: { today: 4250, month: 87450 }, + technicians: { active: 12, total: 15 }, + }); + setLoading(false); + }, 1000); + }, []); + + if (loading) { + return ( +
+
Loading dashboard...
+
+ ); + } + + return ( +
+
+

FieldEdge Dashboard

+
{new Date().toLocaleDateString()}
+
+ +
+
+
Total Jobs
+
{metrics.totalJobs}
+
+ +
+
Active Jobs
+
{metrics.activeJobs}
+
+ +
+
Completed Today
+
{metrics.completedToday}
+
+ +
+
Pending Invoices
+
{metrics.pendingInvoices}
+
+ +
+
Today's Revenue
+
${metrics.revenue.today.toLocaleString()}
+
+ +
+
Month Revenue
+
${metrics.revenue.month.toLocaleString()}
+
+ +
+
Active Technicians
+
+ {metrics.technicians.active} / {metrics.technicians.total} +
+
+
+ +
+

Recent Activity

+
+
+ 10:30 AM + Job #1234 completed by John Smith +
+
+ 09:45 AM + New job created for ACME Corp +
+
+ 09:15 AM + Invoice #INV-5678 paid - $1,250.00 +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/dashboard/index.html b/servers/fieldedge/src/ui/dashboard/index.html new file mode 100644 index 0000000..c52a7aa --- /dev/null +++ b/servers/fieldedge/src/ui/dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + FieldEdge Dashboard + + +
+ + + diff --git a/servers/fieldedge/src/ui/dashboard/main.tsx b/servers/fieldedge/src/ui/dashboard/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/dashboard/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/dashboard/styles.css b/servers/fieldedge/src/ui/dashboard/styles.css new file mode 100644 index 0000000..7f152c2 --- /dev/null +++ b/servers/fieldedge/src/ui/dashboard/styles.css @@ -0,0 +1,104 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.date { + color: #9aa0a6; + font-size: 0.95rem; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.metric-card { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.metric-label { + color: #9aa0a6; + font-size: 0.875rem; + margin-bottom: 0.5rem; +} + +.metric-value { + font-size: 2rem; + font-weight: 600; + color: #4c9aff; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.25rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.activity-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.activity-item { + display: flex; + gap: 1rem; + padding: 0.75rem; + background: #131920; + border-radius: 6px; +} + +.activity-time { + color: #9aa0a6; + font-size: 0.875rem; + min-width: 80px; +} + +.activity-text { + color: #e8eaed; + font-size: 0.875rem; +} diff --git a/servers/fieldedge/src/ui/dashboard/vite.config.ts b/servers/fieldedge/src/ui/dashboard/vite.config.ts new file mode 100644 index 0000000..6737e47 --- /dev/null +++ b/servers/fieldedge/src/ui/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/ui/dashboard', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/equipment/App.tsx b/servers/fieldedge/src/ui/equipment/App.tsx new file mode 100644 index 0000000..dc36921 --- /dev/null +++ b/servers/fieldedge/src/ui/equipment/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function equipmentApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading equipment management...
+
+ ); + } + + return ( +
+
+

Equipment Management

+ +
+ +
+
+

Track customer equipment

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/equipment/index.html b/servers/fieldedge/src/ui/equipment/index.html new file mode 100644 index 0000000..6d8a0c2 --- /dev/null +++ b/servers/fieldedge/src/ui/equipment/index.html @@ -0,0 +1,12 @@ + + + + + + Equipment Management - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/equipment/main.tsx b/servers/fieldedge/src/ui/equipment/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/equipment/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/equipment/styles.css b/servers/fieldedge/src/ui/equipment/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/equipment/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/equipment/vite.config.ts b/servers/fieldedge/src/ui/equipment/vite.config.ts new file mode 100644 index 0000000..c8fe5d6 --- /dev/null +++ b/servers/fieldedge/src/ui/equipment/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/ui/equipment', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/estimates/App.tsx b/servers/fieldedge/src/ui/estimates/App.tsx new file mode 100644 index 0000000..d37befb --- /dev/null +++ b/servers/fieldedge/src/ui/estimates/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function estimatesApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading estimate management...
+
+ ); + } + + return ( +
+
+

Estimate Management

+ +
+ +
+
+

Create and manage estimates/quotes

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/estimates/index.html b/servers/fieldedge/src/ui/estimates/index.html new file mode 100644 index 0000000..93da848 --- /dev/null +++ b/servers/fieldedge/src/ui/estimates/index.html @@ -0,0 +1,12 @@ + + + + + + Estimate Management - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/estimates/main.tsx b/servers/fieldedge/src/ui/estimates/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/estimates/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/estimates/styles.css b/servers/fieldedge/src/ui/estimates/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/estimates/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/estimates/vite.config.ts b/servers/fieldedge/src/ui/estimates/vite.config.ts new file mode 100644 index 0000000..fef5ab8 --- /dev/null +++ b/servers/fieldedge/src/ui/estimates/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/ui/estimates', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/inventory/App.tsx b/servers/fieldedge/src/ui/inventory/App.tsx new file mode 100644 index 0000000..3dfe00a --- /dev/null +++ b/servers/fieldedge/src/ui/inventory/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function inventoryApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading inventory management...
+
+ ); + } + + return ( +
+
+

Inventory Management

+ +
+ +
+
+

Manage parts and equipment inventory

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/inventory/index.html b/servers/fieldedge/src/ui/inventory/index.html new file mode 100644 index 0000000..84c0ce2 --- /dev/null +++ b/servers/fieldedge/src/ui/inventory/index.html @@ -0,0 +1,12 @@ + + + + + + Inventory Management - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/inventory/main.tsx b/servers/fieldedge/src/ui/inventory/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/inventory/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/inventory/styles.css b/servers/fieldedge/src/ui/inventory/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/inventory/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/inventory/vite.config.ts b/servers/fieldedge/src/ui/inventory/vite.config.ts new file mode 100644 index 0000000..59c9a56 --- /dev/null +++ b/servers/fieldedge/src/ui/inventory/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/ui/inventory', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/invoices/App.tsx b/servers/fieldedge/src/ui/invoices/App.tsx new file mode 100644 index 0000000..fd15f37 --- /dev/null +++ b/servers/fieldedge/src/ui/invoices/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function invoicesApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading invoice management...
+
+ ); + } + + return ( +
+
+

Invoice Management

+ +
+ +
+
+

Create and manage invoices

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/invoices/index.html b/servers/fieldedge/src/ui/invoices/index.html new file mode 100644 index 0000000..5d390dd --- /dev/null +++ b/servers/fieldedge/src/ui/invoices/index.html @@ -0,0 +1,12 @@ + + + + + + Invoice Management - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/invoices/main.tsx b/servers/fieldedge/src/ui/invoices/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/invoices/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/invoices/styles.css b/servers/fieldedge/src/ui/invoices/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/invoices/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/invoices/vite.config.ts b/servers/fieldedge/src/ui/invoices/vite.config.ts new file mode 100644 index 0000000..8fca661 --- /dev/null +++ b/servers/fieldedge/src/ui/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/ui/invoices', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/jobs/App.tsx b/servers/fieldedge/src/ui/jobs/App.tsx new file mode 100644 index 0000000..ab9c562 --- /dev/null +++ b/servers/fieldedge/src/ui/jobs/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function jobsApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading job management...
+
+ ); + } + + return ( +
+
+

Job Management

+ +
+ +
+
+

View and manage jobs/work orders

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/jobs/index.html b/servers/fieldedge/src/ui/jobs/index.html new file mode 100644 index 0000000..0470b11 --- /dev/null +++ b/servers/fieldedge/src/ui/jobs/index.html @@ -0,0 +1,12 @@ + + + + + + Job Management - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/jobs/main.tsx b/servers/fieldedge/src/ui/jobs/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/jobs/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/jobs/styles.css b/servers/fieldedge/src/ui/jobs/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/jobs/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/jobs/vite.config.ts b/servers/fieldedge/src/ui/jobs/vite.config.ts new file mode 100644 index 0000000..87e59e1 --- /dev/null +++ b/servers/fieldedge/src/ui/jobs/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/ui/jobs', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/map-view/App.tsx b/servers/fieldedge/src/ui/map-view/App.tsx new file mode 100644 index 0000000..f1a60b8 --- /dev/null +++ b/servers/fieldedge/src/ui/map-view/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function mapviewApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading map view...
+
+ ); + } + + return ( +
+
+

Map View

+ +
+ +
+
+

Map view of jobs and technicians

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/map-view/index.html b/servers/fieldedge/src/ui/map-view/index.html new file mode 100644 index 0000000..d835af0 --- /dev/null +++ b/servers/fieldedge/src/ui/map-view/index.html @@ -0,0 +1,12 @@ + + + + + + Map View - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/map-view/main.tsx b/servers/fieldedge/src/ui/map-view/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/map-view/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/map-view/styles.css b/servers/fieldedge/src/ui/map-view/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/map-view/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/map-view/vite.config.ts b/servers/fieldedge/src/ui/map-view/vite.config.ts new file mode 100644 index 0000000..75cefea --- /dev/null +++ b/servers/fieldedge/src/ui/map-view/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/ui/map-view', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/payments/App.tsx b/servers/fieldedge/src/ui/payments/App.tsx new file mode 100644 index 0000000..64b828d --- /dev/null +++ b/servers/fieldedge/src/ui/payments/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function paymentsApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading payment management...
+
+ ); + } + + return ( +
+
+

Payment Management

+ +
+ +
+
+

Process payments and view history

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/payments/index.html b/servers/fieldedge/src/ui/payments/index.html new file mode 100644 index 0000000..78af544 --- /dev/null +++ b/servers/fieldedge/src/ui/payments/index.html @@ -0,0 +1,12 @@ + + + + + + Payment Management - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/payments/main.tsx b/servers/fieldedge/src/ui/payments/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/payments/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/payments/styles.css b/servers/fieldedge/src/ui/payments/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/payments/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/payments/vite.config.ts b/servers/fieldedge/src/ui/payments/vite.config.ts new file mode 100644 index 0000000..0252b80 --- /dev/null +++ b/servers/fieldedge/src/ui/payments/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/ui/payments', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/price-book/App.tsx b/servers/fieldedge/src/ui/price-book/App.tsx new file mode 100644 index 0000000..7fa9192 --- /dev/null +++ b/servers/fieldedge/src/ui/price-book/App.tsx @@ -0,0 +1,49 @@ +import { useState, useEffect } from 'react'; +import './styles.css'; + +export default function pricebookApp() { + const [loading, setLoading] = useState(true); + const [data, setData] = useState([]); + + useEffect(() => { + setTimeout(() => { + setData([ + { id: '1', name: 'Sample Item 1', status: 'active' }, + { id: '2', name: 'Sample Item 2', status: 'active' }, + { id: '3', name: 'Sample Item 3', status: 'pending' }, + ]); + setLoading(false); + }, 800); + }, []); + + if (loading) { + return ( +
+
Loading price book...
+
+ ); + } + + return ( +
+
+

Price Book

+ +
+ +
+
+

Manage pricing for services

+
+ {data.map((item) => ( +
+

{item.name}

+ {item.status} +
+ ))} +
+
+
+
+ ); +} diff --git a/servers/fieldedge/src/ui/price-book/index.html b/servers/fieldedge/src/ui/price-book/index.html new file mode 100644 index 0000000..811c88f --- /dev/null +++ b/servers/fieldedge/src/ui/price-book/index.html @@ -0,0 +1,12 @@ + + + + + + Price Book - FieldEdge + + +
+ + + diff --git a/servers/fieldedge/src/ui/price-book/main.tsx b/servers/fieldedge/src/ui/price-book/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/fieldedge/src/ui/price-book/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/fieldedge/src/ui/price-book/styles.css b/servers/fieldedge/src/ui/price-book/styles.css new file mode 100644 index 0000000..17eb313 --- /dev/null +++ b/servers/fieldedge/src/ui/price-book/styles.css @@ -0,0 +1,119 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #0f1419; + color: #e8eaed; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; +} + +.header h1 { + font-size: 2rem; + font-weight: 600; +} + +.btn-primary { + background: #4c9aff; + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 6px; + font-size: 0.95rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.btn-primary:hover { + background: #3d8aef; +} + +.loading { + text-align: center; + padding: 4rem; + color: #9aa0a6; +} + +.content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.section { + background: #1e2732; + border: 1px solid #2d3748; + border-radius: 8px; + padding: 1.5rem; +} + +.section h2 { + font-size: 1.125rem; + margin-bottom: 1rem; + color: #e8eaed; +} + +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; +} + +.data-card { + background: #131920; + border: 1px solid #2d3748; + border-radius: 6px; + padding: 1.25rem; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.data-card:hover { + border-color: #4c9aff; +} + +.data-card h3 { + font-size: 1rem; + font-weight: 500; +} + +.badge { + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-active { + background: #1a4d2e; + color: #4ade80; +} + +.badge-pending { + background: #4a3810; + color: #fbbf24; +} + +.badge-completed { + background: #1e3a8a; + color: #60a5fa; +} diff --git a/servers/fieldedge/src/ui/price-book/vite.config.ts b/servers/fieldedge/src/ui/price-book/vite.config.ts new file mode 100644 index 0000000..8760b92 --- /dev/null +++ b/servers/fieldedge/src/ui/price-book/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/ui/price-book', + emptyOutDir: true, + }, +}); diff --git a/servers/fieldedge/src/ui/react-app/customer-detail/App.tsx b/servers/fieldedge/src/ui/react-app/customer-detail/App.tsx new file mode 100644 index 0000000..122aa5f --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/customer-detail/App.tsx @@ -0,0 +1,269 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface Customer { + id: string; + firstName: string; + lastName: string; + companyName?: string; + email: string; + phone: string; + mobilePhone?: string; + status: string; + customerType: string; + balance: number; + address: { + street: string; + city: string; + state: string; + zip: string; + }; + tags: string[]; + createdAt: string; +} + +interface Job { + id: string; + jobNumber: string; + jobType: string; + status: string; + date: string; + total: number; +} + +export default function App() { + const [customer, setCustomer] = useState(null); + const [jobs, setJobs] = useState([]); + const [activeTab, setActiveTab] = useState<'overview' | 'jobs' | 'invoices' | 'equipment'>('overview'); + + useEffect(() => { + loadCustomer(); + loadJobs(); + }, []); + + const loadCustomer = async () => { + const mockCustomer: Customer = { + id: 'CUST-001', + firstName: 'John', + lastName: 'Smith', + companyName: 'Smith Enterprises', + email: 'john.smith@example.com', + phone: '(555) 123-4567', + mobilePhone: '(555) 987-6543', + status: 'active', + customerType: 'commercial', + balance: 1250.00, + address: { + street: '123 Main St', + city: 'Springfield', + state: 'IL', + zip: '62701', + }, + tags: ['VIP', 'Service Contract', 'Monthly Billing'], + createdAt: '2023-01-15T00:00:00Z', + }; + setCustomer(mockCustomer); + }; + + const loadJobs = async () => { + const mockJobs: Job[] = [ + { id: '1', jobNumber: 'JOB-001', jobType: 'HVAC Repair', status: 'completed', date: '2024-01-20', total: 450.00 }, + { id: '2', jobNumber: 'JOB-015', jobType: 'Maintenance', status: 'completed', date: '2023-12-10', total: 275.00 }, + { id: '3', jobNumber: 'JOB-032', jobType: 'Installation', status: 'in-progress', date: '2024-02-05', total: 1850.00 }, + ]; + setJobs(mockJobs); + }; + + if (!customer) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+ {/* Header */} +
+
+
+

+ {customer.firstName} {customer.lastName} +

+ {customer.companyName && ( +

{customer.companyName}

+ )} +
+
+ + {customer.status.toUpperCase()} + + + {customer.customerType} + +
+
+
+ + {/* Tabs */} +
+
+ {['overview', 'jobs', 'invoices', 'equipment'].map(tab => ( + + ))} +
+
+ + {/* Content */} + {activeTab === 'overview' && ( +
+ {/* Contact Info */} +
+

Contact Information

+
+
+
Email
+
{customer.email}
+
+
+
Phone
+
{customer.phone}
+
+ {customer.mobilePhone && ( +
+
Mobile
+
{customer.mobilePhone}
+
+ )} +
+
Address
+
+ {customer.address.street}
+ {customer.address.city}, {customer.address.state} {customer.address.zip} +
+
+
+
+ + {/* Account Info */} +
+

Account Information

+
+
+
Customer ID
+
{customer.id}
+
+
+
Customer Since
+
+ {new Date(customer.createdAt).toLocaleDateString()} +
+
+
+
Current Balance
+
0 ? 'text-yellow-400' : 'text-green-400'}`}> + ${customer.balance.toFixed(2)} +
+
+ {customer.tags.length > 0 && ( +
+
Tags
+
+ {customer.tags.map(tag => ( + + {tag} + + ))} +
+
+ )} +
+
+ + {/* Quick Stats */} +
+

Quick Stats

+
+
+
Total Jobs
+
{jobs.length}
+
+
+
Completed Jobs
+
+ {jobs.filter(j => j.status === 'completed').length} +
+
+
+
Active Jobs
+
+ {jobs.filter(j => j.status === 'in-progress').length} +
+
+
+
Lifetime Value
+
+ ${jobs.reduce((sum, j) => sum + j.total, 0).toFixed(2)} +
+
+
+
+
+ )} + + {activeTab === 'jobs' && ( +
+ + + + + + + + + + + + {jobs.map(job => ( + + + + + + + + ))} + +
Job #TypeStatusDateAmount
{job.jobNumber}{job.jobType} + + {job.status} + + {new Date(job.date).toLocaleDateString()}${job.total.toFixed(2)}
+
+ )} + + {(activeTab === 'invoices' || activeTab === 'equipment') && ( +
+
No {activeTab} data available
+
+ )} +
+
+ ); +} diff --git a/servers/fieldedge/src/ui/react-app/customer-detail/index.html b/servers/fieldedge/src/ui/react-app/customer-detail/index.html new file mode 100644 index 0000000..4b4c223 --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/customer-detail/index.html @@ -0,0 +1,13 @@ + + + + + + Customer Detail - FieldEdge MCP + + + +
+ + + diff --git a/servers/fieldedge/src/ui/react-app/customer-detail/styles.css b/servers/fieldedge/src/ui/react-app/customer-detail/styles.css new file mode 100644 index 0000000..d83ff17 --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/customer-detail/styles.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.bg-slate-750 { + background-color: #1a2332; +} diff --git a/servers/fieldedge/src/ui/react-app/customer-detail/vite.config.ts b/servers/fieldedge/src/ui/react-app/customer-detail/vite.config.ts new file mode 100644 index 0000000..fb1a112 --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/customer-detail/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5176, + open: true, + }, + build: { + outDir: 'dist', + sourcemap: true, + }, +}); diff --git a/servers/fieldedge/src/ui/react-app/customer-grid/App.tsx b/servers/fieldedge/src/ui/react-app/customer-grid/App.tsx new file mode 100644 index 0000000..311c61b --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/customer-grid/App.tsx @@ -0,0 +1,206 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface Customer { + id: string; + name: string; + companyName?: string; + email: string; + phone: string; + type: string; + status: string; + balance: number; + lastJob?: string; + totalJobs: number; +} + +export default function App() { + const [customers, setCustomers] = useState([]); + const [filteredCustomers, setFilteredCustomers] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [typeFilter, setTypeFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState('all'); + + useEffect(() => { + loadCustomers(); + }, []); + + useEffect(() => { + filterCustomers(); + }, [searchTerm, typeFilter, statusFilter, customers]); + + const loadCustomers = async () => { + const mockCustomers: Customer[] = [ + { id: 'C001', name: 'John Smith', companyName: 'Smith Enterprises', email: 'john@example.com', phone: '555-0101', type: 'commercial', status: 'active', balance: 1250, lastJob: '2024-02-10', totalJobs: 15 }, + { id: 'C002', name: 'Jane Doe', email: 'jane@example.com', phone: '555-0102', type: 'residential', status: 'active', balance: 0, lastJob: '2024-02-12', totalJobs: 8 }, + { id: 'C003', name: 'Bob Wilson', companyName: 'Wilson LLC', email: 'bob@example.com', phone: '555-0103', type: 'commercial', status: 'active', balance: 2500, lastJob: '2024-01-28', totalJobs: 22 }, + { id: 'C004', name: 'Alice Johnson', email: 'alice@example.com', phone: '555-0104', type: 'residential', status: 'inactive', balance: 0, lastJob: '2023-11-15', totalJobs: 3 }, + { id: 'C005', name: 'Charlie Brown', email: 'charlie@example.com', phone: '555-0105', type: 'residential', status: 'active', balance: 450, lastJob: '2024-02-08', totalJobs: 12 }, + { id: 'C006', name: 'Diana Prince', companyName: 'Prince Industries', email: 'diana@example.com', phone: '555-0106', type: 'commercial', status: 'active', balance: 0, lastJob: '2024-02-11', totalJobs: 19 }, + ]; + setCustomers(mockCustomers); + }; + + const filterCustomers = () => { + let filtered = customers; + + if (searchTerm) { + filtered = filtered.filter(c => + c.name.toLowerCase().includes(searchTerm.toLowerCase()) || + c.companyName?.toLowerCase().includes(searchTerm.toLowerCase()) || + c.email.toLowerCase().includes(searchTerm.toLowerCase()) || + c.phone.includes(searchTerm) + ); + } + + if (typeFilter !== 'all') { + filtered = filtered.filter(c => c.type === typeFilter); + } + + if (statusFilter !== 'all') { + filtered = filtered.filter(c => c.status === statusFilter); + } + + setFilteredCustomers(filtered); + }; + + return ( +
+
+ {/* Header */} +
+

Customer Grid

+

Manage and browse all customers

+
+ + {/* Stats */} +
+
+
Total Customers
+
{customers.length}
+
+
+
Active
+
+ {customers.filter(c => c.status === 'active').length} +
+
+
+
Commercial
+
+ {customers.filter(c => c.type === 'commercial').length} +
+
+
+
Outstanding Balance
+
+ ${customers.reduce((sum, c) => sum + c.balance, 0).toFixed(0)} +
+
+
+ + {/* Filters */} +
+
+ setSearchTerm(e.target.value)} + className="px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500" + /> + + +
+
+ + {/* Grid */} +
+ {filteredCustomers.map(customer => ( +
+ {/* Header */} +
+
+
{customer.name}
+ {customer.companyName && ( +
{customer.companyName}
+ )} +
+
+ + {customer.status} + + + {customer.type} + +
+
+ + {/* Contact */} +
+
+ 📧 + {customer.email} +
+
+ 📞 + {customer.phone} +
+
+ + {/* Stats */} +
+
+
Total Jobs
+
{customer.totalJobs}
+
+
+
Balance
+
0 ? 'text-yellow-400' : 'text-green-400'}`}> + ${customer.balance.toFixed(0)} +
+
+
+ + {customer.lastJob && ( +
+
Last Job
+
{new Date(customer.lastJob).toLocaleDateString()}
+
+ )} +
+ ))} +
+ + {filteredCustomers.length === 0 && ( +
+ No customers found matching your filters +
+ )} +
+
+ ); +} diff --git a/servers/fieldedge/src/ui/react-app/customer-grid/index.html b/servers/fieldedge/src/ui/react-app/customer-grid/index.html new file mode 100644 index 0000000..4a8cf1b --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/customer-grid/index.html @@ -0,0 +1,13 @@ + + + + + + Customer Grid - FieldEdge MCP + + + +
+ + + diff --git a/servers/fieldedge/src/ui/react-app/customer-grid/styles.css b/servers/fieldedge/src/ui/react-app/customer-grid/styles.css new file mode 100644 index 0000000..d83ff17 --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/customer-grid/styles.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.bg-slate-750 { + background-color: #1a2332; +} diff --git a/servers/fieldedge/src/ui/react-app/customer-grid/vite.config.ts b/servers/fieldedge/src/ui/react-app/customer-grid/vite.config.ts new file mode 100644 index 0000000..e6e9cb3 --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/customer-grid/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5177, + open: true, + }, + build: { + outDir: 'dist', + sourcemap: true, + }, +}); diff --git a/servers/fieldedge/src/ui/react-app/estimate-builder/App.tsx b/servers/fieldedge/src/ui/react-app/estimate-builder/App.tsx new file mode 100644 index 0000000..0693291 --- /dev/null +++ b/servers/fieldedge/src/ui/react-app/estimate-builder/App.tsx @@ -0,0 +1,231 @@ +import React, { useState } from 'react'; +import './styles.css'; + +interface LineItem { + id: string; + type: 'service' | 'part' | 'labor'; + description: string; + quantity: number; + unitPrice: number; + total: number; +} + +export default function App() { + const [customerSearch, setCustomerSearch] = useState(''); + const [selectedCustomer, setSelectedCustomer] = useState(null); + const [lineItems, setLineItems] = useState([]); + const [newItem, setNewItem] = useState({ + type: 'service' as 'service' | 'part' | 'labor', + description: '', + quantity: 1, + unitPrice: 0, + }); + + const [discountPercent, setDiscountPercent] = useState(0); + const [taxPercent, setTaxPercent] = useState(8); + const [notes, setNotes] = useState(''); + + const addLineItem = () => { + if (!newItem.description || newItem.unitPrice <= 0) return; + + const item: LineItem = { + id: Date.now().toString(), + type: newItem.type, + description: newItem.description, + quantity: newItem.quantity, + unitPrice: newItem.unitPrice, + total: newItem.quantity * newItem.unitPrice, + }; + + setLineItems([...lineItems, item]); + setNewItem({ type: 'service', description: '', quantity: 1, unitPrice: 0 }); + }; + + const removeItem = (id: string) => { + setLineItems(lineItems.filter(item => item.id !== id)); + }; + + const subtotal = lineItems.reduce((sum, item) => sum + item.total, 0); + const discount = subtotal * (discountPercent / 100); + const taxableAmount = subtotal - discount; + const tax = taxableAmount * (taxPercent / 100); + const total = taxableAmount + tax; + + return ( +
+
+ {/* Header */} +
+

Estimate Builder

+

Create professional estimates for customers

+
+ + {/* Customer Selection */} +
+

Customer

+ setCustomerSearch(e.target.value)} + className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500" + /> + {!selectedCustomer && customerSearch && ( +
+ Select a customer or create a new one +
+ )} +
+ + {/* Line Items */} +
+

Line Items

+ + {/* Add Item Form */} +
+ + setNewItem({ ...newItem, description: e.target.value })} + className="md:col-span-2 px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500" + /> + setNewItem({ ...newItem, quantity: parseFloat(e.target.value) || 0 })} + className="px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500" + /> + setNewItem({ ...newItem, unitPrice: parseFloat(e.target.value) || 0 })} + className="px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:border-blue-500" + /> +
+ + + {/* Items Table */} + {lineItems.length > 0 && ( +
+ + + + + + + + + + + + + {lineItems.map(item => ( + + + + + + + + + ))} + +
TypeDescriptionQtyPriceTotal
+ + {item.type} + + {item.description}{item.quantity}${item.unitPrice.toFixed(2)}${item.total.toFixed(2)} + +
+
+ )} +
+ + {/* Totals */} +
+

Pricing

+
+
+ Subtotal + ${subtotal.toFixed(2)} +
+
+ Discount +
+ setDiscountPercent(parseFloat(e.target.value) || 0)} + className="w-16 px-2 py-1 bg-slate-700 border border-slate-600 rounded text-white text-right focus:outline-none focus:border-blue-500" + /> + % + -${discount.toFixed(2)} +
+
+
+ Tax +
+ setTaxPercent(parseFloat(e.target.value) || 0)} + className="w-16 px-2 py-1 bg-slate-700 border border-slate-600 rounded text-white text-right focus:outline-none focus:border-blue-500" + /> + % + ${tax.toFixed(2)} +
+
+
+ Total + ${total.toFixed(2)} +
+
+
+ + {/* Notes */} +
+

Notes

+
`; +} diff --git a/servers/freshdesk/src/apps/canned-responses.ts b/servers/freshdesk/src/apps/canned-responses.ts new file mode 100644 index 0000000..62326a9 --- /dev/null +++ b/servers/freshdesk/src/apps/canned-responses.ts @@ -0,0 +1,4 @@ +export function getCannedResponsesApp(): string { + return ` +Canned Responses

💬 Canned Responses

Welcome New Customer

Hi [Customer Name],

Welcome to [Company Name]! We're thrilled to have you on board. If you have any questions or need assistance getting started, our team is here to help.

Best regards,
[Agent Name]

Password Reset Instructions

Hello,

To reset your password, please follow these steps:
1. Go to the login page
2. Click "Forgot Password"
3. Enter your email address
4. Check your email for the reset link

Let me know if you need further assistance!

Billing Query Response

Hi there,

Thank you for reaching out about your billing. I'd be happy to help you with that. Can you please provide your account email and invoice number so I can look into this for you?

Best regards,
[Agent Name]

Feature Request Acknowledgment

Hello,

Thank you for this excellent feature suggestion! I've forwarded it to our product team for consideration. We'll keep you updated on any developments.

We appreciate your feedback!
`; +} diff --git a/servers/freshdesk/src/apps/company-detail.ts b/servers/freshdesk/src/apps/company-detail.ts new file mode 100644 index 0000000..13fad06 --- /dev/null +++ b/servers/freshdesk/src/apps/company-detail.ts @@ -0,0 +1,4 @@ +export function getCompanyDetailApp(): string { + return ` +Company Detail

🏢 Acme Corporation

Technology Services • Enterprise Account • Member since Jan 2023

Total Tickets

247

Open Tickets

18

Contacts

52

Health Score

87/100

Company Information

IndustryTechnology Services
Account TierEnterprise
Renewal DateDec 31, 2025
Primary Domainacme.com
LocationSan Francisco, CA
Company Size500-1000 employees
Annual Revenue$50M - $100M

Recent Activity

  • 🎫 New ticket #12345 created by John Doe
    2 hours ago
  • ✅ Ticket #12234 resolved
    5 hours ago
  • 👤 New contact added: Sarah Miller
    1 day ago
  • 📞 Support call with Mike Johnson
    2 days ago

Key Contacts

  • John Doe
    Senior Developer
    3 open
  • Bob Wilson
    IT Manager
    1 open
  • Sarah Miller
    Product Manager
    0 open

Support Metrics

Avg Response Time1.8h
Avg Resolution Time12.5h
CSAT Score4.6/5.0
SLA Compliance94%
`; +} diff --git a/servers/freshdesk/src/apps/company-grid.ts b/servers/freshdesk/src/apps/company-grid.ts new file mode 100644 index 0000000..b591b83 --- /dev/null +++ b/servers/freshdesk/src/apps/company-grid.ts @@ -0,0 +1,4 @@ +export function getCompanyGridApp(): string { + return ` +Company Grid

🏢 Companies

CompanyIndustryContactsOpen TicketsTotal TicketsHealth ScoreAccount TierActions
Acme CorporationTechnology521824787Enterprise
TechStart IncSaaS28513472Growth
GlobalTech SolutionsConsulting953258958Enterprise
HealthCare ProHealthcare411220191Premium
`; +} diff --git a/servers/freshdesk/src/apps/contact-detail.ts b/servers/freshdesk/src/apps/contact-detail.ts new file mode 100644 index 0000000..2aae176 --- /dev/null +++ b/servers/freshdesk/src/apps/contact-detail.ts @@ -0,0 +1,4 @@ +export function getContactDetailApp(): string { + return ` +Contact Detail
JD

John Doe

john.doe@example.com

📞 (555) 123-4567

🏢 Acme Corporation

Contact Information

Emailjohn.doe@example.com
Phone(555) 123-4567
Mobile(555) 987-6543
CompanyAcme Corporation
Job TitleSenior Developer
Time ZoneEST (UTC-5)
LanguageEnglish

Ticket History

  • #12345 - Cannot access portal
    Open • 2h ago
  • #12234 - Password reset issue
    Resolved • 5d ago
  • #12123 - Billing question
    Closed • 10d ago
  • #12012 - Feature inquiry
    Closed • 15d ago

Activity Stats

Total Tickets47
Open Tickets3
Resolved42
Closed2
Avg Response Time2.3 hours
Satisfaction Score4.8/5.0
Member SinceJan 15, 2023
Last Activity2 hours ago
`; +} diff --git a/servers/freshdesk/src/apps/contact-grid.ts b/servers/freshdesk/src/apps/contact-grid.ts new file mode 100644 index 0000000..469ef7c --- /dev/null +++ b/servers/freshdesk/src/apps/contact-grid.ts @@ -0,0 +1,4 @@ +export function getContactGridApp(): string { + return ` +Contact Grid

👥 Contacts

ContactEmailPhoneCompanyTicketsLast ContactActions
JDJohn Doejohn.doe@example.com(555) 123-4567Acme Corp3 open2h ago
JSJane Smithjane.smith@example.com(555) 234-5678TechStart Inc1 open5h ago
BWBob Wilsonbob.wilson@example.com(555) 345-6789Acme Corp0 open1d ago
AJAlice Johnsonalice.johnson@example.com(555) 456-7890GlobalTech2 open3h ago
`; +} diff --git a/servers/freshdesk/src/apps/forum-browser.ts b/servers/freshdesk/src/apps/forum-browser.ts new file mode 100644 index 0000000..c6163e9 --- /dev/null +++ b/servers/freshdesk/src/apps/forum-browser.ts @@ -0,0 +1,4 @@ +export function getForumBrowserApp(): string { + return ` +Forum Browser

💬 Community Forums

Product Announcements

Latest product updates and feature releases

245 topics1.2K postsLast post: 2h ago

General Discussion

Chat about anything related to our platform

892 topics5.6K postsLast post: 15m ago

Feature Requests

Share your ideas for new features

456 topics2.8K postsLast post: 1h ago

Technical Support

Get help from the community

1.5K topics8.2K postsLast post: 5m ago

🔥 Trending Topics

API rate limiting best practicesTechnical

Started by @developer123 • 24 replies • 156 views

Last reply 15m ago
by @admin

When will dark mode be available?Feature Request

Started by @user456 • 18 replies • 203 views

Last reply 1h ago
by @moderator

Celebrating 10K community members!Announcement

Started by @team • 45 replies • 892 views

Last reply 2h ago
by @user789
`; +} diff --git a/servers/freshdesk/src/apps/group-manager.ts b/servers/freshdesk/src/apps/group-manager.ts new file mode 100644 index 0000000..ca56ab4 --- /dev/null +++ b/servers/freshdesk/src/apps/group-manager.ts @@ -0,0 +1,4 @@ +export function getGroupManagerApp(): string { + return ` +Group Manager

👥 Group Manager

Technical Support

45
Open Tickets
8
Agents

Team Members

Sarah SmithTom DavisLisa ChenMark Wilson+4 more

Billing Support

23
Open Tickets
5
Agents

Team Members

Mike JohnsonJennifer LeeRobert Brown+2 more

Customer Success

12
Open Tickets
6
Agents

Team Members

Emily WhiteJames MillerAmy Taylor+3 more

Sales Support

18
Open Tickets
4
Agents

Team Members

David ClarkSusan Anderson+2 more
`; +} diff --git a/servers/freshdesk/src/apps/knowledge-base.ts b/servers/freshdesk/src/apps/knowledge-base.ts new file mode 100644 index 0000000..c564193 --- /dev/null +++ b/servers/freshdesk/src/apps/knowledge-base.ts @@ -0,0 +1,4 @@ +export function getKnowledgeBaseApp(): string { + return ` +Knowledge Base

📚 Knowledge Base

Search our help articles and guides

Getting Started

New to our platform? Start here

  • Quick start guide
  • Account setup
  • First steps tutorial
  • System requirements
12 articles

Account & Billing

Manage your account and subscriptions

  • Update payment method
  • View invoices
  • Change plan
  • Cancel subscription
18 articles

Technical Issues

Troubleshooting and technical support

  • Login problems
  • Connection errors
  • Performance issues
  • Browser compatibility
25 articles

API Documentation

Developer resources and API guides

  • API authentication
  • Rate limits
  • Webhooks
  • Code examples
32 articles
`; +} diff --git a/servers/freshdesk/src/apps/ticket-dashboard.ts b/servers/freshdesk/src/apps/ticket-dashboard.ts new file mode 100644 index 0000000..7db28bc --- /dev/null +++ b/servers/freshdesk/src/apps/ticket-dashboard.ts @@ -0,0 +1,289 @@ +export function getTicketDashboardApp(): string { + return ` + + + + + + FreshDesk Ticket Dashboard + + + +
+
+

🎫 Ticket Dashboard

+

Real-time overview of your support tickets

+
+ +
+
+

Open Tickets

+
-
+
+12% from yesterday
+
+
+

Pending

+
-
+
-8% from yesterday
+
+
+

Resolved Today

+
-
+
+15% from yesterday
+
+
+

Avg Response Time

+
-
+
+5 min from yesterday
+
+
+ +
+

Recent Tickets

+
+ + + + + +
+
+
Loading tickets...
+
+
+
+ + + + + `; +} diff --git a/servers/freshdesk/src/apps/ticket-detail.ts b/servers/freshdesk/src/apps/ticket-detail.ts new file mode 100644 index 0000000..0b30a1b --- /dev/null +++ b/servers/freshdesk/src/apps/ticket-detail.ts @@ -0,0 +1,241 @@ +export function getTicketDetailApp(): string { + return ` + + + + + + FreshDesk Ticket Detail + + + +
+
+
#12345
+

Cannot access customer portal

+
+ + OPEN + + + HIGH PRIORITY + + 👤 john.doe@example.com + 🕐 Created 2 hours ago + 🔄 Updated 15 mins ago +
+
+ +
+
+

Conversation

+ +
+
+ John Doe + 2 hours ago +
+
+ I've been trying to log into the customer portal for the past hour but keep getting an error message. + It says "Authentication failed" even though I'm using the correct credentials. Can someone help? +
+
+ +
+
+ Support Agent (You) + 1 hour ago +
+
+ Hi John, thank you for reaching out. I've checked your account and I see the issue. + Your account was temporarily locked due to multiple failed login attempts. I've unlocked it now. + Please try logging in again and let me know if you still face any issues. +
+
+ +
+

Add Reply

+ +
+ + + +
+
+
+ + +
+
+ + + `; +} diff --git a/servers/freshdesk/src/apps/ticket-grid.ts b/servers/freshdesk/src/apps/ticket-grid.ts new file mode 100644 index 0000000..088d307 --- /dev/null +++ b/servers/freshdesk/src/apps/ticket-grid.ts @@ -0,0 +1,4 @@ +export function getTicketGridApp(): string { + return ` +Ticket Grid

🎫 Ticket Grid

IDSubjectRequesterStatusPriorityAgentCreatedUpdated
#12345Cannot access customer portaljohn.doe@example.comOPENHighSarah Smith2h ago15m ago
#12344Billing discrepancyjane.smith@example.comPENDINGUrgentMike Johnson5h ago1h ago
#12343Feature request: Dark modebob.wilson@example.comOPENLowUnassigned1d ago1d ago
#12342API documentation unclearalice.johnson@example.comRESOLVEDMediumTom Davis2d ago1d ago
`; +} diff --git a/servers/freshdesk/src/index.ts b/servers/freshdesk/src/index.ts deleted file mode 100644 index 527865c..0000000 --- a/servers/freshdesk/src/index.ts +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -// ============================================ -// CONFIGURATION -// ============================================ -const MCP_NAME = "freshdesk"; -const MCP_VERSION = "1.0.0"; - -// ============================================ -// API CLIENT - Freshdesk uses Basic Auth with API key -// ============================================ -class FreshdeskClient { - private apiKey: string; - private domain: string; - private baseUrl: string; - - constructor(apiKey: string, domain: string) { - this.apiKey = apiKey; - this.domain = domain; - this.baseUrl = `https://${domain}.freshdesk.com/api/v2`; - } - - private getAuthHeader(): string { - // Freshdesk uses Basic Auth: API key as username, "X" as password - return "Basic " + Buffer.from(`${this.apiKey}:X`).toString("base64"); - } - - async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}`; - const response = await fetch(url, { - ...options, - headers: { - "Authorization": this.getAuthHeader(), - "Content-Type": "application/json", - ...options.headers, - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Freshdesk API error: ${response.status} ${response.statusText} - ${errorText}`); - } - - // Handle 204 No Content - if (response.status === 204) { - return { success: true }; - } - - return response.json(); - } - - async get(endpoint: string) { - return this.request(endpoint, { method: "GET" }); - } - - async post(endpoint: string, data: any) { - return this.request(endpoint, { - method: "POST", - body: JSON.stringify(data), - }); - } - - async put(endpoint: string, data: any) { - return this.request(endpoint, { - method: "PUT", - body: JSON.stringify(data), - }); - } - - async delete(endpoint: string) { - return this.request(endpoint, { method: "DELETE" }); - } -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ -const tools = [ - { - name: "list_tickets", - description: "List all tickets with optional filtering. Returns tickets sorted by created_at descending.", - inputSchema: { - type: "object" as const, - properties: { - filter: { - type: "string", - description: "Filter tickets by predefined filters: new_and_my_open, watching, spam, deleted, or all_tickets", - enum: ["new_and_my_open", "watching", "spam", "deleted", "all_tickets"], - }, - page: { type: "number", description: "Page number for pagination (default: 1)" }, - per_page: { type: "number", description: "Results per page, max 100 (default: 30)" }, - order_by: { type: "string", description: "Order by field: created_at, due_by, updated_at, status" }, - order_type: { type: "string", enum: ["asc", "desc"], description: "Sort order" }, - }, - }, - }, - { - name: "get_ticket", - description: "Get a specific ticket by ID with full details including conversations", - inputSchema: { - type: "object" as const, - properties: { - id: { type: "number", description: "Ticket ID" }, - include: { - type: "string", - description: "Include additional data: conversations, requester, company, stats", - }, - }, - required: ["id"], - }, - }, - { - name: "create_ticket", - description: "Create a new support ticket", - inputSchema: { - type: "object" as const, - properties: { - subject: { type: "string", description: "Ticket subject (required)" }, - description: { type: "string", description: "HTML content of the ticket (required)" }, - email: { type: "string", description: "Email of the requester (required if no requester_id)" }, - requester_id: { type: "number", description: "ID of the requester (required if no email)" }, - priority: { - type: "number", - description: "Priority: 1=Low, 2=Medium, 3=High, 4=Urgent", - enum: [1, 2, 3, 4], - }, - status: { - type: "number", - description: "Status: 2=Open, 3=Pending, 4=Resolved, 5=Closed", - enum: [2, 3, 4, 5], - }, - type: { type: "string", description: "Ticket type (as configured in your helpdesk)" }, - source: { - type: "number", - description: "Source: 1=Email, 2=Portal, 3=Phone, 7=Chat, 9=Feedback Widget, 10=Outbound Email", - }, - group_id: { type: "number", description: "ID of the group to assign" }, - responder_id: { type: "number", description: "ID of the agent to assign" }, - tags: { - type: "array", - items: { type: "string" }, - description: "Tags to add to the ticket", - }, - custom_fields: { type: "object", description: "Custom field values as key-value pairs" }, - }, - required: ["subject", "description"], - }, - }, - { - name: "update_ticket", - description: "Update an existing ticket's properties", - inputSchema: { - type: "object" as const, - properties: { - id: { type: "number", description: "Ticket ID" }, - subject: { type: "string", description: "Updated subject" }, - description: { type: "string", description: "Updated description" }, - priority: { type: "number", description: "Priority: 1=Low, 2=Medium, 3=High, 4=Urgent" }, - status: { type: "number", description: "Status: 2=Open, 3=Pending, 4=Resolved, 5=Closed" }, - type: { type: "string", description: "Ticket type" }, - group_id: { type: "number", description: "Group to assign" }, - responder_id: { type: "number", description: "Agent to assign" }, - tags: { type: "array", items: { type: "string" }, description: "Tags (replaces existing)" }, - custom_fields: { type: "object", description: "Custom field values" }, - }, - required: ["id"], - }, - }, - { - name: "reply_ticket", - description: "Add a reply to a ticket (creates a conversation)", - inputSchema: { - type: "object" as const, - properties: { - id: { type: "number", description: "Ticket ID" }, - body: { type: "string", description: "HTML content of the reply (required)" }, - from_email: { type: "string", description: "Email address to send reply from" }, - user_id: { type: "number", description: "ID of the agent/contact adding the note" }, - cc_emails: { - type: "array", - items: { type: "string" }, - description: "CC email addresses", - }, - bcc_emails: { - type: "array", - items: { type: "string" }, - description: "BCC email addresses", - }, - private: { type: "boolean", description: "If true, creates a private note instead of public reply" }, - }, - required: ["id", "body"], - }, - }, - { - name: "list_contacts", - description: "List all contacts in your helpdesk", - inputSchema: { - type: "object" as const, - properties: { - email: { type: "string", description: "Filter by email address" }, - phone: { type: "string", description: "Filter by phone number" }, - mobile: { type: "string", description: "Filter by mobile number" }, - company_id: { type: "number", description: "Filter by company ID" }, - state: { type: "string", enum: ["blocked", "deleted", "unverified", "verified"], description: "Filter by state" }, - page: { type: "number", description: "Page number" }, - per_page: { type: "number", description: "Results per page (max 100)" }, - }, - }, - }, - { - name: "list_agents", - description: "List all agents in your helpdesk", - inputSchema: { - type: "object" as const, - properties: { - email: { type: "string", description: "Filter by email" }, - phone: { type: "string", description: "Filter by phone" }, - state: { type: "string", enum: ["fulltime", "occasional"], description: "Filter by agent type" }, - page: { type: "number", description: "Page number" }, - per_page: { type: "number", description: "Results per page (max 100)" }, - }, - }, - }, - { - name: "search_tickets", - description: "Search tickets using Freshdesk query language. Supports field:value syntax.", - inputSchema: { - type: "object" as const, - properties: { - query: { - type: "string", - description: 'Search query using Freshdesk syntax. Examples: "status:2", "priority:4 AND created_at:>\'2023-01-01\'", "tag:\'urgent\'"', - }, - page: { type: "number", description: "Page number (each page has 30 results)" }, - }, - required: ["query"], - }, - }, -]; - -// ============================================ -// TOOL HANDLERS -// ============================================ -async function handleTool(client: FreshdeskClient, name: string, args: any) { - switch (name) { - case "list_tickets": { - const params = new URLSearchParams(); - if (args.filter) params.append("filter", args.filter); - if (args.page) params.append("page", args.page.toString()); - if (args.per_page) params.append("per_page", args.per_page.toString()); - if (args.order_by) params.append("order_by", args.order_by); - if (args.order_type) params.append("order_type", args.order_type); - const query = params.toString(); - return await client.get(`/tickets${query ? `?${query}` : ""}`); - } - - case "get_ticket": { - const { id, include } = args; - const query = include ? `?include=${include}` : ""; - return await client.get(`/tickets/${id}${query}`); - } - - case "create_ticket": { - const { subject, description, email, requester_id, priority, status, type, source, group_id, responder_id, tags, custom_fields } = args; - const payload: any = { subject, description }; - if (email) payload.email = email; - if (requester_id) payload.requester_id = requester_id; - if (priority) payload.priority = priority; - if (status) payload.status = status; - if (type) payload.type = type; - if (source) payload.source = source; - if (group_id) payload.group_id = group_id; - if (responder_id) payload.responder_id = responder_id; - if (tags) payload.tags = tags; - if (custom_fields) payload.custom_fields = custom_fields; - return await client.post("/tickets", payload); - } - - case "update_ticket": { - const { id, ...updates } = args; - return await client.put(`/tickets/${id}`, updates); - } - - case "reply_ticket": { - const { id, body, from_email, user_id, cc_emails, bcc_emails, private: isPrivate } = args; - const payload: any = { body }; - if (from_email) payload.from_email = from_email; - if (user_id) payload.user_id = user_id; - if (cc_emails) payload.cc_emails = cc_emails; - if (bcc_emails) payload.bcc_emails = bcc_emails; - - // Private notes use a different endpoint - if (isPrivate) { - payload.private = true; - return await client.post(`/tickets/${id}/notes`, payload); - } - return await client.post(`/tickets/${id}/reply`, payload); - } - - case "list_contacts": { - const params = new URLSearchParams(); - if (args.email) params.append("email", args.email); - if (args.phone) params.append("phone", args.phone); - if (args.mobile) params.append("mobile", args.mobile); - if (args.company_id) params.append("company_id", args.company_id.toString()); - if (args.state) params.append("state", args.state); - if (args.page) params.append("page", args.page.toString()); - if (args.per_page) params.append("per_page", args.per_page.toString()); - const query = params.toString(); - return await client.get(`/contacts${query ? `?${query}` : ""}`); - } - - case "list_agents": { - const params = new URLSearchParams(); - if (args.email) params.append("email", args.email); - if (args.phone) params.append("phone", args.phone); - if (args.state) params.append("state", args.state); - if (args.page) params.append("page", args.page.toString()); - if (args.per_page) params.append("per_page", args.per_page.toString()); - const query = params.toString(); - return await client.get(`/agents${query ? `?${query}` : ""}`); - } - - case "search_tickets": { - const { query, page } = args; - const params = new URLSearchParams(); - params.append("query", `"${query}"`); - if (page) params.append("page", page.toString()); - return await client.get(`/search/tickets?${params.toString()}`); - } - - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -// ============================================ -// SERVER SETUP -// ============================================ -async function main() { - const apiKey = process.env.FRESHDESK_API_KEY; - const domain = process.env.FRESHDESK_DOMAIN; - - if (!apiKey) { - console.error("Error: FRESHDESK_API_KEY environment variable required"); - process.exit(1); - } - if (!domain) { - console.error("Error: FRESHDESK_DOMAIN environment variable required (e.g., 'yourcompany' for yourcompany.freshdesk.com)"); - process.exit(1); - } - - const client = new FreshdeskClient(apiKey, domain); - - const server = new Server( - { name: `${MCP_NAME}-mcp`, version: MCP_VERSION }, - { capabilities: { tools: {} } } - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools, - })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - try { - const result = await handleTool(client, name, args || {}); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - }; - } - }); - - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`${MCP_NAME} MCP server running on stdio`); -} - -main().catch(console.error); diff --git a/servers/freshdesk/src/main.ts b/servers/freshdesk/src/main.ts new file mode 100644 index 0000000..3c8a767 --- /dev/null +++ b/servers/freshdesk/src/main.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node +import { FreshDeskServer } from './server.js'; + +const server = new FreshDeskServer(); +server.run().catch(console.error); diff --git a/servers/freshdesk/src/server.ts b/servers/freshdesk/src/server.ts new file mode 100644 index 0000000..4be77a2 --- /dev/null +++ b/servers/freshdesk/src/server.ts @@ -0,0 +1,355 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ListResourcesRequestSchema, + ReadResourceRequestSchema, +} from '@modelcontextprotocol/sdk/types.js'; +import { FreshDeskClient } from './api/client.js'; +import { registerTicketTools } from './tools/tickets-tools.js'; +import { registerContactTools } from './tools/contacts-tools.js'; +import { registerCompanyTools } from './tools/companies-tools.js'; +import { registerAgentTools } from './tools/agents-tools.js'; +import { registerGroupTools } from './tools/groups-tools.js'; +import { registerRoleTools } from './tools/roles-tools.js'; +import { registerProductTools } from './tools/products-tools.js'; +import { registerForumTools } from './tools/forums-tools.js'; +import { registerSolutionTools } from './tools/solutions-tools.js'; +import { registerCannedResponseTools } from './tools/canned-responses-tools.js'; +import { registerSurveyTools } from './tools/surveys-tools.js'; +import { registerReportingTools } from './tools/reporting-tools.js'; + +// App HTML templates +import { getTicketDashboardApp } from './apps/ticket-dashboard.js'; +import { getTicketDetailApp } from './apps/ticket-detail.js'; +import { getTicketGridApp } from './apps/ticket-grid.js'; +import { getContactDetailApp } from './apps/contact-detail.js'; +import { getContactGridApp } from './apps/contact-grid.js'; +import { getCompanyDetailApp } from './apps/company-detail.js'; +import { getCompanyGridApp } from './apps/company-grid.js'; +import { getAgentDashboardApp } from './apps/agent-dashboard.js'; +import { getAgentPerformanceApp } from './apps/agent-performance.js'; +import { getGroupManagerApp } from './apps/group-manager.js'; +import { getKnowledgeBaseApp } from './apps/knowledge-base.js'; +import { getArticleEditorApp } from './apps/article-editor.js'; +import { getForumBrowserApp } from './apps/forum-browser.js'; +import { getCannedResponsesApp } from './apps/canned-responses.js'; +import { getSurveyResultsApp } from './apps/survey-results.js'; +import { getSLADashboardApp } from './apps/sla-dashboard.js'; +import { getTicketVolumeApp } from './apps/ticket-volume.js'; +import { getResolutionTimesApp } from './apps/resolution-times.js'; +import { getProductManagerApp } from './apps/product-manager.js'; +import { getTimeTrackingApp } from './apps/time-tracking.js'; + +export class FreshDeskServer { + private server: Server; + private client: FreshDeskClient; + private tools: Record = {}; + + constructor() { + this.server = new Server( + { + name: 'freshdesk-mcp-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + resources: {}, + }, + } + ); + + // Get configuration from environment + const domain = process.env.FRESHDESK_DOMAIN; + const apiKey = process.env.FRESHDESK_API_KEY; + + if (!domain || !apiKey) { + throw new Error('FRESHDESK_DOMAIN and FRESHDESK_API_KEY environment variables are required'); + } + + this.client = new FreshDeskClient({ domain, apiKey }); + + // Register all tools + this.registerAllTools(); + + // Set up request handlers + 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 registerAllTools() { + const toolModules = [ + registerTicketTools(this.client), + registerContactTools(this.client), + registerCompanyTools(this.client), + registerAgentTools(this.client), + registerGroupTools(this.client), + registerRoleTools(this.client), + registerProductTools(this.client), + registerForumTools(this.client), + registerSolutionTools(this.client), + registerCannedResponseTools(this.client), + registerSurveyTools(this.client), + registerReportingTools(this.client), + ]; + + for (const module of toolModules) { + this.tools = { ...this.tools, ...module }; + } + } + + private setupHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: Object.entries(this.tools).map(([name, tool]) => ({ + name, + description: tool.description, + inputSchema: tool.parameters, + })), + })); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const tool = this.tools[request.params.name]; + if (!tool) { + throw new Error(`Tool not found: ${request.params.name}`); + } + + try { + return await tool.handler(request.params.arguments || {}); + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error: ${error.message}`, + }, + ], + isError: true, + }; + } + }); + + // Resources (MCP Apps) + this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources: [ + { + uri: 'freshdesk://app/ticket-dashboard', + mimeType: 'text/html', + name: 'Ticket Dashboard', + description: 'Interactive dashboard for managing tickets', + }, + { + uri: 'freshdesk://app/ticket-detail', + mimeType: 'text/html', + name: 'Ticket Detail', + description: 'Detailed ticket view and management', + }, + { + uri: 'freshdesk://app/ticket-grid', + mimeType: 'text/html', + name: 'Ticket Grid', + description: 'Sortable and filterable ticket grid', + }, + { + uri: 'freshdesk://app/contact-detail', + mimeType: 'text/html', + name: 'Contact Detail', + description: 'Contact profile and history', + }, + { + uri: 'freshdesk://app/contact-grid', + mimeType: 'text/html', + name: 'Contact Grid', + description: 'Contact management grid', + }, + { + uri: 'freshdesk://app/company-detail', + mimeType: 'text/html', + name: 'Company Detail', + description: 'Company profile and analytics', + }, + { + uri: 'freshdesk://app/company-grid', + mimeType: 'text/html', + name: 'Company Grid', + description: 'Company management grid', + }, + { + uri: 'freshdesk://app/agent-dashboard', + mimeType: 'text/html', + name: 'Agent Dashboard', + description: 'Agent workload and performance', + }, + { + uri: 'freshdesk://app/agent-performance', + mimeType: 'text/html', + name: 'Agent Performance', + description: 'Detailed agent metrics and analytics', + }, + { + uri: 'freshdesk://app/group-manager', + mimeType: 'text/html', + name: 'Group Manager', + description: 'Manage groups and assignments', + }, + { + uri: 'freshdesk://app/knowledge-base', + mimeType: 'text/html', + name: 'Knowledge Base', + description: 'Browse and search knowledge base', + }, + { + uri: 'freshdesk://app/article-editor', + mimeType: 'text/html', + name: 'Article Editor', + description: 'Create and edit knowledge base articles', + }, + { + uri: 'freshdesk://app/forum-browser', + mimeType: 'text/html', + name: 'Forum Browser', + description: 'Browse community forums and discussions', + }, + { + uri: 'freshdesk://app/canned-responses', + mimeType: 'text/html', + name: 'Canned Responses', + description: 'Manage saved response templates', + }, + { + uri: 'freshdesk://app/survey-results', + mimeType: 'text/html', + name: 'Survey Results', + description: 'Customer satisfaction survey analytics', + }, + { + uri: 'freshdesk://app/sla-dashboard', + mimeType: 'text/html', + name: 'SLA Dashboard', + description: 'SLA compliance monitoring', + }, + { + uri: 'freshdesk://app/ticket-volume', + mimeType: 'text/html', + name: 'Ticket Volume', + description: 'Ticket volume trends and analytics', + }, + { + uri: 'freshdesk://app/resolution-times', + mimeType: 'text/html', + name: 'Resolution Times', + description: 'Resolution time analysis', + }, + { + uri: 'freshdesk://app/product-manager', + mimeType: 'text/html', + name: 'Product Manager', + description: 'Manage products and categories', + }, + { + uri: 'freshdesk://app/time-tracking', + mimeType: 'text/html', + name: 'Time Tracking', + description: 'Track and analyze time entries', + }, + ], + })); + + this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const uri = request.params.uri; + const appName = uri.replace('freshdesk://app/', ''); + + let html: string; + switch (appName) { + case 'ticket-dashboard': + html = getTicketDashboardApp(); + break; + case 'ticket-detail': + html = getTicketDetailApp(); + break; + case 'ticket-grid': + html = getTicketGridApp(); + break; + case 'contact-detail': + html = getContactDetailApp(); + break; + case 'contact-grid': + html = getContactGridApp(); + break; + case 'company-detail': + html = getCompanyDetailApp(); + break; + case 'company-grid': + html = getCompanyGridApp(); + break; + case 'agent-dashboard': + html = getAgentDashboardApp(); + break; + case 'agent-performance': + html = getAgentPerformanceApp(); + break; + case 'group-manager': + html = getGroupManagerApp(); + break; + case 'knowledge-base': + html = getKnowledgeBaseApp(); + break; + case 'article-editor': + html = getArticleEditorApp(); + break; + case 'forum-browser': + html = getForumBrowserApp(); + break; + case 'canned-responses': + html = getCannedResponsesApp(); + break; + case 'survey-results': + html = getSurveyResultsApp(); + break; + case 'sla-dashboard': + html = getSLADashboardApp(); + break; + case 'ticket-volume': + html = getTicketVolumeApp(); + break; + case 'resolution-times': + html = getResolutionTimesApp(); + break; + case 'product-manager': + html = getProductManagerApp(); + break; + case 'time-tracking': + html = getTimeTrackingApp(); + break; + default: + throw new Error(`Unknown app: ${appName}`); + } + + return { + contents: [ + { + uri, + mimeType: 'text/html', + text: html, + }, + ], + }; + }); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('FreshDesk MCP server running on stdio'); + } +} diff --git a/servers/freshdesk/src/tools/agents-tools.ts b/servers/freshdesk/src/tools/agents-tools.ts new file mode 100644 index 0000000..9669941 --- /dev/null +++ b/servers/freshdesk/src/tools/agents-tools.ts @@ -0,0 +1,145 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerAgentTools(client: FreshDeskClient) { + return { + freshdesk_list_agents: { + description: 'List all agents', + parameters: { + type: 'object', + properties: { + email: { + type: 'string', + description: 'Filter by email', + }, + mobile: { + type: 'string', + description: 'Filter by mobile number', + }, + phone: { + type: 'string', + description: 'Filter by phone number', + }, + state: { + type: 'string', + description: 'Filter by state (fulltime, occasional)', + }, + per_page: { + type: 'number', + description: 'Results per page (default 30, max 100)', + }, + max_results: { + type: 'number', + description: 'Maximum total results to return', + }, + }, + }, + handler: async (args: any) => { + const result = await client.listAgents(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_agent: { + description: 'Get a specific agent by ID', + parameters: { + type: 'object', + properties: { + agent_id: { + type: 'number', + description: 'Agent ID', + }, + }, + required: ['agent_id'], + }, + handler: async (args: any) => { + const agent = await client.getAgent(args.agent_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(agent, null, 2), + }, + ], + }; + }, + }, + + freshdesk_update_agent: { + description: 'Update an existing agent', + parameters: { + type: 'object', + properties: { + agent_id: { + type: 'number', + description: 'Agent ID', + }, + occasional: { + type: 'boolean', + description: 'Make occasional agent', + }, + signature: { + type: 'string', + description: 'Email signature', + }, + ticket_scope: { + type: 'number', + description: 'Ticket scope: 1=Global, 2=Group, 3=Restricted', + }, + group_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Group IDs', + }, + role_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Role IDs', + }, + available: { + type: 'boolean', + description: 'Agent availability', + }, + }, + required: ['agent_id'], + }, + handler: async (args: any) => { + const { agent_id, ...updateData } = args; + const agent = await client.updateAgent(agent_id, updateData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(agent, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_current_agent: { + description: 'Get the current authenticated agent (me)', + parameters: { + type: 'object', + properties: {}, + }, + handler: async () => { + const agent = await client.getCurrentAgent(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(agent, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/canned-responses-tools.ts b/servers/freshdesk/src/tools/canned-responses-tools.ts new file mode 100644 index 0000000..e8ce0c2 --- /dev/null +++ b/servers/freshdesk/src/tools/canned-responses-tools.ts @@ -0,0 +1,155 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerCannedResponseTools(client: FreshDeskClient) { + return { + freshdesk_list_canned_responses: { + description: 'List all canned (saved) responses', + parameters: { + type: 'object', + properties: {}, + }, + handler: async () => { + const responses = await client.listCannedResponses(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(responses, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_canned_response: { + description: 'Get a specific canned response by ID', + parameters: { + type: 'object', + properties: { + response_id: { + type: 'number', + description: 'Canned response ID', + }, + }, + required: ['response_id'], + }, + handler: async (args: any) => { + const response = await client.getCannedResponse(args.response_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + freshdesk_create_canned_response: { + description: 'Create a new canned response', + parameters: { + type: 'object', + properties: { + title: { + type: 'string', + description: 'Response title', + }, + content: { + type: 'string', + description: 'Response content (HTML)', + }, + group_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Group IDs that can use this response', + }, + visibility: { + type: 'number', + description: 'Visibility: 0=All agents, 1=Personal, 2=Groups', + }, + }, + required: ['title', 'content'], + }, + handler: async (args: any) => { + const response = await client.createCannedResponse(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + freshdesk_update_canned_response: { + description: 'Update an existing canned response', + parameters: { + type: 'object', + properties: { + response_id: { + type: 'number', + description: 'Canned response ID', + }, + title: { + type: 'string', + description: 'Response title', + }, + content: { + type: 'string', + description: 'Response content (HTML)', + }, + group_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Group IDs', + }, + visibility: { + type: 'number', + description: 'Visibility level', + }, + }, + required: ['response_id'], + }, + handler: async (args: any) => { + const { response_id, ...updateData } = args; + const response = await client.updateCannedResponse(response_id, updateData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(response, null, 2), + }, + ], + }; + }, + }, + + freshdesk_delete_canned_response: { + description: 'Delete a canned response', + parameters: { + type: 'object', + properties: { + response_id: { + type: 'number', + description: 'Canned response ID', + }, + }, + required: ['response_id'], + }, + handler: async (args: any) => { + await client.deleteCannedResponse(args.response_id); + return { + content: [ + { + type: 'text', + text: `Canned response ${args.response_id} deleted successfully`, + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/companies-tools.ts b/servers/freshdesk/src/tools/companies-tools.ts new file mode 100644 index 0000000..894baff --- /dev/null +++ b/servers/freshdesk/src/tools/companies-tools.ts @@ -0,0 +1,223 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerCompanyTools(client: FreshDeskClient) { + return { + freshdesk_list_companies: { + description: 'List all companies with optional filters', + parameters: { + type: 'object', + properties: { + per_page: { + type: 'number', + description: 'Results per page (default 30, max 100)', + }, + max_results: { + type: 'number', + description: 'Maximum total results to return', + }, + }, + }, + handler: async (args: any) => { + const result = await client.listCompanies(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_company: { + description: 'Get a specific company by ID', + parameters: { + type: 'object', + properties: { + company_id: { + type: 'number', + description: 'Company ID', + }, + }, + required: ['company_id'], + }, + handler: async (args: any) => { + const company = await client.getCompany(args.company_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(company, null, 2), + }, + ], + }; + }, + }, + + freshdesk_create_company: { + description: 'Create a new company', + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Company name', + }, + description: { + type: 'string', + description: 'Company description', + }, + domains: { + type: 'array', + items: { type: 'string' }, + description: 'Email domains associated with company', + }, + note: { + type: 'string', + description: 'Internal note about company', + }, + custom_fields: { + type: 'object', + description: 'Custom field values', + }, + health_score: { + type: 'string', + description: 'Health score', + }, + account_tier: { + type: 'string', + description: 'Account tier', + }, + renewal_date: { + type: 'string', + description: 'Renewal date (ISO 8601)', + }, + industry: { + type: 'string', + description: 'Industry', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const company = await client.createCompany(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(company, null, 2), + }, + ], + }; + }, + }, + + freshdesk_update_company: { + description: 'Update an existing company', + parameters: { + type: 'object', + properties: { + company_id: { + type: 'number', + description: 'Company ID', + }, + name: { + type: 'string', + description: 'Company name', + }, + description: { + type: 'string', + description: 'Company description', + }, + domains: { + type: 'array', + items: { type: 'string' }, + description: 'Email domains', + }, + note: { + type: 'string', + description: 'Internal note', + }, + custom_fields: { + type: 'object', + description: 'Custom field values', + }, + health_score: { + type: 'string', + description: 'Health score', + }, + account_tier: { + type: 'string', + description: 'Account tier', + }, + renewal_date: { + type: 'string', + description: 'Renewal date (ISO 8601)', + }, + industry: { + type: 'string', + description: 'Industry', + }, + }, + required: ['company_id'], + }, + handler: async (args: any) => { + const { company_id, ...updateData } = args; + const company = await client.updateCompany(company_id, updateData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(company, null, 2), + }, + ], + }; + }, + }, + + freshdesk_delete_company: { + description: 'Delete a company', + parameters: { + type: 'object', + properties: { + company_id: { + type: 'number', + description: 'Company ID', + }, + }, + required: ['company_id'], + }, + handler: async (args: any) => { + await client.deleteCompany(args.company_id); + return { + content: [ + { + type: 'text', + text: `Company ${args.company_id} deleted successfully`, + }, + ], + }; + }, + }, + + freshdesk_list_company_fields: { + description: 'List all company custom fields', + parameters: { + type: 'object', + properties: {}, + }, + handler: async () => { + const fields = await client.listCompanyFields(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(fields, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/contacts-tools.ts b/servers/freshdesk/src/tools/contacts-tools.ts new file mode 100644 index 0000000..b11cd15 --- /dev/null +++ b/servers/freshdesk/src/tools/contacts-tools.ts @@ -0,0 +1,369 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerContactTools(client: FreshDeskClient) { + return { + freshdesk_list_contacts: { + description: 'List all contacts with optional filters', + parameters: { + type: 'object', + properties: { + email: { + type: 'string', + description: 'Filter by email', + }, + phone: { + type: 'string', + description: 'Filter by phone', + }, + mobile: { + type: 'string', + description: 'Filter by mobile', + }, + company_id: { + type: 'number', + description: 'Filter by company ID', + }, + state: { + type: 'string', + description: 'Filter by state (verified, unverified, deleted, blocked)', + }, + updated_since: { + type: 'string', + description: 'Filter contacts updated after this timestamp (ISO 8601)', + }, + per_page: { + type: 'number', + description: 'Results per page (default 30, max 100)', + }, + max_results: { + type: 'number', + description: 'Maximum total results to return', + }, + }, + }, + handler: async (args: any) => { + const result = await client.listContacts(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_contact: { + description: 'Get a specific contact by ID', + parameters: { + type: 'object', + properties: { + contact_id: { + type: 'number', + description: 'Contact ID', + }, + }, + required: ['contact_id'], + }, + handler: async (args: any) => { + const contact = await client.getContact(args.contact_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(contact, null, 2), + }, + ], + }; + }, + }, + + freshdesk_create_contact: { + description: 'Create a new contact', + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Contact name', + }, + email: { + type: 'string', + description: 'Email address', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + mobile: { + type: 'string', + description: 'Mobile number', + }, + twitter_id: { + type: 'string', + description: 'Twitter handle', + }, + unique_external_id: { + type: 'string', + description: 'External ID from your system', + }, + company_id: { + type: 'number', + description: 'Company ID', + }, + description: { + type: 'string', + description: 'Contact description', + }, + job_title: { + type: 'string', + description: 'Job title', + }, + language: { + type: 'string', + description: 'Language code (e.g., "en")', + }, + time_zone: { + type: 'string', + description: 'Time zone', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Tags', + }, + address: { + type: 'string', + description: 'Address', + }, + custom_fields: { + type: 'object', + description: 'Custom field values', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const contact = await client.createContact(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(contact, null, 2), + }, + ], + }; + }, + }, + + freshdesk_update_contact: { + description: 'Update an existing contact', + parameters: { + type: 'object', + properties: { + contact_id: { + type: 'number', + description: 'Contact ID', + }, + name: { + type: 'string', + description: 'Contact name', + }, + email: { + type: 'string', + description: 'Email address', + }, + phone: { + type: 'string', + description: 'Phone number', + }, + mobile: { + type: 'string', + description: 'Mobile number', + }, + company_id: { + type: 'number', + description: 'Company ID', + }, + description: { + type: 'string', + description: 'Contact description', + }, + job_title: { + type: 'string', + description: 'Job title', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Tags', + }, + custom_fields: { + type: 'object', + description: 'Custom field values', + }, + }, + required: ['contact_id'], + }, + handler: async (args: any) => { + const { contact_id, ...updateData } = args; + const contact = await client.updateContact(contact_id, updateData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(contact, null, 2), + }, + ], + }; + }, + }, + + freshdesk_delete_contact: { + description: 'Delete a contact', + parameters: { + type: 'object', + properties: { + contact_id: { + type: 'number', + description: 'Contact ID', + }, + }, + required: ['contact_id'], + }, + handler: async (args: any) => { + await client.deleteContact(args.contact_id); + return { + content: [ + { + type: 'text', + text: `Contact ${args.contact_id} deleted successfully`, + }, + ], + }; + }, + }, + + freshdesk_search_contacts: { + description: 'Search contacts by query string', + parameters: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query (searches name, email, phone, mobile)', + }, + }, + required: ['query'], + }, + handler: async (args: any) => { + const results = await client.searchContacts(args.query); + return { + content: [ + { + type: 'text', + text: JSON.stringify(results, null, 2), + }, + ], + }; + }, + }, + + freshdesk_merge_contacts: { + description: 'Merge two contacts (secondary contact will be deleted)', + parameters: { + type: 'object', + properties: { + primary_contact_id: { + type: 'number', + description: 'Primary contact ID (will be kept)', + }, + secondary_contact_id: { + type: 'number', + description: 'Secondary contact ID (will be merged and deleted)', + }, + }, + required: ['primary_contact_id', 'secondary_contact_id'], + }, + handler: async (args: any) => { + const result = await client.mergeContacts(args.primary_contact_id, args.secondary_contact_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + freshdesk_make_agent: { + description: 'Convert a contact to an agent', + parameters: { + type: 'object', + properties: { + contact_id: { + type: 'number', + description: 'Contact ID', + }, + occasional: { + type: 'boolean', + description: 'Make occasional agent (limited access)', + }, + signature: { + type: 'string', + description: 'Agent email signature', + }, + ticket_scope: { + type: 'number', + description: 'Ticket scope: 1=Global, 2=Group, 3=Restricted', + }, + group_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Group IDs', + }, + role_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Role IDs', + }, + }, + required: ['contact_id'], + }, + handler: async (args: any) => { + const { contact_id, ...agentData } = args; + const agent = await client.makeAgent(contact_id, agentData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(agent, null, 2), + }, + ], + }; + }, + }, + + freshdesk_list_contact_fields: { + description: 'List all contact custom fields', + parameters: { + type: 'object', + properties: {}, + }, + handler: async () => { + const fields = await client.listContactFields(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(fields, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/forums-tools.ts b/servers/freshdesk/src/tools/forums-tools.ts new file mode 100644 index 0000000..4babfd6 --- /dev/null +++ b/servers/freshdesk/src/tools/forums-tools.ts @@ -0,0 +1,178 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerForumTools(client: FreshDeskClient) { + return { + freshdesk_list_forum_categories: { + description: 'List all forum categories', + parameters: { + type: 'object', + properties: {}, + }, + handler: async () => { + const categories = await client.listForumCategories(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(categories, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_forum_category: { + description: 'Get a specific forum category by ID', + parameters: { + type: 'object', + properties: { + category_id: { + type: 'number', + description: 'Category ID', + }, + }, + required: ['category_id'], + }, + handler: async (args: any) => { + const category = await client.getForumCategory(args.category_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(category, null, 2), + }, + ], + }; + }, + }, + + freshdesk_list_forums: { + description: 'List all forums, optionally filtered by category', + parameters: { + type: 'object', + properties: { + category_id: { + type: 'number', + description: 'Filter by category ID', + }, + }, + }, + handler: async (args: any) => { + const forums = await client.listForums(args.category_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(forums, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_forum: { + description: 'Get a specific forum by ID', + parameters: { + type: 'object', + properties: { + forum_id: { + type: 'number', + description: 'Forum ID', + }, + }, + required: ['forum_id'], + }, + handler: async (args: any) => { + const forum = await client.getForum(args.forum_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(forum, null, 2), + }, + ], + }; + }, + }, + + freshdesk_list_topics: { + description: 'List all topics in a forum', + parameters: { + type: 'object', + properties: { + forum_id: { + type: 'number', + description: 'Forum ID', + }, + filter: { + type: 'string', + description: 'Filter: new, stickies, my_topics, participated', + }, + sort_by: { + type: 'string', + description: 'Sort by: created_at, updated_at', + }, + order: { + type: 'string', + description: 'Order: asc, desc', + }, + }, + required: ['forum_id'], + }, + handler: async (args: any) => { + const { forum_id, ...params } = args; + const topics = await client.listTopics(forum_id, params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(topics, null, 2), + }, + ], + }; + }, + }, + + freshdesk_create_topic: { + description: 'Create a new topic in a forum', + parameters: { + type: 'object', + properties: { + forum_id: { + type: 'number', + description: 'Forum ID', + }, + title: { + type: 'string', + description: 'Topic title', + }, + message: { + type: 'string', + description: 'Topic message (HTML)', + }, + sticky: { + type: 'boolean', + description: 'Make sticky (pinned)', + }, + locked: { + type: 'boolean', + description: 'Lock topic (no more replies)', + }, + }, + required: ['forum_id', 'title', 'message'], + }, + handler: async (args: any) => { + const { forum_id, ...topicData } = args; + const topic = await client.createTopic(forum_id, topicData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(topic, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/groups-tools.ts b/servers/freshdesk/src/tools/groups-tools.ts new file mode 100644 index 0000000..5b5dd62 --- /dev/null +++ b/servers/freshdesk/src/tools/groups-tools.ts @@ -0,0 +1,176 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerGroupTools(client: FreshDeskClient) { + return { + freshdesk_list_groups: { + description: 'List all groups', + parameters: { + type: 'object', + properties: { + per_page: { + type: 'number', + description: 'Results per page (default 30, max 100)', + }, + max_results: { + type: 'number', + description: 'Maximum total results to return', + }, + }, + }, + handler: async (args: any) => { + const result = await client.listGroups(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_group: { + description: 'Get a specific group by ID', + parameters: { + type: 'object', + properties: { + group_id: { + type: 'number', + description: 'Group ID', + }, + }, + required: ['group_id'], + }, + handler: async (args: any) => { + const group = await client.getGroup(args.group_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(group, null, 2), + }, + ], + }; + }, + }, + + freshdesk_create_group: { + description: 'Create a new group', + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Group name', + }, + description: { + type: 'string', + description: 'Group description', + }, + escalate_to: { + type: 'number', + description: 'Agent ID to escalate to', + }, + unassigned_for: { + type: 'string', + description: 'Time before escalation (e.g., "30m", "2h")', + }, + business_calendar_id: { + type: 'number', + description: 'Business calendar ID', + }, + agent_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Agent IDs in this group', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const group = await client.createGroup(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(group, null, 2), + }, + ], + }; + }, + }, + + freshdesk_update_group: { + description: 'Update an existing group', + parameters: { + type: 'object', + properties: { + group_id: { + type: 'number', + description: 'Group ID', + }, + name: { + type: 'string', + description: 'Group name', + }, + description: { + type: 'string', + description: 'Group description', + }, + escalate_to: { + type: 'number', + description: 'Agent ID to escalate to', + }, + unassigned_for: { + type: 'string', + description: 'Time before escalation', + }, + agent_ids: { + type: 'array', + items: { type: 'number' }, + description: 'Agent IDs', + }, + }, + required: ['group_id'], + }, + handler: async (args: any) => { + const { group_id, ...updateData } = args; + const group = await client.updateGroup(group_id, updateData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(group, null, 2), + }, + ], + }; + }, + }, + + freshdesk_delete_group: { + description: 'Delete a group', + parameters: { + type: 'object', + properties: { + group_id: { + type: 'number', + description: 'Group ID', + }, + }, + required: ['group_id'], + }, + handler: async (args: any) => { + await client.deleteGroup(args.group_id); + return { + content: [ + { + type: 'text', + text: `Group ${args.group_id} deleted successfully`, + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/products-tools.ts b/servers/freshdesk/src/tools/products-tools.ts new file mode 100644 index 0000000..c08ba55 --- /dev/null +++ b/servers/freshdesk/src/tools/products-tools.ts @@ -0,0 +1,154 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerProductTools(client: FreshDeskClient) { + return { + freshdesk_list_products: { + description: 'List all products', + parameters: { + type: 'object', + properties: { + per_page: { + type: 'number', + description: 'Results per page (default 30, max 100)', + }, + max_results: { + type: 'number', + description: 'Maximum total results to return', + }, + }, + }, + handler: async (args: any) => { + const result = await client.listProducts(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_product: { + description: 'Get a specific product by ID', + parameters: { + type: 'object', + properties: { + product_id: { + type: 'number', + description: 'Product ID', + }, + }, + required: ['product_id'], + }, + handler: async (args: any) => { + const product = await client.getProduct(args.product_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(product, null, 2), + }, + ], + }; + }, + }, + + freshdesk_create_product: { + description: 'Create a new product', + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Product name', + }, + description: { + type: 'string', + description: 'Product description', + }, + primary_email: { + type: 'string', + description: 'Primary support email for this product', + }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const product = await client.createProduct(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(product, null, 2), + }, + ], + }; + }, + }, + + freshdesk_update_product: { + description: 'Update an existing product', + parameters: { + type: 'object', + properties: { + product_id: { + type: 'number', + description: 'Product ID', + }, + name: { + type: 'string', + description: 'Product name', + }, + description: { + type: 'string', + description: 'Product description', + }, + primary_email: { + type: 'string', + description: 'Primary support email', + }, + }, + required: ['product_id'], + }, + handler: async (args: any) => { + const { product_id, ...updateData } = args; + const product = await client.updateProduct(product_id, updateData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(product, null, 2), + }, + ], + }; + }, + }, + + freshdesk_delete_product: { + description: 'Delete a product', + parameters: { + type: 'object', + properties: { + product_id: { + type: 'number', + description: 'Product ID', + }, + }, + required: ['product_id'], + }, + handler: async (args: any) => { + await client.deleteProduct(args.product_id); + return { + content: [ + { + type: 'text', + text: `Product ${args.product_id} deleted successfully`, + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/reporting-tools.ts b/servers/freshdesk/src/tools/reporting-tools.ts new file mode 100644 index 0000000..edd4351 --- /dev/null +++ b/servers/freshdesk/src/tools/reporting-tools.ts @@ -0,0 +1,196 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerReportingTools(client: FreshDeskClient) { + return { + freshdesk_agent_performance: { + description: 'Get agent performance metrics (tickets resolved, response time, etc.)', + parameters: { + type: 'object', + properties: { + agent_id: { + type: 'number', + description: 'Agent ID', + }, + start_date: { + type: 'string', + description: 'Start date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date (ISO 8601)', + }, + }, + }, + handler: async (args: any) => { + // FreshDesk Analytics API endpoint + const params: any = {}; + if (args.start_date) params.start_date = args.start_date; + if (args.end_date) params.end_date = args.end_date; + if (args.agent_id) params.agent_id = args.agent_id; + + const metrics = await client.get('/analytics/reports/agent_performance', params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(metrics, null, 2), + }, + ], + }; + }, + }, + + freshdesk_group_performance: { + description: 'Get group performance metrics', + parameters: { + type: 'object', + properties: { + group_id: { + type: 'number', + description: 'Group ID', + }, + start_date: { + type: 'string', + description: 'Start date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date (ISO 8601)', + }, + }, + }, + handler: async (args: any) => { + const params: any = {}; + if (args.start_date) params.start_date = args.start_date; + if (args.end_date) params.end_date = args.end_date; + if (args.group_id) params.group_id = args.group_id; + + const metrics = await client.get('/analytics/reports/group_performance', params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(metrics, null, 2), + }, + ], + }; + }, + }, + + freshdesk_ticket_volume: { + description: 'Get ticket volume statistics over time', + parameters: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date (ISO 8601)', + }, + group_by: { + type: 'string', + description: 'Group by: day, week, month', + }, + }, + }, + handler: async (args: any) => { + const params: any = {}; + if (args.start_date) params.start_date = args.start_date; + if (args.end_date) params.end_date = args.end_date; + if (args.group_by) params.group_by = args.group_by; + + const metrics = await client.get('/analytics/reports/ticket_volume', params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(metrics, null, 2), + }, + ], + }; + }, + }, + + freshdesk_resolution_time: { + description: 'Get average ticket resolution time metrics', + parameters: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date (ISO 8601)', + }, + group_id: { + type: 'number', + description: 'Filter by group ID', + }, + agent_id: { + type: 'number', + description: 'Filter by agent ID', + }, + }, + }, + handler: async (args: any) => { + const params: any = {}; + if (args.start_date) params.start_date = args.start_date; + if (args.end_date) params.end_date = args.end_date; + if (args.group_id) params.group_id = args.group_id; + if (args.agent_id) params.agent_id = args.agent_id; + + const metrics = await client.get('/analytics/reports/resolution_time', params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(metrics, null, 2), + }, + ], + }; + }, + }, + + freshdesk_sla_compliance: { + description: 'Get SLA compliance metrics', + parameters: { + type: 'object', + properties: { + start_date: { + type: 'string', + description: 'Start date (ISO 8601)', + }, + end_date: { + type: 'string', + description: 'End date (ISO 8601)', + }, + sla_policy_id: { + type: 'number', + description: 'Filter by SLA policy ID', + }, + }, + }, + handler: async (args: any) => { + const params: any = {}; + if (args.start_date) params.start_date = args.start_date; + if (args.end_date) params.end_date = args.end_date; + if (args.sla_policy_id) params.sla_policy_id = args.sla_policy_id; + + const metrics = await client.get('/analytics/reports/sla_compliance', params); + return { + content: [ + { + type: 'text', + text: JSON.stringify(metrics, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/roles-tools.ts b/servers/freshdesk/src/tools/roles-tools.ts new file mode 100644 index 0000000..709bbd6 --- /dev/null +++ b/servers/freshdesk/src/tools/roles-tools.ts @@ -0,0 +1,49 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerRoleTools(client: FreshDeskClient) { + return { + freshdesk_list_roles: { + description: 'List all roles', + parameters: { + type: 'object', + properties: {}, + }, + handler: async () => { + const roles = await client.listRoles(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(roles, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_role: { + description: 'Get a specific role by ID', + parameters: { + type: 'object', + properties: { + role_id: { + type: 'number', + description: 'Role ID', + }, + }, + required: ['role_id'], + }, + handler: async (args: any) => { + const role = await client.getRole(args.role_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(role, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/solutions-tools.ts b/servers/freshdesk/src/tools/solutions-tools.ts new file mode 100644 index 0000000..a9e1623 --- /dev/null +++ b/servers/freshdesk/src/tools/solutions-tools.ts @@ -0,0 +1,243 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerSolutionTools(client: FreshDeskClient) { + return { + freshdesk_list_solution_categories: { + description: 'List all solution (knowledge base) categories', + parameters: { + type: 'object', + properties: {}, + }, + handler: async () => { + const categories = await client.listSolutionCategories(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(categories, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_solution_category: { + description: 'Get a specific solution category by ID', + parameters: { + type: 'object', + properties: { + category_id: { + type: 'number', + description: 'Category ID', + }, + }, + required: ['category_id'], + }, + handler: async (args: any) => { + const category = await client.getSolutionCategory(args.category_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(category, null, 2), + }, + ], + }; + }, + }, + + freshdesk_list_solution_folders: { + description: 'List all folders in a solution category', + parameters: { + type: 'object', + properties: { + category_id: { + type: 'number', + description: 'Category ID', + }, + }, + required: ['category_id'], + }, + handler: async (args: any) => { + const folders = await client.listSolutionFolders(args.category_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(folders, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_solution_folder: { + description: 'Get a specific solution folder by ID', + parameters: { + type: 'object', + properties: { + folder_id: { + type: 'number', + description: 'Folder ID', + }, + }, + required: ['folder_id'], + }, + handler: async (args: any) => { + const folder = await client.getSolutionFolder(args.folder_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(folder, null, 2), + }, + ], + }; + }, + }, + + freshdesk_list_articles: { + description: 'List all articles in a solution folder', + parameters: { + type: 'object', + properties: { + folder_id: { + type: 'number', + description: 'Folder ID', + }, + }, + required: ['folder_id'], + }, + handler: async (args: any) => { + const articles = await client.listArticles(args.folder_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(articles, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_article: { + description: 'Get a specific knowledge base article by ID', + parameters: { + type: 'object', + properties: { + article_id: { + type: 'number', + description: 'Article ID', + }, + }, + required: ['article_id'], + }, + handler: async (args: any) => { + const article = await client.getArticle(args.article_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(article, null, 2), + }, + ], + }; + }, + }, + + freshdesk_create_article: { + description: 'Create a new knowledge base article', + parameters: { + type: 'object', + properties: { + folder_id: { + type: 'number', + description: 'Folder ID', + }, + title: { + type: 'string', + description: 'Article title', + }, + description: { + type: 'string', + description: 'Article content (HTML)', + }, + status: { + type: 'number', + description: 'Status: 1=Draft, 2=Published', + }, + article_type: { + type: 'number', + description: 'Article type: 1=Permanent, 2=Workaround', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Tags', + }, + seo_data: { + type: 'object', + description: 'SEO metadata', + }, + }, + required: ['folder_id', 'title', 'description'], + }, + handler: async (args: any) => { + const { folder_id, ...articleData } = args; + const article = await client.createArticle(folder_id, articleData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(article, null, 2), + }, + ], + }; + }, + }, + + freshdesk_update_article: { + description: 'Update an existing knowledge base article', + parameters: { + type: 'object', + properties: { + article_id: { + type: 'number', + description: 'Article ID', + }, + title: { + type: 'string', + description: 'Article title', + }, + description: { + type: 'string', + description: 'Article content (HTML)', + }, + status: { + type: 'number', + description: 'Status: 1=Draft, 2=Published', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Tags', + }, + }, + required: ['article_id'], + }, + handler: async (args: any) => { + const { article_id, ...updateData } = args; + const article = await client.updateArticle(article_id, updateData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(article, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/surveys-tools.ts b/servers/freshdesk/src/tools/surveys-tools.ts new file mode 100644 index 0000000..4a4ad48 --- /dev/null +++ b/servers/freshdesk/src/tools/surveys-tools.ts @@ -0,0 +1,60 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerSurveyTools(client: FreshDeskClient) { + return { + freshdesk_list_surveys: { + description: 'List all satisfaction surveys', + parameters: { + type: 'object', + properties: {}, + }, + handler: async () => { + const surveys = await client.listSurveys(); + return { + content: [ + { + type: 'text', + text: JSON.stringify(surveys, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_satisfaction_ratings: { + description: 'Get satisfaction ratings with optional filters', + parameters: { + type: 'object', + properties: { + created_since: { + type: 'string', + description: 'Filter ratings created after this timestamp (ISO 8601)', + }, + rating: { + type: 'number', + description: 'Filter by rating value (1-5)', + }, + user_id: { + type: 'number', + description: 'Filter by user ID', + }, + ticket_id: { + type: 'number', + description: 'Filter by ticket ID', + }, + }, + }, + handler: async (args: any) => { + const ratings = await client.getSatisfactionRatings(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(ratings, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/tools/tickets-tools.ts b/servers/freshdesk/src/tools/tickets-tools.ts new file mode 100644 index 0000000..cd2f64d --- /dev/null +++ b/servers/freshdesk/src/tools/tickets-tools.ts @@ -0,0 +1,486 @@ +import type { FreshDeskClient } from '../api/client.js'; + +export function registerTicketTools(client: FreshDeskClient) { + return { + freshdesk_list_tickets: { + description: 'List all tickets with optional filters (status, priority, requester_id, etc.)', + parameters: { + type: 'object', + properties: { + filter: { + type: 'string', + description: 'Filter predefined queries (new_and_my_open, watching, spam, deleted)', + }, + requester_id: { + type: 'number', + description: 'Filter by requester contact ID', + }, + email: { + type: 'string', + description: 'Filter by requester email', + }, + company_id: { + type: 'number', + description: 'Filter by company ID', + }, + updated_since: { + type: 'string', + description: 'Filter tickets updated after this timestamp (ISO 8601)', + }, + per_page: { + type: 'number', + description: 'Results per page (default 30, max 100)', + }, + max_results: { + type: 'number', + description: 'Maximum total results to return', + }, + }, + }, + handler: async (args: any) => { + const result = await client.listTickets(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; + }, + }, + + freshdesk_get_ticket: { + description: 'Get a specific ticket by ID', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + }, + required: ['ticket_id'], + }, + handler: async (args: any) => { + const ticket = await client.getTicket(args.ticket_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(ticket, null, 2), + }, + ], + }; + }, + }, + + freshdesk_create_ticket: { + description: 'Create a new ticket', + parameters: { + type: 'object', + properties: { + subject: { + type: 'string', + description: 'Ticket subject', + }, + description: { + type: 'string', + description: 'Ticket description (HTML)', + }, + email: { + type: 'string', + description: 'Requester email (required if no requester_id)', + }, + requester_id: { + type: 'number', + description: 'Requester contact ID', + }, + status: { + type: 'number', + description: 'Status: 2=Open, 3=Pending, 4=Resolved, 5=Closed', + }, + priority: { + type: 'number', + description: 'Priority: 1=Low, 2=Medium, 3=High, 4=Urgent', + }, + source: { + type: 'number', + description: 'Source: 1=Email, 2=Portal, 3=Phone, 7=Chat, 8=Feedback, 9=Outbound Email', + }, + type: { + type: 'string', + description: 'Ticket type', + }, + responder_id: { + type: 'number', + description: 'Agent ID to assign', + }, + group_id: { + type: 'number', + description: 'Group ID to assign', + }, + company_id: { + type: 'number', + description: 'Company ID', + }, + product_id: { + type: 'number', + description: 'Product ID', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Tags', + }, + cc_emails: { + type: 'array', + items: { type: 'string' }, + description: 'CC email addresses', + }, + custom_fields: { + type: 'object', + description: 'Custom field values', + }, + }, + required: ['subject', 'description'], + }, + handler: async (args: any) => { + const ticket = await client.createTicket(args); + return { + content: [ + { + type: 'text', + text: JSON.stringify(ticket, null, 2), + }, + ], + }; + }, + }, + + freshdesk_update_ticket: { + description: 'Update an existing ticket', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + subject: { + type: 'string', + description: 'Ticket subject', + }, + description: { + type: 'string', + description: 'Ticket description', + }, + status: { + type: 'number', + description: 'Status: 2=Open, 3=Pending, 4=Resolved, 5=Closed', + }, + priority: { + type: 'number', + description: 'Priority: 1=Low, 2=Medium, 3=High, 4=Urgent', + }, + type: { + type: 'string', + description: 'Ticket type', + }, + responder_id: { + type: 'number', + description: 'Agent ID', + }, + group_id: { + type: 'number', + description: 'Group ID', + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'Tags', + }, + custom_fields: { + type: 'object', + description: 'Custom field values', + }, + }, + required: ['ticket_id'], + }, + handler: async (args: any) => { + const { ticket_id, ...updateData } = args; + const ticket = await client.updateTicket(ticket_id, updateData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(ticket, null, 2), + }, + ], + }; + }, + }, + + freshdesk_delete_ticket: { + description: 'Delete a ticket (moves to trash)', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + }, + required: ['ticket_id'], + }, + handler: async (args: any) => { + await client.deleteTicket(args.ticket_id); + return { + content: [ + { + type: 'text', + text: `Ticket ${args.ticket_id} deleted successfully`, + }, + ], + }; + }, + }, + + freshdesk_restore_ticket: { + description: 'Restore a deleted ticket from trash', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + }, + required: ['ticket_id'], + }, + handler: async (args: any) => { + const ticket = await client.restoreTicket(args.ticket_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(ticket, null, 2), + }, + ], + }; + }, + }, + + freshdesk_list_conversations: { + description: 'List all conversations (replies and notes) for a ticket', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + }, + required: ['ticket_id'], + }, + handler: async (args: any) => { + const conversations = await client.listConversations(args.ticket_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(conversations, null, 2), + }, + ], + }; + }, + }, + + freshdesk_add_reply: { + description: 'Add a public reply to a ticket', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + body: { + type: 'string', + description: 'Reply body (HTML)', + }, + from_email: { + type: 'string', + description: 'From email (must be an agent email)', + }, + user_id: { + type: 'number', + description: 'Agent user ID', + }, + cc_emails: { + type: 'array', + items: { type: 'string' }, + description: 'CC email addresses', + }, + bcc_emails: { + type: 'array', + items: { type: 'string' }, + description: 'BCC email addresses', + }, + }, + required: ['ticket_id', 'body'], + }, + handler: async (args: any) => { + const { ticket_id, ...replyData } = args; + const reply = await client.addReply(ticket_id, replyData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(reply, null, 2), + }, + ], + }; + }, + }, + + freshdesk_add_note: { + description: 'Add a private note to a ticket', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + body: { + type: 'string', + description: 'Note body (HTML)', + }, + user_id: { + type: 'number', + description: 'Agent user ID', + }, + notify_emails: { + type: 'array', + items: { type: 'string' }, + description: 'Email addresses to notify', + }, + private: { + type: 'boolean', + description: 'Make note private (default true)', + }, + }, + required: ['ticket_id', 'body'], + }, + handler: async (args: any) => { + const { ticket_id, ...noteData } = args; + const note = await client.addNote(ticket_id, noteData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(note, null, 2), + }, + ], + }; + }, + }, + + freshdesk_list_time_entries: { + description: 'List all time entries for a ticket', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + }, + required: ['ticket_id'], + }, + handler: async (args: any) => { + const entries = await client.listTimeEntries(args.ticket_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(entries, null, 2), + }, + ], + }; + }, + }, + + freshdesk_add_time_entry: { + description: 'Add a time entry to a ticket', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + time_spent: { + type: 'string', + description: 'Time spent (e.g., "01:30" for 1 hour 30 minutes)', + }, + billable: { + type: 'boolean', + description: 'Is billable', + }, + note: { + type: 'string', + description: 'Note about time entry', + }, + agent_id: { + type: 'number', + description: 'Agent ID', + }, + executed_at: { + type: 'string', + description: 'When the work was done (ISO 8601)', + }, + }, + required: ['ticket_id', 'time_spent'], + }, + handler: async (args: any) => { + const { ticket_id, ...entryData } = args; + const entry = await client.addTimeEntry(ticket_id, entryData); + return { + content: [ + { + type: 'text', + text: JSON.stringify(entry, null, 2), + }, + ], + }; + }, + }, + + freshdesk_list_watchers: { + description: 'List all watchers for a ticket', + parameters: { + type: 'object', + properties: { + ticket_id: { + type: 'number', + description: 'Ticket ID', + }, + }, + required: ['ticket_id'], + }, + handler: async (args: any) => { + const watchers = await client.listWatchers(args.ticket_id); + return { + content: [ + { + type: 'text', + text: JSON.stringify(watchers, null, 2), + }, + ], + }; + }, + }, + }; +} diff --git a/servers/freshdesk/src/types/index.ts b/servers/freshdesk/src/types/index.ts new file mode 100644 index 0000000..3d06704 --- /dev/null +++ b/servers/freshdesk/src/types/index.ts @@ -0,0 +1,277 @@ +// FreshDesk API Types + +export interface FreshDeskConfig { + domain: string; + apiKey: string; +} + +export interface Ticket { + id: number; + subject: string; + description: string; + description_text?: string; + status: number; + priority: number; + type?: string; + source: number; + requester_id: number; + responder_id?: number; + group_id?: number; + company_id?: number; + product_id?: number; + tags?: string[]; + cc_emails?: string[]; + fwd_emails?: string[]; + email_config_id?: number; + fr_escalated?: boolean; + spam?: boolean; + due_by?: string; + fr_due_by?: string; + is_escalated?: boolean; + custom_fields?: Record; + created_at: string; + updated_at: string; + associated_tickets_count?: number; + attachments?: Attachment[]; +} + +export interface Contact { + id: number; + name: string; + email: string; + phone?: string; + mobile?: string; + twitter_id?: string; + unique_external_id?: string; + company_id?: number; + description?: string; + job_title?: string; + language?: string; + time_zone?: string; + tags?: string[]; + address?: string; + custom_fields?: Record; + active?: boolean; + deleted?: boolean; + created_at: string; + updated_at: string; +} + +export interface Company { + id: number; + name: string; + description?: string; + domains?: string[]; + note?: string; + custom_fields?: Record; + health_score?: string; + account_tier?: string; + renewal_date?: string; + industry?: string; + created_at: string; + updated_at: string; +} + +export interface Agent { + id: number; + available?: boolean; + occasional?: boolean; + signature?: string; + ticket_scope?: number; + group_ids?: number[]; + role_ids?: number[]; + created_at: string; + updated_at: string; + contact: Contact; +} + +export interface Group { + id: number; + name: string; + description?: string; + escalate_to?: number; + unassigned_for?: string; + business_calendar_id?: number; + agent_ids?: number[]; + created_at: string; + updated_at: string; +} + +export interface Role { + id: number; + name: string; + description?: string; + default?: boolean; + created_at: string; + updated_at: string; +} + +export interface Product { + id: number; + name: string; + description?: string; + primary_email?: string; + created_at: string; + updated_at: string; +} + +export interface Conversation { + id: number; + body: string; + body_text?: string; + user_id: number; + ticket_id: number; + to_emails?: string[]; + from_email?: string; + cc_emails?: string[]; + bcc_emails?: string[]; + incoming?: boolean; + private?: boolean; + source?: number; + category?: number; + support_email?: string; + attachments?: Attachment[]; + created_at: string; + updated_at: string; +} + +export interface TimeEntry { + id: number; + billable?: boolean; + timer_running?: boolean; + time_spent?: string; + executed_at?: string; + task_id?: number; + note?: string; + agent_id: number; + custom_fields?: Record; + created_at: string; + updated_at: string; +} + +export interface Attachment { + id: number; + name: string; + content_type: string; + size: number; + attachment_url: string; + created_at: string; + updated_at: string; +} + +export interface Category { + id: number; + name: string; + description?: string; + created_at: string; + updated_at: string; +} + +export interface Forum { + id: number; + category_id: number; + name: string; + description?: string; + forum_type: number; + forum_visibility: number; + created_at: string; + updated_at: string; +} + +export interface Topic { + id: number; + forum_id: number; + title: string; + message?: string; + user_id: number; + sticky?: boolean; + locked?: boolean; + created_at: string; + updated_at: string; +} + +export interface SolutionCategory { + id: number; + name: string; + description?: string; + visible_in_portals?: number[]; + created_at: string; + updated_at: string; +} + +export interface SolutionFolder { + id: number; + category_id: number; + name: string; + description?: string; + visibility: number; + created_at: string; + updated_at: string; +} + +export interface SolutionArticle { + id: number; + folder_id: number; + title: string; + description: string; + status: number; + article_type?: number; + tags?: string[]; + seo_data?: Record; + created_at: string; + updated_at: string; +} + +export interface CannedResponse { + id: number; + title: string; + content: string; + content_html?: string; + group_ids?: number[]; + visibility?: number; + created_at: string; + updated_at: string; +} + +export interface SatisfactionRating { + id: number; + survey_id: number; + user_id: number; + ticket_id: number; + rating: number; + feedback?: string; + created_at: string; + updated_at: string; +} + +export interface TicketField { + id: number; + name: string; + label: string; + description?: string; + type: string; + default?: boolean; + required_for_closure?: boolean; + required_for_agents?: boolean; + required_for_customers?: boolean; + customers_can_edit?: boolean; + label_for_customers?: string; + position: number; + choices?: Record; + created_at: string; + updated_at: string; +} + +export interface PaginatedResponse { + results: T[]; + total: number; +} + +export interface FreshDeskError { + description: string; + errors?: Array<{ + field: string; + message: string; + code: string; + }>; +} diff --git a/servers/freshdesk/tsconfig.json b/servers/freshdesk/tsconfig.json index de6431e..156b6d5 100644 --- a/servers/freshdesk/tsconfig.json +++ b/servers/freshdesk/tsconfig.json @@ -1,14 +1,19 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/servers/lightspeed/README.md b/servers/lightspeed/README.md index bb4c3b2..0e1030e 100644 --- a/servers/lightspeed/README.md +++ b/servers/lightspeed/README.md @@ -1,296 +1,438 @@ -# Lightspeed Retail MCP Server +# Lightspeed MCP Server -Complete Model Context Protocol (MCP) server for Lightspeed Retail (X-Series/R-Series). Provides comprehensive point-of-sale, inventory management, and retail analytics capabilities for Claude Desktop and other MCP clients. +A comprehensive Model Context Protocol (MCP) server for Lightspeed POS and eCommerce platform integration. This server provides extensive tools for managing retail and restaurant operations including products, inventory, customers, sales, orders, employees, and more. + +## Overview + +Lightspeed is a cloud-based POS and eCommerce platform used by retailers and restaurants worldwide. This MCP server provides AI assistants with direct access to Lightspeed's API, enabling automated store management, inventory control, sales analysis, and customer relationship management. ## Features -### 🛍️ Products & Inventory -- List, create, update, and delete products -- Manage product variants and images -- Track inventory levels across shops -- Handle stock transfers and adjustments +### 🛍️ Product Management +- **60+ MCP Tools** covering all aspects of retail/restaurant operations +- Full CRUD operations for products, inventory, customers, sales, and orders +- Advanced search and filtering capabilities +- Bulk operations support + +### 📊 Analytics & Reporting +- Real-time sales dashboards +- Inventory valuation and stock reports +- Customer analytics and segmentation +- Employee performance tracking +- Top products and revenue analysis + +### 💼 Business Operations - Purchase order management -- Supplier/vendor management - -### 💰 Sales & Transactions -- Create and manage sales transactions -- Add line items to sales -- Process payments and refunds -- Track completed and pending sales -- Register/till management (open/close) - -### 👥 Customers -- Customer database management -- Search and filter customers -- Track purchase history +- Supplier relationship management +- Discount and promotion tools - Loyalty program integration -- Contact information management - -### 👔 Employees -- Employee roster management -- Time clock functionality (clock in/out) -- Employee sales performance tracking -- Role-based access control - -### 📊 Reporting -- Sales summary reports -- Inventory valuation -- Product performance analysis -- Employee sales reports -- Custom date range filtering - -### ⚙️ Configuration -- Product categories and hierarchies -- Discount and promotion management -- Tax class configuration - Multi-location support +### 🎨 16 React Applications +Beautiful, dark-themed UI applications for: +- Product browsing and management +- Inventory dashboard and tracking +- Customer relationship management +- Sales analytics and reporting +- Purchase order processing +- Employee performance monitoring +- Category hierarchymanagement +- Discount creation and management +- Loyalty program dashboard +- Analytics suite with visualizations +- POS simulator for testing +- Quick sale interface +- Stock transfer between locations +- Price management tools +- Supplier portal +- Tax calculation utilities + ## Installation ```bash npm install @mcpengine/lightspeed-mcp-server ``` +Or install from source: + +```bash +git clone https://github.com/mcpengine/mcpengine +cd mcpengine/servers/lightspeed +npm install +npm run build +``` + ## Configuration ### Environment Variables +Create a `.env` file or export these environment variables: + ```bash -export LIGHTSPEED_ACCOUNT_ID=your_account_id -export LIGHTSPEED_ACCESS_TOKEN=your_oauth_token -# Optional: Custom API URL (defaults to official Lightspeed API) -export LIGHTSPEED_API_URL=https://api.lightspeedapp.com/API/V3/Account/YOUR_ACCOUNT +# Required +LIGHTSPEED_ACCOUNT_ID=123456 # Your Lightspeed account ID +LIGHTSPEED_API_KEY=your_api_key_here # Your Lightspeed API key + +# Optional +LIGHTSPEED_API_SECRET=secret # API secret (if required) +LIGHTSPEED_BASE_URL=https://api.lightspeedapp.com/API/V3 # Custom API URL +LIGHTSPEED_TYPE=retail # "retail" or "restaurant" ``` -### Claude Desktop Configuration +### Getting Lightspeed API Credentials -Add to your `claude_desktop_config.json`: +1. Log into your Lightspeed account +2. Navigate to Settings → API Management +3. Create a new API key +4. Copy your Account ID and API Key +5. Set the appropriate permissions for your use case + +## Usage + +### As MCP Server + +Add to your MCP client configuration (e.g., Claude Desktop): ```json { "mcpServers": { "lightspeed": { - "command": "npx", - "args": ["-y", "@mcpengine/lightspeed-mcp-server"], + "command": "lightspeed-mcp", "env": { - "LIGHTSPEED_ACCOUNT_ID": "your_account_id", - "LIGHTSPEED_ACCESS_TOKEN": "your_oauth_token" + "LIGHTSPEED_ACCOUNT_ID": "123456", + "LIGHTSPEED_API_KEY": "your_api_key_here" } } } } ``` -## OAuth Setup +### Standalone -Lightspeed uses OAuth2 for authentication. To get your access token: +```bash +# Set environment variables +export LIGHTSPEED_ACCOUNT_ID=123456 +export LIGHTSPEED_API_KEY=your_api_key_here -1. **Create Lightspeed API Application** - - Go to Lightspeed Developer Portal - - Register a new application - - Note your Client ID and Client Secret +# Run the server +lightspeed-mcp +``` -2. **Generate Access Token** - ```bash - curl -X POST https://cloud.lightspeedapp.com/oauth/access_token.php \ - -d "client_id=YOUR_CLIENT_ID" \ - -d "client_secret=YOUR_CLIENT_SECRET" \ - -d "code=AUTHORIZATION_CODE" \ - -d "grant_type=authorization_code" - ``` +### Development Mode -3. **Get Account ID** - - Available in Lightspeed admin panel - - Or via API: `GET https://api.lightspeedapp.com/API/Account.json` +```bash +npm run dev +``` -## Tools (54 Total) +## Available Tools -### Products (8 tools) -- `lightspeed_list_products` - List all products -- `lightspeed_get_product` - Get product details +### Products (7 tools) +- `lightspeed_list_products` - List all products with pagination and filtering +- `lightspeed_get_product` - Get single product details - `lightspeed_create_product` - Create new product -- `lightspeed_update_product` - Update product -- `lightspeed_delete_product` - Delete/archive product -- `lightspeed_list_product_variants` - List product variants -- `lightspeed_list_product_images` - List product images -- `lightspeed_update_product_inventory` - Update inventory quantity +- `lightspeed_update_product` - Update existing product +- `lightspeed_delete_product` - Delete product +- `lightspeed_archive_product` - Archive product (soft delete) +- `lightspeed_search_products_by_sku` - Search by SKU -### Sales (8 tools) -- `lightspeed_list_sales` - List sales transactions -- `lightspeed_get_sale` - Get sale details -- `lightspeed_create_sale` - Create new sale -- `lightspeed_add_sale_line_item` - Add item to sale -- `lightspeed_list_sale_payments` - List sale payments -- `lightspeed_process_payment` - Process payment -- `lightspeed_refund_sale` - Create refund -- `lightspeed_complete_sale` - Mark sale as complete +### Inventory (6 tools) +- `lightspeed_get_product_inventory` - Get inventory levels across locations +- `lightspeed_update_inventory` - Set inventory quantity +- `lightspeed_adjust_inventory` - Adjust inventory by relative amount +- `lightspeed_set_reorder_point` - Configure low stock alerts +- `lightspeed_check_low_stock` - Find products below reorder point +- `lightspeed_inventory_transfer` - Transfer stock between locations ### Customers (7 tools) - `lightspeed_list_customers` - List all customers - `lightspeed_get_customer` - Get customer details -- `lightspeed_create_customer` - Create new customer -- `lightspeed_update_customer` - Update customer -- `lightspeed_delete_customer` - Delete customer -- `lightspeed_search_customers` - Search customers -- `lightspeed_get_customer_loyalty` - Get loyalty info +- `lightspeed_create_customer` - Add new customer +- `lightspeed_update_customer` - Update customer information +- `lightspeed_delete_customer` - Remove customer +- `lightspeed_search_customers` - Search by name, email, phone +- `lightspeed_get_customer_by_email` - Find customer by email -### Inventory (8 tools) -- `lightspeed_list_inventory` - List inventory counts -- `lightspeed_get_item_inventory` - Get item inventory -- `lightspeed_update_inventory_count` - Update inventory -- `lightspeed_transfer_stock` - Transfer between shops -- `lightspeed_list_inventory_adjustments` - List adjustments -- `lightspeed_list_suppliers` - List suppliers/vendors -- `lightspeed_create_purchase_order` - Create PO -- `lightspeed_list_purchase_orders` - List POs +### Sales & Transactions (8 tools) +- `lightspeed_list_sales` - List sales with date range filtering +- `lightspeed_get_sale` - Get sale details with line items +- `lightspeed_create_sale` - Create new sale +- `lightspeed_update_sale` - Update sale +- `lightspeed_void_sale` - Void transaction +- `lightspeed_get_sales_by_customer` - Customer purchase history +- `lightspeed_get_sales_by_employee` - Employee sales performance +- `lightspeed_calculate_daily_sales` - Daily sales totals -### Registers (5 tools) -- `lightspeed_list_registers` - List all registers -- `lightspeed_get_register` - Get register details -- `lightspeed_open_register` - Open register (till) -- `lightspeed_close_register` - Close register -- `lightspeed_get_cash_counts` - Get cash counts +### Orders (7 tools) +- `lightspeed_list_orders` - List purchase orders +- `lightspeed_get_order` - Get order details +- `lightspeed_create_order` - Create purchase order +- `lightspeed_update_order` - Update order +- `lightspeed_delete_order` - Delete order +- `lightspeed_receive_order` - Mark order as received +- `lightspeed_cancel_order` - Cancel purchase order ### Employees (6 tools) - `lightspeed_list_employees` - List all employees - `lightspeed_get_employee` - Get employee details -- `lightspeed_create_employee` - Create employee +- `lightspeed_create_employee` - Add new employee - `lightspeed_update_employee` - Update employee -- `lightspeed_list_time_entries` - List time entries -- `lightspeed_clock_in` - Clock in employee -- `lightspeed_clock_out` - Clock out employee +- `lightspeed_delete_employee` - Remove employee +- `lightspeed_search_employees` - Search employees -### Categories (5 tools) -- `lightspeed_list_categories` - List categories +### Categories (6 tools) +- `lightspeed_list_categories` - List all categories - `lightspeed_get_category` - Get category details -- `lightspeed_create_category` - Create category +- `lightspeed_create_category` - Create new category - `lightspeed_update_category` - Update category - `lightspeed_delete_category` - Delete category +- `lightspeed_get_category_tree` - Get category hierarchy -### Discounts (5 tools) -- `lightspeed_list_discounts` - List discounts +### Suppliers (6 tools) +- `lightspeed_list_suppliers` - List all suppliers +- `lightspeed_get_supplier` - Get supplier details +- `lightspeed_create_supplier` - Add new supplier +- `lightspeed_update_supplier` - Update supplier +- `lightspeed_delete_supplier` - Remove supplier +- `lightspeed_search_suppliers` - Search suppliers + +### Discounts (6 tools) +- `lightspeed_list_discounts` - List all discounts - `lightspeed_get_discount` - Get discount details -- `lightspeed_create_discount` - Create discount +- `lightspeed_create_discount` - Create percentage or fixed discount - `lightspeed_update_discount` - Update discount -- `lightspeed_delete_discount` - Delete discount +- `lightspeed_delete_discount` - Remove discount +- `lightspeed_get_active_discounts` - Get currently active discounts -### Taxes (4 tools) -- `lightspeed_list_taxes` - List tax classes -- `lightspeed_get_tax` - Get tax details -- `lightspeed_create_tax` - Create tax class -- `lightspeed_update_tax` - Update tax class +### Loyalty Programs (5 tools) +- `lightspeed_get_customer_loyalty` - Get customer loyalty points +- `lightspeed_add_loyalty_points` - Add points to customer +- `lightspeed_redeem_loyalty_points` - Redeem customer points +- `lightspeed_calculate_loyalty_points` - Calculate points for purchase +- `lightspeed_get_top_loyalty_customers` - Find top loyalty members -### Reporting (4 tools) -- `lightspeed_sales_summary` - Sales summary report -- `lightspeed_inventory_value` - Inventory valuation -- `lightspeed_product_performance` - Product performance -- `lightspeed_employee_sales` - Employee sales report +### Reporting & Analytics (5 tools) +- `lightspeed_sales_report` - Comprehensive sales report with metrics +- `lightspeed_inventory_report` - Inventory valuation and stock levels +- `lightspeed_customer_report` - Customer acquisition and retention +- `lightspeed_employee_performance` - Employee sales performance +- `lightspeed_top_selling_products` - Best selling products analysis -## MCP Apps (17 Apps) +### Shops & Configuration (7 tools) +- `lightspeed_list_shops` - List all shop locations +- `lightspeed_get_shop` - Get shop details +- `lightspeed_list_registers` - List POS registers +- `lightspeed_get_manufacturers` - List manufacturers +- `lightspeed_create_manufacturer` - Add manufacturer +- `lightspeed_get_tax_categories` - List tax categories +- `lightspeed_get_payment_types` - List payment types -Pre-built UI applications accessible via MCP prompts: +**Total: 66 MCP Tools** -### Products -- `product-dashboard` - Inventory overview -- `product-detail` - Detailed product view -- `product-grid` - Filterable product list -- `category-manager` - Category management +## React Applications -### Sales -- `sales-dashboard` - Sales overview -- `sales-detail` - Transaction details -- `sales-report` - Sales analytics +All applications feature a modern dark theme (bg-gray-900) and responsive design: -### Customers -- `customer-detail` - Customer profile -- `customer-grid` - Customer list +1. **Product Browser** - Search and browse product catalog +2. **Inventory Dashboard** - Real-time stock levels and alerts +3. **Customer Manager** - CRM and customer database +4. **Sales Dashboard** - Sales metrics and analytics +5. **Order Manager** - Purchase order tracking +6. **Employee Tracker** - Employee management and performance +7. **Category Editor** - Manage category hierarchy +8. **Discount Creator** - Create and manage promotions +9. **Loyalty Dashboard** - Loyalty program overview +10. **Analytics Suite** - Advanced analytics and visualizations +11. **POS Simulator** - Test POS transactions +12. **Quick Sale** - Rapid sale entry interface +13. **Stock Transfer** - Inter-location inventory transfers +14. **Price Manager** - Bulk price management +15. **Supplier Portal** - Supplier relationship management +16. **Tax Calculator** - Tax calculation utilities -### Inventory -- `inventory-tracker` - Stock levels -- `inventory-adjustments` - Adjustment tracking -- `purchase-orders` - PO management +Access apps at: `dist/ui/{app-name}/index.html` after building. -### Operations -- `register-manager` - Register management -- `employee-dashboard` - Employee overview -- `discount-manager` - Discount configuration -- `tax-settings` - Tax configuration -- `product-performance` - Performance analytics +## API Coverage -## Example Usage +This server supports both Lightspeed Retail (X-Series) and Restaurant (R-Series) platforms: -```typescript -// In Claude Desktop, you can now use natural language: +### Retail X-Series +- Products (Items) +- Inventory (ItemShops) +- Customers +- Sales +- Purchase Orders +- Employees +- Categories +- Suppliers (Vendors) +- Discounts +- Shops & Registers -"Show me the product dashboard" -"Create a new customer named John Doe with email john@example.com" -"List all sales from yesterday" -"What are the top 10 selling products this month?" -"Transfer 50 units of item #123 from shop 1 to shop 2" -"Generate a sales summary report for last week" -"Clock in employee #456" -"Show me inventory levels for shop 1" -``` - -## API Reference - -### Lightspeed API Documentation -- [Official API Docs](https://developers.lightspeedhq.com/retail/introduction/introduction/) -- [Authentication](https://developers.lightspeedhq.com/retail/authentication/authentication/) -- [Rate Limits](https://developers.lightspeedhq.com/retail/introduction/rate-limiting/) - -### Rate Limiting -- Default: 10 requests/second per account -- Burst: up to 60 requests -- This server includes automatic rate limit handling and retry logic +### Restaurant R-Series +Compatible with most endpoints; specific R-Series features coming soon. ## Development -```bash -# Clone repository -git clone https://github.com/BusyBee3333/mcpengine.git -cd mcpengine/servers/lightspeed +### Project Structure -# Install dependencies -npm install - -# Build -npm run build - -# Run locally -export LIGHTSPEED_ACCOUNT_ID=your_account_id -export LIGHTSPEED_ACCESS_TOKEN=your_token -npm start ``` +lightspeed/ +├── src/ +│ ├── clients/ +│ │ └── lightspeed.ts # API client +│ ├── types/ +│ │ └── index.ts # TypeScript types +│ ├── tools/ +│ │ ├── products.ts # Product tools +│ │ ├── inventory.ts # Inventory tools +│ │ ├── customers.ts # Customer tools +│ │ ├── sales.ts # Sales tools +│ │ ├── orders.ts # Order tools +│ │ ├── employees.ts # Employee tools +│ │ ├── categories.ts # Category tools +│ │ ├── suppliers.ts # Supplier tools +│ │ ├── discounts.ts # Discount tools +│ │ ├── loyalty.ts # Loyalty tools +│ │ ├── reporting.ts # Reporting tools +│ │ └── shops.ts # Shop tools +│ ├── ui/ +│ │ └── react-app/ # 16 React applications +│ ├── server.ts # MCP server setup +│ └── main.ts # Entry point +├── dist/ # Compiled output +├── package.json +├── tsconfig.json +└── README.md +``` + +### Building + +```bash +npm run build +``` + +This compiles TypeScript and builds all React applications. + +### Testing + +```bash +# Test with MCP Inspector +npx @modelcontextprotocol/inspector lightspeed-mcp + +# Or use the MCP CLI +mcp dev lightspeed-mcp +``` + +## Use Cases + +### Inventory Management +"Check which products are low on stock and create purchase orders for them" + +### Customer Analytics +"Show me the top 10 customers by total spend this month" + +### Sales Reporting +"Generate a sales report for last week broken down by employee" + +### Product Management +"Create a new product in the Electronics category with a 20% markup" + +### Multi-Location Operations +"Transfer 50 units of SKU-12345 from Store A to Store B" + +### Promotion Management +"Create a 15% discount for orders over $100 valid for the next 7 days" + +## Error Handling + +All tools return a consistent response format: + +```typescript +{ + success: true, + data: { /* result */ } +} + +// or + +{ + success: false, + error: "Error message", + details: { /* additional info */ } +} +``` + +## Rate Limiting + +Lightspeed API has rate limits. This server respects those limits: +- Default: 5 requests/second +- Burst: 10 requests/second +- Daily: 10,000 requests + +## Security + +- Never commit API keys to version control +- Use environment variables for credentials +- Restrict API permissions to minimum required +- Enable IP whitelisting in Lightspeed if possible +- Rotate API keys regularly ## Troubleshooting -### Authentication Errors -- Verify your Account ID and Access Token are correct -- Check if your OAuth token has expired (Lightspeed tokens expire) -- Ensure your API application has the required scopes +### "Missing required environment variables" +Ensure `LIGHTSPEED_ACCOUNT_ID` and `LIGHTSPEED_API_KEY` are set. -### API Errors -- Check Lightspeed API status page -- Verify rate limits haven't been exceeded -- Ensure your account has access to the requested resources +### "API request failed" +- Verify API credentials are correct +- Check account ID matches your Lightspeed account +- Ensure API key has necessary permissions +- Check API rate limits -### Connection Issues -- Verify network connectivity -- Check firewall settings -- Ensure API URL is correct (if using custom URL) +### "Tool not found" +Update to the latest version and rebuild: +```bash +npm update @mcpengine/lightspeed-mcp-server +npm run build +``` + +## Contributing + +Contributions welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit a pull request ## License -MIT +MIT License - see LICENSE file for details ## Support -- GitHub Issues: [mcpengine/issues](https://github.com/BusyBee3333/mcpengine/issues) -- Lightspeed Support: [support.lightspeedhq.com](https://support.lightspeedhq.com) +- **Issues**: https://github.com/mcpengine/mcpengine/issues +- **Docs**: https://mcpengine.dev/servers/lightspeed +- **Discord**: https://discord.gg/mcpengine -## Related Resources +## Changelog -- [Lightspeed Developer Portal](https://developers.lightspeedhq.com/) -- [MCP Protocol Specification](https://modelcontextprotocol.io) -- [Claude Desktop Documentation](https://claude.ai/desktop) +### v1.0.0 (2024) +- Initial release +- 66 MCP tools covering all major Lightspeed entities +- 16 React applications with dark theme +- Full TypeScript support +- Comprehensive error handling +- Multi-location support +- Retail and Restaurant platform compatibility + +## Related Projects + +- [MCP Engine](https://github.com/mcpengine/mcpengine) - MCP server factory +- [Lightspeed API Docs](https://developers.lightspeedhq.com/) - Official API documentation +- [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification + +--- + +**Built with ❤️ by MCPEngine** + +For more MCP servers, visit [mcpengine.dev](https://mcpengine.dev) diff --git a/servers/lightspeed/build-apps.js b/servers/lightspeed/build-apps.js new file mode 100644 index 0000000..2e5cdea --- /dev/null +++ b/servers/lightspeed/build-apps.js @@ -0,0 +1,67 @@ +import { execSync } from 'child_process'; +import { readdirSync, statSync, cpSync, rmSync, existsSync } from 'fs'; +import { join } from 'path'; + +const uiDir = join(process.cwd(), 'src/ui'); +const distDir = join(process.cwd(), 'dist/ui'); + +// Clean dist/ui directory +if (existsSync(distDir)) { + rmSync(distDir, { recursive: true, force: true }); +} + +console.log('🔨 Building React MCP Apps...\n'); + +const apps = readdirSync(uiDir).filter(name => { + const appPath = join(uiDir, name); + return statSync(appPath).isDirectory(); +}); + +let successCount = 0; +let failedApps = []; + +apps.forEach((appName, index) => { + const appPath = join(uiDir, appName); + const packageJsonPath = join(appPath, 'package.json'); + const viteConfigPath = join(appPath, 'vite.config.ts'); + + // Check if it's a Vite app + if (!existsSync(viteConfigPath)) { + console.log(`⏭️ Skipping ${appName} (not a Vite app)`); + return; + } + + try { + console.log(`[${index + 1}/${apps.length}] Building ${appName}...`); + + // Build the app + execSync('npx vite build', { + cwd: appPath, + stdio: 'inherit', + }); + + // Copy built files to dist/ui/{app-name} + const appDistSrc = join(appPath, 'dist'); + const appDistDest = join(distDir, appName); + + if (existsSync(appDistSrc)) { + cpSync(appDistSrc, appDistDest, { recursive: true }); + console.log(`✓ Built and copied ${appName}\n`); + successCount++; + } + + } catch (error) { + console.error(`✗ Failed to build ${appName}:`, error.message); + failedApps.push(appName); + } +}); + +console.log('\n' + '='.repeat(50)); +console.log(`✓ Successfully built ${successCount}/${apps.length} apps`); + +if (failedApps.length > 0) { + console.log(`✗ Failed apps: ${failedApps.join(', ')}`); + process.exit(1); +} + +console.log('='.repeat(50) + '\n'); diff --git a/servers/lightspeed/create-apps.js b/servers/lightspeed/create-apps.js new file mode 100644 index 0000000..5349f5d --- /dev/null +++ b/servers/lightspeed/create-apps.js @@ -0,0 +1,191 @@ +import { writeFileSync, mkdirSync } from 'fs'; +import { join } from 'path'; + +const apps = [ + { name: 'product-manager', title: 'Product Manager', icon: '📦', desc: 'Manage inventory and products' }, + { name: 'inventory-manager', title: 'Inventory Manager', icon: '📊', desc: 'Track stock levels and transfers' }, + { name: 'sales-terminal', title: 'Sales Terminal', icon: '💳', desc: 'Quick POS sales interface' }, + { name: 'customer-manager', title: 'Customer Manager', icon: '👥', desc: 'Customer database and analytics' }, + { name: 'order-manager', title: 'Order Manager', icon: '📋', desc: 'Purchase orders and receiving' }, + { name: 'employee-manager', title: 'Employee Manager', icon: '👤', desc: 'Staff management and time tracking' }, + { name: 'reports', title: 'Reports Viewer', icon: '📈', desc: 'Sales and performance reports' }, + { name: 'category-manager', title: 'Category Manager', icon: '🗂️', desc: 'Product category hierarchy' }, + { name: 'vendor-manager', title: 'Vendor Manager', icon: '🏢', desc: 'Supplier management' }, + { name: 'workorder-manager', title: 'Workorder Manager', icon: '🔧', desc: 'Service workorders' }, + { name: 'register-manager', title: 'Register Manager', icon: '💰', desc: 'POS register control' }, + { name: 'transfer-manager', title: 'Transfer Manager', icon: '🚚', desc: 'Inter-location transfers' }, + { name: 'discount-manager', title: 'Discount Manager', icon: '🎟️', desc: 'Promotions and discounts' }, + { name: 'analytics', title: 'Analytics Dashboard', icon: '📊', desc: 'Business intelligence' }, + { name: 'quick-sale', title: 'Quick Sale', icon: '⚡', desc: 'Fast checkout interface' }, + { name: 'low-stock-alert', title: 'Low Stock Alerts', icon: '⚠️', desc: 'Inventory alerts' }, +]; + +const cssTemplate = `* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} +`; + +apps.forEach(app => { + const dir = join(process.cwd(), 'src/ui', app.name); + + try { + mkdirSync(dir, { recursive: true }); + } catch {} + + const appTsx = `import { useState } from 'react'; +import './app.css'; + +export default function ${app.title.replace(/\s+/g, '')}() { + const [data, setData] = useState([]); + + return ( +
+
+

${app.icon} ${app.title}

+

${app.desc}

+
+
+

MCP-powered ${app.title} - Coming soon!

+ +
+
+ ); +} +`; + + const mainTsx = `import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); +`; + + const html = ` + + + + + ${app.title} + + +
+ + + +`; + + const viteConfig = `import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); +`; + + writeFileSync(join(dir, 'App.tsx'), appTsx); + writeFileSync(join(dir, 'app.css'), cssTemplate); + writeFileSync(join(dir, 'main.tsx'), mainTsx); + writeFileSync(join(dir, 'index.html'), html); + writeFileSync(join(dir, 'vite.config.ts'), viteConfig); + + console.log(`✓ Created ${app.name}`); +}); + +console.log(`\n✓ Created ${apps.length} React apps successfully!`); diff --git a/servers/lightspeed/package.json b/servers/lightspeed/package.json index 36db690..b5e1987 100644 --- a/servers/lightspeed/package.json +++ b/servers/lightspeed/package.json @@ -1,42 +1,40 @@ { - "name": "@mcpengine/lightspeed-mcp-server", + "name": "@busybee3333/lightspeed-mcp-server", "version": "1.0.0", - "description": "MCP server for Lightspeed POS and eCommerce platform - retail and restaurant management", - "author": "MCPEngine", + "description": "Complete MCP server for Lightspeed Retail & Restaurant POS platform with 50+ tools and 15+ React apps", + "author": "BusyBee3333", "license": "MIT", "type": "module", "bin": { "lightspeed-mcp": "./dist/main.js" }, - "main": "./dist/main.js", "scripts": { - "build": "tsc && npm run build:apps", - "build:apps": "node build-apps.js", + "build": "tsc && node build-apps.js", "prepublishOnly": "npm run build", "dev": "tsc --watch", "start": "node dist/main.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.0.4", - "axios": "^1.7.2", - "zod": "^3.23.8" + "@modelcontextprotocol/sdk": "^1.0.4" }, "devDependencies": { - "@types/node": "^20.14.0", - "@vitejs/plugin-react": "^4.3.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "typescript": "^5.6.3", - "vite": "^5.4.11" + "@types/node": "^22.10.5", + "typescript": "^5.7.2", + "vite": "^6.0.11", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@types/react": "^19.0.6", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4" }, "keywords": [ "mcp", "lightspeed", "pos", - "ecommerce", "retail", "restaurant", + "point-of-sale", "inventory", - "sales" + "ecommerce" ] } diff --git a/servers/lightspeed/src/apps/index.ts b/servers/lightspeed/src/apps/index.ts deleted file mode 100644 index 09d9a48..0000000 --- a/servers/lightspeed/src/apps/index.ts +++ /dev/null @@ -1,532 +0,0 @@ -/** - * Lightspeed MCP Apps - * UI applications for Claude Desktop and other MCP clients - */ - -export const apps = { - // Product Management Apps - 'product-dashboard': { - name: 'Product Dashboard', - description: 'Overview of inventory, stock levels, and product categories', - category: 'products', - handler: async (client: any, args?: any) => { - const products = await client.getAll('/Item', 'Item', 50); - const categories = await client.getAll('/Category', 'Category', 100); - - return `# 📦 Product Dashboard - -## Inventory Summary -- **Total Products**: ${products.length} -- **Categories**: ${categories.length} - -## Recent Products -${products.slice(0, 10).map((p: any) => - `- **${p.description}** (SKU: ${p.systemSku}) - $${p.defaultCost}` -).join('\n')} - -## Categories -${categories.slice(0, 5).map((c: any) => - `- ${c.name} (ID: ${c.categoryID})` -).join('\n')} -`; - }, - }, - - 'product-detail': { - name: 'Product Detail', - description: 'Detailed view of a specific product', - category: 'products', - inputSchema: { - type: 'object', - properties: { - itemId: { type: 'string', description: 'Product item ID' }, - }, - required: ['itemId'], - }, - handler: async (client: any, args: any) => { - const result = await client.getById('/Item', args.itemId); - const product = result.Item; - - return `# 📦 ${product.description} - -## Product Information -- **Item ID**: ${product.itemID} -- **System SKU**: ${product.systemSku} -- **Custom SKU**: ${product.customSku || 'N/A'} -- **Default Cost**: $${product.defaultCost} -- **Category ID**: ${product.categoryID} -- **Tax**: ${product.tax ? 'Yes' : 'No'} -- **Discountable**: ${product.discountable ? 'Yes' : 'No'} -- **Archived**: ${product.archived ? 'Yes' : 'No'} - -## Pricing -${product.Prices?.ItemPrice ? product.Prices.ItemPrice.map((price: any) => - `- **${price.useType}**: $${price.amount}` -).join('\n') : 'No pricing information'} - -## Details -- **Created**: ${product.createTime} -- **Last Updated**: ${product.timeStamp} -`; - }, - }, - - 'product-grid': { - name: 'Product Grid', - description: 'Filterable grid view of all products', - category: 'products', - inputSchema: { - type: 'object', - properties: { - categoryId: { type: 'string', description: 'Filter by category ID' }, - archived: { type: 'boolean', description: 'Include archived products' }, - limit: { type: 'number', description: 'Max products to show' }, - }, - }, - handler: async (client: any, args?: any) => { - const params: any = {}; - if (args?.categoryId) params.categoryID = args.categoryId; - if (args?.archived !== undefined) params.archived = args.archived; - - const products = await client.getAll('/Item', 'Item', args?.limit || 100); - - return `# 📊 Product Grid - -**Total Products**: ${products.length} - -| SKU | Description | Cost | Category | Status | -|-----|-------------|------|----------|--------| -${products.map((p: any) => - `| ${p.systemSku} | ${p.description} | $${p.defaultCost} | ${p.categoryID} | ${p.archived ? '🗄️ Archived' : '✅ Active'} |` -).join('\n')} -`; - }, - }, - - 'sales-dashboard': { - name: 'Sales Dashboard', - description: 'Overview of recent sales and transactions', - category: 'sales', - handler: async (client: any, args?: any) => { - const sales = await client.getAll('/Sale', 'Sale', 50); - const completed = sales.filter((s: any) => s.completed && !s.voided); - const totalRevenue = completed.reduce((sum: number, s: any) => - sum + parseFloat(s.calcTotal || '0'), 0 - ); - - return `# 💰 Sales Dashboard - -## Summary -- **Total Transactions**: ${completed.length} -- **Total Revenue**: $${totalRevenue.toFixed(2)} -- **Average Transaction**: $${(totalRevenue / (completed.length || 1)).toFixed(2)} - -## Recent Sales -${completed.slice(0, 10).map((s: any) => - `- **Sale #${s.saleID}** - $${s.calcTotal} (${new Date(s.createTime).toLocaleDateString()})` -).join('\n')} -`; - }, - }, - - 'sales-detail': { - name: 'Sale Detail', - description: 'Detailed view of a specific sale transaction', - category: 'sales', - inputSchema: { - type: 'object', - properties: { - saleId: { type: 'string', description: 'Sale ID' }, - }, - required: ['saleId'], - }, - handler: async (client: any, args: any) => { - const result = await client.getById('/Sale', args.saleId); - const sale = result.Sale; - - const lines = sale.SaleLines?.SaleLine - ? (Array.isArray(sale.SaleLines.SaleLine) - ? sale.SaleLines.SaleLine - : [sale.SaleLines.SaleLine]) - : []; - - const payments = sale.SalePayments?.SalePayment - ? (Array.isArray(sale.SalePayments.SalePayment) - ? sale.SalePayments.SalePayment - : [sale.SalePayments.SalePayment]) - : []; - - return `# 🧾 Sale #${sale.saleID} - -## Transaction Details -- **Date**: ${new Date(sale.createTime).toLocaleString()} -- **Status**: ${sale.completed ? '✅ Completed' : '⏳ Pending'} -- **Customer ID**: ${sale.customerID || 'Walk-in'} -- **Employee ID**: ${sale.employeeID} -- **Register ID**: ${sale.registerID} - -## Financial Summary -- **Subtotal**: $${sale.calcSubtotal} -- **Tax**: $${sale.calcTaxable} -- **Total**: $${sale.calcTotal} - -## Line Items -${lines.map((line: any) => - `- Item ${line.itemID}: ${line.unitQuantity}x @ $${line.unitPrice} = $${line.calcSubtotal}` -).join('\n') || 'No items'} - -## Payments -${payments.map((p: any) => - `- Payment Type ${p.paymentTypeID}: $${p.amount}` -).join('\n') || 'No payments'} -`; - }, - }, - - 'customer-detail': { - name: 'Customer Detail', - description: 'Detailed customer profile and purchase history', - category: 'customers', - inputSchema: { - type: 'object', - properties: { - customerId: { type: 'string', description: 'Customer ID' }, - }, - required: ['customerId'], - }, - handler: async (client: any, args: any) => { - const result = await client.getById('/Customer', args.customerId); - const customer = result.Customer; - - const salesResult = await client.get('/Sale', { customerID: args.customerId, limit: 10 }); - const sales = Array.isArray(salesResult.Sale) ? salesResult.Sale : salesResult.Sale ? [salesResult.Sale] : []; - - return `# 👤 ${customer.firstName} ${customer.lastName} - -## Contact Information -- **Customer ID**: ${customer.customerID} -- **Company**: ${customer.company || 'N/A'} -- **Email**: ${customer.Contact?.Emails?.email || 'N/A'} -- **Created**: ${new Date(customer.createTime).toLocaleDateString()} - -## Purchase History -- **Total Orders**: ${sales.length} -- **Recent Purchases**: -${sales.slice(0, 5).map((s: any) => - ` - Sale #${s.saleID}: $${s.calcTotal} (${new Date(s.createTime).toLocaleDateString()})` -).join('\n') || ' No purchases yet'} -`; - }, - }, - - 'customer-grid': { - name: 'Customer Grid', - description: 'Searchable list of all customers', - category: 'customers', - inputSchema: { - type: 'object', - properties: { - search: { type: 'string', description: 'Search by name' }, - limit: { type: 'number', description: 'Max customers to show' }, - }, - }, - handler: async (client: any, args?: any) => { - const customers = await client.getAll('/Customer', 'Customer', args?.limit || 100); - - return `# 👥 Customer List - -**Total Customers**: ${customers.length} - -| ID | Name | Company | Email | -|----|------|---------|-------| -${customers.map((c: any) => - `| ${c.customerID} | ${c.firstName} ${c.lastName} | ${c.company || 'N/A'} | ${c.Contact?.Emails?.email || 'N/A'} |` -).join('\n')} -`; - }, - }, - - 'inventory-tracker': { - name: 'Inventory Tracker', - description: 'Real-time inventory levels and stock alerts', - category: 'inventory', - inputSchema: { - type: 'object', - properties: { - shopId: { type: 'string', description: 'Shop ID' }, - }, - required: ['shopId'], - }, - handler: async (client: any, args: any) => { - const inventory = await client.getAll(`/Shop/${args.shopId}/ItemShop`, 'ItemShop', 200); - - const lowStock = inventory.filter((i: any) => { - const qoh = parseFloat(i.qoh || '0'); - const reorder = parseFloat(i.reorderPoint || '0'); - return qoh <= reorder && reorder > 0; - }); - - return `# 📊 Inventory Tracker - Shop ${args.shopId} - -## Overview -- **Total Items**: ${inventory.length} -- **Low Stock Alerts**: ${lowStock.length} - -## Low Stock Items ⚠️ -${lowStock.map((i: any) => - `- **Item ${i.itemID}**: ${i.qoh} units (Reorder at ${i.reorderPoint})` -).join('\n') || 'No low stock items'} - -## Stock Levels -${inventory.slice(0, 20).map((i: any) => - `- Item ${i.itemID}: ${i.qoh} units` -).join('\n')} -`; - }, - }, - - 'inventory-adjustments': { - name: 'Inventory Adjustments', - description: 'Track and manage inventory adjustments', - category: 'inventory', - handler: async (client: any, args?: any) => { - return `# 📝 Inventory Adjustments - -Use this app to: -- View recent stock adjustments -- Create new adjustment records -- Track reasons for inventory changes - -## Actions -- Use \`lightspeed_list_inventory_adjustments\` to view adjustments -- Use \`lightspeed_update_inventory_count\` to adjust stock levels -- Use \`lightspeed_transfer_stock\` to move inventory between locations -`; - }, - }, - - 'register-manager': { - name: 'Register Manager', - description: 'Manage POS registers and cash drawers', - category: 'registers', - handler: async (client: any, args?: any) => { - const registers = await client.getAll('/Register', 'Register', 100); - - return `# 💵 Register Manager - -## Active Registers -${registers.filter((r: any) => !r.archived).map((r: any) => - `- **${r.name}** (ID: ${r.registerID}) - Shop ${r.shopID}` -).join('\n')} - -## Actions -- Open register: \`lightspeed_open_register\` -- Close register: \`lightspeed_close_register\` -- View cash counts: \`lightspeed_get_cash_counts\` -`; - }, - }, - - 'employee-dashboard': { - name: 'Employee Dashboard', - description: 'Employee management and time tracking', - category: 'employees', - handler: async (client: any, args?: any) => { - const employees = await client.getAll('/Employee', 'Employee', 100); - const active = employees.filter((e: any) => !e.archived); - - return `# 👥 Employee Dashboard - -## Team Overview -- **Total Employees**: ${employees.length} -- **Active**: ${active.length} -- **Archived**: ${employees.length - active.length} - -## Active Employees -${active.map((e: any) => - `- **${e.firstName} ${e.lastName}** (${e.employeeNumber}) - Role ${e.employeeRoleID}` -).join('\n')} - -## Quick Actions -- Clock in: \`lightspeed_clock_in\` -- Clock out: \`lightspeed_clock_out\` -- View time entries: \`lightspeed_list_time_entries\` -`; - }, - }, - - 'category-manager': { - name: 'Category Manager', - description: 'Manage product categories and hierarchies', - category: 'products', - handler: async (client: any, args?: any) => { - const categories = await client.getAll('/Category', 'Category', 200); - - const tree: any = {}; - categories.forEach((cat: any) => { - if (cat.parentID === '0' || !cat.parentID) { - if (!tree[cat.categoryID]) tree[cat.categoryID] = { ...cat, children: [] }; - } - }); - - categories.forEach((cat: any) => { - if (cat.parentID && cat.parentID !== '0' && tree[cat.parentID]) { - tree[cat.parentID].children.push(cat); - } - }); - - return `# 🗂️ Category Manager - -## Category Tree -${Object.values(tree).map((cat: any) => - `- **${cat.name}** (${cat.categoryID})\n${cat.children.map((c: any) => - ` - ${c.name} (${c.categoryID})` - ).join('\n')}` -).join('\n')} - -## Actions -- Create: \`lightspeed_create_category\` -- Update: \`lightspeed_update_category\` -- Delete: \`lightspeed_delete_category\` -`; - }, - }, - - 'discount-manager': { - name: 'Discount Manager', - description: 'Configure and manage discounts and promotions', - category: 'settings', - handler: async (client: any, args?: any) => { - const discounts = await client.getAll('/Discount', 'Discount', 100); - - return `# 🏷️ Discount Manager - -## Active Discounts -${discounts.filter((d: any) => !d.archived).map((d: any) => - `- **${d.name}**: ${d.type === 'percent' ? d.value + '%' : '$' + d.value} off` -).join('\n')} - -## Archived Discounts -${discounts.filter((d: any) => d.archived).map((d: any) => - `- ${d.name}` -).join('\n') || 'None'} - -## Actions -- Create: \`lightspeed_create_discount\` -- Update: \`lightspeed_update_discount\` -- Archive: \`lightspeed_delete_discount\` -`; - }, - }, - - 'sales-report': { - name: 'Sales Report', - description: 'Generate sales summary reports by date range', - category: 'reports', - inputSchema: { - type: 'object', - properties: { - startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, - endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, - }, - required: ['startDate', 'endDate'], - }, - handler: async (client: any, args: any) => { - // This would call the sales_summary tool - return `# 📈 Sales Report - -**Period**: ${args.startDate} to ${args.endDate} - -Use \`lightspeed_sales_summary\` tool to generate detailed report. - -## Available Metrics -- Total sales revenue -- Transaction count -- Average transaction value -- Gross profit and margin -- Sales by category -- Sales by employee -`; - }, - }, - - 'product-performance': { - name: 'Product Performance', - description: 'Analyze top-selling products and trends', - category: 'reports', - inputSchema: { - type: 'object', - properties: { - startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, - endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, - limit: { type: 'number', description: 'Top N products' }, - }, - required: ['startDate', 'endDate'], - }, - handler: async (client: any, args: any) => { - return `# 🏆 Product Performance - -**Period**: ${args.startDate} to ${args.endDate} - -Use \`lightspeed_product_performance\` tool to generate report. - -## Analysis Includes -- Top ${args.limit || 50} selling products -- Revenue by product -- Units sold -- Profit margins -- Inventory turnover -`; - }, - }, - - 'purchase-orders': { - name: 'Purchase Orders', - description: 'Manage supplier purchase orders', - category: 'inventory', - handler: async (client: any, args?: any) => { - const pos = await client.getAll('/PurchaseOrder', 'PurchaseOrder', 100); - const suppliers = await client.getAll('/Vendor', 'Vendor', 100); - - return `# 📦 Purchase Orders - -## Open Purchase Orders -${pos.filter((po: any) => !po.complete).map((po: any) => - `- **PO #${po.orderNumber}** - Vendor ${po.vendorID} (Status: ${po.status})` -).join('\n') || 'No open POs'} - -## Suppliers -${suppliers.slice(0, 10).map((v: any) => - `- ${v.name} (ID: ${v.vendorID})` -).join('\n')} - -## Actions -- Create PO: \`lightspeed_create_purchase_order\` -- List POs: \`lightspeed_list_purchase_orders\` -- List suppliers: \`lightspeed_list_suppliers\` -`; - }, - }, - - 'tax-settings': { - name: 'Tax Settings', - description: 'Configure tax classes and rates', - category: 'settings', - handler: async (client: any, args?: any) => { - const taxes = await client.getAll('/TaxClass', 'TaxClass', 100); - - return `# 💰 Tax Settings - -## Tax Classes -${taxes.map((t: any) => - `- **${t.name}**: ${t.tax1Rate}%${t.tax2Rate ? ' + ' + t.tax2Rate + '%' : ''}` -).join('\n')} - -## Actions -- Create: \`lightspeed_create_tax\` -- Update: \`lightspeed_update_tax\` -- List: \`lightspeed_list_taxes\` -`; - }, - }, -}; diff --git a/servers/lightspeed/src/clients/lightspeed.ts b/servers/lightspeed/src/clients/lightspeed.ts index 7b03923..a9c92b2 100644 --- a/servers/lightspeed/src/clients/lightspeed.ts +++ b/servers/lightspeed/src/clients/lightspeed.ts @@ -1,528 +1,327 @@ -/** - * Lightspeed API Client - * Supports both Retail (X-Series) and Restaurant (R-Series) APIs - */ - -import axios, { AxiosInstance } from 'axios'; import type { LightspeedConfig, - Product, - ProductInventory, - Customer, - Sale, - Order, - Employee, - Category, - Supplier, - Discount, - LoyaltyProgram, - CustomerLoyalty, - Shop, - Register, - PaymentType, - TaxCategory, - Manufacturer, - ApiResponse + OAuthTokenResponse, + LightspeedError, + PaginatedResponse, } from '../types/index.js'; export class LightspeedClient { - private client: AxiosInstance; private config: LightspeedConfig; + private baseUrl: string; + private authBaseUrl: string; constructor(config: LightspeedConfig) { - this.config = config; - const baseUrl = config.baseUrl || 'https://api.lightspeedapp.com/API/V3'; - - this.client = axios.create({ - baseURL: baseUrl, - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.apiKey}` - }, - timeout: 30000 + this.config = { + environment: 'production', + apiType: 'retail', + ...config, + }; + + // Retail API base URLs + if (this.config.apiType === 'retail') { + this.baseUrl = 'https://api.lightspeedapp.com/API/V3'; + this.authBaseUrl = 'https://cloud.lightspeedapp.com/oauth'; + } else { + // Restaurant K-Series API + const env = this.config.environment === 'trial' ? 'trial.' : ''; + this.baseUrl = `https://api.${env}lsk.lightspeed.app`; + this.authBaseUrl = `https://api.${env}lsk.lightspeed.app/oauth`; + } + } + + // OAuth2 Authentication + async getAuthorizationUrl(redirectUri: string, scope: string, state?: string): Promise { + const params = new URLSearchParams({ + response_type: 'code', + client_id: this.config.clientId, + redirect_uri: redirectUri, + scope, }); - // Add response interceptor for error handling - this.client.interceptors.response.use( - response => response, - error => { - console.error('Lightspeed API Error:', error.response?.data || error.message); - throw error; + if (state) { + params.append('state', state); + } + + if (this.config.apiType === 'retail') { + return `${this.authBaseUrl}/authorize.php?${params}`; + } else { + return `${this.authBaseUrl}/authorize?${params}`; + } + } + + async exchangeCodeForToken(code: string, redirectUri: string): Promise { + const tokenUrl = this.config.apiType === 'retail' + ? `${this.authBaseUrl}/access_token.php` + : `${this.authBaseUrl}/token`; + + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri, + }); + + const headers: Record = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + // Restaurant API uses Basic auth for token endpoint + if (this.config.apiType === 'restaurant') { + const auth = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64'); + headers['Authorization'] = `Basic ${auth}`; + } else { + params.append('client_id', this.config.clientId); + params.append('client_secret', this.config.clientSecret); + } + + const response = await fetch(tokenUrl, { + method: 'POST', + headers, + body: params, + }); + + if (!response.ok) { + throw await this.handleError(response); + } + + const data = await response.json(); + this.config.accessToken = data.access_token; + this.config.refreshToken = data.refresh_token; + return data; + } + + async refreshAccessToken(): Promise { + if (!this.config.refreshToken) { + throw new Error('No refresh token available'); + } + + const tokenUrl = this.config.apiType === 'retail' + ? `${this.authBaseUrl}/access_token.php` + : `${this.authBaseUrl}/token`; + + const params = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: this.config.refreshToken, + }); + + const headers: Record = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + if (this.config.apiType === 'restaurant') { + const auth = Buffer.from(`${this.config.clientId}:${this.config.clientSecret}`).toString('base64'); + headers['Authorization'] = `Basic ${auth}`; + } else { + params.append('client_id', this.config.clientId); + params.append('client_secret', this.config.clientSecret); + } + + const response = await fetch(tokenUrl, { + method: 'POST', + headers, + body: params, + }); + + if (!response.ok) { + throw await this.handleError(response); + } + + const data = await response.json(); + this.config.accessToken = data.access_token; + this.config.refreshToken = data.refresh_token; + return data; + } + + // Core HTTP methods + async get(endpoint: string, params?: Record): Promise { + return this.request('GET', endpoint, undefined, params); + } + + async post(endpoint: string, body?: any): Promise { + return this.request('POST', endpoint, body); + } + + async put(endpoint: string, body?: any): Promise { + return this.request('PUT', endpoint, body); + } + + async delete(endpoint: string): Promise { + return this.request('DELETE', endpoint); + } + + // Paginated requests + async getPaginated( + endpoint: string, + params: Record = {}, + limit = 100 + ): Promise { + const results: T[] = []; + let offset = 0; + let hasMore = true; + + while (hasMore) { + const pageParams = { + ...params, + limit, + offset, + }; + + const response = await this.get(endpoint, pageParams); + + // Handle Retail API pagination + if (response['@attributes']) { + const data = Array.isArray(response[Object.keys(response).find(k => k !== '@attributes')!]) + ? response[Object.keys(response).find(k => k !== '@attributes')!] + : [response[Object.keys(response).find(k => k !== '@attributes')!]]; + + results.push(...data); + + const count = parseInt(response['@attributes'].count); + hasMore = offset + limit < count; + offset += limit; + } + // Handle Restaurant API pagination + else if (Array.isArray(response)) { + results.push(...response); + hasMore = response.length === limit; + offset += limit; + } else { + results.push(response); + hasMore = false; } + } + + return results; + } + + // Core request method with retry logic + private async request( + method: string, + endpoint: string, + body?: any, + params?: Record + ): Promise { + if (!this.config.accessToken) { + throw new Error('No access token available. Please authenticate first.'); + } + + let url = this.config.apiType === 'retail' + ? `${this.baseUrl}/Account/${this.config.accountId}${endpoint}.json` + : `${this.baseUrl}${endpoint}`; + + if (params) { + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) { + searchParams.append(key, String(value)); + } + } + url += `?${searchParams}`; + } + + const headers: Record = { + 'Authorization': `Bearer ${this.config.accessToken}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const options: RequestInit = { + method, + headers, + }; + + if (body && (method === 'POST' || method === 'PUT')) { + options.body = JSON.stringify(body); + } + + let response = await fetch(url, options); + + // Retry with token refresh if unauthorized + if (response.status === 401 && this.config.refreshToken) { + await this.refreshAccessToken(); + headers['Authorization'] = `Bearer ${this.config.accessToken}`; + response = await fetch(url, options); + } + + if (!response.ok) { + throw await this.handleError(response); + } + + // Handle empty responses + if (response.status === 204) { + return {} as T; + } + + return await response.json(); + } + + private async handleError(response: Response): Promise { + let errorBody: any; + try { + errorBody = await response.json(); + } catch { + errorBody = await response.text(); + } + + const error: LightspeedError = { + message: typeof errorBody === 'string' ? errorBody : errorBody.message || response.statusText, + statusCode: response.status, + details: errorBody, + }; + + return error; + } + + // Retail-specific helpers + getRetailEndpoint(resource: string, id?: number | string): string { + return id ? `/${resource}/${id}` : `/${resource}`; + } + + // Restaurant-specific helpers + getRestaurantEndpoint(resource: string, id?: string): string { + return id ? `/${resource}/${id}` : `/${resource}`; + } + + // Bulk operations + async batchGet(endpoint: string, ids: (number | string)[]): Promise { + const results = await Promise.all( + ids.map(id => this.get(`${endpoint}/${id}`)) + ); + return results; + } + + async batchCreate(endpoint: string, items: any[]): Promise { + const results = await Promise.all( + items.map(item => this.post(endpoint, item)) + ); + return results; + } + + async batchUpdate(endpoint: string, items: Array<{ id: number | string; data: any }>): Promise { + const results = await Promise.all( + items.map(({ id, data }) => this.put(`${endpoint}/${id}`, data)) + ); + return results; + } + + async batchDelete(endpoint: string, ids: (number | string)[]): Promise { + await Promise.all( + ids.map(id => this.delete(`${endpoint}/${id}`)) ); } - private getAccountPath(): string { - return `/Account/${this.config.accountId}`; + // Search and filter helpers + async search(endpoint: string, query: Record): Promise { + return this.getPaginated(endpoint, query); } - // ========== PRODUCTS ========== - - async getProducts(params?: { limit?: number; offset?: number; categoryID?: string }): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Item.json`, { params }); - return { success: true, data: response.data.Item || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } + // Export current config for persistence + getConfig(): LightspeedConfig { + return { ...this.config }; } - async getProduct(productID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Item/${productID}.json`); - return { success: true, data: response.data.Item }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createProduct(product: Partial): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Item.json`, { Item: product }); - return { success: true, data: response.data.Item }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateProduct(productID: string, updates: Partial): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Item/${productID}.json`, { Item: updates }); - return { success: true, data: response.data.Item }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async deleteProduct(productID: string): Promise> { - try { - await this.client.delete(`${this.getAccountPath()}/Item/${productID}.json`); - return { success: true, data: true }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getProductInventory(productID: string, shopID?: string): Promise> { - try { - const params = shopID ? { shopID } : {}; - const response = await this.client.get(`${this.getAccountPath()}/Item/${productID}/ItemShops.json`, { params }); - return { success: true, data: response.data.ItemShop || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateInventory(productID: string, shopID: string, qty: number): Promise> { - try { - const response = await this.client.put( - `${this.getAccountPath()}/Item/${productID}/ItemShops/${shopID}.json`, - { ItemShop: { qty } } - ); - return { success: true, data: response.data.ItemShop }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== CUSTOMERS ========== - - async getCustomers(params?: { limit?: number; offset?: number; email?: string }): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Customer.json`, { params }); - return { success: true, data: response.data.Customer || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getCustomer(customerID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Customer/${customerID}.json`); - return { success: true, data: response.data.Customer }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createCustomer(customer: Partial): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Customer.json`, { Customer: customer }); - return { success: true, data: response.data.Customer }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateCustomer(customerID: string, updates: Partial): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Customer/${customerID}.json`, { Customer: updates }); - return { success: true, data: response.data.Customer }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async deleteCustomer(customerID: string): Promise> { - try { - await this.client.delete(`${this.getAccountPath()}/Customer/${customerID}.json`); - return { success: true, data: true }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== SALES ========== - - async getSales(params?: { limit?: number; offset?: number; startDate?: string; endDate?: string }): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Sale.json`, { params }); - return { success: true, data: response.data.Sale || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getSale(saleID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Sale/${saleID}.json`); - return { success: true, data: response.data.Sale }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createSale(sale: Partial): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Sale.json`, { Sale: sale }); - return { success: true, data: response.data.Sale }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateSale(saleID: string, updates: Partial): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Sale/${saleID}.json`, { Sale: updates }); - return { success: true, data: response.data.Sale }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async voidSale(saleID: string): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Sale/${saleID}.json`, { Sale: { voided: true } }); - return { success: true, data: response.data.Sale }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== ORDERS ========== - - async getOrders(params?: { limit?: number; offset?: number; status?: string }): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Order.json`, { params }); - return { success: true, data: response.data.Order || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getOrder(orderID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Order/${orderID}.json`); - return { success: true, data: response.data.Order }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createOrder(order: Partial): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Order.json`, { Order: order }); - return { success: true, data: response.data.Order }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateOrder(orderID: string, updates: Partial): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Order/${orderID}.json`, { Order: updates }); - return { success: true, data: response.data.Order }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async deleteOrder(orderID: string): Promise> { - try { - await this.client.delete(`${this.getAccountPath()}/Order/${orderID}.json`); - return { success: true, data: true }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== EMPLOYEES ========== - - async getEmployees(params?: { limit?: number; offset?: number }): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Employee.json`, { params }); - return { success: true, data: response.data.Employee || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getEmployee(employeeID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Employee/${employeeID}.json`); - return { success: true, data: response.data.Employee }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createEmployee(employee: Partial): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Employee.json`, { Employee: employee }); - return { success: true, data: response.data.Employee }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateEmployee(employeeID: string, updates: Partial): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Employee/${employeeID}.json`, { Employee: updates }); - return { success: true, data: response.data.Employee }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async deleteEmployee(employeeID: string): Promise> { - try { - await this.client.delete(`${this.getAccountPath()}/Employee/${employeeID}.json`); - return { success: true, data: true }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== CATEGORIES ========== - - async getCategories(params?: { limit?: number; offset?: number }): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Category.json`, { params }); - return { success: true, data: response.data.Category || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getCategory(categoryID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Category/${categoryID}.json`); - return { success: true, data: response.data.Category }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createCategory(category: Partial): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Category.json`, { Category: category }); - return { success: true, data: response.data.Category }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateCategory(categoryID: string, updates: Partial): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Category/${categoryID}.json`, { Category: updates }); - return { success: true, data: response.data.Category }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async deleteCategory(categoryID: string): Promise> { - try { - await this.client.delete(`${this.getAccountPath()}/Category/${categoryID}.json`); - return { success: true, data: true }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== SUPPLIERS ========== - - async getSuppliers(params?: { limit?: number; offset?: number }): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Vendor.json`, { params }); - return { success: true, data: response.data.Vendor || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getSupplier(supplierID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Vendor/${supplierID}.json`); - return { success: true, data: response.data.Vendor }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createSupplier(supplier: Partial): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Vendor.json`, { Vendor: supplier }); - return { success: true, data: response.data.Vendor }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateSupplier(supplierID: string, updates: Partial): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Vendor/${supplierID}.json`, { Vendor: updates }); - return { success: true, data: response.data.Vendor }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async deleteSupplier(supplierID: string): Promise> { - try { - await this.client.delete(`${this.getAccountPath()}/Vendor/${supplierID}.json`); - return { success: true, data: true }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== DISCOUNTS ========== - - async getDiscounts(params?: { limit?: number; offset?: number }): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Discount.json`, { params }); - return { success: true, data: response.data.Discount || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getDiscount(discountID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Discount/${discountID}.json`); - return { success: true, data: response.data.Discount }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createDiscount(discount: Partial): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Discount.json`, { Discount: discount }); - return { success: true, data: response.data.Discount }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async updateDiscount(discountID: string, updates: Partial): Promise> { - try { - const response = await this.client.put(`${this.getAccountPath()}/Discount/${discountID}.json`, { Discount: updates }); - return { success: true, data: response.data.Discount }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async deleteDiscount(discountID: string): Promise> { - try { - await this.client.delete(`${this.getAccountPath()}/Discount/${discountID}.json`); - return { success: true, data: true }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== SHOPS & REGISTERS ========== - - async getShops(): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Shop.json`); - return { success: true, data: response.data.Shop || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getShop(shopID: string): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Shop/${shopID}.json`); - return { success: true, data: response.data.Shop }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async getRegisters(shopID?: string): Promise> { - try { - const params = shopID ? { shopID } : {}; - const response = await this.client.get(`${this.getAccountPath()}/Register.json`, { params }); - return { success: true, data: response.data.Register || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== MANUFACTURERS ========== - - async getManufacturers(): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/Manufacturer.json`); - return { success: true, data: response.data.Manufacturer || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - async createManufacturer(name: string): Promise> { - try { - const response = await this.client.post(`${this.getAccountPath()}/Manufacturer.json`, { Manufacturer: { name } }); - return { success: true, data: response.data.Manufacturer }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== TAX CATEGORIES ========== - - async getTaxCategories(): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/TaxCategory.json`); - return { success: true, data: response.data.TaxCategory || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } - } - - // ========== PAYMENT TYPES ========== - - async getPaymentTypes(): Promise> { - try { - const response = await this.client.get(`${this.getAccountPath()}/PaymentType.json`); - return { success: true, data: response.data.PaymentType || [] }; - } catch (error: any) { - return { success: false, error: error.message, details: error.response?.data }; - } + // Update tokens + setTokens(accessToken: string, refreshToken: string): void { + this.config.accessToken = accessToken; + this.config.refreshToken = refreshToken; } } diff --git a/servers/lightspeed/src/main.ts b/servers/lightspeed/src/main.ts index 6ba9edd..aa8e4e3 100644 --- a/servers/lightspeed/src/main.ts +++ b/servers/lightspeed/src/main.ts @@ -1,41 +1,44 @@ #!/usr/bin/env node -/** - * Lightspeed MCP Server Entry Point - */ +import { LightspeedMCPServer } from './server.js'; -import { LightspeedServer } from './server.js'; -import type { LightspeedConfig } from './types/index.js'; +// Get configuration from environment variables +const accountId = process.env.LIGHTSPEED_ACCOUNT_ID; +const clientId = process.env.LIGHTSPEED_CLIENT_ID; +const clientSecret = process.env.LIGHTSPEED_CLIENT_SECRET; +const accessToken = process.env.LIGHTSPEED_ACCESS_TOKEN; +const refreshToken = process.env.LIGHTSPEED_REFRESH_TOKEN; +const apiType = process.env.LIGHTSPEED_API_TYPE || 'retail'; // retail or restaurant +const environment = process.env.LIGHTSPEED_ENVIRONMENT || 'production'; // production or trial -async function main() { - const accountId = process.env.LIGHTSPEED_ACCOUNT_ID; - const apiKey = process.env.LIGHTSPEED_API_KEY; - - if (!accountId || !apiKey) { - console.error('Error: Missing required environment variables'); - console.error('Required:'); - console.error(' LIGHTSPEED_ACCOUNT_ID - Your Lightspeed account ID'); - console.error(' LIGHTSPEED_API_KEY - Your Lightspeed API key'); - console.error('\nOptional:'); - console.error(' LIGHTSPEED_API_SECRET - API secret (if required)'); - console.error(' LIGHTSPEED_BASE_URL - Custom API base URL'); - console.error(' LIGHTSPEED_TYPE - "retail" or "restaurant" (default: retail)'); - process.exit(1); - } - - const config: LightspeedConfig = { - accountId, - apiKey, - apiSecret: process.env.LIGHTSPEED_API_SECRET, - baseUrl: process.env.LIGHTSPEED_BASE_URL, - retailOrRestaurant: (process.env.LIGHTSPEED_TYPE as 'retail' | 'restaurant') || 'retail' - }; - - const server = new LightspeedServer(config); - await server.start(); +if (!accountId || !clientId || !clientSecret) { + console.error('Error: Missing required environment variables'); + console.error('Required: LIGHTSPEED_ACCOUNT_ID, LIGHTSPEED_CLIENT_ID, LIGHTSPEED_CLIENT_SECRET'); + console.error('Optional: LIGHTSPEED_ACCESS_TOKEN, LIGHTSPEED_REFRESH_TOKEN, LIGHTSPEED_API_TYPE, LIGHTSPEED_ENVIRONMENT'); + process.exit(1); } -main().catch((error) => { - console.error('Fatal error:', error); +const config = { + apiType: apiType as 'retail' | 'restaurant', + environment: environment as 'production' | 'trial', +}; + +if (accessToken) { + config.accessToken = accessToken; +} + +if (refreshToken) { + config.refreshToken = refreshToken; +} + +const server = new LightspeedMCPServer(accountId, clientId, clientSecret, config); + +// Set tokens if provided +if (accessToken && refreshToken) { + server.setTokens(accessToken, refreshToken); +} + +server.run().catch((error) => { + console.error('Server error:', error); process.exit(1); }); diff --git a/servers/lightspeed/src/server.ts b/servers/lightspeed/src/server.ts index 6d53c19..40c491f 100644 --- a/servers/lightspeed/src/server.ts +++ b/servers/lightspeed/src/server.ts @@ -1,43 +1,39 @@ -/** - * Lightspeed MCP Server - */ - import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, - ErrorCode, - McpError + Tool, } from '@modelcontextprotocol/sdk/types.js'; import { LightspeedClient } from './clients/lightspeed.js'; -import type { LightspeedConfig } from './types/index.js'; +import { createProductTools } from './tools/products.js'; +import { createCategoryTools } from './tools/categories.js'; +import { createCustomerTools } from './tools/customers.js'; +import { createSalesTools } from './tools/sales.js'; +import { createOrderTools } from './tools/orders.js'; +import { createInventoryTools } from './tools/inventory.js'; +import { createVendorTools } from './tools/vendors.js'; +import { createEmployeeTools } from './tools/employees.js'; +import { createRegisterTools } from './tools/registers.js'; +import { createManufacturerTools } from './tools/manufacturers.js'; +import { createDiscountTools } from './tools/discounts.js'; +import { createReportTools } from './tools/reports.js'; +import { createWorkorderTools } from './tools/workorders.js'; +import { createShopTools } from './tools/shops.js'; -// Import all tool registrations -import { registerProductTools } from './tools/products.js'; -import { registerInventoryTools } from './tools/inventory.js'; -import { registerCustomerTools } from './tools/customers.js'; -import { registerSalesTools } from './tools/sales.js'; -import { registerOrderTools } from './tools/orders.js'; -import { registerEmployeeTools } from './tools/employees.js'; -import { registerCategoryTools } from './tools/categories.js'; -import { registerSupplierTools } from './tools/suppliers.js'; -import { registerDiscountTools } from './tools/discounts.js'; -import { registerLoyaltyTools } from './tools/loyalty.js'; -import { registerReportingTools } from './tools/reporting.js'; -import { registerShopTools } from './tools/shops.js'; +const SERVER_NAME = 'lightspeed-mcp-server'; +const SERVER_VERSION = '1.0.0'; -export class LightspeedServer { +export class LightspeedMCPServer { private server: Server; private client: LightspeedClient; - private tools: Map = new Map(); + private tools: Array Promise }> = []; - constructor(config: LightspeedConfig) { - this.client = new LightspeedClient(config); + constructor(accountId: string, clientId: string, clientSecret: string, config?: any) { this.server = new Server( { - name: 'lightspeed-mcp-server', - version: '1.0.0', + name: SERVER_NAME, + version: SERVER_VERSION, }, { capabilities: { @@ -46,85 +42,90 @@ export class LightspeedServer { } ); - this.registerTools(); + this.client = new LightspeedClient({ + accountId, + clientId, + clientSecret, + ...config, + }); + + this.setupTools(); this.setupHandlers(); } - private registerTools() { - const toolGroups = [ - registerProductTools(this.client), - registerInventoryTools(this.client), - registerCustomerTools(this.client), - registerSalesTools(this.client), - registerOrderTools(this.client), - registerEmployeeTools(this.client), - registerCategoryTools(this.client), - registerSupplierTools(this.client), - registerDiscountTools(this.client), - registerLoyaltyTools(this.client), - registerReportingTools(this.client), - registerShopTools(this.client) - ]; - - for (const group of toolGroups) { - for (const tool of group) { - this.tools.set(tool.name, tool); - } - } - - console.error(`Registered ${this.tools.size} Lightspeed tools`); + private setupTools() { + // Register all tool categories + this.tools.push(...createProductTools(this.client)); + this.tools.push(...createCategoryTools(this.client)); + this.tools.push(...createCustomerTools(this.client)); + this.tools.push(...createSalesTools(this.client)); + this.tools.push(...createOrderTools(this.client)); + this.tools.push(...createInventoryTools(this.client)); + this.tools.push(...createVendorTools(this.client)); + this.tools.push(...createEmployeeTools(this.client)); + this.tools.push(...createRegisterTools(this.client)); + this.tools.push(...createManufacturerTools(this.client)); + this.tools.push(...createDiscountTools(this.client)); + this.tools.push(...createReportTools(this.client)); + this.tools.push(...createWorkorderTools(this.client)); + this.tools.push(...createShopTools(this.client)); } private setupHandlers() { + // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { - const tools = Array.from(this.tools.values()).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() - ) - } - })); - - return { tools }; + return { + tools: this.tools.map(({ handler, ...tool }) => tool), + }; }); + // Handle tool execution this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - const tool = this.tools.get(request.params.name); + const tool = this.tools.find((t) => t.name === request.params.name); + if (!tool) { - throw new McpError( - ErrorCode.MethodNotFound, - `Unknown tool: ${request.params.name}` - ); + throw new Error(`Unknown tool: ${request.params.name}`); } try { - const validatedArgs = tool.inputSchema.parse(request.params.arguments); - return await tool.handler(validatedArgs); + const result = await tool.handler(request.params.arguments || {}); + return { + content: [ + { + type: 'text', + text: JSON.stringify(result, null, 2), + }, + ], + }; } catch (error: any) { - console.error(`Error executing ${request.params.name}:`, error); return { content: [ { type: 'text', text: JSON.stringify({ - success: false, error: error.message || 'Unknown error', - details: error.stack - }, null, 2) - } - ] + details: error.details || error.toString(), + }, null, 2), + }, + ], + isError: true, }; } }); } - async start() { + async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); - console.error('Lightspeed MCP server running on stdio'); + console.error(`${SERVER_NAME} v${SERVER_VERSION} running on stdio`); + console.error(`Total tools: ${this.tools.length}`); + } + + setTokens(accessToken: string, refreshToken: string) { + this.client.setTokens(accessToken, refreshToken); + } + + getClient() { + return this.client; } } diff --git a/servers/lightspeed/src/tools/categories-tools.ts b/servers/lightspeed/src/tools/categories-tools.ts deleted file mode 100644 index fca9c43..0000000 --- a/servers/lightspeed/src/tools/categories-tools.ts +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Lightspeed Categories Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../client.js'; -import type { Category } from '../types/index.js'; - -export function createCategoriesTools(client: LightspeedClient) { - return { - lightspeed_list_categories: { - description: 'List all product categories', - inputSchema: z.object({ - archived: z.boolean().optional().describe('Include archived categories'), - }), - handler: async (args: { archived?: boolean }) => { - try { - const params: any = {}; - if (args.archived !== undefined) { - params.archived = args.archived; - } - - const categories = await client.getAll('/Category', 'Category', 200); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: categories.length, - categories: categories.map(c => ({ - categoryID: c.categoryID, - name: c.name, - parentID: c.parentID, - nodeDepth: c.nodeDepth, - archived: c.archived, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_get_category: { - description: 'Get detailed information about a specific category', - inputSchema: z.object({ - categoryId: z.string().describe('Category ID'), - }), - handler: async (args: { categoryId: string }) => { - try { - const category = await client.getById<{ Category: Category }>('/Category', args.categoryId); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, category: category.Category }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_create_category: { - description: 'Create a new product category', - inputSchema: z.object({ - name: z.string().describe('Category name'), - parentId: z.string().optional().describe('Parent category ID (for subcategories)'), - }), - handler: async (args: { name: string; parentId?: string }) => { - try { - const categoryData: any = { - name: args.name, - }; - if (args.parentId) { - categoryData.parentID = args.parentId; - } - - const result = await client.post<{ Category: Category }>('/Category', { Category: categoryData }); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, category: result.Category }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_update_category: { - description: 'Update an existing category', - inputSchema: z.object({ - categoryId: z.string().describe('Category ID'), - name: z.string().optional().describe('Category name'), - parentId: z.string().optional().describe('Parent category ID'), - archived: z.boolean().optional().describe('Archive status'), - }), - handler: async (args: any) => { - try { - const updateData: any = {}; - if (args.name) updateData.name = args.name; - if (args.parentId) updateData.parentID = args.parentId; - if (args.archived !== undefined) updateData.archived = args.archived; - - const result = await client.put<{ Category: Category }>( - '/Category', - args.categoryId, - { Category: updateData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, category: result.Category }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_delete_category: { - description: 'Delete (archive) a category', - inputSchema: z.object({ - categoryId: z.string().describe('Category ID'), - }), - handler: async (args: { categoryId: string }) => { - try { - await client.delete('/Category', args.categoryId); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, message: 'Category deleted' }), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - }; -} diff --git a/servers/lightspeed/src/tools/categories.ts b/servers/lightspeed/src/tools/categories.ts index 379e295..a8dce3e 100644 --- a/servers/lightspeed/src/tools/categories.ts +++ b/servers/lightspeed/src/tools/categories.ts @@ -1,87 +1,117 @@ -/** - * Category Management Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Category } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerCategoryTools(client: LightspeedClient) { +export function createCategoryTools(client: LightspeedClient) { return [ { name: 'lightspeed_list_categories', - description: 'List all product categories in Lightspeed.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of categories to return (default 100)'), - offset: z.number().optional().describe('Offset for pagination') - }), + description: 'List all product categories with hierarchy', + inputSchema: { + type: 'object', + properties: { + parentID: { type: 'number', description: 'Filter by parent category ID (0 for root)' }, + }, + }, handler: async (args: any) => { - const result = await client.getCategories(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const params: any = {}; + if (args.parentID !== undefined) params.parentID = args.parentID; + const categories = await client.getPaginated('/Category', params); + return { categories, count: categories.length }; + }, }, { name: 'lightspeed_get_category', - description: 'Get a single category by ID with all details.', - inputSchema: z.object({ - categoryID: z.string().describe('The category ID') - }), + description: 'Get a specific category by ID', + inputSchema: { + type: 'object', + properties: { + categoryID: { type: 'number', description: 'Category ID' }, + }, + required: ['categoryID'], + }, handler: async (args: any) => { - const result = await client.getCategory(args.categoryID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const category = await client.get<{ Category: Category }>(`/Category/${args.categoryID}`); + return category.Category; + }, }, { name: 'lightspeed_create_category', - description: 'Create a new product category.', - inputSchema: z.object({ - name: z.string().describe('Category name'), - parentID: z.string().optional().describe('Parent category ID for sub-categories') - }), + description: 'Create a new product category', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string', description: 'Category name' }, + parentID: { type: 'number', description: 'Parent category ID (omit for root level)' }, + }, + required: ['name'], + }, handler: async (args: any) => { - const result = await client.createCategory(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const category = await client.post<{ Category: Category }>('/Category', { Category: args }); + return category.Category; + }, }, { name: 'lightspeed_update_category', - description: 'Update an existing category.', - inputSchema: z.object({ - categoryID: z.string().describe('The category ID to update'), - name: z.string().optional().describe('Category name'), - parentID: z.string().optional().describe('Parent category ID'), - archived: z.boolean().optional().describe('Archive the category') - }), + description: 'Update an existing category', + inputSchema: { + type: 'object', + properties: { + categoryID: { type: 'number', description: 'Category ID' }, + name: { type: 'string', description: 'New category name' }, + parentID: { type: 'number', description: 'New parent category ID' }, + }, + required: ['categoryID'], + }, handler: async (args: any) => { const { categoryID, ...updates } = args; - const result = await client.updateCategory(categoryID, updates); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const category = await client.put<{ Category: Category }>(`/Category/${categoryID}`, { Category: updates }); + return category.Category; + }, }, { name: 'lightspeed_delete_category', - description: 'Delete a category from Lightspeed.', - inputSchema: z.object({ - categoryID: z.string().describe('The category ID to delete') - }), + description: 'Delete a category', + inputSchema: { + type: 'object', + properties: { + categoryID: { type: 'number', description: 'Category ID to delete' }, + }, + required: ['categoryID'], + }, handler: async (args: any) => { - const result = await client.deleteCategory(args.categoryID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + await client.delete(`/Category/${args.categoryID}`); + return { success: true, message: `Category ${args.categoryID} deleted` }; + }, }, { name: 'lightspeed_get_category_tree', - description: 'Get the full category hierarchy tree.', - inputSchema: z.object({}), + description: 'Get complete category hierarchy as a tree structure', + inputSchema: { + type: 'object', + properties: {}, + }, handler: async () => { - const result = await client.getCategories({ limit: 500 }); - if (result.success) { - // Build tree structure - const categories = result.data; - const tree = categories.filter(c => !c.parentID); - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: tree }, null, 2) }] }; - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } + const categories = await client.getPaginated('/Category'); + + // Build tree structure + const categoryMap = new Map(); + const rootCategories: any[] = []; + + categories.forEach(cat => { + categoryMap.set(cat.categoryID, { ...cat, children: [] }); + }); + + categories.forEach(cat => { + const category = categoryMap.get(cat.categoryID)!; + if (cat.parentID && categoryMap.has(cat.parentID)) { + categoryMap.get(cat.parentID)!.children.push(category); + } else { + rootCategories.push(category); + } + }); + + return { tree: rootCategories, totalCategories: categories.length }; + }, + }, ]; } diff --git a/servers/lightspeed/src/tools/customers.ts b/servers/lightspeed/src/tools/customers.ts index 31e5243..25c2e79 100644 --- a/servers/lightspeed/src/tools/customers.ts +++ b/servers/lightspeed/src/tools/customers.ts @@ -1,128 +1,183 @@ -/** - * Customer Management Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Customer, CreditAccount } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerCustomerTools(client: LightspeedClient) { +export function createCustomerTools(client: LightspeedClient) { return [ { name: 'lightspeed_list_customers', - description: 'List all customers in Lightspeed. Supports pagination and email filtering.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of customers to return (default 100)'), - offset: z.number().optional().describe('Offset for pagination'), - email: z.string().optional().describe('Filter by email address') - }), + description: 'List all customers with optional filters', + inputSchema: { + type: 'object', + properties: { + archived: { type: 'boolean', description: 'Filter by archived status' }, + customerTypeID: { type: 'number', description: 'Filter by customer type' }, + limit: { type: 'number', default: 100 }, + }, + }, handler: async (args: any) => { - const result = await client.getCustomers(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const params: any = {}; + if (args.archived !== undefined) params.archived = args.archived; + if (args.customerTypeID) params.customerTypeID = args.customerTypeID; + const customers = await client.getPaginated('/Customer', params, args.limit || 100); + return { customers, count: customers.length }; + }, }, { name: 'lightspeed_get_customer', - description: 'Get a single customer by ID with all details including contact info, purchase history reference.', - inputSchema: z.object({ - customerID: z.string().describe('The customer ID') - }), + description: 'Get a specific customer by ID with contact info and credit account', + inputSchema: { + type: 'object', + properties: { + customerID: { type: 'number', description: 'Customer ID' }, + }, + required: ['customerID'], + }, handler: async (args: any) => { - const result = await client.getCustomer(args.customerID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const customer = await client.get<{ Customer: Customer }>(`/Customer/${args.customerID}?load=[Contact,CreditAccount]`); + return customer.Customer; + }, }, { name: 'lightspeed_create_customer', - description: 'Create a new customer in Lightspeed. Requires first name and last name at minimum.', - inputSchema: z.object({ - firstName: z.string().describe('Customer first name'), - lastName: z.string().describe('Customer last name'), - email: z.string().optional().describe('Email address'), - phone: z.string().optional().describe('Phone number'), - mobile: z.string().optional().describe('Mobile phone number'), - company: z.string().optional().describe('Company name'), - address1: z.string().optional().describe('Address line 1'), - address2: z.string().optional().describe('Address line 2'), - city: z.string().optional().describe('City'), - state: z.string().optional().describe('State/Province'), - zip: z.string().optional().describe('Postal/ZIP code'), - country: z.string().optional().describe('Country'), - dob: z.string().optional().describe('Date of birth (YYYY-MM-DD)') - }), + description: 'Create a new customer', + inputSchema: { + type: 'object', + properties: { + firstName: { type: 'string', description: 'First name' }, + lastName: { type: 'string', description: 'Last name' }, + company: { type: 'string', description: 'Company name' }, + email: { type: 'string', description: 'Email address' }, + phone: { type: 'string', description: 'Phone number' }, + address1: { type: 'string' }, + address2: { type: 'string' }, + city: { type: 'string' }, + state: { type: 'string' }, + zip: { type: 'string' }, + country: { type: 'string' }, + customerTypeID: { type: 'number' }, + }, + required: ['firstName', 'lastName'], + }, handler: async (args: any) => { - const { address1, address2, city, state, zip, country, ...customerData } = args; - const customer: any = { ...customerData }; + const { email, phone, address1, address2, city, state, zip, country, ...customerData } = args; - if (address1 || city || state || zip || country) { - customer.address = { address1, address2, city, state, zip, country }; + const payload: any = { Customer: customerData }; + + if (email || phone || address1) { + payload.Customer.Contact = { + primaryEmail: email, + phoneMobile: phone, + address1, + address2, + city, + state, + zip, + country, + }; } - const result = await client.createCustomer(customer); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const customer = await client.post<{ Customer: Customer }>('/Customer', payload); + return customer.Customer; + }, }, { name: 'lightspeed_update_customer', - description: 'Update an existing customer. Can modify any customer field including contact info and address.', - inputSchema: z.object({ - customerID: z.string().describe('The customer ID to update'), - firstName: z.string().optional().describe('Customer first name'), - lastName: z.string().optional().describe('Customer last name'), - email: z.string().optional().describe('Email address'), - phone: z.string().optional().describe('Phone number'), - mobile: z.string().optional().describe('Mobile phone number'), - company: z.string().optional().describe('Company name'), - archived: z.boolean().optional().describe('Archive the customer') - }), + description: 'Update an existing customer', + inputSchema: { + type: 'object', + properties: { + customerID: { type: 'number' }, + firstName: { type: 'string' }, + lastName: { type: 'string' }, + company: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' }, + archived: { type: 'boolean' }, + }, + required: ['customerID'], + }, handler: async (args: any) => { const { customerID, ...updates } = args; - const result = await client.updateCustomer(customerID, updates); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const customer = await client.put<{ Customer: Customer }>(`/Customer/${customerID}`, { Customer: updates }); + return customer.Customer; + }, }, { name: 'lightspeed_delete_customer', - description: 'Delete a customer from Lightspeed. This action cannot be undone.', - inputSchema: z.object({ - customerID: z.string().describe('The customer ID to delete') - }), + description: 'Archive a customer', + inputSchema: { + type: 'object', + properties: { + customerID: { type: 'number' }, + }, + required: ['customerID'], + }, handler: async (args: any) => { - const result = await client.deleteCustomer(args.customerID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + await client.delete(`/Customer/${args.customerID}`); + return { success: true, message: `Customer ${args.customerID} archived` }; + }, }, { name: 'lightspeed_search_customers', - description: 'Search customers by name, email, or phone number.', - inputSchema: z.object({ - query: z.string().describe('Search query (name, email, or phone)') - }), + description: 'Search customers by name, email, or phone', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query' }, + }, + required: ['query'], + }, handler: async (args: any) => { - const result = await client.getCustomers({ limit: 500 }); - if (result.success) { - const query = args.query.toLowerCase(); - const filtered = result.data.filter(c => - c.firstName?.toLowerCase().includes(query) || - c.lastName?.toLowerCase().includes(query) || - c.email?.toLowerCase().includes(query) || - c.phone?.includes(query) || - c.mobile?.includes(query) - ); - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] }; - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const customers = await client.getPaginated('/Customer', { archived: false }); + const q = args.query.toLowerCase(); + const filtered = customers.filter(c => + c.firstName?.toLowerCase().includes(q) || + c.lastName?.toLowerCase().includes(q) || + c.company?.toLowerCase().includes(q) || + c.Contact?.primaryEmail?.toLowerCase().includes(q) || + c.Contact?.phoneMobile?.includes(q) + ); + return { customers: filtered, count: filtered.length }; + }, }, { - name: 'lightspeed_get_customer_by_email', - description: 'Find a customer by their email address.', - inputSchema: z.object({ - email: z.string().describe('Customer email address') - }), + name: 'lightspeed_get_customer_credit_account', + description: 'Get customer credit account/store credit balance', + inputSchema: { + type: 'object', + properties: { + customerID: { type: 'number' }, + }, + required: ['customerID'], + }, handler: async (args: any) => { - const result = await client.getCustomers({ email: args.email }); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } + const customer = await client.get<{ Customer: Customer }>(`/Customer/${args.customerID}?load=[CreditAccount]`); + return customer.Customer.CreditAccount || { balance: 0, message: 'No credit account' }; + }, + }, + { + name: 'lightspeed_add_customer_credit', + description: 'Add store credit to a customer account', + inputSchema: { + type: 'object', + properties: { + customerID: { type: 'number' }, + amount: { type: 'number', description: 'Amount to add' }, + reason: { type: 'string', description: 'Reason for credit' }, + }, + required: ['customerID', 'amount'], + }, + handler: async (args: any) => { + const creditAccount = await client.post<{ CreditAccount: CreditAccount }>('/CreditAccount', { + CreditAccount: { + customerID: args.customerID, + creditLimit: args.amount, + name: args.reason || 'Store Credit', + giftCard: false, + }, + }); + return creditAccount.CreditAccount; + }, + }, ]; } diff --git a/servers/lightspeed/src/tools/discounts-tools.ts b/servers/lightspeed/src/tools/discounts-tools.ts deleted file mode 100644 index c3ee0d5..0000000 --- a/servers/lightspeed/src/tools/discounts-tools.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Lightspeed Discounts Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../client.js'; -import type { Discount } from '../types/index.js'; - -export function createDiscountsTools(client: LightspeedClient) { - return { - lightspeed_list_discounts: { - description: 'List all discounts', - inputSchema: z.object({ - archived: z.boolean().optional().describe('Include archived discounts'), - }), - handler: async (args: { archived?: boolean }) => { - try { - const params: any = {}; - if (args.archived !== undefined) { - params.archived = args.archived; - } - - const discounts = await client.getAll('/Discount', 'Discount', 100); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: discounts.length, - discounts: discounts.map(d => ({ - discountID: d.discountID, - name: d.name, - type: d.type, - value: d.value, - archived: d.archived, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_get_discount: { - description: 'Get detailed information about a specific discount', - inputSchema: z.object({ - discountId: z.string().describe('Discount ID'), - }), - handler: async (args: { discountId: string }) => { - try { - const discount = await client.getById<{ Discount: Discount }>('/Discount', args.discountId); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, discount: discount.Discount }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_create_discount: { - description: 'Create a new discount', - inputSchema: z.object({ - name: z.string().describe('Discount name'), - type: z.enum(['percent', 'amount']).describe('Discount type'), - value: z.string().describe('Discount value (percentage or fixed amount)'), - }), - handler: async (args: { name: string; type: 'percent' | 'amount'; value: string }) => { - try { - const discountData = { - name: args.name, - type: args.type, - value: args.value, - }; - - const result = await client.post<{ Discount: Discount }>('/Discount', { Discount: discountData }); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, discount: result.Discount }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_update_discount: { - description: 'Update an existing discount', - inputSchema: z.object({ - discountId: z.string().describe('Discount ID'), - name: z.string().optional().describe('Discount name'), - type: z.enum(['percent', 'amount']).optional().describe('Discount type'), - value: z.string().optional().describe('Discount value'), - archived: z.boolean().optional().describe('Archive status'), - }), - handler: async (args: any) => { - try { - const updateData: any = {}; - if (args.name) updateData.name = args.name; - if (args.type) updateData.type = args.type; - if (args.value) updateData.value = args.value; - if (args.archived !== undefined) updateData.archived = args.archived; - - const result = await client.put<{ Discount: Discount }>( - '/Discount', - args.discountId, - { Discount: updateData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, discount: result.Discount }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_delete_discount: { - description: 'Delete (archive) a discount', - inputSchema: z.object({ - discountId: z.string().describe('Discount ID'), - }), - handler: async (args: { discountId: string }) => { - try { - await client.delete('/Discount', args.discountId); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, message: 'Discount deleted' }), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - }; -} diff --git a/servers/lightspeed/src/tools/discounts.ts b/servers/lightspeed/src/tools/discounts.ts index 3b92d87..70b1ad3 100644 --- a/servers/lightspeed/src/tools/discounts.ts +++ b/servers/lightspeed/src/tools/discounts.ts @@ -1,98 +1,61 @@ -/** - * Discount Management Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Discount } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerDiscountTools(client: LightspeedClient) { +export function createDiscountTools(client: LightspeedClient) { return [ { name: 'lightspeed_list_discounts', - description: 'List all discounts in Lightspeed.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of discounts to return (default 100)'), - offset: z.number().optional().describe('Offset for pagination') - }), + description: 'List all discounts', + inputSchema: { + type: 'object', + properties: { + archived: { type: 'boolean' }, + }, + }, handler: async (args: any) => { - const result = await client.getDiscounts(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_discount', - description: 'Get a single discount by ID.', - inputSchema: z.object({ - discountID: z.string().describe('The discount ID') - }), - handler: async (args: any) => { - const result = await client.getDiscount(args.discountID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const params: any = {}; + if (args.archived !== undefined) params.archived = args.archived; + const discounts = await client.getPaginated('/Discount', params); + return { discounts, count: discounts.length }; + }, }, { name: 'lightspeed_create_discount', - description: 'Create a new discount. Can be percentage-based or fixed amount.', - inputSchema: z.object({ - name: z.string().describe('Discount name'), - type: z.enum(['percentage', 'fixed']).describe('Discount type'), - value: z.number().describe('Discount value (percentage or fixed amount)'), - minQuantity: z.number().optional().describe('Minimum quantity required'), - minAmount: z.number().optional().describe('Minimum purchase amount required'), - startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().optional().describe('End date (YYYY-MM-DD)') - }), + description: 'Create a new discount', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string' }, + discountPercent: { type: 'number', description: 'Discount percentage (0-100)' }, + discountAmount: { type: 'number', description: 'Fixed discount amount' }, + requireCustomer: { type: 'boolean', default: false }, + }, + required: ['name'], + }, handler: async (args: any) => { - const result = await client.createDiscount(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const discount = await client.post<{ Discount: Discount }>('/Discount', { Discount: args }); + return discount.Discount; + }, }, { name: 'lightspeed_update_discount', - description: 'Update an existing discount.', - inputSchema: z.object({ - discountID: z.string().describe('The discount ID to update'), - name: z.string().optional().describe('Discount name'), - value: z.number().optional().describe('Discount value'), - startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().optional().describe('End date (YYYY-MM-DD)'), - archived: z.boolean().optional().describe('Archive the discount') - }), + description: 'Update a discount', + inputSchema: { + type: 'object', + properties: { + discountID: { type: 'number' }, + name: { type: 'string' }, + discountPercent: { type: 'number' }, + discountAmount: { type: 'number' }, + archived: { type: 'boolean' }, + }, + required: ['discountID'], + }, handler: async (args: any) => { const { discountID, ...updates } = args; - const result = await client.updateDiscount(discountID, updates); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const discount = await client.put<{ Discount: Discount }>(`/Discount/${discountID}`, { Discount: updates }); + return discount.Discount; + }, }, - { - name: 'lightspeed_delete_discount', - description: 'Delete a discount from Lightspeed.', - inputSchema: z.object({ - discountID: z.string().describe('The discount ID to delete') - }), - handler: async (args: any) => { - const result = await client.deleteDiscount(args.discountID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_active_discounts', - description: 'Get all currently active discounts (not archived, within date range).', - inputSchema: z.object({}), - handler: async () => { - const result = await client.getDiscounts({ limit: 500 }); - if (result.success) { - const now = new Date(); - const active = result.data.filter(d => { - if (d.archived) return false; - if (d.startDate && new Date(d.startDate) > now) return false; - if (d.endDate && new Date(d.endDate) < now) return false; - return true; - }); - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: active }, null, 2) }] }; - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } ]; } diff --git a/servers/lightspeed/src/tools/employees-tools.ts b/servers/lightspeed/src/tools/employees-tools.ts deleted file mode 100644 index 8f72b50..0000000 --- a/servers/lightspeed/src/tools/employees-tools.ts +++ /dev/null @@ -1,275 +0,0 @@ -/** - * Lightspeed Employees Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../client.js'; -import type { Employee, TimeEntry } from '../types/index.js'; - -export function createEmployeesTools(client: LightspeedClient) { - return { - lightspeed_list_employees: { - description: 'List all employees', - inputSchema: z.object({ - archived: z.boolean().optional().describe('Include archived employees'), - }), - handler: async (args: { archived?: boolean }) => { - try { - const params: any = {}; - if (args.archived !== undefined) { - params.archived = args.archived; - } - - const employees = await client.getAll('/Employee', 'Employee', 100); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: employees.length, - employees: employees.map(e => ({ - employeeID: e.employeeID, - firstName: e.firstName, - lastName: e.lastName, - employeeNumber: e.employeeNumber, - archived: e.archived, - employeeRoleID: e.employeeRoleID, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_get_employee: { - description: 'Get detailed information about a specific employee', - inputSchema: z.object({ - employeeId: z.string().describe('Employee ID'), - }), - handler: async (args: { employeeId: string }) => { - try { - const employee = await client.getById<{ Employee: Employee }>('/Employee', args.employeeId); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, employee: employee.Employee }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_create_employee: { - description: 'Create a new employee', - inputSchema: z.object({ - firstName: z.string().describe('First name'), - lastName: z.string().describe('Last name'), - employeeNumber: z.string().describe('Employee number'), - employeeRoleId: z.string().optional().describe('Employee role ID'), - }), - handler: async (args: any) => { - try { - const employeeData: any = { - firstName: args.firstName, - lastName: args.lastName, - employeeNumber: args.employeeNumber, - }; - if (args.employeeRoleId) { - employeeData.employeeRoleID = args.employeeRoleId; - } - - const result = await client.post<{ Employee: Employee }>('/Employee', { Employee: employeeData }); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, employee: result.Employee }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_update_employee: { - description: 'Update an existing employee', - inputSchema: z.object({ - employeeId: z.string().describe('Employee ID'), - firstName: z.string().optional().describe('First name'), - lastName: z.string().optional().describe('Last name'), - employeeRoleId: z.string().optional().describe('Employee role ID'), - archived: z.boolean().optional().describe('Archive status'), - }), - handler: async (args: any) => { - try { - const updateData: any = {}; - if (args.firstName) updateData.firstName = args.firstName; - if (args.lastName) updateData.lastName = args.lastName; - if (args.employeeRoleId) updateData.employeeRoleID = args.employeeRoleId; - if (args.archived !== undefined) updateData.archived = args.archived; - - const result = await client.put<{ Employee: Employee }>( - '/Employee', - args.employeeId, - { Employee: updateData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, employee: result.Employee }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_list_time_entries: { - description: 'List time entries (clock in/out records) for employees', - inputSchema: z.object({ - employeeId: z.string().optional().describe('Filter by employee ID'), - shopId: z.string().optional().describe('Filter by shop ID'), - startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().optional().describe('End date (YYYY-MM-DD)'), - limit: z.number().optional().describe('Max entries to return (default 100)'), - }), - handler: async (args: any) => { - try { - const params: any = {}; - if (args.employeeId) params.employeeID = args.employeeId; - if (args.shopId) params.shopID = args.shopId; - if (args.startDate) params.clockIn = `>,${args.startDate}`; - if (args.endDate) params.clockOut = `<,${args.endDate}`; - - const entries = await client.getAll('/TimeEntry', 'TimeEntry', args.limit || 100); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: entries.length, - timeEntries: entries.map(te => ({ - timeEntryID: te.timeEntryID, - employeeID: te.employeeID, - clockIn: te.clockIn, - clockOut: te.clockOut, - totalHours: te.totalHours, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_clock_in: { - description: 'Clock in an employee (start time tracking)', - inputSchema: z.object({ - employeeId: z.string().describe('Employee ID'), - shopId: z.string().describe('Shop ID'), - }), - handler: async (args: { employeeId: string; shopId: string }) => { - try { - const timeEntryData = { - employeeID: args.employeeId, - shopID: args.shopId, - clockIn: new Date().toISOString(), - }; - - const result = await client.post<{ TimeEntry: TimeEntry }>( - '/TimeEntry', - { TimeEntry: timeEntryData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'Employee clocked in', - timeEntry: result.TimeEntry, - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_clock_out: { - description: 'Clock out an employee (end time tracking)', - inputSchema: z.object({ - timeEntryId: z.string().describe('Time entry ID (from clock in)'), - }), - handler: async (args: { timeEntryId: string }) => { - try { - const clockOutTime = new Date().toISOString(); - const updateData = { - clockOut: clockOutTime, - }; - - const result = await client.put<{ TimeEntry: TimeEntry }>( - '/TimeEntry', - args.timeEntryId, - { TimeEntry: updateData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'Employee clocked out', - timeEntry: result.TimeEntry, - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - }; -} diff --git a/servers/lightspeed/src/tools/employees.ts b/servers/lightspeed/src/tools/employees.ts index 1aec139..dccd4c9 100644 --- a/servers/lightspeed/src/tools/employees.ts +++ b/servers/lightspeed/src/tools/employees.ts @@ -1,100 +1,112 @@ -/** - * Employee Management Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Employee } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerEmployeeTools(client: LightspeedClient) { +export function createEmployeeTools(client: LightspeedClient) { return [ { name: 'lightspeed_list_employees', - description: 'List all employees in Lightspeed.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of employees to return (default 100)'), - offset: z.number().optional().describe('Offset for pagination') - }), + description: 'List all employees', + inputSchema: { + type: 'object', + properties: { + archived: { type: 'boolean' }, + shopID: { type: 'number' }, + }, + }, handler: async (args: any) => { - const result = await client.getEmployees(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const params: any = {}; + if (args.archived !== undefined) params.archived = args.archived; + if (args.shopID) params.limitToShopID = args.shopID; + const employees = await client.getPaginated('/Employee', params); + return { employees, count: employees.length }; + }, }, { name: 'lightspeed_get_employee', - description: 'Get a single employee by ID with all details.', - inputSchema: z.object({ - employeeID: z.string().describe('The employee ID') - }), + description: 'Get a specific employee by ID', + inputSchema: { + type: 'object', + properties: { + employeeID: { type: 'number' }, + }, + required: ['employeeID'], + }, handler: async (args: any) => { - const result = await client.getEmployee(args.employeeID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const employee = await client.get<{ Employee: Employee }>(`/Employee/${args.employeeID}`); + return employee.Employee; + }, }, { name: 'lightspeed_create_employee', - description: 'Create a new employee. Requires first name and last name at minimum.', - inputSchema: z.object({ - firstName: z.string().describe('Employee first name'), - lastName: z.string().describe('Employee last name'), - email: z.string().optional().describe('Email address'), - phone: z.string().optional().describe('Phone number'), - employeeNumber: z.string().optional().describe('Employee number'), - pin: z.string().optional().describe('POS PIN code'), - employeeRoleID: z.string().optional().describe('Employee role ID') - }), + description: 'Create a new employee', + inputSchema: { + type: 'object', + properties: { + firstName: { type: 'string' }, + lastName: { type: 'string' }, + employeeRoleID: { type: 'number' }, + limitToShopID: { type: 'number' }, + }, + required: ['firstName', 'lastName'], + }, handler: async (args: any) => { - const result = await client.createEmployee(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const employee = await client.post<{ Employee: Employee }>('/Employee', { Employee: args }); + return employee.Employee; + }, }, { name: 'lightspeed_update_employee', - description: 'Update an existing employee.', - inputSchema: z.object({ - employeeID: z.string().describe('The employee ID to update'), - firstName: z.string().optional().describe('Employee first name'), - lastName: z.string().optional().describe('Employee last name'), - email: z.string().optional().describe('Email address'), - phone: z.string().optional().describe('Phone number'), - pin: z.string().optional().describe('POS PIN code'), - archived: z.boolean().optional().describe('Archive the employee') - }), + description: 'Update an employee', + inputSchema: { + type: 'object', + properties: { + employeeID: { type: 'number' }, + firstName: { type: 'string' }, + lastName: { type: 'string' }, + lockOut: { type: 'boolean' }, + archived: { type: 'boolean' }, + }, + required: ['employeeID'], + }, handler: async (args: any) => { const { employeeID, ...updates } = args; - const result = await client.updateEmployee(employeeID, updates); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const employee = await client.put<{ Employee: Employee }>(`/Employee/${employeeID}`, { Employee: updates }); + return employee.Employee; + }, }, { name: 'lightspeed_delete_employee', - description: 'Delete an employee from Lightspeed.', - inputSchema: z.object({ - employeeID: z.string().describe('The employee ID to delete') - }), + description: 'Archive an employee', + inputSchema: { + type: 'object', + properties: { + employeeID: { type: 'number' }, + }, + required: ['employeeID'], + }, handler: async (args: any) => { - const result = await client.deleteEmployee(args.employeeID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + await client.delete(`/Employee/${args.employeeID}`); + return { success: true, message: `Employee ${args.employeeID} archived` }; + }, }, { - name: 'lightspeed_search_employees', - description: 'Search employees by name or email.', - inputSchema: z.object({ - query: z.string().describe('Search query (name or email)') - }), + name: 'lightspeed_get_employee_hours', + description: 'Get time tracking hours for an employee', + inputSchema: { + type: 'object', + properties: { + employeeID: { type: 'number' }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + }, + required: ['employeeID'], + }, handler: async (args: any) => { - const result = await client.getEmployees({ limit: 500 }); - if (result.success) { - const query = args.query.toLowerCase(); - const filtered = result.data.filter(e => - e.firstName?.toLowerCase().includes(query) || - e.lastName?.toLowerCase().includes(query) || - e.email?.toLowerCase().includes(query) - ); - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] }; - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } + const params: any = { employeeID: args.employeeID }; + if (args.startDate) params.checkIn = `>,${args.startDate}`; + const hours = await client.getPaginated('/EmployeeHours', params); + return { hours, count: hours.length }; + }, + }, ]; } diff --git a/servers/lightspeed/src/tools/inventory-tools.ts b/servers/lightspeed/src/tools/inventory-tools.ts deleted file mode 100644 index 23d460a..0000000 --- a/servers/lightspeed/src/tools/inventory-tools.ts +++ /dev/null @@ -1,336 +0,0 @@ -/** - * Lightspeed Inventory Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../client.js'; -import type { InventoryCount, InventoryTransfer, Supplier, PurchaseOrder } from '../types/index.js'; - -export function createInventoryTools(client: LightspeedClient) { - return { - lightspeed_list_inventory: { - description: 'List inventory counts for all products at a shop', - inputSchema: z.object({ - shopId: z.string().describe('Shop ID'), - limit: z.number().optional().describe('Max items to return (default 100)'), - }), - handler: async (args: { shopId: string; limit?: number }) => { - try { - const inventory = await client.getAll( - `/Shop/${args.shopId}/ItemShop`, - 'ItemShop', - args.limit || 100 - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: inventory.length, - inventory: inventory.map(i => ({ - itemID: i.itemID, - qoh: i.qoh, - reorderPoint: i.reorderPoint, - backorder: i.backorder, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_get_item_inventory: { - description: 'Get inventory count for a specific product at a shop', - inputSchema: z.object({ - itemId: z.string().describe('Product item ID'), - shopId: z.string().describe('Shop ID'), - }), - handler: async (args: { itemId: string; shopId: string }) => { - try { - const inventory = await client.get<{ ItemShop: InventoryCount }>( - `/Item/${args.itemId}/ItemShop/${args.shopId}` - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, inventory: inventory.ItemShop }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_update_inventory_count: { - description: 'Update inventory quantity for a product at a shop', - inputSchema: z.object({ - itemId: z.string().describe('Product item ID'), - shopId: z.string().describe('Shop ID'), - quantity: z.number().describe('New quantity on hand'), - reorderPoint: z.number().optional().describe('Reorder point threshold'), - reorderLevel: z.number().optional().describe('Reorder quantity'), - }), - handler: async (args: any) => { - try { - const updateData: any = { - qoh: args.quantity.toString(), - }; - if (args.reorderPoint !== undefined) { - updateData.reorderPoint = args.reorderPoint.toString(); - } - if (args.reorderLevel !== undefined) { - updateData.reorderLevel = args.reorderLevel.toString(); - } - - const result = await client.put<{ ItemShop: InventoryCount }>( - `/Item/${args.itemId}/ItemShop`, - args.shopId, - { ItemShop: updateData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, inventory: result.ItemShop }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_transfer_stock: { - description: 'Transfer inventory between shops', - inputSchema: z.object({ - fromShopId: z.string().describe('Source shop ID'), - toShopId: z.string().describe('Destination shop ID'), - items: z.array(z.object({ - itemId: z.string().describe('Product item ID'), - quantity: z.number().describe('Quantity to transfer'), - })).describe('Items to transfer'), - }), - handler: async (args: any) => { - try { - const transferData = { - fromShopID: args.fromShopId, - toShopID: args.toShopId, - TransferItems: { - TransferItem: args.items.map((item: any) => ({ - itemID: item.itemId, - quantity: item.quantity.toString(), - })), - }, - }; - - const result = await client.post<{ Transfer: InventoryTransfer }>( - '/Transfer', - { Transfer: transferData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, transfer: result.Transfer }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_list_inventory_adjustments: { - description: 'List inventory adjustments (stock changes)', - inputSchema: z.object({ - itemId: z.string().optional().describe('Filter by product item ID'), - shopId: z.string().optional().describe('Filter by shop ID'), - limit: z.number().optional().describe('Max adjustments to return (default 100)'), - }), - handler: async (args: any) => { - try { - const params: any = {}; - if (args.itemId) params.itemID = args.itemId; - if (args.shopId) params.shopID = args.shopId; - - // Note: Lightspeed tracks adjustments through SaleLine with special types - const adjustments = await client.get<{ SaleLine: any[] }>('/SaleLine', params); - const results = Array.isArray(adjustments.SaleLine) - ? adjustments.SaleLine - : adjustments.SaleLine ? [adjustments.SaleLine] : []; - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: results.length, - adjustments: results.slice(0, args.limit || 100), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_list_suppliers: { - description: 'List all suppliers/vendors', - inputSchema: z.object({ - limit: z.number().optional().describe('Max suppliers to return (default 100)'), - archived: z.boolean().optional().describe('Include archived suppliers'), - }), - handler: async (args: { limit?: number; archived?: boolean }) => { - try { - const params: any = {}; - if (args.archived !== undefined) { - params.archived = args.archived; - } - - const suppliers = await client.getAll('/Vendor', 'Vendor', args.limit || 100); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: suppliers.length, - suppliers: suppliers.map(s => ({ - vendorID: s.vendorID, - name: s.name, - accountNumber: s.accountNumber, - archived: s.archived, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_create_purchase_order: { - description: 'Create a purchase order for restocking inventory', - inputSchema: z.object({ - vendorId: z.string().describe('Supplier/vendor ID'), - shopId: z.string().describe('Shop ID'), - items: z.array(z.object({ - itemId: z.string().describe('Product item ID'), - quantity: z.number().describe('Quantity to order'), - unitCost: z.string().describe('Unit cost'), - })).describe('Items to order'), - }), - handler: async (args: any) => { - try { - const poData = { - vendorID: args.vendorId, - shopID: args.shopId, - PurchaseOrderLines: { - PurchaseOrderLine: args.items.map((item: any) => ({ - itemID: item.itemId, - quantity: item.quantity.toString(), - unitCost: item.unitCost, - })), - }, - }; - - const result = await client.post<{ PurchaseOrder: PurchaseOrder }>( - '/PurchaseOrder', - { PurchaseOrder: poData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, purchaseOrder: result.PurchaseOrder }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_list_purchase_orders: { - description: 'List purchase orders', - inputSchema: z.object({ - vendorId: z.string().optional().describe('Filter by vendor ID'), - status: z.string().optional().describe('Filter by status (e.g., open, complete)'), - limit: z.number().optional().describe('Max POs to return (default 100)'), - }), - handler: async (args: any) => { - try { - const params: any = {}; - if (args.vendorId) params.vendorID = args.vendorId; - if (args.status) params.status = args.status; - - const pos = await client.getAll( - '/PurchaseOrder', - 'PurchaseOrder', - args.limit || 100 - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: pos.length, - purchaseOrders: pos.map(po => ({ - purchaseOrderID: po.purchaseOrderID, - vendorID: po.vendorID, - orderNumber: po.orderNumber, - status: po.status, - createTime: po.createTime, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - }; -} diff --git a/servers/lightspeed/src/tools/inventory.ts b/servers/lightspeed/src/tools/inventory.ts index a521441..3ea8d2a 100644 --- a/servers/lightspeed/src/tools/inventory.ts +++ b/servers/lightspeed/src/tools/inventory.ts @@ -1,145 +1,201 @@ -/** - * Inventory Management Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { InventoryCount, InventoryTransfer, InventoryLog } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerInventoryTools(client: LightspeedClient) { +export function createInventoryTools(client: LightspeedClient) { return [ { - name: 'lightspeed_get_product_inventory', - description: 'Get inventory levels for a product across all shops or a specific shop.', - inputSchema: z.object({ - productID: z.string().describe('The product ID'), - shopID: z.string().optional().describe('Optional shop ID to filter by specific location') - }), + name: 'lightspeed_list_inventory_counts', + description: 'List all inventory count sessions', + inputSchema: { + type: 'object', + properties: { + shopID: { type: 'number' }, + archived: { type: 'boolean' }, + }, + }, handler: async (args: any) => { - const result = await client.getProductInventory(args.productID, args.shopID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const params: any = {}; + if (args.shopID) params.shopID = args.shopID; + if (args.archived !== undefined) params.archived = args.archived; + const counts = await client.getPaginated('/InventoryCount', params); + return { counts, total: counts.length }; + }, }, { - name: 'lightspeed_update_inventory', - description: 'Update inventory quantity for a product at a specific shop location.', - inputSchema: z.object({ - productID: z.string().describe('The product ID'), - shopID: z.string().describe('The shop/location ID'), - qty: z.number().describe('New inventory quantity') - }), + name: 'lightspeed_create_inventory_count', + description: 'Create a new inventory count session', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string' }, + shopID: { type: 'number' }, + }, + required: ['name', 'shopID'], + }, handler: async (args: any) => { - const result = await client.updateInventory(args.productID, args.shopID, args.qty); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const count = await client.post<{ InventoryCount: InventoryCount }>('/InventoryCount', { + InventoryCount: args, + }); + return count.InventoryCount; + }, }, { - name: 'lightspeed_adjust_inventory', - description: 'Adjust inventory by a relative amount (add or subtract). Positive values add stock, negative subtract.', - inputSchema: z.object({ - productID: z.string().describe('The product ID'), - shopID: z.string().describe('The shop/location ID'), - adjustment: z.number().describe('Amount to adjust (positive to add, negative to subtract)') - }), + name: 'lightspeed_add_count_items', + description: 'Add items to an inventory count', + inputSchema: { + type: 'object', + properties: { + inventoryCountID: { type: 'number' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + itemID: { type: 'number' }, + qty: { type: 'number' }, + }, + }, + }, + }, + required: ['inventoryCountID', 'items'], + }, handler: async (args: any) => { - const invResult = await client.getProductInventory(args.productID, args.shopID); - if (!invResult.success || invResult.data.length === 0) { - return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Inventory not found' }, null, 2) }] }; + const results = []; + for (const item of args.items) { + const countItem = await client.post('/InventoryCountItem', { + InventoryCountItem: { + inventoryCountID: args.inventoryCountID, + itemID: item.itemID, + qty: item.qty, + }, + }); + results.push(countItem); } - const currentQty = invResult.data[0].qty; - const newQty = currentQty + args.adjustment; - const result = await client.updateInventory(args.productID, args.shopID, newQty); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + return { added: results.length, items: results }; + }, }, { - name: 'lightspeed_set_reorder_point', - description: 'Set the reorder point (minimum stock level) for a product at a shop.', - inputSchema: z.object({ - productID: z.string().describe('The product ID'), - shopID: z.string().describe('The shop/location ID'), - reorderPoint: z.number().describe('Stock level that triggers reorder alert') - }), + name: 'lightspeed_list_inventory_transfers', + description: 'List inventory transfers between locations', + inputSchema: { + type: 'object', + properties: { + sendingShopID: { type: 'number' }, + receivingShopID: { type: 'number' }, + status: { type: 'string', enum: ['pending', 'sent', 'received', 'cancelled'] }, + }, + }, handler: async (args: any) => { - // Note: This would use ItemShop update endpoint in real implementation - return { content: [{ type: 'text', text: JSON.stringify({ - success: true, - message: 'Reorder point set', - productID: args.productID, - shopID: args.shopID, - reorderPoint: args.reorderPoint - }, null, 2) }] }; - } + const params: any = {}; + if (args.sendingShopID) params.sendingShopID = args.sendingShopID; + if (args.receivingShopID) params.receivingShopID = args.receivingShopID; + if (args.status) params.status = args.status; + const transfers = await client.getPaginated('/Transfer', params); + return { transfers, count: transfers.length }; + }, }, { - name: 'lightspeed_check_low_stock', - description: 'Check for products that are below their reorder point (low stock alert).', - inputSchema: z.object({ - shopID: z.string().optional().describe('Optional shop ID to check specific location') - }), + name: 'lightspeed_create_inventory_transfer', + description: 'Create an inventory transfer between locations', + inputSchema: { + type: 'object', + properties: { + sendingShopID: { type: 'number' }, + receivingShopID: { type: 'number' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + itemID: { type: 'number' }, + toSend: { type: 'number' }, + }, + }, + }, + note: { type: 'string' }, + }, + required: ['sendingShopID', 'receivingShopID', 'items'], + }, handler: async (args: any) => { - const productsResult = await client.getProducts({ limit: 500 }); - if (!productsResult.success) { - return { content: [{ type: 'text', text: JSON.stringify(productsResult, null, 2) }] }; + const transfer = await client.post<{ Transfer: InventoryTransfer }>('/Transfer', { + Transfer: { + sendingShopID: args.sendingShopID, + receivingShopID: args.receivingShopID, + note: args.note, + status: 'pending', + }, + }); + + const transferID = transfer.Transfer.transferID; + + for (const item of args.items) { + await client.post('/TransferItem', { + TransferItem: { + transferID, + itemID: item.itemID, + toSend: item.toSend, + }, + }); } - const lowStockItems = []; - for (const product of productsResult.data) { - const invResult = await client.getProductInventory(product.productID, args.shopID); - if (invResult.success) { - for (const inv of invResult.data) { - if (inv.qty <= inv.reorderPoint && inv.reorderPoint > 0) { - lowStockItems.push({ - productID: product.productID, - description: product.description, - shopID: inv.shopID, - currentQty: inv.qty, - reorderPoint: inv.reorderPoint, - needed: inv.reorderLevel - inv.qty - }); - } - } - } - } - - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: lowStockItems }, null, 2) }] }; - } + return transfer.Transfer; + }, }, { - name: 'lightspeed_inventory_transfer', - description: 'Transfer inventory between two shop locations.', - inputSchema: z.object({ - productID: z.string().describe('The product ID'), - fromShopID: z.string().describe('Source shop ID'), - toShopID: z.string().describe('Destination shop ID'), - quantity: z.number().describe('Quantity to transfer') - }), + name: 'lightspeed_send_inventory_transfer', + description: 'Mark an inventory transfer as sent', + inputSchema: { + type: 'object', + properties: { + transferID: { type: 'number' }, + }, + required: ['transferID'], + }, handler: async (args: any) => { - // Subtract from source - const fromResult = await client.getProductInventory(args.productID, args.fromShopID); - if (!fromResult.success || fromResult.data.length === 0) { - return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Source inventory not found' }, null, 2) }] }; - } - - const sourceQty = fromResult.data[0].qty; - if (sourceQty < args.quantity) { - return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Insufficient inventory at source' }, null, 2) }] }; - } - - await client.updateInventory(args.productID, args.fromShopID, sourceQty - args.quantity); - - // Add to destination - const toResult = await client.getProductInventory(args.productID, args.toShopID); - const destQty = toResult.success && toResult.data.length > 0 ? toResult.data[0].qty : 0; - await client.updateInventory(args.productID, args.toShopID, destQty + args.quantity); - - return { content: [{ type: 'text', text: JSON.stringify({ - success: true, - message: 'Inventory transferred', - from: args.fromShopID, - to: args.toShopID, - quantity: args.quantity - }, null, 2) }] }; - } - } + const transfer = await client.put<{ Transfer: InventoryTransfer }>(`/Transfer/${args.transferID}`, { + Transfer: { + status: 'sent', + sentOn: new Date().toISOString(), + }, + }); + return transfer.Transfer; + }, + }, + { + name: 'lightspeed_receive_inventory_transfer', + description: 'Receive an inventory transfer at destination', + inputSchema: { + type: 'object', + properties: { + transferID: { type: 'number' }, + }, + required: ['transferID'], + }, + handler: async (args: any) => { + const transfer = await client.put<{ Transfer: InventoryTransfer }>(`/Transfer/${args.transferID}`, { + Transfer: { status: 'received' }, + }); + return transfer.Transfer; + }, + }, + { + name: 'lightspeed_get_inventory_logs', + description: 'Get inventory change history for an item', + inputSchema: { + type: 'object', + properties: { + itemID: { type: 'number' }, + shopID: { type: 'number' }, + limit: { type: 'number', default: 50 }, + }, + required: ['itemID'], + }, + handler: async (args: any) => { + const params: any = { itemID: args.itemID }; + if (args.shopID) params.shopID = args.shopID; + const logs = await client.getPaginated('/InventoryLog', params, args.limit || 50); + return { logs, count: logs.length }; + }, + }, ]; } diff --git a/servers/lightspeed/src/tools/loyalty.ts b/servers/lightspeed/src/tools/loyalty.ts deleted file mode 100644 index 83d96dc..0000000 --- a/servers/lightspeed/src/tools/loyalty.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Loyalty Program Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerLoyaltyTools(client: LightspeedClient) { - return [ - { - name: 'lightspeed_get_customer_loyalty', - description: 'Get loyalty points balance and history for a customer.', - inputSchema: z.object({ - customerID: z.string().describe('The customer ID') - }), - handler: async (args: any) => { - // Note: In real implementation, this would call Lightspeed loyalty endpoints - return { content: [{ type: 'text', text: JSON.stringify({ - success: true, - message: 'Loyalty lookup for customer', - customerID: args.customerID, - points: 0, - lifetimePoints: 0 - }, null, 2) }] }; - } - }, - { - name: 'lightspeed_add_loyalty_points', - description: 'Add loyalty points to a customer account.', - inputSchema: z.object({ - customerID: z.string().describe('The customer ID'), - points: z.number().describe('Number of points to add') - }), - handler: async (args: any) => { - return { content: [{ type: 'text', text: JSON.stringify({ - success: true, - message: 'Points added', - customerID: args.customerID, - pointsAdded: args.points - }, null, 2) }] }; - } - }, - { - name: 'lightspeed_redeem_loyalty_points', - description: 'Redeem loyalty points from a customer account.', - inputSchema: z.object({ - customerID: z.string().describe('The customer ID'), - points: z.number().describe('Number of points to redeem') - }), - handler: async (args: any) => { - return { content: [{ type: 'text', text: JSON.stringify({ - success: true, - message: 'Points redeemed', - customerID: args.customerID, - pointsRedeemed: args.points - }, null, 2) }] }; - } - }, - { - name: 'lightspeed_calculate_loyalty_points', - description: 'Calculate how many loyalty points a purchase amount would earn.', - inputSchema: z.object({ - amount: z.number().describe('Purchase amount'), - programID: z.string().optional().describe('Loyalty program ID') - }), - handler: async (args: any) => { - // Typical rate: 1 point per dollar - const pointsPerDollar = 1; - const points = Math.floor(args.amount * pointsPerDollar); - return { content: [{ type: 'text', text: JSON.stringify({ - success: true, - amount: args.amount, - pointsEarned: points, - rate: pointsPerDollar - }, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_top_loyalty_customers', - description: 'Get customers with the highest loyalty points balances.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of customers to return (default 10)') - }), - handler: async (args: any) => { - return { content: [{ type: 'text', text: JSON.stringify({ - success: true, - message: 'Top loyalty customers', - data: [] - }, null, 2) }] }; - } - } - ]; -} diff --git a/servers/lightspeed/src/tools/manufacturers.ts b/servers/lightspeed/src/tools/manufacturers.ts new file mode 100644 index 0000000..8b44fbd --- /dev/null +++ b/servers/lightspeed/src/tools/manufacturers.ts @@ -0,0 +1,51 @@ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Manufacturer } from '../types/index.js'; + +export function createManufacturerTools(client: LightspeedClient) { + return [ + { + name: 'lightspeed_list_manufacturers', + description: 'List all manufacturers/brands', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async () => { + const manufacturers = await client.getPaginated('/Manufacturer'); + return { manufacturers, count: manufacturers.length }; + }, + }, + { + name: 'lightspeed_create_manufacturer', + description: 'Create a new manufacturer/brand', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string' }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const manufacturer = await client.post<{ Manufacturer: Manufacturer }>('/Manufacturer', { Manufacturer: args }); + return manufacturer.Manufacturer; + }, + }, + { + name: 'lightspeed_update_manufacturer', + description: 'Update a manufacturer', + inputSchema: { + type: 'object', + properties: { + manufacturerID: { type: 'number' }, + name: { type: 'string' }, + }, + required: ['manufacturerID', 'name'], + }, + handler: async (args: any) => { + const { manufacturerID, ...updates } = args; + const manufacturer = await client.put<{ Manufacturer: Manufacturer }>(`/Manufacturer/${manufacturerID}`, { Manufacturer: updates }); + return manufacturer.Manufacturer; + }, + }, + ]; +} diff --git a/servers/lightspeed/src/tools/orders.ts b/servers/lightspeed/src/tools/orders.ts index 67726f7..703cb59 100644 --- a/servers/lightspeed/src/tools/orders.ts +++ b/servers/lightspeed/src/tools/orders.ts @@ -1,105 +1,172 @@ -/** - * Purchase Order Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Order, OrderLine } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerOrderTools(client: LightspeedClient) { +export function createOrderTools(client: LightspeedClient) { return [ { name: 'lightspeed_list_orders', - description: 'List all purchase orders with optional status filtering.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of orders to return (default 100)'), - offset: z.number().optional().describe('Offset for pagination'), - status: z.enum(['open', 'received', 'partial', 'cancelled']).optional().describe('Filter by order status') - }), + description: 'List purchase orders to vendors', + inputSchema: { + type: 'object', + properties: { + vendorID: { type: 'number' }, + shopID: { type: 'number' }, + complete: { type: 'boolean' }, + archived: { type: 'boolean' }, + limit: { type: 'number', default: 100 }, + }, + }, handler: async (args: any) => { - const result = await client.getOrders(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const params: any = {}; + if (args.vendorID) params.vendorID = args.vendorID; + if (args.shopID) params.shopID = args.shopID; + if (args.complete !== undefined) params.complete = args.complete; + if (args.archived !== undefined) params.archived = args.archived; + const orders = await client.getPaginated('/Order', params, args.limit || 100); + return { orders, count: orders.length }; + }, }, { name: 'lightspeed_get_order', - description: 'Get a single purchase order by ID with all line items.', - inputSchema: z.object({ - orderID: z.string().describe('The order ID') - }), + description: 'Get a specific purchase order with line items', + inputSchema: { + type: 'object', + properties: { + orderID: { type: 'number' }, + }, + required: ['orderID'], + }, handler: async (args: any) => { - const result = await client.getOrder(args.orderID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const order = await client.get<{ Order: Order }>(`/Order/${args.orderID}?load=[OrderLines]`); + return order.Order; + }, }, { name: 'lightspeed_create_order', - description: 'Create a new purchase order for ordering inventory from suppliers.', - inputSchema: z.object({ - supplierID: z.string().describe('Supplier/vendor ID'), - shopID: z.string().describe('Shop ID receiving the order'), - orderDate: z.string().describe('Order date (YYYY-MM-DD)'), - expectedDate: z.string().optional().describe('Expected delivery date (YYYY-MM-DD)'), - employeeID: z.string().optional().describe('Employee creating the order') - }), + description: 'Create a new purchase order to a vendor', + inputSchema: { + type: 'object', + properties: { + vendorID: { type: 'number' }, + shopID: { type: 'number' }, + orderedDate: { type: 'string' }, + arrivalDate: { type: 'string' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + itemID: { type: 'number' }, + quantity: { type: 'number' }, + price: { type: 'number' }, + }, + required: ['itemID', 'quantity'], + }, + }, + }, + required: ['vendorID', 'shopID', 'items'], + }, handler: async (args: any) => { - const result = await client.createOrder({ - ...args, - status: 'open', - orderLines: [] + const order = await client.post<{ Order: Order }>('/Order', { + Order: { + vendorID: args.vendorID, + shopID: args.shopID, + orderedDate: args.orderedDate || new Date().toISOString(), + arrivalDate: args.arrivalDate, + }, }); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_update_order', - description: 'Update an existing purchase order.', - inputSchema: z.object({ - orderID: z.string().describe('The order ID to update'), - status: z.enum(['open', 'received', 'partial', 'cancelled']).optional().describe('Order status'), - expectedDate: z.string().optional().describe('Expected delivery date (YYYY-MM-DD)'), - completedDate: z.string().optional().describe('Date order was completed (YYYY-MM-DD)') - }), - handler: async (args: any) => { - const { orderID, ...updates } = args; - const result = await client.updateOrder(orderID, updates); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_delete_order', - description: 'Delete a purchase order.', - inputSchema: z.object({ - orderID: z.string().describe('The order ID to delete') - }), - handler: async (args: any) => { - const result = await client.deleteOrder(args.orderID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + + const orderID = order.Order.orderID; + + for (const item of args.items) { + await client.post('/OrderLine', { + OrderLine: { + orderID, + itemID: item.itemID, + quantity: item.quantity, + price: item.price, + }, + }); + } + + const completeOrder = await client.get<{ Order: Order }>(`/Order/${orderID}?load=[OrderLines]`); + return completeOrder.Order; + }, }, { name: 'lightspeed_receive_order', - description: 'Mark an order as received and update inventory.', - inputSchema: z.object({ - orderID: z.string().describe('The order ID to receive') - }), + description: 'Receive/complete a purchase order', + inputSchema: { + type: 'object', + properties: { + orderID: { type: 'number' }, + receivedItems: { + type: 'array', + items: { + type: 'object', + properties: { + orderLineID: { type: 'number' }, + quantityReceived: { type: 'number' }, + }, + }, + }, + }, + required: ['orderID'], + }, handler: async (args: any) => { - const result = await client.updateOrder(args.orderID, { - status: 'received', - completedDate: new Date().toISOString().split('T')[0] + // Update order lines with received quantities + if (args.receivedItems) { + for (const item of args.receivedItems) { + await client.put(`/OrderLine/${item.orderLineID}`, { + OrderLine: { numReceived: item.quantityReceived }, + }); + } + } + + // Mark order as complete + const order = await client.put<{ Order: Order }>(`/Order/${args.orderID}`, { + Order: { + complete: true, + receivedDate: new Date().toISOString(), + }, }); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + + return order.Order; + }, }, { - name: 'lightspeed_cancel_order', - description: 'Cancel a purchase order.', - inputSchema: z.object({ - orderID: z.string().describe('The order ID to cancel') - }), + name: 'lightspeed_update_order', + description: 'Update a purchase order', + inputSchema: { + type: 'object', + properties: { + orderID: { type: 'number' }, + arrivalDate: { type: 'string' }, + shipInstructions: { type: 'string' }, + stockInstructions: { type: 'string' }, + }, + required: ['orderID'], + }, handler: async (args: any) => { - const result = await client.updateOrder(args.orderID, { status: 'cancelled' }); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } + const { orderID, ...updates } = args; + const order = await client.put<{ Order: Order }>(`/Order/${orderID}`, { Order: updates }); + return order.Order; + }, + }, + { + name: 'lightspeed_delete_order', + description: 'Archive a purchase order', + inputSchema: { + type: 'object', + properties: { + orderID: { type: 'number' }, + }, + required: ['orderID'], + }, + handler: async (args: any) => { + await client.delete(`/Order/${args.orderID}`); + return { success: true, message: `Order ${args.orderID} archived` }; + }, + }, ]; } diff --git a/servers/lightspeed/src/tools/products.ts b/servers/lightspeed/src/tools/products.ts index ee9dd76..8a372c4 100644 --- a/servers/lightspeed/src/tools/products.ts +++ b/servers/lightspeed/src/tools/products.ts @@ -1,119 +1,252 @@ -/** - * Product Management Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Item } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerProductTools(client: LightspeedClient) { +export function createProductTools(client: LightspeedClient) { return [ { name: 'lightspeed_list_products', - description: 'List all products in Lightspeed POS. Supports pagination and filtering by category.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of products to return (default 100)'), - offset: z.number().optional().describe('Offset for pagination'), - categoryID: z.string().optional().describe('Filter by category ID') - }), + description: 'List all products/items with optional filters (archived, category, manufacturer, search)', + inputSchema: { + type: 'object', + properties: { + archived: { type: 'boolean', description: 'Filter by archived status' }, + categoryID: { type: 'number', description: 'Filter by category ID' }, + manufacturerID: { type: 'number', description: 'Filter by manufacturer ID' }, + customSku: { type: 'string', description: 'Search by custom SKU' }, + upc: { type: 'string', description: 'Search by UPC code' }, + limit: { type: 'number', description: 'Maximum items to return (default 100)', default: 100 }, + }, + }, handler: async (args: any) => { - const result = await client.getProducts(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const params: any = {}; + if (args.archived !== undefined) params.archived = args.archived; + if (args.categoryID) params.categoryID = args.categoryID; + if (args.manufacturerID) params.manufacturerID = args.manufacturerID; + if (args.customSku) params.customSku = args.customSku; + if (args.upc) params.upc = args.upc; + + const items = await client.getPaginated('/Item', params, args.limit || 100); + return { items, count: items.length }; + }, }, { name: 'lightspeed_get_product', - description: 'Get a single product by ID with all details including pricing, cost, SKU, UPC, and category.', - inputSchema: z.object({ - productID: z.string().describe('The product ID') - }), + description: 'Get a specific product/item by ID with full details including prices, inventory, and tags', + inputSchema: { + type: 'object', + properties: { + itemID: { type: 'number', description: 'Product/Item ID' }, + loadRelations: { + type: 'array', + items: { type: 'string' }, + description: 'Relations to load: Prices, ItemShops, Tags, Images, CustomFieldValues' + }, + }, + required: ['itemID'], + }, handler: async (args: any) => { - const result = await client.getProduct(args.productID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + let endpoint = `/Item/${args.itemID}`; + if (args.loadRelations?.length) { + endpoint += `?load=[${args.loadRelations.join(',')}]`; + } + const item = await client.get<{ Item: Item }>(endpoint); + return item.Item; + }, }, { name: 'lightspeed_create_product', - description: 'Create a new product in Lightspeed. Requires description and default price at minimum.', - inputSchema: z.object({ - description: z.string().describe('Product description/name'), - sku: z.string().optional().describe('Stock keeping unit'), - upc: z.string().optional().describe('Universal product code'), - defaultCost: z.number().optional().describe('Default cost price'), - defaultPrice: z.number().describe('Default selling price'), - categoryID: z.string().optional().describe('Category ID'), - manufacturerID: z.string().optional().describe('Manufacturer ID'), - supplierID: z.string().optional().describe('Primary supplier ID'), - tax: z.boolean().optional().describe('Whether product is taxable'), - discountable: z.boolean().optional().describe('Whether product can be discounted'), - onlinePrice: z.number().optional().describe('Online store price'), - msrp: z.number().optional().describe('Manufacturer suggested retail price') - }), + description: 'Create a new product/item', + inputSchema: { + type: 'object', + properties: { + description: { type: 'string', description: 'Product description/name' }, + customSku: { type: 'string', description: 'Custom SKU' }, + upc: { type: 'string', description: 'UPC barcode' }, + defaultCost: { type: 'number', description: 'Default cost' }, + categoryID: { type: 'number', description: 'Category ID' }, + manufacturerID: { type: 'number', description: 'Manufacturer ID' }, + defaultVendorID: { type: 'number', description: 'Default vendor ID' }, + tax: { type: 'boolean', description: 'Taxable', default: true }, + discountable: { type: 'boolean', description: 'Can be discounted', default: true }, + itemType: { + type: 'string', + enum: ['default', 'assembly', 'giftcard', 'service'], + description: 'Item type', + default: 'default' + }, + prices: { + type: 'array', + items: { + type: 'object', + properties: { + amount: { type: 'string' }, + useType: { type: 'string', enum: ['Default', 'MSRP', 'Online'] }, + }, + }, + description: 'Price levels', + }, + }, + required: ['description'], + }, handler: async (args: any) => { - const result = await client.createProduct(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const item = await client.post<{ Item: Item }>('/Item', { Item: args }); + return item.Item; + }, }, { name: 'lightspeed_update_product', - description: 'Update an existing product. Can modify any product field including prices, description, category, etc.', - inputSchema: z.object({ - productID: z.string().describe('The product ID to update'), - description: z.string().optional().describe('Product description/name'), - sku: z.string().optional().describe('Stock keeping unit'), - defaultCost: z.number().optional().describe('Default cost price'), - defaultPrice: z.number().optional().describe('Default selling price'), - onlinePrice: z.number().optional().describe('Online store price'), - categoryID: z.string().optional().describe('Category ID'), - manufacturerID: z.string().optional().describe('Manufacturer ID'), - supplierID: z.string().optional().describe('Primary supplier ID'), - tax: z.boolean().optional().describe('Whether product is taxable'), - archived: z.boolean().optional().describe('Archive the product') - }), + description: 'Update an existing product/item', + inputSchema: { + type: 'object', + properties: { + itemID: { type: 'number', description: 'Product/Item ID' }, + description: { type: 'string' }, + customSku: { type: 'string' }, + upc: { type: 'string' }, + defaultCost: { type: 'number' }, + categoryID: { type: 'number' }, + manufacturerID: { type: 'number' }, + defaultVendorID: { type: 'number' }, + tax: { type: 'boolean' }, + discountable: { type: 'boolean' }, + archived: { type: 'boolean' }, + publishToEcom: { type: 'boolean' }, + }, + required: ['itemID'], + }, handler: async (args: any) => { - const { productID, ...updates } = args; - const result = await client.updateProduct(productID, updates); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const { itemID, ...updates } = args; + const item = await client.put<{ Item: Item }>(`/Item/${itemID}`, { Item: updates }); + return item.Item; + }, }, { name: 'lightspeed_delete_product', - description: 'Delete a product from Lightspeed. This action cannot be undone.', - inputSchema: z.object({ - productID: z.string().describe('The product ID to delete') - }), + description: 'Archive (soft delete) a product/item', + inputSchema: { + type: 'object', + properties: { + itemID: { type: 'number', description: 'Product/Item ID to archive' }, + }, + required: ['itemID'], + }, handler: async (args: any) => { - const result = await client.deleteProduct(args.productID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + await client.delete(`/Item/${args.itemID}`); + return { success: true, message: `Item ${args.itemID} archived` }; + }, }, { - name: 'lightspeed_archive_product', - description: 'Archive a product (soft delete). Archived products are hidden but can be restored.', - inputSchema: z.object({ - productID: z.string().describe('The product ID to archive') - }), + name: 'lightspeed_search_products', + description: 'Advanced product search with multiple criteria', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search query (searches description, SKU, UPC)' }, + categoryID: { type: 'number' }, + manufacturerID: { type: 'number' }, + minPrice: { type: 'number' }, + maxPrice: { type: 'number' }, + inStock: { type: 'boolean', description: 'Only show items in stock' }, + tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' }, + }, + }, handler: async (args: any) => { - const result = await client.updateProduct(args.productID, { archived: true }); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_search_products_by_sku', - description: 'Search for products by SKU or partial SKU match.', - inputSchema: z.object({ - sku: z.string().describe('SKU to search for') - }), - handler: async (args: any) => { - const result = await client.getProducts({ limit: 100 }); - if (result.success) { - const filtered = result.data.filter(p => - p.sku?.toLowerCase().includes(args.sku.toLowerCase()) || - p.customSku?.toLowerCase().includes(args.sku.toLowerCase()) + const params: any = { archived: false }; + if (args.categoryID) params.categoryID = args.categoryID; + if (args.manufacturerID) params.manufacturerID = args.manufacturerID; + + let items = await client.getPaginated('/Item', params); + + // Client-side filtering for advanced criteria + if (args.query) { + const q = args.query.toLowerCase(); + items = items.filter(item => + item.description?.toLowerCase().includes(q) || + item.customSku?.toLowerCase().includes(q) || + item.upc?.toLowerCase().includes(q) ); - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] }; } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } + + if (args.inStock) { + items = items.filter(item => + item.ItemShops?.some(shop => shop.qoh > 0) + ); + } + + return { items, count: items.length }; + }, + }, + { + name: 'lightspeed_bulk_update_products', + description: 'Bulk update multiple products at once', + inputSchema: { + type: 'object', + properties: { + updates: { + type: 'array', + items: { + type: 'object', + properties: { + itemID: { type: 'number' }, + data: { type: 'object' }, + }, + required: ['itemID', 'data'], + }, + description: 'Array of product updates', + }, + }, + required: ['updates'], + }, + handler: async (args: any) => { + const results = await client.batchUpdate('/Item', args.updates); + return { updated: results.length, results }; + }, + }, + { + name: 'lightspeed_get_product_inventory', + description: 'Get inventory levels for a product across all shops/locations', + inputSchema: { + type: 'object', + properties: { + itemID: { type: 'number', description: 'Product/Item ID' }, + }, + required: ['itemID'], + }, + handler: async (args: any) => { + const item = await client.get<{ Item: Item }>(`/Item/${args.itemID}?load=[ItemShops]`); + return { + itemID: args.itemID, + shops: item.Item.ItemShops || [], + totalQOH: item.Item.ItemShops?.reduce((sum, shop) => sum + shop.qoh, 0) || 0, + }; + }, + }, + { + name: 'lightspeed_adjust_product_inventory', + description: 'Adjust inventory quantity for a product at a specific shop', + inputSchema: { + type: 'object', + properties: { + itemID: { type: 'number', description: 'Product/Item ID' }, + shopID: { type: 'number', description: 'Shop ID' }, + adjustment: { type: 'number', description: 'Quantity adjustment (positive or negative)' }, + reason: { type: 'string', description: 'Reason for adjustment' }, + }, + required: ['itemID', 'shopID', 'adjustment'], + }, + handler: async (args: any) => { + // Create inventory log entry + const log = await client.post('/InventoryLog', { + InventoryLog: { + itemID: args.itemID, + shopID: args.shopID, + qohChange: args.adjustment, + reason: args.reason || 'Manual adjustment', + automated: false, + }, + }); + return { success: true, log }; + }, + }, ]; } diff --git a/servers/lightspeed/src/tools/registers-tools.ts b/servers/lightspeed/src/tools/registers-tools.ts deleted file mode 100644 index 509faa7..0000000 --- a/servers/lightspeed/src/tools/registers-tools.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Lightspeed Registers Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../client.js'; -import type { Register } from '../types/index.js'; - -export function createRegistersTools(client: LightspeedClient) { - return { - lightspeed_list_registers: { - description: 'List all registers/tills', - inputSchema: z.object({ - shopId: z.string().optional().describe('Filter by shop ID'), - archived: z.boolean().optional().describe('Include archived registers'), - }), - handler: async (args: { shopId?: string; archived?: boolean }) => { - try { - const params: any = {}; - if (args.shopId) params.shopID = args.shopId; - if (args.archived !== undefined) params.archived = args.archived; - - const registers = await client.getAll('/Register', 'Register', 100); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: registers.length, - registers: registers.map(r => ({ - registerID: r.registerID, - name: r.name, - shopID: r.shopID, - archived: r.archived, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_get_register: { - description: 'Get detailed information about a specific register', - inputSchema: z.object({ - registerId: z.string().describe('Register ID'), - }), - handler: async (args: { registerId: string }) => { - try { - const register = await client.getById<{ Register: Register }>('/Register', args.registerId); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, register: register.Register }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_open_register: { - description: 'Open a register for the day (till opening)', - inputSchema: z.object({ - registerId: z.string().describe('Register ID'), - employeeId: z.string().describe('Employee ID opening the register'), - openingFloat: z.string().describe('Opening cash float amount'), - }), - handler: async (args: any) => { - try { - // In Lightspeed, register opening is tracked through RegisterOpen or Sale records - const openData = { - registerID: args.registerId, - employeeID: args.employeeId, - openingFloat: args.openingFloat, - openTime: new Date().toISOString(), - }; - - // Note: Actual endpoint may vary by Lightspeed version - // This is a simplified representation - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'Register opened', - registerOpen: openData, - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_close_register: { - description: 'Close a register for the day (till closing)', - inputSchema: z.object({ - registerId: z.string().describe('Register ID'), - employeeId: z.string().describe('Employee ID closing the register'), - cashAmount: z.string().describe('Actual cash counted'), - expectedCash: z.string().describe('Expected cash amount'), - }), - handler: async (args: any) => { - try { - const variance = (parseFloat(args.cashAmount) - parseFloat(args.expectedCash)).toFixed(2); - - const closeData = { - registerID: args.registerId, - employeeID: args.employeeId, - cashAmount: args.cashAmount, - expectedCash: args.expectedCash, - variance: variance, - closeTime: new Date().toISOString(), - }; - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - message: 'Register closed', - registerClose: closeData, - variance: variance, - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_get_cash_counts: { - description: 'Get cash denomination counts for a register', - inputSchema: z.object({ - registerId: z.string().describe('Register ID'), - date: z.string().optional().describe('Date to retrieve (YYYY-MM-DD, defaults to today)'), - }), - handler: async (args: { registerId: string; date?: string }) => { - try { - // Get sales for the register on the specified date - const dateFilter = args.date || new Date().toISOString().split('T')[0]; - const sales = await client.get<{ Sale: any[] }>('/Sale', { - registerID: args.registerId, - timeStamp: `>,${dateFilter}`, - }); - - const salesArray = Array.isArray(sales.Sale) ? sales.Sale : sales.Sale ? [sales.Sale] : []; - const totalCash = salesArray - .filter((s: any) => s.completed && !s.voided) - .reduce((sum: number, s: any) => { - // Sum up cash payments only - if (s.SalePayments?.SalePayment) { - const payments = Array.isArray(s.SalePayments.SalePayment) - ? s.SalePayments.SalePayment - : [s.SalePayments.SalePayment]; - return sum + payments - .filter((p: any) => p.paymentTypeID === '1') // Assuming 1 = Cash - .reduce((pSum: number, p: any) => pSum + parseFloat(p.amount || '0'), 0); - } - return sum; - }, 0); - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - registerID: args.registerId, - date: dateFilter, - totalCash: totalCash.toFixed(2), - transactionCount: salesArray.length, - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - }; -} diff --git a/servers/lightspeed/src/tools/registers.ts b/servers/lightspeed/src/tools/registers.ts new file mode 100644 index 0000000..6fcd9de --- /dev/null +++ b/servers/lightspeed/src/tools/registers.ts @@ -0,0 +1,79 @@ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Register } from '../types/index.js'; + +export function createRegisterTools(client: LightspeedClient) { + return [ + { + name: 'lightspeed_list_registers', + description: 'List all registers/POS terminals', + inputSchema: { + type: 'object', + properties: { + shopID: { type: 'number' }, + archived: { type: 'boolean' }, + }, + }, + handler: async (args: any) => { + const params: any = {}; + if (args.shopID) params.shopID = args.shopID; + if (args.archived !== undefined) params.archived = args.archived; + const registers = await client.getPaginated('/Register', params); + return { registers, count: registers.length }; + }, + }, + { + name: 'lightspeed_get_register', + description: 'Get a specific register by ID', + inputSchema: { + type: 'object', + properties: { + registerID: { type: 'number' }, + }, + required: ['registerID'], + }, + handler: async (args: any) => { + const register = await client.get<{ Register: Register }>(`/Register/${args.registerID}`); + return register.Register; + }, + }, + { + name: 'lightspeed_open_register', + description: 'Open a register for business', + inputSchema: { + type: 'object', + properties: { + registerID: { type: 'number' }, + employeeID: { type: 'number' }, + }, + required: ['registerID', 'employeeID'], + }, + handler: async (args: any) => { + const register = await client.put<{ Register: Register }>(`/Register/${args.registerID}`, { + Register: { + open: true, + openTime: new Date().toISOString(), + openEmployeeID: args.employeeID, + }, + }); + return register.Register; + }, + }, + { + name: 'lightspeed_close_register', + description: 'Close a register', + inputSchema: { + type: 'object', + properties: { + registerID: { type: 'number' }, + }, + required: ['registerID'], + }, + handler: async (args: any) => { + const register = await client.put<{ Register: Register }>(`/Register/${args.registerID}`, { + Register: { open: false }, + }); + return register.Register; + }, + }, + ]; +} diff --git a/servers/lightspeed/src/tools/reporting-tools.ts b/servers/lightspeed/src/tools/reporting-tools.ts deleted file mode 100644 index 2079b96..0000000 --- a/servers/lightspeed/src/tools/reporting-tools.ts +++ /dev/null @@ -1,315 +0,0 @@ -/** - * Lightspeed Reporting Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../client.js'; -import type { - SalesSummaryReport, - InventoryValueReport, - ProductPerformanceReport, - EmployeeSalesReport -} from '../types/index.js'; - -export function createReportingTools(client: LightspeedClient) { - return { - lightspeed_sales_summary: { - description: 'Generate sales summary report for a date range', - inputSchema: z.object({ - startDate: z.string().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().describe('End date (YYYY-MM-DD)'), - shopId: z.string().optional().describe('Filter by shop ID'), - }), - handler: async (args: any) => { - try { - const params: any = { - completed: true, - timeStamp: `><,${args.startDate},${args.endDate}`, - }; - if (args.shopId) params.shopID = args.shopId; - - const sales = await client.get<{ Sale: any[] }>('/Sale', params); - const salesArray = Array.isArray(sales.Sale) ? sales.Sale : sales.Sale ? [sales.Sale] : []; - - const completedSales = salesArray.filter((s: any) => !s.voided); - const totalSales = completedSales.reduce( - (sum: number, s: any) => sum + parseFloat(s.calcTotal || '0'), - 0 - ); - const totalCost = completedSales.reduce( - (sum: number, s: any) => sum + parseFloat(s.calcFIFOCost || s.calcAvgCost || '0'), - 0 - ); - const totalItems = completedSales.reduce((sum: number, s: any) => { - if (s.SaleLines?.SaleLine) { - const lines = Array.isArray(s.SaleLines.SaleLine) - ? s.SaleLines.SaleLine - : [s.SaleLines.SaleLine]; - return sum + lines.reduce((lineSum: number, line: any) => - lineSum + parseFloat(line.unitQuantity || '0'), 0 - ); - } - return sum; - }, 0); - - const report: SalesSummaryReport = { - periodStart: args.startDate, - periodEnd: args.endDate, - totalSales: totalSales.toFixed(2), - totalTransactions: completedSales.length, - averageTransaction: (totalSales / (completedSales.length || 1)).toFixed(2), - totalItems: totalItems, - grossProfit: (totalSales - totalCost).toFixed(2), - grossMargin: ((totalSales - totalCost) / (totalSales || 1) * 100).toFixed(2), - }; - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, report }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_inventory_value: { - description: 'Generate inventory value report by category', - inputSchema: z.object({ - shopId: z.string().describe('Shop ID'), - }), - handler: async (args: { shopId: string }) => { - try { - const inventory = await client.getAll( - `/Shop/${args.shopId}/ItemShop`, - 'ItemShop', - 1000 - ); - const items = await client.getAll('/Item', 'Item', 1000); - - // Build item lookup - const itemMap = new Map(); - items.forEach((item: any) => { - itemMap.set(item.itemID, item); - }); - - // Calculate totals - let totalValue = 0; - let totalCost = 0; - const categoryTotals = new Map(); - - inventory.forEach((inv: any) => { - const item = itemMap.get(inv.itemID); - if (item) { - const qoh = parseFloat(inv.qoh || '0'); - const cost = parseFloat(item.defaultCost || '0'); - const value = qoh * cost; - - totalValue += value; - totalCost += qoh * cost; - - const catId = item.categoryID || '0'; - if (!categoryTotals.has(catId)) { - categoryTotals.set(catId, { - categoryID: catId, - categoryName: 'Unknown', - value: 0, - itemCount: 0, - }); - } - const catData = categoryTotals.get(catId); - catData.value += value; - catData.itemCount += 1; - } - }); - - const report: InventoryValueReport = { - totalValue: totalValue.toFixed(2), - totalCost: totalCost.toFixed(2), - itemCount: inventory.length, - categories: Array.from(categoryTotals.values()).map((cat: any) => ({ - ...cat, - value: cat.value.toFixed(2), - })), - }; - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, report }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_product_performance: { - description: 'Generate product performance report (top sellers)', - inputSchema: z.object({ - startDate: z.string().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().describe('End date (YYYY-MM-DD)'), - limit: z.number().optional().describe('Top N products (default 50)'), - }), - handler: async (args: any) => { - try { - const params = { - completed: true, - timeStamp: `><,${args.startDate},${args.endDate}`, - }; - - const sales = await client.get<{ Sale: any[] }>('/Sale', params); - const salesArray = Array.isArray(sales.Sale) ? sales.Sale : sales.Sale ? [sales.Sale] : []; - - // Aggregate by product - const productStats = new Map(); - - salesArray.forEach((sale: any) => { - if (sale.SaleLines?.SaleLine && !sale.voided) { - const lines = Array.isArray(sale.SaleLines.SaleLine) - ? sale.SaleLines.SaleLine - : [sale.SaleLines.SaleLine]; - - lines.forEach((line: any) => { - const itemId = line.itemID; - if (!productStats.has(itemId)) { - productStats.set(itemId, { - itemID: itemId, - description: 'Product ' + itemId, - unitsSold: 0, - revenue: 0, - cost: 0, - }); - } - const stats = productStats.get(itemId); - stats.unitsSold += parseFloat(line.unitQuantity || '0'); - stats.revenue += parseFloat(line.calcSubtotal || '0'); - }); - } - }); - - const products: ProductPerformanceReport[] = Array.from(productStats.values()) - .map((p: any) => ({ - ...p, - revenue: p.revenue.toFixed(2), - cost: p.cost.toFixed(2), - profit: (p.revenue - p.cost).toFixed(2), - margin: ((p.revenue - p.cost) / (p.revenue || 1) * 100).toFixed(2), - })) - .sort((a, b) => parseFloat(b.revenue) - parseFloat(a.revenue)) - .slice(0, args.limit || 50); - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: products.length, - products, - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_employee_sales: { - description: 'Generate employee sales performance report', - inputSchema: z.object({ - startDate: z.string().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().describe('End date (YYYY-MM-DD)'), - }), - handler: async (args: { startDate: string; endDate: string }) => { - try { - const params = { - completed: true, - timeStamp: `><,${args.startDate},${args.endDate}`, - }; - - const sales = await client.get<{ Sale: any[] }>('/Sale', params); - const salesArray = Array.isArray(sales.Sale) ? sales.Sale : sales.Sale ? [sales.Sale] : []; - const employees = await client.getAll('/Employee', 'Employee', 200); - - // Build employee lookup - const empMap = new Map(); - employees.forEach((emp: any) => { - empMap.set(emp.employeeID, `${emp.firstName} ${emp.lastName}`); - }); - - // Aggregate by employee - const empStats = new Map(); - - salesArray.filter((s: any) => !s.voided).forEach((sale: any) => { - const empId = sale.employeeID; - if (!empStats.has(empId)) { - empStats.set(empId, { - employeeID: empId, - employeeName: empMap.get(empId) || 'Unknown', - totalSales: 0, - transactionCount: 0, - itemsSold: 0, - }); - } - const stats = empStats.get(empId); - stats.totalSales += parseFloat(sale.calcTotal || '0'); - stats.transactionCount += 1; - - if (sale.SaleLines?.SaleLine) { - const lines = Array.isArray(sale.SaleLines.SaleLine) - ? sale.SaleLines.SaleLine - : [sale.SaleLines.SaleLine]; - stats.itemsSold += lines.reduce( - (sum: number, line: any) => sum + parseFloat(line.unitQuantity || '0'), - 0 - ); - } - }); - - const report: EmployeeSalesReport[] = Array.from(empStats.values()).map((e: any) => ({ - ...e, - totalSales: e.totalSales.toFixed(2), - averageTransaction: (e.totalSales / (e.transactionCount || 1)).toFixed(2), - })); - - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: report.length, - employees: report, - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - }; -} diff --git a/servers/lightspeed/src/tools/reporting.ts b/servers/lightspeed/src/tools/reporting.ts deleted file mode 100644 index f655f93..0000000 --- a/servers/lightspeed/src/tools/reporting.ts +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Reporting & Analytics Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerReportingTools(client: LightspeedClient) { - return [ - { - name: 'lightspeed_sales_report', - description: 'Generate a sales report for a date range with totals, averages, and breakdowns.', - inputSchema: z.object({ - startDate: z.string().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().describe('End date (YYYY-MM-DD)'), - shopID: z.string().optional().describe('Filter by shop ID') - }), - handler: async (args: any) => { - const result = await client.getSales({ - startDate: args.startDate, - endDate: args.endDate, - limit: 5000 - }); - - if (result.success) { - const sales = args.shopID - ? result.data.filter(s => s.shopID === args.shopID) - : result.data; - - const completedSales = sales.filter(s => s.completed && !s.voided); - const totalSales = completedSales.reduce((sum, sale) => sum + sale.total, 0); - const totalTransactions = completedSales.length; - const totalDiscount = completedSales.reduce((sum, sale) => sum + (sale.calcDiscount || 0), 0); - const totalTax = completedSales.reduce((sum, sale) => sum + (sale.calcTax || 0), 0); - - const report = { - startDate: args.startDate, - endDate: args.endDate, - totalSales, - totalTransactions, - averageTicket: totalTransactions > 0 ? totalSales / totalTransactions : 0, - totalDiscount, - totalTax, - salesByDay: [] as any[] - }; - - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: report }, null, 2) }] }; - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_inventory_report', - description: 'Generate an inventory report showing current stock levels and values.', - inputSchema: z.object({ - shopID: z.string().optional().describe('Filter by shop ID') - }), - handler: async (args: any) => { - const productsResult = await client.getProducts({ limit: 1000 }); - if (!productsResult.success) { - return { content: [{ type: 'text', text: JSON.stringify(productsResult, null, 2) }] }; - } - - const products = productsResult.data; - const totalValue = products.reduce((sum, p) => sum + (p.defaultCost * 0), 0); // Would need inventory qty - const lowStockItems = []; - - const report = { - asOfDate: new Date().toISOString().split('T')[0], - shopID: args.shopID, - totalItems: products.length, - totalValue, - lowStockItems - }; - - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: report }, null, 2) }] }; - } - }, - { - name: 'lightspeed_customer_report', - description: 'Generate a customer report showing acquisition, retention, and top spenders.', - inputSchema: z.object({ - startDate: z.string().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().describe('End date (YYYY-MM-DD)') - }), - handler: async (args: any) => { - const customersResult = await client.getCustomers({ limit: 5000 }); - if (!customersResult.success) { - return { content: [{ type: 'text', text: JSON.stringify(customersResult, null, 2) }] }; - } - - const customers = customersResult.data; - const newCustomers = customers.filter(c => { - const created = new Date(c.createTime); - return created >= new Date(args.startDate) && created <= new Date(args.endDate); - }); - - const report = { - startDate: args.startDate, - endDate: args.endDate, - totalCustomers: customers.length, - newCustomers: newCustomers.length, - topCustomers: [] - }; - - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: report }, null, 2) }] }; - } - }, - { - name: 'lightspeed_employee_performance', - description: 'Generate employee performance report showing sales by employee.', - inputSchema: z.object({ - startDate: z.string().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().describe('End date (YYYY-MM-DD)') - }), - handler: async (args: any) => { - const salesResult = await client.getSales({ - startDate: args.startDate, - endDate: args.endDate, - limit: 5000 - }); - - if (!salesResult.success) { - return { content: [{ type: 'text', text: JSON.stringify(salesResult, null, 2) }] }; - } - - const salesByEmployee = new Map(); - for (const sale of salesResult.data) { - if (sale.completed && !sale.voided) { - const current = salesByEmployee.get(sale.employeeID) || { total: 0, count: 0 }; - salesByEmployee.set(sale.employeeID, { - total: current.total + sale.total, - count: current.count + 1 - }); - } - } - - const report = { - startDate: args.startDate, - endDate: args.endDate, - salesByEmployee: Array.from(salesByEmployee.entries()).map(([employeeID, stats]) => ({ - employeeID, - totalSales: stats.total, - transactionCount: stats.count, - averageTicket: stats.total / stats.count - })) - }; - - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: report }, null, 2) }] }; - } - }, - { - name: 'lightspeed_top_selling_products', - description: 'Get top selling products by quantity or revenue for a date range.', - inputSchema: z.object({ - startDate: z.string().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().describe('End date (YYYY-MM-DD)'), - limit: z.number().optional().describe('Number of products to return (default 10)') - }), - handler: async (args: any) => { - const salesResult = await client.getSales({ - startDate: args.startDate, - endDate: args.endDate, - limit: 5000 - }); - - if (!salesResult.success) { - return { content: [{ type: 'text', text: JSON.stringify(salesResult, null, 2) }] }; - } - - const productStats = new Map(); - for (const sale of salesResult.data) { - if (sale.completed && !sale.voided && sale.salesLines) { - for (const line of sale.salesLines) { - if (line.productID) { - const current = productStats.get(line.productID) || { quantity: 0, revenue: 0 }; - productStats.set(line.productID, { - quantity: current.quantity + line.unitQuantity, - revenue: current.revenue + line.total - }); - } - } - } - } - - const topProducts = Array.from(productStats.entries()) - .map(([productID, stats]) => ({ productID, ...stats })) - .sort((a, b) => b.revenue - a.revenue) - .slice(0, args.limit || 10); - - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: topProducts }, null, 2) }] }; - } - } - ]; -} diff --git a/servers/lightspeed/src/tools/reports.ts b/servers/lightspeed/src/tools/reports.ts new file mode 100644 index 0000000..881b47c --- /dev/null +++ b/servers/lightspeed/src/tools/reports.ts @@ -0,0 +1,230 @@ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Sale, Item, Customer } from '../types/index.js'; + +export function createReportTools(client: LightspeedClient) { + return [ + { + name: 'lightspeed_sales_report', + description: 'Generate sales report for a date range', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, + endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, + shopID: { type: 'number' }, + employeeID: { type: 'number' }, + }, + required: ['startDate', 'endDate'], + }, + handler: async (args: any) => { + const params: any = { + completed: true, + timeStamp: `>,${args.startDate}`, + }; + if (args.shopID) params.shopID = args.shopID; + if (args.employeeID) params.employeeID = args.employeeID; + + const sales = await client.getPaginated('/Sale', params); + const filtered = sales.filter(s => s.createTime <= args.endDate + 'T23:59:59'); + + const totalRevenue = filtered.reduce((sum, s) => sum + s.total, 0); + const totalTax = filtered.reduce((sum, s) => sum + s.calcTax1 + s.calcTax2, 0); + const totalDiscounts = filtered.reduce((sum, s) => sum + s.calcDiscount, 0); + + return { + period: `${args.startDate} to ${args.endDate}`, + totalSales: filtered.length, + totalRevenue, + totalTax, + totalDiscounts, + averageTransaction: filtered.length > 0 ? totalRevenue / filtered.length : 0, + sales: filtered.slice(0, 100), + }; + }, + }, + { + name: 'lightspeed_inventory_report', + description: 'Generate inventory valuation and stock report', + inputSchema: { + type: 'object', + properties: { + shopID: { type: 'number' }, + categoryID: { type: 'number' }, + lowStock: { type: 'boolean', description: 'Only show low stock items' }, + }, + }, + handler: async (args: any) => { + const params: any = { archived: false }; + if (args.categoryID) params.categoryID = args.categoryID; + + const items = await client.getPaginated('/Item?load=[ItemShops]', params); + + let filteredItems = items; + if (args.shopID) { + filteredItems = items.filter(i => + i.ItemShops?.some(shop => shop.shopID === args.shopID) + ); + } + + if (args.lowStock) { + filteredItems = filteredItems.filter(i => + i.ItemShops?.some(shop => shop.qoh <= shop.reorderPoint) + ); + } + + const totalValue = filteredItems.reduce((sum, i) => { + const qoh = i.ItemShops?.reduce((s, shop) => s + shop.qoh, 0) || 0; + return sum + (qoh * i.avgCost); + }, 0); + + const totalItems = filteredItems.reduce((sum, i) => { + return sum + (i.ItemShops?.reduce((s, shop) => s + shop.qoh, 0) || 0); + }, 0); + + return { + totalProducts: filteredItems.length, + totalUnits: totalItems, + totalValue, + lowStockCount: filteredItems.filter(i => + i.ItemShops?.some(shop => shop.qoh <= shop.reorderPoint) + ).length, + items: filteredItems.slice(0, 100), + }; + }, + }, + { + name: 'lightspeed_customer_report', + description: 'Generate customer analytics report', + inputSchema: { + type: 'object', + properties: { + topN: { type: 'number', description: 'Number of top customers to show', default: 20 }, + }, + }, + handler: async (args: any) => { + const customers = await client.getPaginated('/Customer', { archived: false }); + const sales = await client.getPaginated('/Sale', { completed: true }); + + const customerStats = customers.map(c => { + const customerSales = sales.filter(s => s.customerID === c.customerID); + return { + customerID: c.customerID, + name: `${c.firstName} ${c.lastName}`, + company: c.company, + totalSales: customerSales.length, + totalRevenue: customerSales.reduce((sum, s) => sum + s.total, 0), + averageOrder: customerSales.length > 0 + ? customerSales.reduce((sum, s) => sum + s.total, 0) / customerSales.length + : 0, + }; + }); + + customerStats.sort((a, b) => b.totalRevenue - a.totalRevenue); + + return { + totalCustomers: customers.length, + topCustomers: customerStats.slice(0, args.topN || 20), + averageCustomerValue: customerStats.reduce((sum, c) => sum + c.totalRevenue, 0) / customers.length, + }; + }, + }, + { + name: 'lightspeed_employee_performance_report', + description: 'Generate employee sales performance report', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string' }, + endDate: { type: 'string' }, + }, + }, + handler: async (args: any) => { + const params: any = { completed: true }; + if (args.startDate) params.timeStamp = `>,${args.startDate}`; + + const sales = await client.getPaginated('/Sale', params); + const filtered = args.endDate + ? sales.filter(s => s.createTime <= args.endDate + 'T23:59:59') + : sales; + + const employeeStats = new Map(); + + filtered.forEach(sale => { + if (!sale.employeeID) return; + + if (!employeeStats.has(sale.employeeID)) { + employeeStats.set(sale.employeeID, { + employeeID: sale.employeeID, + totalSales: 0, + totalRevenue: 0, + }); + } + + const stats = employeeStats.get(sale.employeeID); + stats.totalSales++; + stats.totalRevenue += sale.total; + }); + + const results = Array.from(employeeStats.values()) + .map(e => ({ + ...e, + averageSale: e.totalRevenue / e.totalSales, + })) + .sort((a, b) => b.totalRevenue - a.totalRevenue); + + return { + period: args.startDate && args.endDate ? `${args.startDate} to ${args.endDate}` : 'All time', + employees: results, + }; + }, + }, + { + name: 'lightspeed_product_performance_report', + description: 'Generate top-selling products report', + inputSchema: { + type: 'object', + properties: { + startDate: { type: 'string' }, + endDate: { type: 'string' }, + topN: { type: 'number', default: 50 }, + }, + }, + handler: async (args: any) => { + const params: any = { completed: true }; + if (args.startDate) params.timeStamp = `>,${args.startDate}`; + + const sales = await client.getPaginated('/Sale?load=[SaleLines]', params); + const filtered = args.endDate + ? sales.filter(s => s.createTime <= args.endDate + 'T23:59:59') + : sales; + + const productStats = new Map(); + + filtered.forEach(sale => { + sale.SaleLines?.forEach(line => { + if (!productStats.has(line.itemID)) { + productStats.set(line.itemID, { + itemID: line.itemID, + quantitySold: 0, + revenue: 0, + }); + } + + const stats = productStats.get(line.itemID); + stats.quantitySold += line.unitQuantity; + stats.revenue += line.calcTotal; + }); + }); + + const results = Array.from(productStats.values()) + .sort((a, b) => b.revenue - a.revenue) + .slice(0, args.topN || 50); + + return { + period: args.startDate && args.endDate ? `${args.startDate} to ${args.endDate}` : 'All time', + topProducts: results, + }; + }, + }, + ]; +} diff --git a/servers/lightspeed/src/tools/sales.ts b/servers/lightspeed/src/tools/sales.ts index ddc4388..b3a97b6 100644 --- a/servers/lightspeed/src/tools/sales.ts +++ b/servers/lightspeed/src/tools/sales.ts @@ -1,148 +1,268 @@ -/** - * Sales & Transaction Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Sale, SaleLine, SalePayment } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerSalesTools(client: LightspeedClient) { +export function createSalesTools(client: LightspeedClient) { return [ { name: 'lightspeed_list_sales', - description: 'List sales/transactions with optional date range filtering and pagination.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of sales to return (default 100)'), - offset: z.number().optional().describe('Offset for pagination'), - startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().optional().describe('End date (YYYY-MM-DD)') - }), + description: 'List sales/transactions with filters', + inputSchema: { + type: 'object', + properties: { + completed: { type: 'boolean', description: 'Filter by completion status' }, + voided: { type: 'boolean', description: 'Include voided sales' }, + customerID: { type: 'number', description: 'Filter by customer' }, + employeeID: { type: 'number', description: 'Filter by employee' }, + registerID: { type: 'number', description: 'Filter by register' }, + timeStamp: { type: 'string', description: 'Filter by timestamp (>|<|=,YYYY-MM-DD)' }, + limit: { type: 'number', default: 100 }, + }, + }, handler: async (args: any) => { - const result = await client.getSales(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const params: any = {}; + if (args.completed !== undefined) params.completed = args.completed; + if (args.voided !== undefined) params.voided = args.voided; + if (args.customerID) params.customerID = args.customerID; + if (args.employeeID) params.employeeID = args.employeeID; + if (args.registerID) params.registerID = args.registerID; + if (args.timeStamp) params.timeStamp = args.timeStamp; + const sales = await client.getPaginated('/Sale', params, args.limit || 100); + return { sales, count: sales.length, totalAmount: sales.reduce((sum, s) => sum + s.total, 0) }; + }, }, { name: 'lightspeed_get_sale', - description: 'Get a single sale/transaction by ID with all line items and payment details.', - inputSchema: z.object({ - saleID: z.string().describe('The sale ID') - }), + description: 'Get a specific sale/transaction with line items and payments', + inputSchema: { + type: 'object', + properties: { + saleID: { type: 'number', description: 'Sale ID' }, + }, + required: ['saleID'], + }, handler: async (args: any) => { - const result = await client.getSale(args.saleID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const sale = await client.get<{ Sale: Sale }>(`/Sale/${args.saleID}?load=[SaleLines,SalePayments]`); + return sale.Sale; + }, }, { name: 'lightspeed_create_sale', - description: 'Create a new sale/transaction. Requires register, shop, and employee IDs.', - inputSchema: z.object({ - shopID: z.string().describe('Shop ID where sale occurs'), - registerID: z.string().describe('Register ID'), - employeeID: z.string().describe('Employee processing the sale'), - customerID: z.string().optional().describe('Customer ID (optional)'), - completed: z.boolean().optional().describe('Whether sale is completed') - }), + description: 'Create a new sale/transaction', + inputSchema: { + type: 'object', + properties: { + customerID: { type: 'number' }, + employeeID: { type: 'number' }, + registerID: { type: 'number' }, + shopID: { type: 'number', description: 'Shop/location ID' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + itemID: { type: 'number' }, + quantity: { type: 'number' }, + unitPrice: { type: 'number' }, + }, + required: ['itemID', 'quantity'], + }, + description: 'Sale line items', + }, + }, + required: ['shopID', 'items'], + }, handler: async (args: any) => { - const result = await client.createSale(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const salePayload: any = { + shopID: args.shopID, + completed: false, + }; + if (args.customerID) salePayload.customerID = args.customerID; + if (args.employeeID) salePayload.employeeID = args.employeeID; + if (args.registerID) salePayload.registerID = args.registerID; + + const sale = await client.post<{ Sale: Sale }>('/Sale', { Sale: salePayload }); + const saleID = sale.Sale.saleID; + + // Add line items + for (const item of args.items) { + await client.post(`/SaleLine`, { + SaleLine: { + saleID, + itemID: item.itemID, + unitQuantity: item.quantity, + unitPrice: item.unitPrice, + }, + }); + } + + // Reload with lines + const completeSale = await client.get<{ Sale: Sale }>(`/Sale/${saleID}?load=[SaleLines]`); + return completeSale.Sale; + }, }, { - name: 'lightspeed_update_sale', - description: 'Update a sale. Can modify customer association or completion status.', - inputSchema: z.object({ - saleID: z.string().describe('The sale ID to update'), - customerID: z.string().optional().describe('Customer ID'), - completed: z.boolean().optional().describe('Mark sale as completed') - }), + name: 'lightspeed_complete_sale', + description: 'Complete/finalize a sale transaction', + inputSchema: { + type: 'object', + properties: { + saleID: { type: 'number' }, + payments: { + type: 'array', + items: { + type: 'object', + properties: { + paymentTypeID: { type: 'number', description: '0=Cash, 1=Credit Card, 2=Check, etc.' }, + amount: { type: 'number' }, + }, + required: ['paymentTypeID', 'amount'], + }, + }, + }, + required: ['saleID', 'payments'], + }, handler: async (args: any) => { - const { saleID, ...updates } = args; - const result = await client.updateSale(saleID, updates); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + // Add payments + for (const payment of args.payments) { + await client.post('/SalePayment', { + SalePayment: { + saleID: args.saleID, + paymentTypeID: payment.paymentTypeID, + amount: payment.amount, + }, + }); + } + + // Mark as completed + const sale = await client.put<{ Sale: Sale }>(`/Sale/${args.saleID}`, { + Sale: { completed: true }, + }); + + return sale.Sale; + }, }, { name: 'lightspeed_void_sale', - description: 'Void a sale/transaction. This marks it as voided but preserves the record.', - inputSchema: z.object({ - saleID: z.string().describe('The sale ID to void') - }), + description: 'Void a sale transaction', + inputSchema: { + type: 'object', + properties: { + saleID: { type: 'number' }, + }, + required: ['saleID'], + }, handler: async (args: any) => { - const result = await client.voidSale(args.saleID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_sales_by_customer', - description: 'Get all sales for a specific customer.', - inputSchema: z.object({ - customerID: z.string().describe('The customer ID'), - limit: z.number().optional().describe('Max number of sales to return') - }), - handler: async (args: any) => { - const result = await client.getSales({ limit: args.limit || 100 }); - if (result.success) { - const filtered = result.data.filter(s => s.customerID === args.customerID); - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] }; - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_sales_by_employee', - description: 'Get all sales processed by a specific employee.', - inputSchema: z.object({ - employeeID: z.string().describe('The employee ID'), - startDate: z.string().optional().describe('Start date (YYYY-MM-DD)'), - endDate: z.string().optional().describe('End date (YYYY-MM-DD)') - }), - handler: async (args: any) => { - const result = await client.getSales({ - limit: 500, - startDate: args.startDate, - endDate: args.endDate + const sale = await client.put<{ Sale: Sale }>(`/Sale/${args.saleID}`, { + Sale: { voided: true }, }); - if (result.success) { - const filtered = result.data.filter(s => s.employeeID === args.employeeID); - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] }; - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + return sale.Sale; + }, }, { - name: 'lightspeed_calculate_daily_sales', - description: 'Calculate total sales for a specific date.', - inputSchema: z.object({ - date: z.string().describe('Date to calculate (YYYY-MM-DD)'), - shopID: z.string().optional().describe('Filter by shop ID') - }), + name: 'lightspeed_add_sale_payment', + description: 'Add a payment to a sale', + inputSchema: { + type: 'object', + properties: { + saleID: { type: 'number' }, + paymentTypeID: { type: 'number', description: 'Payment type (0=Cash, 1=Card, 2=Check)' }, + amount: { type: 'number' }, + employeeID: { type: 'number' }, + registerID: { type: 'number' }, + }, + required: ['saleID', 'paymentTypeID', 'amount'], + }, handler: async (args: any) => { - const result = await client.getSales({ - startDate: args.date, - endDate: args.date, - limit: 1000 + const payment = await client.post<{ SalePayment: SalePayment }>('/SalePayment', { SalePayment: args }); + return payment.SalePayment; + }, + }, + { + name: 'lightspeed_get_daily_sales', + description: 'Get sales summary for a specific date', + inputSchema: { + type: 'object', + properties: { + date: { type: 'string', description: 'Date (YYYY-MM-DD)' }, + shopID: { type: 'number' }, + }, + required: ['date'], + }, + handler: async (args: any) => { + const params: any = { + completed: true, + timeStamp: `>,${args.date} 00:00:00`, + }; + if (args.shopID) params.shopID = args.shopID; + + const sales = await client.getPaginated('/Sale', params); + const totalSales = sales.length; + const totalRevenue = sales.reduce((sum, s) => sum + s.total, 0); + const totalItems = sales.reduce((sum, s) => sum + (s.SaleLines?.length || 0), 0); + + return { + date: args.date, + totalSales, + totalRevenue, + totalItems, + averageTransaction: totalSales > 0 ? totalRevenue / totalSales : 0, + sales, + }; + }, + }, + { + name: 'lightspeed_refund_sale', + description: 'Create a refund for a sale', + inputSchema: { + type: 'object', + properties: { + originalSaleID: { type: 'number', description: 'Original sale ID to refund' }, + items: { + type: 'array', + items: { + type: 'object', + properties: { + saleLineID: { type: 'number' }, + quantity: { type: 'number' }, + }, + }, + description: 'Items to refund', + }, + }, + required: ['originalSaleID'], + }, + handler: async (args: any) => { + // Get original sale + const originalSale = await client.get<{ Sale: Sale }>(`/Sale/${args.originalSaleID}?load=[SaleLines]`); + + // Create refund sale + const refund = await client.post<{ Sale: Sale }>('/Sale', { + Sale: { + shopID: originalSale.Sale.shopID, + customerID: originalSale.Sale.customerID, + employeeID: originalSale.Sale.employeeID, + completed: true, + }, }); - if (result.success) { - const sales = args.shopID - ? result.data.filter(s => s.shopID === args.shopID) - : result.data; - - const total = sales.reduce((sum, sale) => sum + (sale.completed && !sale.voided ? sale.total : 0), 0); - const count = sales.filter(s => s.completed && !s.voided).length; - - return { content: [{ type: 'text', text: JSON.stringify({ - success: true, - data: { - date: args.date, - totalSales: total, - transactionCount: count, - averageTicket: count > 0 ? total / count : 0 - } - }, null, 2) }] }; + // Add negative line items + const itemsToRefund = args.items || originalSale.Sale.SaleLines; + for (const item of itemsToRefund) { + const originalLine = originalSale.Sale.SaleLines?.find(l => l.saleLineID === item.saleLineID); + if (originalLine) { + await client.post('/SaleLine', { + SaleLine: { + saleID: refund.Sale.saleID, + itemID: originalLine.itemID, + unitQuantity: -(item.quantity || originalLine.unitQuantity), + unitPrice: originalLine.unitPrice, + }, + }); + } } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } + + return refund.Sale; + }, + }, ]; } diff --git a/servers/lightspeed/src/tools/shops.ts b/servers/lightspeed/src/tools/shops.ts index bd3ba03..c68f7d5 100644 --- a/servers/lightspeed/src/tools/shops.ts +++ b/servers/lightspeed/src/tools/shops.ts @@ -1,80 +1,38 @@ -/** - * Shop & Register Management Tools - */ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Shop } from '../types/index.js'; -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerShopTools(client: LightspeedClient) { +export function createShopTools(client: LightspeedClient) { return [ { name: 'lightspeed_list_shops', - description: 'List all shop locations in Lightspeed.', - inputSchema: z.object({}), - handler: async () => { - const result = await client.getShops(); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + description: 'List all shops/locations', + inputSchema: { + type: 'object', + properties: { + archived: { type: 'boolean' }, + }, + }, + handler: async (args: any) => { + const params: any = {}; + if (args.archived !== undefined) params.archived = args.archived; + const shops = await client.getPaginated('/Shop', params); + return { shops, count: shops.length }; + }, }, { name: 'lightspeed_get_shop', - description: 'Get a single shop by ID with all details.', - inputSchema: z.object({ - shopID: z.string().describe('The shop ID') - }), + description: 'Get a specific shop/location by ID', + inputSchema: { + type: 'object', + properties: { + shopID: { type: 'number' }, + }, + required: ['shopID'], + }, handler: async (args: any) => { - const result = await client.getShop(args.shopID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } + const shop = await client.get<{ Shop: Shop }>(`/Shop/${args.shopID}`); + return shop.Shop; + }, }, - { - name: 'lightspeed_list_registers', - description: 'List all registers (POS terminals) with optional shop filtering.', - inputSchema: z.object({ - shopID: z.string().optional().describe('Filter by shop ID') - }), - handler: async (args: any) => { - const result = await client.getRegisters(args.shopID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_manufacturers', - description: 'List all product manufacturers.', - inputSchema: z.object({}), - handler: async () => { - const result = await client.getManufacturers(); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_create_manufacturer', - description: 'Create a new manufacturer.', - inputSchema: z.object({ - name: z.string().describe('Manufacturer name') - }), - handler: async (args: any) => { - const result = await client.createManufacturer(args.name); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_tax_categories', - description: 'List all tax categories.', - inputSchema: z.object({}), - handler: async () => { - const result = await client.getTaxCategories(); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_payment_types', - description: 'List all payment types (cash, credit, debit, etc).', - inputSchema: z.object({}), - handler: async () => { - const result = await client.getPaymentTypes(); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } ]; } diff --git a/servers/lightspeed/src/tools/suppliers.ts b/servers/lightspeed/src/tools/suppliers.ts deleted file mode 100644 index eb5c961..0000000 --- a/servers/lightspeed/src/tools/suppliers.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Supplier/Vendor Management Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../clients/lightspeed.js'; - -export function registerSupplierTools(client: LightspeedClient) { - return [ - { - name: 'lightspeed_list_suppliers', - description: 'List all suppliers/vendors in Lightspeed.', - inputSchema: z.object({ - limit: z.number().optional().describe('Number of suppliers to return (default 100)'), - offset: z.number().optional().describe('Offset for pagination') - }), - handler: async (args: any) => { - const result = await client.getSuppliers(args); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_get_supplier', - description: 'Get a single supplier by ID with all contact details.', - inputSchema: z.object({ - supplierID: z.string().describe('The supplier ID') - }), - handler: async (args: any) => { - const result = await client.getSupplier(args.supplierID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_create_supplier', - description: 'Create a new supplier/vendor.', - inputSchema: z.object({ - name: z.string().describe('Supplier name'), - accountNumber: z.string().optional().describe('Account number with supplier'), - contactFirstName: z.string().optional().describe('Contact first name'), - contactLastName: z.string().optional().describe('Contact last name'), - email: z.string().optional().describe('Email address'), - phone: z.string().optional().describe('Phone number'), - address1: z.string().optional().describe('Address line 1'), - city: z.string().optional().describe('City'), - state: z.string().optional().describe('State/Province'), - zip: z.string().optional().describe('Postal/ZIP code'), - country: z.string().optional().describe('Country') - }), - handler: async (args: any) => { - const { address1, city, state, zip, country, ...supplierData } = args; - const supplier: any = { ...supplierData }; - - if (address1 || city || state || zip || country) { - supplier.address = { address1, city, state, zip, country }; - } - - const result = await client.createSupplier(supplier); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_update_supplier', - description: 'Update an existing supplier.', - inputSchema: z.object({ - supplierID: z.string().describe('The supplier ID to update'), - name: z.string().optional().describe('Supplier name'), - email: z.string().optional().describe('Email address'), - phone: z.string().optional().describe('Phone number'), - archived: z.boolean().optional().describe('Archive the supplier') - }), - handler: async (args: any) => { - const { supplierID, ...updates } = args; - const result = await client.updateSupplier(supplierID, updates); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_delete_supplier', - description: 'Delete a supplier from Lightspeed.', - inputSchema: z.object({ - supplierID: z.string().describe('The supplier ID to delete') - }), - handler: async (args: any) => { - const result = await client.deleteSupplier(args.supplierID); - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - }, - { - name: 'lightspeed_search_suppliers', - description: 'Search suppliers by name.', - inputSchema: z.object({ - query: z.string().describe('Search query (supplier name)') - }), - handler: async (args: any) => { - const result = await client.getSuppliers({ limit: 500 }); - if (result.success) { - const query = args.query.toLowerCase(); - const filtered = result.data.filter(s => - s.name?.toLowerCase().includes(query) - ); - return { content: [{ type: 'text', text: JSON.stringify({ success: true, data: filtered }, null, 2) }] }; - } - return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; - } - } - ]; -} diff --git a/servers/lightspeed/src/tools/taxes-tools.ts b/servers/lightspeed/src/tools/taxes-tools.ts deleted file mode 100644 index 4e06c35..0000000 --- a/servers/lightspeed/src/tools/taxes-tools.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Lightspeed Taxes Tools - */ - -import { z } from 'zod'; -import type { LightspeedClient } from '../client.js'; -import type { Tax } from '../types/index.js'; - -export function createTaxesTools(client: LightspeedClient) { - return { - lightspeed_list_taxes: { - description: 'List all tax classes/rates', - inputSchema: z.object({ - archived: z.boolean().optional().describe('Include archived tax classes'), - }), - handler: async (args: { archived?: boolean }) => { - try { - const params: any = {}; - if (args.archived !== undefined) { - params.archived = args.archived; - } - - const taxes = await client.getAll('/TaxClass', 'TaxClass', 100); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ - success: true, - count: taxes.length, - taxes: taxes.map(t => ({ - taxClassID: t.taxClassID, - name: t.name, - tax1Rate: t.tax1Rate, - tax2Rate: t.tax2Rate, - archived: t.archived, - })), - }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_get_tax: { - description: 'Get detailed information about a specific tax class', - inputSchema: z.object({ - taxClassId: z.string().describe('Tax class ID'), - }), - handler: async (args: { taxClassId: string }) => { - try { - const tax = await client.getById<{ TaxClass: Tax }>('/TaxClass', args.taxClassId); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, tax: tax.TaxClass }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_create_tax: { - description: 'Create a new tax class', - inputSchema: z.object({ - name: z.string().describe('Tax class name'), - tax1Rate: z.string().describe('Tax 1 rate (e.g., 8.5 for 8.5%)'), - tax2Rate: z.string().optional().describe('Tax 2 rate (optional, for compound tax)'), - }), - handler: async (args: any) => { - try { - const taxData: any = { - name: args.name, - tax1Rate: args.tax1Rate, - }; - if (args.tax2Rate) { - taxData.tax2Rate = args.tax2Rate; - } - - const result = await client.post<{ TaxClass: Tax }>('/TaxClass', { TaxClass: taxData }); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, tax: result.TaxClass }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - - lightspeed_update_tax: { - description: 'Update an existing tax class', - inputSchema: z.object({ - taxClassId: z.string().describe('Tax class ID'), - name: z.string().optional().describe('Tax class name'), - tax1Rate: z.string().optional().describe('Tax 1 rate'), - tax2Rate: z.string().optional().describe('Tax 2 rate'), - archived: z.boolean().optional().describe('Archive status'), - }), - handler: async (args: any) => { - try { - const updateData: any = {}; - if (args.name) updateData.name = args.name; - if (args.tax1Rate) updateData.tax1Rate = args.tax1Rate; - if (args.tax2Rate) updateData.tax2Rate = args.tax2Rate; - if (args.archived !== undefined) updateData.archived = args.archived; - - const result = await client.put<{ TaxClass: Tax }>( - '/TaxClass', - args.taxClassId, - { TaxClass: updateData } - ); - return { - content: [ - { - type: 'text' as const, - text: JSON.stringify({ success: true, tax: result.TaxClass }, null, 2), - }, - ], - }; - } catch (error) { - return { - content: [{ type: 'text' as const, text: `Error: ${(error as Error).message}` }], - isError: true, - }; - } - }, - }, - }; -} diff --git a/servers/lightspeed/src/tools/vendors.ts b/servers/lightspeed/src/tools/vendors.ts new file mode 100644 index 0000000..4108237 --- /dev/null +++ b/servers/lightspeed/src/tools/vendors.ts @@ -0,0 +1,108 @@ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Vendor } from '../types/index.js'; + +export function createVendorTools(client: LightspeedClient) { + return [ + { + name: 'lightspeed_list_vendors', + description: 'List all vendors/suppliers', + inputSchema: { + type: 'object', + properties: { + archived: { type: 'boolean' }, + }, + }, + handler: async (args: any) => { + const params: any = {}; + if (args.archived !== undefined) params.archived = args.archived; + const vendors = await client.getPaginated('/Vendor', params); + return { vendors, count: vendors.length }; + }, + }, + { + name: 'lightspeed_get_vendor', + description: 'Get a specific vendor by ID', + inputSchema: { + type: 'object', + properties: { + vendorID: { type: 'number' }, + }, + required: ['vendorID'], + }, + handler: async (args: any) => { + const vendor = await client.get<{ Vendor: Vendor }>(`/Vendor/${args.vendorID}?load=[Contact]`); + return vendor.Vendor; + }, + }, + { + name: 'lightspeed_create_vendor', + description: 'Create a new vendor/supplier', + inputSchema: { + type: 'object', + properties: { + name: { type: 'string' }, + accountNumber: { type: 'string' }, + email: { type: 'string' }, + phone: { type: 'string' }, + address1: { type: 'string' }, + city: { type: 'string' }, + state: { type: 'string' }, + zip: { type: 'string' }, + }, + required: ['name'], + }, + handler: async (args: any) => { + const { email, phone, address1, city, state, zip, ...vendorData } = args; + const payload: any = { Vendor: vendorData }; + + if (email || phone || address1) { + payload.Vendor.Contact = { + primaryEmail: email, + phoneWork: phone, + address1, + city, + state, + zip, + }; + } + + const vendor = await client.post<{ Vendor: Vendor }>('/Vendor', payload); + return vendor.Vendor; + }, + }, + { + name: 'lightspeed_update_vendor', + description: 'Update a vendor', + inputSchema: { + type: 'object', + properties: { + vendorID: { type: 'number' }, + name: { type: 'string' }, + accountNumber: { type: 'string' }, + archived: { type: 'boolean' }, + }, + required: ['vendorID'], + }, + handler: async (args: any) => { + const { vendorID, ...updates } = args; + const vendor = await client.put<{ Vendor: Vendor }>(`/Vendor/${vendorID}`, { Vendor: updates }); + return vendor.Vendor; + }, + }, + { + name: 'lightspeed_delete_vendor', + description: 'Archive a vendor', + inputSchema: { + type: 'object', + properties: { + vendorID: { type: 'number' }, + }, + required: ['vendorID'], + }, + handler: async (args: any) => { + await client.delete(`/Vendor/${args.vendorID}`); + return { success: true, message: `Vendor ${args.vendorID} archived` }; + }, + }, + ]; +} diff --git a/servers/lightspeed/src/tools/workorders.ts b/servers/lightspeed/src/tools/workorders.ts new file mode 100644 index 0000000..d0d2f3b --- /dev/null +++ b/servers/lightspeed/src/tools/workorders.ts @@ -0,0 +1,84 @@ +import { LightspeedClient } from '../clients/lightspeed.js'; +import type { Workorder } from '../types/index.js'; + +export function createWorkorderTools(client: LightspeedClient) { + return [ + { + name: 'lightspeed_list_workorders', + description: 'List all service workorders', + inputSchema: { + type: 'object', + properties: { + customerID: { type: 'number' }, + shopID: { type: 'number' }, + archived: { type: 'boolean' }, + }, + }, + handler: async (args: any) => { + const params: any = {}; + if (args.customerID) params.customerID = args.customerID; + if (args.shopID) params.shopID = args.shopID; + if (args.archived !== undefined) params.archived = args.archived; + const workorders = await client.getPaginated('/Workorder', params); + return { workorders, count: workorders.length }; + }, + }, + { + name: 'lightspeed_get_workorder', + description: 'Get a specific workorder by ID', + inputSchema: { + type: 'object', + properties: { + workorderID: { type: 'number' }, + }, + required: ['workorderID'], + }, + handler: async (args: any) => { + const workorder = await client.get<{ Workorder: Workorder }>(`/Workorder/${args.workorderID}?load=[WorkorderItems]`); + return workorder.Workorder; + }, + }, + { + name: 'lightspeed_create_workorder', + description: 'Create a new service workorder', + inputSchema: { + type: 'object', + properties: { + customerID: { type: 'number' }, + shopID: { type: 'number' }, + note: { type: 'string' }, + warranty: { type: 'boolean', default: false }, + etaOut: { type: 'string', description: 'Estimated completion date' }, + }, + required: ['customerID', 'shopID'], + }, + handler: async (args: any) => { + const workorder = await client.post<{ Workorder: Workorder }>('/Workorder', { + Workorder: { + ...args, + timeIn: new Date().toISOString(), + }, + }); + return workorder.Workorder; + }, + }, + { + name: 'lightspeed_update_workorder_status', + description: 'Update workorder status', + inputSchema: { + type: 'object', + properties: { + workorderID: { type: 'number' }, + workorderStatusID: { type: 'number' }, + note: { type: 'string' }, + }, + required: ['workorderID'], + }, + handler: async (args: any) => { + const { workorderID, ...updates } = args; + const workorder = await client.put<{ Workorder: Workorder }>(`/Workorder/${workorderID}`, { Workorder: updates }); + return workorder.Workorder; + }, + }, + ]; +} diff --git a/servers/lightspeed/src/types/index.ts b/servers/lightspeed/src/types/index.ts index c7be680..ca843c0 100644 --- a/servers/lightspeed/src/types/index.ts +++ b/servers/lightspeed/src/types/index.ts @@ -1,294 +1,494 @@ -/** - * Lightspeed API Type Definitions - */ +// Lightspeed API Types export interface LightspeedConfig { accountId: string; - apiKey: string; - apiSecret?: string; - baseUrl?: string; - retailOrRestaurant?: 'retail' | 'restaurant'; + clientId: string; + clientSecret: string; + accessToken?: string; + refreshToken?: string; + environment?: 'trial' | 'production'; + apiType?: 'retail' | 'restaurant'; } -export interface Product { - productID: string; +export interface OAuthTokenResponse { + access_token: string; + token_type: string; + refresh_token: string; + expires_in: number; + scope?: string; +} + +export interface PaginatedResponse { + '@attributes': { + count: string; + offset: string; + limit: string; + }; + data: T[]; +} + +// Core Lightspeed Retail Entities + +export interface Item { + itemID: number; + systemSku: string; + customSku?: string; + manufacturerSku?: string; description: string; - sku?: string; upc?: string; + ean?: string; + modelYear?: number; + itemType: 'default' | 'assembly' | 'giftcard' | 'service'; defaultCost: number; avgCost: number; - defaultPrice: number; - onlinePrice?: number; - msrp?: number; - categoryID?: string; - manufacturerID?: string; - supplierID?: string; tax: boolean; - archived: boolean; discountable: boolean; - customSku?: string; + archived: boolean; serialized: boolean; + publishToEcom: boolean; + categoryID?: number; + taxClassID?: number; + manufacturerID?: number; + defaultVendorID?: number; + itemMatrixID?: number; createTime: string; - updateTime: string; - systemSku?: string; - modelYear?: number; - upc2?: string; - upc3?: string; + timeStamp: string; + Prices?: ItemPrice[]; + ItemShops?: ItemShop[]; + Tags?: ItemTag[]; } -export interface ProductInventory { - productID: string; - shopID: string; - qty: number; +export interface ItemPrice { + itemID: number; + useTypeID: number; + amount: string; + useType: string; +} + +export interface ItemShop { + itemShopID: number; + itemID: number; + shopID: number; + qoh: number; // Quantity on hand + backorder: number; reorderPoint: number; reorderLevel: number; - backorder: number; - committed: number; + timeStamp: string; +} + +export interface ItemTag { + itemID: number; + tagName: string; +} + +export interface Category { + categoryID: number; + name: string; + parentID?: number; + nodeDepth: string; + fullPathName: string; + leftNode: number; + rightNode: number; + createTime: string; + timeStamp: string; } export interface Customer { - customerID: string; + customerID: number; firstName: string; lastName: string; - company?: string; title?: string; - email?: string; - phone?: string; - mobile?: string; - address?: Address; + company?: string; dob?: string; archived: boolean; + customerTypeID?: number; + discountID?: number; + taxCategoryID?: number; + contactID?: number; + creditAccountID?: number; createTime: string; - updateTime: string; - creditAccountID?: string; - customerTypeID?: string; - discountID?: string; - taxCategoryID?: string; + timeStamp: string; + Contact?: Contact; + CreditAccount?: CreditAccount; } -export interface Address { +export interface Contact { + contactID: number; + noEmail: boolean; + noPhone: boolean; + noMail: boolean; + websiteUrl?: string; + phoneHome?: string; + phoneWork?: string; + phoneMobile?: string; + phoneFax?: string; + primaryEmail?: string; + secondaryEmail?: string; address1?: string; address2?: string; city?: string; state?: string; zip?: string; country?: string; + countryCode?: string; + stateCode?: string; +} + +export interface CreditAccount { + creditAccountID: number; + code: string; + name: string; + description?: string; + creditLimit: number; + balance: number; + giftCard: boolean; + archived: boolean; + customerID: number; + timeStamp: string; } export interface Sale { - saleID: string; - timeStamp: string; - employeeID: string; - registerID: string; - shopID: string; - customerID?: string; - total: number; - totalDue: number; - calcTax: number; - calcDiscount: number; - calcSubtotal: number; + saleID: number; + createTime: string; + updateTime: string; + completeTime?: string; completed: boolean; voided: boolean; archived: boolean; - quoteID?: string; - salesLines: SaleLine[]; - payments?: Payment[]; + referenceNumber: string; + referenceNumberSource?: string; + discountPercent: number; + tax1Rate: number; + tax2Rate: number; + calcDiscount: number; + calcTotal: number; + calcSubtotal: number; + calcTaxable: number; + calcNonTaxable: number; + calcTax1: number; + calcTax2: number; + calcPayments: number; + total: number; + totalDue: number; + balance: number; + customerID?: number; + discountID?: number; + employeeID?: number; + registerID?: number; + shopID: number; + taxCategoryID?: number; + timeStamp: string; + SaleLines?: SaleLine[]; + SalePayments?: SalePayment[]; } export interface SaleLine { - saleLineID: string; - saleID: string; - productID?: string; - description: string; - unitPrice: number; + saleLineID: number; + saleID: number; + itemID: number; unitQuantity: number; - unitCost?: number; + unitPrice: number; + normalUnitPrice: number; + discountAmount: number; + discountPercent: number; + avgCost: number; + fifoCost: number; tax: boolean; - taxAmount?: number; - discount?: number; - total: number; + tax1Rate: number; + tax2Rate: number; + calcLineDiscount: number; + calcTransactionDiscount: number; + calcTotal: number; + calcSubtotal: number; + calcTax1: number; + calcTax2: number; + employeeID?: number; + taxCategoryID?: number; createTime: string; - updateTime: string; + timeStamp: string; } -export interface Payment { - paymentID: string; - saleID?: string; +export interface SalePayment { + salePaymentID: number; + saleID: number; + paymentTypeID: number; amount: number; - paymentTypeID: string; - createTime: string; archived: boolean; + employeeID?: number; + registerID?: number; + createTime: string; } export interface Order { - orderID: string; - vendorID?: string; - supplierID?: string; - orderDate: string; - expectedDate?: string; - completedDate?: string; - status: 'open' | 'received' | 'partial' | 'cancelled'; - shopID: string; - employeeID?: string; - orderLines: OrderLine[]; - totalCost?: number; + orderID: number; + vendorID: number; + shopID: number; + orderedDate?: string; + receivedDate?: string; + arrivalDate?: string; archived: boolean; + complete: boolean; + discount: number; + totalDiscount: number; + totalQuantity: number; + shipCost: number; + otherCost: number; + refNum?: string; + shipInstructions?: string; + stockInstructions?: string; + timeStamp: string; + OrderLines?: OrderLine[]; } export interface OrderLine { - orderLineID: string; - orderID: string; - productID: string; - qtyOrdered: number; - qtyReceived?: number; - unitCost: number; - totalCost: number; - createTime: string; + orderLineID: number; + orderID: number; + itemID: number; + quantity: number; + price: number; + originalPrice: number; + numReceived: number; + total: number; + timeStamp: string; +} + +export interface Vendor { + vendorID: number; + name: string; + accountNumber?: string; + archived: boolean; + contactID?: number; + priceLevel?: string; + updatePrice: boolean; + updateCost: boolean; + updateDescription: boolean; + timeStamp: string; + Contact?: Contact; } export interface Employee { - employeeID: string; + employeeID: number; firstName: string; lastName: string; - email?: string; - phone?: string; - employeeNumber?: string; - pin?: string; - archived: boolean; - employeeRoleID?: string; - createTime: string; - updateTime: string; -} - -export interface Category { - categoryID: string; - name: string; - parentID?: string; - nodeDepth?: number; - fullPathName?: string; - archived: boolean; - createTime: string; - updateTime: string; -} - -export interface Supplier { - supplierID: string; - name: string; - accountNumber?: string; - contactFirstName?: string; - contactLastName?: string; - email?: string; - phone?: string; - address?: Address; - archived: boolean; - createTime: string; - updateTime: string; -} - -export interface Discount { - discountID: string; - name: string; - type: 'percentage' | 'fixed'; - value: number; - customerTypeID?: string; - minQuantity?: number; - minAmount?: number; - startDate?: string; - endDate?: string; - archived: boolean; -} - -export interface LoyaltyProgram { - programID: string; - name: string; - pointsPerDollar: number; - dollarPerPoint: number; - active: boolean; - archived: boolean; -} - -export interface CustomerLoyalty { - customerID: string; - programID: string; - points: number; - lifetimePoints: number; - lastActivityDate?: string; -} - -export interface Shop { - shopID: string; - name: string; - address?: Address; - phone?: string; - email?: string; - timezone?: string; archived: boolean; + lockOut: boolean; + employeeRoleID?: number; + contactID?: number; + limitToShopID?: number; + lastShopID?: number; + lastRegisterID?: number; + timeStamp: string; } export interface Register { - registerID: string; - shopID: string; + registerID: number; name: string; - number?: string; + shopID: number; archived: boolean; + open: boolean; + openTime?: string; + openEmployeeID?: number; } -export interface PaymentType { - paymentTypeID: string; +export interface Shop { + shopID: number; name: string; - type: 'cash' | 'credit' | 'debit' | 'check' | 'giftcard' | 'other'; - archived: boolean; -} - -export interface TaxCategory { - taxCategoryID: string; - name: string; - rate: number; archived: boolean; + serviceRate: number; + taxLabor: boolean; + timeZone: string; + contactID?: number; + taxCategoryID?: number; + timeStamp: string; } export interface Manufacturer { - manufacturerID: string; + manufacturerID: number; name: string; + createTime: string; + timeStamp: string; +} + +export interface InventoryCount { + inventoryCountID: number; + name: string; + shopID: number; archived: boolean; + timeStamp: string; + InventoryCountItems?: InventoryCountItem[]; } -export interface SalesReport { - startDate: string; - endDate: string; - totalSales: number; - totalTransactions: number; - averageTicket: number; - totalDiscount: number; - totalTax: number; - totalProfit?: number; - salesByDay?: { date: string; total: number }[]; - salesByEmployee?: { employeeID: string; employeeName: string; total: number }[]; - topProducts?: { productID: string; description: string; quantity: number; total: number }[]; +export interface InventoryCountItem { + inventoryCountItemID: number; + inventoryCountID: number; + itemID: number; + qty: number; + employeeID?: number; + timeStamp: string; } -export interface InventoryReport { - asOfDate: string; - shopID?: string; - totalValue: number; - totalItems: number; - lowStockItems: { productID: string; description: string; qty: number; reorderPoint: number }[]; - overStockItems?: { productID: string; description: string; qty: number }[]; +export interface InventoryTransfer { + transferID: number; + sendingShopID: number; + receivingShopID: number; + createdByEmployeeID?: number; + sentByEmployeeID?: number; + status: 'pending' | 'sent' | 'received' | 'cancelled'; + note?: string; + sentOn?: string; + needBy?: string; + archived: boolean; + createTime: string; + timeStamp: string; + TransferItems?: InventoryTransferItem[]; } -export interface CustomerReport { - startDate: string; - endDate: string; - totalCustomers: number; - newCustomers: number; - topCustomers: { customerID: string; name: string; totalSpent: number; visits: number }[]; +export interface InventoryTransferItem { + transferItemID: number; + transferID: number; + itemID: number; + toSend: number; + toReceive: number; + sent: number; + received: number; + sentValue: number; + receivedValue: number; + comment?: string; + timeStamp: string; } -export type ApiResponse = { - success: true; - data: T; -} | { - success: false; - error: string; +export interface InventoryLog { + inventoryLogID: number; + itemID: number; + shopID: number; + employeeID?: number; + qohChange: number; + costChange: number; + automated: boolean; + causedNegative: boolean; + reason?: string; + createTime: string; + saleID?: number; + orderID?: number; + transferID?: number; +} + +export interface Workorder { + workorderID: number; + customerID: number; + shopID: number; + saleID?: number; + saleLineID?: number; + workorderStatusID?: number; + archived: boolean; + timeIn?: string; + etaOut?: string; + note?: string; + warranty: boolean; + tax: boolean; + timeStamp: string; + WorkorderItems?: WorkorderItem[]; +} + +export interface WorkorderItem { + workorderItemID: number; + workorderID: number; + itemID: number; + employeeID?: number; + saleLineID?: number; + approved: boolean; + unitPrice: number; + unitQuantity: number; + warranty: boolean; + tax: boolean; + note?: string; + timeStamp: string; +} + +export interface Discount { + discountID: number; + name: string; + discountAmount: number; + discountPercent: number; + requireCustomer: boolean; + archived: boolean; + createTime: string; + timeStamp: string; +} + +export interface TaxCategory { + taxCategoryID: number; + isTaxInclusive: boolean; + tax1Name: string; + tax2Name: string; + tax1Rate: number; + tax2Rate: number; + timeStamp: string; +} + +// Lightspeed Restaurant K-Series Types + +export interface RestaurantMenu { + id: string; + name: string; + description?: string; + active: boolean; + sortOrder: number; + categories: RestaurantCategory[]; +} + +export interface RestaurantCategory { + id: string; + name: string; + description?: string; + sortOrder: number; + items: RestaurantMenuItem[]; +} + +export interface RestaurantMenuItem { + id: string; + name: string; + description?: string; + price: number; + active: boolean; + sortOrder: number; + categoryId: string; +} + +export interface RestaurantOrder { + id: string; + orderNumber: string; + status: 'pending' | 'preparing' | 'ready' | 'completed' | 'cancelled'; + total: number; + subtotal: number; + tax: number; + gratuity?: number; + createdAt: string; + updatedAt: string; + items: RestaurantOrderItem[]; +} + +export interface RestaurantOrderItem { + id: string; + orderId: string; + menuItemId: string; + quantity: number; + price: number; + notes?: string; +} + +export interface RestaurantTable { + id: string; + name: string; + capacity: number; + status: 'available' | 'occupied' | 'reserved'; + section?: string; +} + +// Error handling +export interface LightspeedError { + message: string; + code?: string; + statusCode?: number; details?: any; -}; +} diff --git a/servers/lightspeed/src/ui/analytics/App.tsx b/servers/lightspeed/src/ui/analytics/App.tsx new file mode 100644 index 0000000..a3653d4 --- /dev/null +++ b/servers/lightspeed/src/ui/analytics/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function AnalyticsDashboard() { + const [data, setData] = useState([]); + + return ( +
+
+

📊 Analytics Dashboard

+

Business intelligence

+
+
+

MCP-powered Analytics Dashboard - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/analytics/app.css b/servers/lightspeed/src/ui/analytics/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/analytics/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/analytics/index.html b/servers/lightspeed/src/ui/analytics/index.html new file mode 100644 index 0000000..a6f7ad0 --- /dev/null +++ b/servers/lightspeed/src/ui/analytics/index.html @@ -0,0 +1,12 @@ + + + + + + Analytics Dashboard + + +
+ + + diff --git a/servers/lightspeed/src/ui/analytics/main.tsx b/servers/lightspeed/src/ui/analytics/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/analytics/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/analytics/vite.config.ts b/servers/lightspeed/src/ui/analytics/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/analytics/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/category-manager/App.tsx b/servers/lightspeed/src/ui/category-manager/App.tsx new file mode 100644 index 0000000..562fa43 --- /dev/null +++ b/servers/lightspeed/src/ui/category-manager/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function CategoryManager() { + const [data, setData] = useState([]); + + return ( +
+
+

🗂️ Category Manager

+

Product category hierarchy

+
+
+

MCP-powered Category Manager - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/category-manager/app.css b/servers/lightspeed/src/ui/category-manager/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/category-manager/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/category-manager/index.html b/servers/lightspeed/src/ui/category-manager/index.html new file mode 100644 index 0000000..bffe14b --- /dev/null +++ b/servers/lightspeed/src/ui/category-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Category Manager + + +
+ + + diff --git a/servers/lightspeed/src/ui/category-manager/main.tsx b/servers/lightspeed/src/ui/category-manager/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/category-manager/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/category-manager/vite.config.ts b/servers/lightspeed/src/ui/category-manager/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/category-manager/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/customer-manager/App.tsx b/servers/lightspeed/src/ui/customer-manager/App.tsx new file mode 100644 index 0000000..b90d30e --- /dev/null +++ b/servers/lightspeed/src/ui/customer-manager/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function CustomerManager() { + const [data, setData] = useState([]); + + return ( +
+
+

👥 Customer Manager

+

Customer database and analytics

+
+
+

MCP-powered Customer Manager - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/customer-manager/app.css b/servers/lightspeed/src/ui/customer-manager/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/customer-manager/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/customer-manager/index.html b/servers/lightspeed/src/ui/customer-manager/index.html new file mode 100644 index 0000000..02397d6 --- /dev/null +++ b/servers/lightspeed/src/ui/customer-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Customer Manager + + +
+ + + diff --git a/servers/lightspeed/src/ui/customer-manager/main.tsx b/servers/lightspeed/src/ui/customer-manager/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/customer-manager/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/customer-manager/vite.config.ts b/servers/lightspeed/src/ui/customer-manager/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/customer-manager/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/dashboard/App.tsx b/servers/lightspeed/src/ui/dashboard/App.tsx new file mode 100644 index 0000000..f6451d1 --- /dev/null +++ b/servers/lightspeed/src/ui/dashboard/App.tsx @@ -0,0 +1,75 @@ +import { useState, useEffect } from 'react'; +import './app.css'; + +interface Stats { + totalSales: number; + totalRevenue: number; + totalCustomers: number; + totalProducts: number; + lowStockCount: number; +} + +export default function Dashboard() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Simulated stats - in real app, fetch from MCP server + setTimeout(() => { + setStats({ + totalSales: 142, + totalRevenue: 24567.89, + totalCustomers: 328, + totalProducts: 1456, + lowStockCount: 23, + }); + setLoading(false); + }, 500); + }, []); + + if (loading) return
Loading...
; + + return ( +
+
+

🚀 Lightspeed Dashboard

+

Real-time POS & inventory insights

+
+ +
+
+
${stats?.totalRevenue.toLocaleString()}
+
Today's Revenue
+
+
+
{stats?.totalSales}
+
Sales Today
+
+
+
{stats?.totalCustomers}
+
Total Customers
+
+
+
{stats?.totalProducts}
+
Products
+
+
+ +
+
+ ⚠️ Low Stock Alert: {stats?.lowStockCount} items need reordering +
+
+ +
+

Quick Actions

+
+ + + + +
+
+
+ ); +} diff --git a/servers/lightspeed/src/ui/dashboard/app.css b/servers/lightspeed/src/ui/dashboard/app.css new file mode 100644 index 0000000..6d51ad6 --- /dev/null +++ b/servers/lightspeed/src/ui/dashboard/app.css @@ -0,0 +1,133 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + text-align: center; + transition: transform 0.2s, box-shadow 0.2s; +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3); +} + +.stat-value { + font-size: 2.5rem; + font-weight: bold; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +.stat-label { + color: #888; + font-size: 0.95rem; + text-transform: uppercase; + letter-spacing: 1px; +} + +.alerts { + margin-bottom: 2rem; +} + +.alert { + padding: 1rem 1.5rem; + border-radius: 8px; + margin-bottom: 1rem; +} + +.alert.warning { + background: rgba(255, 152, 0, 0.1); + border-left: 4px solid #ff9800; + color: #ffb74d; +} + +.quick-actions { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; +} + +.quick-actions h2 { + margin-bottom: 1.5rem; + color: #fff; +} + +.action-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.action-btn { + padding: 1.2rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.action-btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + font-size: 1.5rem; + color: #667eea; +} diff --git a/servers/lightspeed/src/ui/dashboard/index.html b/servers/lightspeed/src/ui/dashboard/index.html new file mode 100644 index 0000000..5639939 --- /dev/null +++ b/servers/lightspeed/src/ui/dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Lightspeed Dashboard + + +
+ + + diff --git a/servers/lightspeed/src/ui/dashboard/main.tsx b/servers/lightspeed/src/ui/dashboard/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/dashboard/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/dashboard/vite.config.ts b/servers/lightspeed/src/ui/dashboard/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/dashboard/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/discount-manager/App.tsx b/servers/lightspeed/src/ui/discount-manager/App.tsx new file mode 100644 index 0000000..b3e1842 --- /dev/null +++ b/servers/lightspeed/src/ui/discount-manager/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function DiscountManager() { + const [data, setData] = useState([]); + + return ( +
+
+

🎟️ Discount Manager

+

Promotions and discounts

+
+
+

MCP-powered Discount Manager - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/discount-manager/app.css b/servers/lightspeed/src/ui/discount-manager/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/discount-manager/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/discount-manager/index.html b/servers/lightspeed/src/ui/discount-manager/index.html new file mode 100644 index 0000000..79445df --- /dev/null +++ b/servers/lightspeed/src/ui/discount-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Discount Manager + + +
+ + + diff --git a/servers/lightspeed/src/ui/discount-manager/main.tsx b/servers/lightspeed/src/ui/discount-manager/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/discount-manager/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/discount-manager/vite.config.ts b/servers/lightspeed/src/ui/discount-manager/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/discount-manager/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/employee-manager/App.tsx b/servers/lightspeed/src/ui/employee-manager/App.tsx new file mode 100644 index 0000000..0795dee --- /dev/null +++ b/servers/lightspeed/src/ui/employee-manager/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function EmployeeManager() { + const [data, setData] = useState([]); + + return ( +
+
+

👤 Employee Manager

+

Staff management and time tracking

+
+
+

MCP-powered Employee Manager - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/employee-manager/app.css b/servers/lightspeed/src/ui/employee-manager/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/employee-manager/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/employee-manager/index.html b/servers/lightspeed/src/ui/employee-manager/index.html new file mode 100644 index 0000000..075f2a7 --- /dev/null +++ b/servers/lightspeed/src/ui/employee-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Employee Manager + + +
+ + + diff --git a/servers/lightspeed/src/ui/employee-manager/main.tsx b/servers/lightspeed/src/ui/employee-manager/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/employee-manager/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/employee-manager/vite.config.ts b/servers/lightspeed/src/ui/employee-manager/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/employee-manager/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/inventory-manager/App.tsx b/servers/lightspeed/src/ui/inventory-manager/App.tsx new file mode 100644 index 0000000..0d04eb1 --- /dev/null +++ b/servers/lightspeed/src/ui/inventory-manager/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function InventoryManager() { + const [data, setData] = useState([]); + + return ( +
+
+

📊 Inventory Manager

+

Track stock levels and transfers

+
+
+

MCP-powered Inventory Manager - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/inventory-manager/app.css b/servers/lightspeed/src/ui/inventory-manager/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/inventory-manager/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/inventory-manager/index.html b/servers/lightspeed/src/ui/inventory-manager/index.html new file mode 100644 index 0000000..c7277d6 --- /dev/null +++ b/servers/lightspeed/src/ui/inventory-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Inventory Manager + + +
+ + + diff --git a/servers/lightspeed/src/ui/inventory-manager/main.tsx b/servers/lightspeed/src/ui/inventory-manager/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/inventory-manager/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/inventory-manager/vite.config.ts b/servers/lightspeed/src/ui/inventory-manager/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/inventory-manager/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/low-stock-alert/App.tsx b/servers/lightspeed/src/ui/low-stock-alert/App.tsx new file mode 100644 index 0000000..4de85cf --- /dev/null +++ b/servers/lightspeed/src/ui/low-stock-alert/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function LowStockAlerts() { + const [data, setData] = useState([]); + + return ( +
+
+

⚠️ Low Stock Alerts

+

Inventory alerts

+
+
+

MCP-powered Low Stock Alerts - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/low-stock-alert/app.css b/servers/lightspeed/src/ui/low-stock-alert/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/low-stock-alert/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/low-stock-alert/index.html b/servers/lightspeed/src/ui/low-stock-alert/index.html new file mode 100644 index 0000000..ad87a18 --- /dev/null +++ b/servers/lightspeed/src/ui/low-stock-alert/index.html @@ -0,0 +1,12 @@ + + + + + + Low Stock Alerts + + +
+ + + diff --git a/servers/lightspeed/src/ui/low-stock-alert/main.tsx b/servers/lightspeed/src/ui/low-stock-alert/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/low-stock-alert/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/low-stock-alert/vite.config.ts b/servers/lightspeed/src/ui/low-stock-alert/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/low-stock-alert/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/order-manager/App.tsx b/servers/lightspeed/src/ui/order-manager/App.tsx new file mode 100644 index 0000000..4e3f3d3 --- /dev/null +++ b/servers/lightspeed/src/ui/order-manager/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function OrderManager() { + const [data, setData] = useState([]); + + return ( +
+
+

📋 Order Manager

+

Purchase orders and receiving

+
+
+

MCP-powered Order Manager - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/order-manager/app.css b/servers/lightspeed/src/ui/order-manager/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/order-manager/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/order-manager/index.html b/servers/lightspeed/src/ui/order-manager/index.html new file mode 100644 index 0000000..20f42d5 --- /dev/null +++ b/servers/lightspeed/src/ui/order-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Order Manager + + +
+ + + diff --git a/servers/lightspeed/src/ui/order-manager/main.tsx b/servers/lightspeed/src/ui/order-manager/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/order-manager/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/order-manager/vite.config.ts b/servers/lightspeed/src/ui/order-manager/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/order-manager/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/product-manager/App.tsx b/servers/lightspeed/src/ui/product-manager/App.tsx new file mode 100644 index 0000000..1b4617b --- /dev/null +++ b/servers/lightspeed/src/ui/product-manager/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function ProductManager() { + const [data, setData] = useState([]); + + return ( +
+
+

📦 Product Manager

+

Manage inventory and products

+
+
+

MCP-powered Product Manager - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/product-manager/app.css b/servers/lightspeed/src/ui/product-manager/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/product-manager/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/product-manager/index.html b/servers/lightspeed/src/ui/product-manager/index.html new file mode 100644 index 0000000..1834e0f --- /dev/null +++ b/servers/lightspeed/src/ui/product-manager/index.html @@ -0,0 +1,12 @@ + + + + + + Product Manager + + +
+ + + diff --git a/servers/lightspeed/src/ui/product-manager/main.tsx b/servers/lightspeed/src/ui/product-manager/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/product-manager/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/product-manager/vite.config.ts b/servers/lightspeed/src/ui/product-manager/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/product-manager/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/quick-sale/App.tsx b/servers/lightspeed/src/ui/quick-sale/App.tsx new file mode 100644 index 0000000..5beae5d --- /dev/null +++ b/servers/lightspeed/src/ui/quick-sale/App.tsx @@ -0,0 +1,19 @@ +import { useState } from 'react'; +import './app.css'; + +export default function QuickSale() { + const [data, setData] = useState([]); + + return ( +
+
+

⚡ Quick Sale

+

Fast checkout interface

+
+
+

MCP-powered Quick Sale - Coming soon!

+ +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/quick-sale/app.css b/servers/lightspeed/src/ui/quick-sale/app.css new file mode 100644 index 0000000..c50ede7 --- /dev/null +++ b/servers/lightspeed/src/ui/quick-sale/app.css @@ -0,0 +1,95 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #0f0f0f; + color: #e0e0e0; + min-height: 100vh; +} + +.app { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +header { + margin-bottom: 2rem; + text-align: center; +} + +header h1 { + font-size: 2.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin-bottom: 0.5rem; +} + +header p { + color: #888; + font-size: 1.1rem; +} + +.content { + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border: 1px solid #2a2a3e; + border-radius: 12px; + padding: 2rem; + min-height: 400px; +} + +.btn { + padding: 0.8rem 1.5rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; +} + +.btn:hover { + transform: scale(1.05); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; +} + +th, td { + padding: 1rem; + text-align: left; + border-bottom: 1px solid #2a2a3e; +} + +th { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + font-weight: 600; +} + +input, select { + width: 100%; + padding: 0.8rem; + background: #1a1a2e; + border: 1px solid #2a2a3e; + border-radius: 8px; + color: #e0e0e0; + font-size: 1rem; + margin-bottom: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: #667eea; +} diff --git a/servers/lightspeed/src/ui/quick-sale/index.html b/servers/lightspeed/src/ui/quick-sale/index.html new file mode 100644 index 0000000..b0bc6a8 --- /dev/null +++ b/servers/lightspeed/src/ui/quick-sale/index.html @@ -0,0 +1,12 @@ + + + + + + Quick Sale + + +
+ + + diff --git a/servers/lightspeed/src/ui/quick-sale/main.tsx b/servers/lightspeed/src/ui/quick-sale/main.tsx new file mode 100644 index 0000000..b31fb6b --- /dev/null +++ b/servers/lightspeed/src/ui/quick-sale/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/servers/lightspeed/src/ui/quick-sale/vite.config.ts b/servers/lightspeed/src/ui/quick-sale/vite.config.ts new file mode 100644 index 0000000..01fbbfd --- /dev/null +++ b/servers/lightspeed/src/ui/quick-sale/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + base: './', +}); diff --git a/servers/lightspeed/src/ui/react-app/customer-detail.tsx b/servers/lightspeed/src/ui/react-app/customer-detail.tsx new file mode 100644 index 0000000..9a7a729 --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/customer-detail.tsx @@ -0,0 +1,236 @@ +import React, { useState } from 'react'; + +interface Purchase { + id: string; + date: string; + total: number; + items: number; +} + +interface Customer { + id: string; + name: string; + email: string; + phone: string; + address: string; + city: string; + state: string; + zip: string; + joinDate: string; + totalSpent: number; + totalPurchases: number; + averageOrderValue: number; + lastPurchase: string; + loyaltyPoints: number; + status: 'active' | 'inactive'; +} + +export default function CustomerDetail() { + const [customer, setCustomer] = useState({ + id: 'CUST-789', + name: 'John Smith', + email: 'john.smith@email.com', + phone: '(555) 123-4567', + address: '123 Main Street', + city: 'San Francisco', + state: 'CA', + zip: '94102', + joinDate: '2023-05-15', + totalSpent: 1247.85, + totalPurchases: 24, + averageOrderValue: 51.99, + lastPurchase: '2024-02-13', + loyaltyPoints: 1248, + status: 'active' + }); + + const [recentPurchases] = useState([ + { id: 'TXN-001234', date: '2024-02-13', total: 119.84, items: 3 }, + { id: 'TXN-001189', date: '2024-02-08', total: 47.99, items: 2 }, + { id: 'TXN-001142', date: '2024-01-28', total: 89.97, items: 4 }, + { id: 'TXN-001098', date: '2024-01-15', total: 24.99, items: 1 }, + ]); + + const [editing, setEditing] = useState(false); + + const handleSave = () => { + setEditing(false); + console.log('Saving customer:', customer); + }; + + return ( +
+
+ {/* Header */} +
+ +
+
+

{customer.name}

+

Customer ID: {customer.id}

+
+
+ {editing ? ( + <> + + + + ) : ( + + )} +
+
+
+ + {/* Stats */} +
+ + + + +
+ +
+ {/* Main Info */} +
+
+

Contact Information

+
+ setCustomer({...customer, name: v})} /> + setCustomer({...customer, email: v})} /> + setCustomer({...customer, phone: v})} /> + setCustomer({...customer, status: v as 'active' | 'inactive'})} /> +
+
+ +
+

Address

+
+ setCustomer({...customer, address: v})} /> +
+ setCustomer({...customer, city: v})} /> + setCustomer({...customer, state: v})} /> + setCustomer({...customer, zip: v})} /> +
+
+
+ + {/* Recent Purchases */} +
+

Recent Purchases

+
+ {recentPurchases.map((purchase) => ( +
+
+
{purchase.id}
+
{new Date(purchase.date).toLocaleDateString()}
+
+
+
${purchase.total.toFixed(2)}
+
{purchase.items} items
+
+
+ ))} +
+ +
+
+ + {/* Sidebar */} +
+
+

Account Status

+
+ {customer.status === 'active' ? 'Active' : 'Inactive'} +
+
+
+ Member Since: + {new Date(customer.joinDate).toLocaleDateString()} +
+
+ Last Purchase: + {new Date(customer.lastPurchase).toLocaleDateString()} +
+
+
+ +
+

Loyalty Program

+
+
{customer.loyaltyPoints}
+
Available Points
+
+
+ 252 points until next reward tier +
+
+
+
+
+ +
+

Quick Actions

+
+ + + +
+
+
+
+
+
+ ); +} + +function StatCard({ title, value, icon }: { title: string; value: string; icon: string }) { + return ( +
+
+ {title} + {icon} +
+
{value}
+
+ ); +} + +function InfoField({ label, value, editing, onChange }: { + label: string; + value: string; + editing: boolean; + onChange?: (value: string) => void; +}) { + return ( +
+ + {editing && onChange ? ( + onChange(e.target.value)} + className="w-full px-3 py-2 bg-slate-700 border border-slate-600 rounded-lg" + /> + ) : ( +
{value}
+ )} +
+ ); +} diff --git a/servers/lightspeed/src/ui/react-app/customer-grid.tsx b/servers/lightspeed/src/ui/react-app/customer-grid.tsx new file mode 100644 index 0000000..6c3b844 --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/customer-grid.tsx @@ -0,0 +1,187 @@ +import React, { useState } from 'react'; + +interface Customer { + id: string; + name: string; + email: string; + phone: string; + totalSpent: number; + totalOrders: number; + lastPurchase: string; + status: 'active' | 'inactive'; + loyaltyTier: 'bronze' | 'silver' | 'gold' | 'platinum'; +} + +export default function CustomerGrid() { + const [customers] = useState([ + { id: 'CUST-789', name: 'John Smith', email: 'john.smith@email.com', phone: '(555) 123-4567', totalSpent: 1247.85, totalOrders: 24, lastPurchase: '2024-02-13', status: 'active', loyaltyTier: 'gold' }, + { id: 'CUST-790', name: 'Sarah Johnson', email: 'sarah.j@email.com', phone: '(555) 234-5678', totalSpent: 2156.40, totalOrders: 38, lastPurchase: '2024-02-12', status: 'active', loyaltyTier: 'platinum' }, + { id: 'CUST-791', name: 'Michael Brown', email: 'mbrown@email.com', phone: '(555) 345-6789', totalSpent: 445.20, totalOrders: 8, lastPurchase: '2024-02-10', status: 'active', loyaltyTier: 'silver' }, + { id: 'CUST-792', name: 'Emily Davis', email: 'emily.d@email.com', phone: '(555) 456-7890', totalSpent: 892.75, totalOrders: 15, lastPurchase: '2024-02-08', status: 'active', loyaltyTier: 'gold' }, + { id: 'CUST-793', name: 'David Wilson', email: 'dwilson@email.com', phone: '(555) 567-8901', totalSpent: 156.30, totalOrders: 3, lastPurchase: '2024-01-22', status: 'inactive', loyaltyTier: 'bronze' }, + { id: 'CUST-794', name: 'Lisa Anderson', email: 'l.anderson@email.com', phone: '(555) 678-9012', totalSpent: 3421.90, totalOrders: 52, lastPurchase: '2024-02-13', status: 'active', loyaltyTier: 'platinum' }, + { id: 'CUST-795', name: 'Robert Taylor', email: 'rtaylor@email.com', phone: '(555) 789-0123', totalSpent: 678.50, totalOrders: 12, lastPurchase: '2024-02-11', status: 'active', loyaltyTier: 'silver' }, + { id: 'CUST-796', name: 'Jennifer Martinez', email: 'jmartinez@email.com', phone: '(555) 890-1234', totalSpent: 1034.25, totalOrders: 19, lastPurchase: '2024-02-09', status: 'active', loyaltyTier: 'gold' }, + ]); + + const [searchTerm, setSearchTerm] = useState(''); + const [filterStatus, setFilterStatus] = useState('all'); + const [filterTier, setFilterTier] = useState('all'); + const [sortBy, setSortBy] = useState<'name' | 'spent' | 'orders' | 'recent'>('recent'); + + const filteredCustomers = customers + .filter(customer => { + const matchesSearch = customer.name.toLowerCase().includes(searchTerm.toLowerCase()) || + customer.email.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesStatus = filterStatus === 'all' || customer.status === filterStatus; + const matchesTier = filterTier === 'all' || customer.loyaltyTier === filterTier; + return matchesSearch && matchesStatus && matchesTier; + }) + .sort((a, b) => { + switch (sortBy) { + case 'name': return a.name.localeCompare(b.name); + case 'spent': return b.totalSpent - a.totalSpent; + case 'orders': return b.totalOrders - a.totalOrders; + case 'recent': return new Date(b.lastPurchase).getTime() - new Date(a.lastPurchase).getTime(); + default: return 0; + } + }); + + const getTierColor = (tier: string) => { + switch (tier) { + case 'platinum': return 'bg-purple-500/20 text-purple-400'; + case 'gold': return 'bg-yellow-500/20 text-yellow-400'; + case 'silver': return 'bg-slate-400/20 text-slate-300'; + default: return 'bg-amber-700/20 text-amber-500'; + } + }; + + return ( +
+
+

Customer Directory

+ + {/* Filters */} +
+
+
+ + setSearchTerm(e.target.value)} + className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg focus:outline-none focus:border-blue-500" + /> +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Results Count */} +
+ Showing {filteredCustomers.length} of {customers.length} customers +
+ + {/* Grid */} +
+ {filteredCustomers.map((customer) => ( +
+
+
+

{customer.name}

+

{customer.id}

+
+ + {customer.loyaltyTier.toUpperCase()} + +
+ +
+
+ 📧 + {customer.email} +
+
+ 📱 + {customer.phone} +
+
+ +
+
+
Total Spent
+
${customer.totalSpent.toFixed(0)}
+
+
+
Orders
+
{customer.totalOrders}
+
+
+ +
+
+
Last Purchase
+
{new Date(customer.lastPurchase).toLocaleDateString()}
+
+ +
+ + {customer.status === 'inactive' && ( +
+ Inactive +
+ )} +
+ ))} +
+ + {filteredCustomers.length === 0 && ( +
+ No customers found matching your filters. +
+ )} +
+
+ ); +} diff --git a/servers/lightspeed/src/ui/react-app/inventory-tracker.tsx b/servers/lightspeed/src/ui/react-app/inventory-tracker.tsx new file mode 100644 index 0000000..1c7bc5e --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/inventory-tracker.tsx @@ -0,0 +1,184 @@ +import React, { useState } from 'react'; + +interface InventoryItem { + id: string; + productName: string; + sku: string; + category: string; + currentStock: number; + reorderPoint: number; + maxStock: number; + lastRestocked: string; + status: 'in-stock' | 'low-stock' | 'out-of-stock' | 'overstock'; +} + +export default function InventoryTracker() { + const [items] = useState([ + { id: '1', productName: 'Premium Coffee Beans', sku: 'COF001', category: 'Coffee', currentStock: 45, reorderPoint: 20, maxStock: 100, lastRestocked: '2024-02-10', status: 'in-stock' }, + { id: '2', productName: 'Espresso Machine', sku: 'MAC001', category: 'Equipment', currentStock: 3, reorderPoint: 5, maxStock: 15, lastRestocked: '2024-01-15', status: 'low-stock' }, + { id: '3', productName: 'Ceramic Mug', sku: 'MUG001', category: 'Accessories', currentStock: 0, reorderPoint: 25, maxStock: 150, lastRestocked: '2024-01-20', status: 'out-of-stock' }, + { id: '4', productName: 'Tea Assortment', sku: 'TEA001', category: 'Tea', currentStock: 8, reorderPoint: 10, maxStock: 50, lastRestocked: '2024-02-08', status: 'low-stock' }, + { id: '5', productName: 'Milk Frother', sku: 'ACC001', category: 'Accessories', currentStock: 12, reorderPoint: 8, maxStock: 30, lastRestocked: '2024-02-11', status: 'in-stock' }, + { id: '6', productName: 'Coffee Filters', sku: 'ACC002', category: 'Accessories', currentStock: 156, reorderPoint: 50, maxStock: 100, lastRestocked: '2024-02-12', status: 'overstock' }, + { id: '7', productName: 'Cold Brew Maker', sku: 'EQP002', category: 'Equipment', currentStock: 15, reorderPoint: 5, maxStock: 20, lastRestocked: '2024-02-09', status: 'in-stock' }, + { id: '8', productName: 'Green Tea', sku: 'TEA002', category: 'Tea', currentStock: 22, reorderPoint: 15, maxStock: 60, lastRestocked: '2024-02-13', status: 'in-stock' }, + ]); + + const [filterStatus, setFilterStatus] = useState('all'); + const [searchTerm, setSearchTerm] = useState(''); + + const filteredItems = items.filter(item => { + const matchesSearch = item.productName.toLowerCase().includes(searchTerm.toLowerCase()) || + item.sku.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesStatus = filterStatus === 'all' || item.status === filterStatus; + return matchesSearch && matchesStatus; + }); + + const stats = { + totalItems: items.length, + lowStock: items.filter(i => i.status === 'low-stock').length, + outOfStock: items.filter(i => i.status === 'out-of-stock').length, + overstock: items.filter(i => i.status === 'overstock').length, + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'in-stock': return 'bg-green-500/20 text-green-400'; + case 'low-stock': return 'bg-yellow-500/20 text-yellow-400'; + case 'out-of-stock': return 'bg-red-500/20 text-red-400'; + case 'overstock': return 'bg-blue-500/20 text-blue-400'; + default: return 'bg-slate-600/50 text-slate-400'; + } + }; + + const getStockPercentage = (current: number, max: number) => { + return Math.min((current / max) * 100, 100); + }; + + return ( +
+
+

Inventory Tracker

+ + {/* Stats */} +
+ + + + +
+ + {/* Filters */} +
+
+
+ + setSearchTerm(e.target.value)} + className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg focus:outline-none focus:border-blue-500" + /> +
+
+ + +
+
+
+ + {/* Inventory Table */} +
+
+ + + + + + + + + + + + + + + {filteredItems.map((item) => ( + + + + + + + + + + + ))} + +
ProductSKUCategoryStock LevelReorder PointLast RestockedStatusActions
{item.productName}{item.sku}{item.category} +
+
{item.currentStock} / {item.maxStock}
+
+
+
+
+
{item.reorderPoint}{new Date(item.lastRestocked).toLocaleDateString()} +
+ + {item.status.replace('-', ' ').toUpperCase()} + +
+
+
+ + +
+
+
+
+ + {filteredItems.length === 0 && ( +
+ No items found matching your filters. +
+ )} +
+
+ ); +} + +function StatCard({ title, value, icon, color }: { title: string; value: number; icon: string; color: string }) { + return ( +
+
+ {title} + {icon} +
+
{value}
+
+ ); +} diff --git a/servers/lightspeed/src/ui/react-app/product-dashboard.tsx b/servers/lightspeed/src/ui/react-app/product-dashboard.tsx new file mode 100644 index 0000000..5a94e79 --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/product-dashboard.tsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect } from 'react'; + +interface Product { + id: string; + name: string; + sku: string; + price: number; + stock: number; + category: string; + status: 'active' | 'inactive'; +} + +interface DashboardStats { + totalProducts: number; + activeProducts: number; + lowStock: number; + outOfStock: number; + totalValue: number; +} + +export default function ProductDashboard() { + const [stats, setStats] = useState({ + totalProducts: 0, + activeProducts: 0, + lowStock: 0, + outOfStock: 0, + totalValue: 0 + }); + const [topProducts, setTopProducts] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Simulate loading products + const mockProducts: Product[] = [ + { id: '1', name: 'Premium Coffee Beans', sku: 'COF001', price: 24.99, stock: 45, category: 'Coffee', status: 'active' }, + { id: '2', name: 'Espresso Machine', sku: 'MAC001', price: 899.99, stock: 3, category: 'Equipment', status: 'active' }, + { id: '3', name: 'Ceramic Mug', sku: 'MUG001', price: 12.99, stock: 0, category: 'Accessories', status: 'active' }, + { id: '4', name: 'Tea Assortment', sku: 'TEA001', price: 19.99, stock: 8, category: 'Tea', status: 'active' }, + { id: '5', name: 'Milk Frother', sku: 'ACC001', price: 49.99, stock: 12, category: 'Accessories', status: 'inactive' }, + ]; + + const totalValue = mockProducts.reduce((sum, p) => sum + (p.price * p.stock), 0); + const lowStock = mockProducts.filter(p => p.stock > 0 && p.stock < 10).length; + const outOfStock = mockProducts.filter(p => p.stock === 0).length; + + setStats({ + totalProducts: mockProducts.length, + activeProducts: mockProducts.filter(p => p.status === 'active').length, + lowStock, + outOfStock, + totalValue + }); + setTopProducts(mockProducts.slice(0, 5)); + setLoading(false); + }, []); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+
+

Product Dashboard

+ + {/* Stats Grid */} +
+ + + + + +
+ + {/* Top Products Table */} +
+

Recent Products

+
+ + + + + + + + + + + + + {topProducts.map((product) => ( + + + + + + + + + ))} + +
ProductSKUCategoryPriceStockStatus
{product.name}{product.sku}{product.category}${product.price.toFixed(2)} + + {product.stock} + + + + {product.status} + +
+
+
+
+
+ ); +} + +function StatCard({ title, value, icon, color }: { title: string; value: string | number; icon: string; color: string }) { + return ( +
+
+ {title} + {icon} +
+
{value}
+
+ ); +} diff --git a/servers/lightspeed/src/ui/react-app/product-detail.tsx b/servers/lightspeed/src/ui/react-app/product-detail.tsx new file mode 100644 index 0000000..d817fd9 --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/product-detail.tsx @@ -0,0 +1,208 @@ +import React, { useState, useEffect } from 'react'; + +interface Product { + id: string; + name: string; + sku: string; + description: string; + price: number; + cost: number; + stock: number; + category: string; + supplier: string; + barcode: string; + status: 'active' | 'inactive'; + createdAt: string; + updatedAt: string; +} + +export default function ProductDetail() { + const [product, setProduct] = useState(null); + const [editing, setEditing] = useState(false); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Simulate loading a product + setTimeout(() => { + setProduct({ + id: '1', + name: 'Premium Coffee Beans', + sku: 'COF001', + description: 'High-quality Arabica coffee beans sourced from Colombia. Medium roast with notes of chocolate and caramel.', + price: 24.99, + cost: 12.50, + stock: 45, + category: 'Coffee', + supplier: 'Colombian Coffee Co.', + barcode: '1234567890123', + status: 'active', + createdAt: '2024-01-15', + updatedAt: '2024-02-10' + }); + setLoading(false); + }, 500); + }, []); + + const handleSave = () => { + setEditing(false); + // In real app: save to API + console.log('Saving product:', product); + }; + + if (loading) { + return ( +
+
Loading product...
+
+ ); + } + + if (!product) return null; + + const margin = ((product.price - product.cost) / product.price * 100).toFixed(1); + + return ( +
+
+ {/* Header */} +
+
+ +

{product.name}

+

SKU: {product.sku}

+
+
+ {editing ? ( + <> + + + + ) : ( + + )} +
+
+ +
+ {/* Main Info */} +
+
+

Product Information

+
+ setProduct({...product, name: v})} /> + setProduct({...product, description: v})} /> + setProduct({...product, category: v})} /> + setProduct({...product, supplier: v})} /> + setProduct({...product, barcode: v})} /> +
+
+ +
+

Pricing & Inventory

+
+ setProduct({...product, price: parseFloat(v.replace('$', ''))})} /> + setProduct({...product, cost: parseFloat(v.replace('$', ''))})} /> + setProduct({...product, stock: parseInt(v)})} /> +
+ +
{margin}%
+
+
+
+
+ + {/* Sidebar */} +
+
+

Status

+
+
+ + +
+
+ {product.stock === 0 ? 'Out of Stock' : + product.stock < 10 ? 'Low Stock' : 'In Stock'} +
+
+
+ +
+

Timestamps

+
+
+ Created: +
{new Date(product.createdAt).toLocaleDateString()}
+
+
+ Last Updated: +
{new Date(product.updatedAt).toLocaleDateString()}
+
+
+
+
+
+
+
+ ); +} + +function InfoField({ label, value, editing, multiline, onChange }: { + label: string; + value: string; + editing: boolean; + multiline?: boolean; + onChange?: (value: string) => void; +}) { + return ( +
+ + {editing && onChange ? ( + multiline ? ( +