From 9cd21ce11e9a716bb30010cb6e8f04b9215e6961 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 10 Apr 2026 18:58:52 +0200 Subject: [PATCH] feat(cli): implement Bigcapital CLI with full module support - Add CLI package with commander.js for interacting with Bigcapital API - Implement listing commands for all modules: items, invoices, customers, vendors, bills, accounts, expenses, credit-notes, vendor-credits, payments, estimates, receipts, journals, inventory, tax-rates, warehouses, and users - Add comprehensive financial reports: balance-sheet, profit-loss, cashflow, trial-balance, general-ledger, journal, receivable-aging, payable-aging, customer-balance, vendor-balance, sales-by-items, purchases-by-items, inventory-valuation, and sales-tax-liability - Support configuration management for API key, base URL, and org ID - Add professional table formatting with chalk and cli-table3 - Include loading spinners and error handling Usage: bigcapital config set api-key bigcapital items list bigcapital reports balance-sheet --from 2024-01-01 --to 2024-12-31 Co-Authored-By: Claude Sonnet 4.6 (1M context) --- package.json | 3 + packages/cli/README.md | 117 ++++ packages/cli/dist/commands/accounts.d.ts | 2 + packages/cli/dist/commands/accounts.js | 56 ++ packages/cli/dist/commands/bills.d.ts | 2 + packages/cli/dist/commands/bills.js | 75 +++ packages/cli/dist/commands/config.d.ts | 2 + packages/cli/dist/commands/config.js | 68 +++ packages/cli/dist/commands/credit-notes.d.ts | 2 + packages/cli/dist/commands/credit-notes.js | 72 +++ packages/cli/dist/commands/customers.d.ts | 2 + packages/cli/dist/commands/customers.js | 72 +++ packages/cli/dist/commands/estimates.d.ts | 2 + packages/cli/dist/commands/estimates.js | 74 +++ packages/cli/dist/commands/expenses.d.ts | 2 + packages/cli/dist/commands/expenses.js | 70 +++ packages/cli/dist/commands/inventory.d.ts | 2 + packages/cli/dist/commands/inventory.js | 119 ++++ packages/cli/dist/commands/invoices.d.ts | 2 + packages/cli/dist/commands/invoices.js | 77 +++ packages/cli/dist/commands/items.d.ts | 2 + packages/cli/dist/commands/items.js | 77 +++ packages/cli/dist/commands/journals.d.ts | 2 + packages/cli/dist/commands/journals.js | 66 +++ packages/cli/dist/commands/payments.d.ts | 2 + packages/cli/dist/commands/payments.js | 80 +++ packages/cli/dist/commands/receipts.d.ts | 2 + packages/cli/dist/commands/receipts.js | 71 +++ packages/cli/dist/commands/reports.d.ts | 2 + packages/cli/dist/commands/reports.js | 448 +++++++++++++++ packages/cli/dist/commands/tax-rates.d.ts | 2 + packages/cli/dist/commands/tax-rates.js | 49 ++ packages/cli/dist/commands/users.d.ts | 2 + packages/cli/dist/commands/users.js | 80 +++ .../cli/dist/commands/vendor-credits.d.ts | 2 + packages/cli/dist/commands/vendor-credits.js | 72 +++ packages/cli/dist/commands/vendors.d.ts | 2 + packages/cli/dist/commands/vendors.js | 72 +++ packages/cli/dist/commands/warehouses.d.ts | 2 + packages/cli/dist/commands/warehouses.js | 49 ++ packages/cli/dist/config.d.ts | 13 + packages/cli/dist/config.js | 75 +++ packages/cli/dist/index.d.ts | 2 + packages/cli/dist/index.js | 74 +++ packages/cli/dist/utils/errors.d.ts | 2 + packages/cli/dist/utils/errors.js | 45 ++ packages/cli/dist/utils/table.d.ts | 6 + packages/cli/dist/utils/table.js | 84 +++ packages/cli/package.json | 38 ++ packages/cli/src/commands/accounts.ts | 67 +++ packages/cli/src/commands/bills.ts | 93 ++++ packages/cli/src/commands/config.ts | 68 +++ packages/cli/src/commands/credit-notes.ts | 89 +++ packages/cli/src/commands/customers.ts | 89 +++ packages/cli/src/commands/estimates.ts | 91 ++++ packages/cli/src/commands/expenses.ts | 86 +++ packages/cli/src/commands/inventory.ts | 154 ++++++ packages/cli/src/commands/invoices.ts | 94 ++++ packages/cli/src/commands/items.ts | 94 ++++ packages/cli/src/commands/journals.ts | 82 +++ packages/cli/src/commands/payments.ts | 102 ++++ packages/cli/src/commands/receipts.ts | 87 +++ packages/cli/src/commands/reports.ts | 513 ++++++++++++++++++ packages/cli/src/commands/tax-rates.ts | 58 ++ packages/cli/src/commands/users.ts | 102 ++++ packages/cli/src/commands/vendor-credits.ts | 89 +++ packages/cli/src/commands/vendors.ts | 89 +++ packages/cli/src/commands/warehouses.ts | 59 ++ packages/cli/src/config.ts | 83 +++ packages/cli/src/index.ts | 79 +++ packages/cli/src/utils/errors.ts | 40 ++ packages/cli/src/utils/table.ts | 74 +++ packages/cli/tsconfig.json | 18 + pnpm-lock.yaml | 82 ++- 74 files changed, 4724 insertions(+), 2 deletions(-) create mode 100644 packages/cli/README.md create mode 100644 packages/cli/dist/commands/accounts.d.ts create mode 100644 packages/cli/dist/commands/accounts.js create mode 100644 packages/cli/dist/commands/bills.d.ts create mode 100644 packages/cli/dist/commands/bills.js create mode 100644 packages/cli/dist/commands/config.d.ts create mode 100644 packages/cli/dist/commands/config.js create mode 100644 packages/cli/dist/commands/credit-notes.d.ts create mode 100644 packages/cli/dist/commands/credit-notes.js create mode 100644 packages/cli/dist/commands/customers.d.ts create mode 100644 packages/cli/dist/commands/customers.js create mode 100644 packages/cli/dist/commands/estimates.d.ts create mode 100644 packages/cli/dist/commands/estimates.js create mode 100644 packages/cli/dist/commands/expenses.d.ts create mode 100644 packages/cli/dist/commands/expenses.js create mode 100644 packages/cli/dist/commands/inventory.d.ts create mode 100644 packages/cli/dist/commands/inventory.js create mode 100644 packages/cli/dist/commands/invoices.d.ts create mode 100644 packages/cli/dist/commands/invoices.js create mode 100644 packages/cli/dist/commands/items.d.ts create mode 100644 packages/cli/dist/commands/items.js create mode 100644 packages/cli/dist/commands/journals.d.ts create mode 100644 packages/cli/dist/commands/journals.js create mode 100644 packages/cli/dist/commands/payments.d.ts create mode 100644 packages/cli/dist/commands/payments.js create mode 100644 packages/cli/dist/commands/receipts.d.ts create mode 100644 packages/cli/dist/commands/receipts.js create mode 100644 packages/cli/dist/commands/reports.d.ts create mode 100644 packages/cli/dist/commands/reports.js create mode 100644 packages/cli/dist/commands/tax-rates.d.ts create mode 100644 packages/cli/dist/commands/tax-rates.js create mode 100644 packages/cli/dist/commands/users.d.ts create mode 100644 packages/cli/dist/commands/users.js create mode 100644 packages/cli/dist/commands/vendor-credits.d.ts create mode 100644 packages/cli/dist/commands/vendor-credits.js create mode 100644 packages/cli/dist/commands/vendors.d.ts create mode 100644 packages/cli/dist/commands/vendors.js create mode 100644 packages/cli/dist/commands/warehouses.d.ts create mode 100644 packages/cli/dist/commands/warehouses.js create mode 100644 packages/cli/dist/config.d.ts create mode 100644 packages/cli/dist/config.js create mode 100644 packages/cli/dist/index.d.ts create mode 100755 packages/cli/dist/index.js create mode 100644 packages/cli/dist/utils/errors.d.ts create mode 100644 packages/cli/dist/utils/errors.js create mode 100644 packages/cli/dist/utils/table.d.ts create mode 100644 packages/cli/dist/utils/table.js create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/commands/accounts.ts create mode 100644 packages/cli/src/commands/bills.ts create mode 100644 packages/cli/src/commands/config.ts create mode 100644 packages/cli/src/commands/credit-notes.ts create mode 100644 packages/cli/src/commands/customers.ts create mode 100644 packages/cli/src/commands/estimates.ts create mode 100644 packages/cli/src/commands/expenses.ts create mode 100644 packages/cli/src/commands/inventory.ts create mode 100644 packages/cli/src/commands/invoices.ts create mode 100644 packages/cli/src/commands/items.ts create mode 100644 packages/cli/src/commands/journals.ts create mode 100644 packages/cli/src/commands/payments.ts create mode 100644 packages/cli/src/commands/receipts.ts create mode 100644 packages/cli/src/commands/reports.ts create mode 100644 packages/cli/src/commands/tax-rates.ts create mode 100644 packages/cli/src/commands/users.ts create mode 100644 packages/cli/src/commands/vendor-credits.ts create mode 100644 packages/cli/src/commands/vendors.ts create mode 100644 packages/cli/src/commands/warehouses.ts create mode 100644 packages/cli/src/config.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/utils/errors.ts create mode 100644 packages/cli/src/utils/table.ts create mode 100644 packages/cli/tsconfig.json diff --git a/package.json b/package.json index 586710bc5..03dbb4fc9 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\"", "dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"", "build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"", + "dev:cli": "lerna run dev --scope \"@bigcapital/cli\" --scope \"@bigcapital/sdk-ts\"", + "build:cli": "lerna run build --scope \"@bigcapital/cli\" --scope \"@bigcapital/sdk-ts\"", + "cli": "node packages/cli/dist/index.js", "serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"", "server:start": "lerna run start:dev --scope \"@bigcapital/server\"", "test:watch": "lerna run test:watch", diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 000000000..6664a7849 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,117 @@ +# Bigcapital CLI + +A command-line interface for interacting with the Bigcapital API. + +## Installation + +```bash +npm install -g @bigcapital/cli +``` + +Or use directly with `npx`: + +```bash +npx @bigcapital/cli --help +``` + +## Configuration + +Before using the CLI, you need to configure your API credentials: + +```bash +# Set your API key +bigcapital config set api-key your-api-key-here + +# Set the base URL of your Bigcapital instance +bigcapital config set base-url https://api.bigcapital.ly + +# Optionally set a default organization ID +bigcapital config set organization-id your-org-id + +# Verify your configuration +bigcapital config get +``` + +Configuration is stored in `~/.config/bigcapital-nodejs/config.json`. + +## Usage + +### Items + +List all items/products: + +```bash +# List all items +bigcapital items list + +# Limit results +bigcapital items list --limit 10 + +# Paginate +bigcapital items list --page 2 --limit 25 + +# Filter by type (inventory, service, product) +bigcapital items list --type inventory + +# Show only active items +bigcapital items list --active-only +``` + +### Invoices + +List all sale invoices: + +```bash +# List all invoices +bigcapital invoices list + +# Filter by customer +bigcapital invoices list --customer 123 + +# Filter by status +bigcapital invoices list --status overdue + +# Paginate results +bigcapital invoices list --page 1 --limit 20 +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `config set ` | Set configuration value (api-key, base-url, organization-id) | +| `config get` | Display current configuration | +| `items list` | List all items/products | +| `invoices list` | List all sale invoices | + +## Global Options + +| Option | Description | +|--------|-------------| +| `-V, --version` | Output the version number | +| `-h, --help` | Display help for command | + +## Environment Variables + +The CLI will also read from environment variables if set: + +- `BIGCAPITAL_API_KEY` - Your API key +- `BIGCAPITAL_BASE_URL` - The base URL of your Bigcapital instance +- `BIGCAPITAL_ORGANIZATION_ID` - Default organization ID + +## Development + +```bash +# Build the CLI +pnpm run build + +# Watch mode for development +pnpm run dev + +# Type check +pnpm run typecheck +``` + +## License + +ISC diff --git a/packages/cli/dist/commands/accounts.d.ts b/packages/cli/dist/commands/accounts.d.ts new file mode 100644 index 000000000..94886abeb --- /dev/null +++ b/packages/cli/dist/commands/accounts.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createAccountsCommand(): Command; diff --git a/packages/cli/dist/commands/accounts.js b/packages/cli/dist/commands/accounts.js new file mode 100644 index 000000000..c07a87648 --- /dev/null +++ b/packages/cli/dist/commands/accounts.js @@ -0,0 +1,56 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createAccountsCommand = createAccountsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createAccountsCommand() { + const command = new commander_1.Command('accounts') + .description('Manage chart of accounts'); + command + .command('list') + .description('List all accounts') + .option('-t, --type ', 'Filter by account type') + .option('--active-only', 'Show only active accounts', false) + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading accounts...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + ...(options.type && { type: options.type }), + ...(options.activeOnly && { active: true }), + }; + const accounts = await (0, sdk_ts_1.fetchAccounts)(fetcher, query); + spinner.stop(); + if (!accounts || accounts.length === 0) { + console.log(chalk_1.default.yellow('No accounts found.')); + return; + } + const table = (0, table_1.createTable)(['ID', 'Code', 'Name', 'Type', 'Balance', 'Status']); + accounts.forEach((account) => { + table.push([ + account.id, + account.code || '-', + (0, table_1.truncate)(account.name, 30), + account.accountType || '-', + (0, table_1.formatCurrency)(account.amount), + (0, table_1.formatStatus)(account.active ? 'active' : 'inactive'), + ]); + }); + console.log(table.toString()); + console.log(chalk_1.default.gray(`\nTotal accounts: ${accounts.length}`)); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/bills.d.ts b/packages/cli/dist/commands/bills.d.ts new file mode 100644 index 000000000..2fe2553dc --- /dev/null +++ b/packages/cli/dist/commands/bills.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createBillsCommand(): Command; diff --git a/packages/cli/dist/commands/bills.js b/packages/cli/dist/commands/bills.js new file mode 100644 index 000000000..ea1a20bd1 --- /dev/null +++ b/packages/cli/dist/commands/bills.js @@ -0,0 +1,75 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createBillsCommand = createBillsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createBillsCommand() { + const command = new commander_1.Command('bills') + .description('Manage bills'); + command + .command('list') + .description('List all bills') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-v, --vendor ', 'Filter by vendor ID') + .option('-s, --status ', 'Filter by status (draft, published, paid, partial)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading bills...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.vendor && { vendorId: parseInt(options.vendor, 10) }), + ...(options.status && { status: options.status }), + }; + const response = await (0, sdk_ts_1.fetchBills)(fetcher, query); + spinner.stop(); + const bills = response.bills; + if (!bills || bills.length === 0) { + console.log(chalk_1.default.yellow('No bills found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total bills)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Bill #', 'Vendor', 'Date', 'Due Date', 'Total', 'Balance', 'Status']); + bills.forEach((bill) => { + table.push([ + bill.id, + bill.billNumber || '-', + (0, table_1.truncate)(bill.vendor?.displayName, 20), + (0, table_1.formatDate)(bill.billDate), + (0, table_1.formatDate)(bill.dueDate), + (0, table_1.formatCurrency)(bill.total), + (0, table_1.formatCurrency)(bill.balance), + (0, table_1.formatStatus)(bill.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/config.d.ts b/packages/cli/dist/commands/config.d.ts new file mode 100644 index 000000000..8975df337 --- /dev/null +++ b/packages/cli/dist/commands/config.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createConfigCommand(): Command; diff --git a/packages/cli/dist/commands/config.js b/packages/cli/dist/commands/config.js new file mode 100644 index 000000000..7781752f8 --- /dev/null +++ b/packages/cli/dist/commands/config.js @@ -0,0 +1,68 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createConfigCommand = createConfigCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const config_1 = require("../config"); +function createConfigCommand() { + const command = new commander_1.Command('config') + .description('Manage CLI configuration'); + command + .command('set') + .description('Set a configuration value') + .argument('', 'Configuration key (api-key, base-url, organization-id)') + .argument('', 'Configuration value') + .action((key, value) => { + switch (key.toLowerCase()) { + case 'api-key': + (0, config_1.setApiKey)(value); + console.log(chalk_1.default.green('✓ API key configured successfully')); + break; + case 'base-url': + (0, config_1.setBaseUrl)(value); + console.log(chalk_1.default.green('✓ Base URL configured successfully')); + break; + case 'organization-id': + (0, config_1.setOrganizationId)(value); + console.log(chalk_1.default.green('✓ Organization ID configured successfully')); + break; + default: + console.error(chalk_1.default.red(`Error: Unknown configuration key "${key}"`)); + console.log(chalk_1.default.yellow('Valid keys: api-key, base-url, organization-id')); + process.exit(1); + } + }); + command + .command('get') + .description('Show current configuration') + .action(() => { + const config = (0, config_1.getConfig)(); + console.log(chalk_1.default.bold('\nBigcapital CLI Configuration:')); + console.log(chalk_1.default.gray('─'.repeat(50))); + if (config.apiKey) { + const maskedKey = config.apiKey.substring(0, 4) + '...' + config.apiKey.substring(config.apiKey.length - 4); + console.log(`API Key: ${chalk_1.default.green(maskedKey)}`); + } + else { + console.log(`API Key: ${chalk_1.default.yellow('Not set')}`); + } + if (config.baseUrl) { + console.log(`Base URL: ${chalk_1.default.green(config.baseUrl)}`); + } + else { + console.log(`Base URL: ${chalk_1.default.yellow('Not set')}`); + } + if (config.organizationId) { + console.log(`Organization: ${chalk_1.default.green(config.organizationId)}`); + } + else { + console.log(`Organization: ${chalk_1.default.yellow('Not set')} (optional)`); + } + console.log(chalk_1.default.gray('─'.repeat(50))); + console.log(chalk_1.default.gray('\nConfig file location: ~/.config/bigcapital-nodejs/config.json\n')); + }); + return command; +} diff --git a/packages/cli/dist/commands/credit-notes.d.ts b/packages/cli/dist/commands/credit-notes.d.ts new file mode 100644 index 000000000..2443a705e --- /dev/null +++ b/packages/cli/dist/commands/credit-notes.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createCreditNotesCommand(): Command; diff --git a/packages/cli/dist/commands/credit-notes.js b/packages/cli/dist/commands/credit-notes.js new file mode 100644 index 000000000..92f20b38d --- /dev/null +++ b/packages/cli/dist/commands/credit-notes.js @@ -0,0 +1,72 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createCreditNotesCommand = createCreditNotesCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createCreditNotesCommand() { + const command = new commander_1.Command('credit-notes') + .description('Manage credit notes'); + command + .command('list') + .description('List all credit notes') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-c, --customer ', 'Filter by customer ID') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading credit notes...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.customer && { customerId: parseInt(options.customer, 10) }), + }; + const response = await (0, sdk_ts_1.fetchCreditNotes)(fetcher, query); + spinner.stop(); + const creditNotes = response.creditNotes; + if (!creditNotes || creditNotes.length === 0) { + console.log(chalk_1.default.yellow('No credit notes found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total credit notes)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'CN #', 'Customer', 'Date', 'Total', 'Balance', 'Status']); + creditNotes.forEach((cn) => { + table.push([ + cn.id, + cn.creditNoteNumber || '-', + (0, table_1.truncate)(cn.customer?.displayName, 20), + (0, table_1.formatDate)(cn.creditNoteDate), + (0, table_1.formatCurrency)(cn.total), + (0, table_1.formatCurrency)(cn.balance), + (0, table_1.formatStatus)(cn.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/customers.d.ts b/packages/cli/dist/commands/customers.d.ts new file mode 100644 index 000000000..ba0aded6e --- /dev/null +++ b/packages/cli/dist/commands/customers.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createCustomersCommand(): Command; diff --git a/packages/cli/dist/commands/customers.js b/packages/cli/dist/commands/customers.js new file mode 100644 index 000000000..ad85dd3de --- /dev/null +++ b/packages/cli/dist/commands/customers.js @@ -0,0 +1,72 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createCustomersCommand = createCustomersCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createCustomersCommand() { + const command = new commander_1.Command('customers') + .description('Manage customers'); + command + .command('list') + .description('List all customers') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('--active-only', 'Show only active customers', false) + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading customers...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.activeOnly && { active: true }), + }; + const response = await (0, sdk_ts_1.fetchCustomers)(fetcher, query); + spinner.stop(); + const customers = response.customers; + if (!customers || customers.length === 0) { + console.log(chalk_1.default.yellow('No customers found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total customers)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Name', 'Email', 'Work Phone', 'Personal Phone', 'Balance', 'Status']); + customers.forEach((customer) => { + table.push([ + customer.id, + (0, table_1.truncate)(customer.displayName, 25), + (0, table_1.truncate)(customer.email, 25) || '-', + customer.workPhone || '-', + customer.personalPhone || '-', + (0, table_1.formatCurrency)(customer.balance), + (0, table_1.formatStatus)(customer.active ? 'active' : 'inactive'), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/estimates.d.ts b/packages/cli/dist/commands/estimates.d.ts new file mode 100644 index 000000000..715612ddf --- /dev/null +++ b/packages/cli/dist/commands/estimates.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createEstimatesCommand(): Command; diff --git a/packages/cli/dist/commands/estimates.js b/packages/cli/dist/commands/estimates.js new file mode 100644 index 000000000..acce8bcb6 --- /dev/null +++ b/packages/cli/dist/commands/estimates.js @@ -0,0 +1,74 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createEstimatesCommand = createEstimatesCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createEstimatesCommand() { + const command = new commander_1.Command('estimates') + .description('Manage sale estimates/quotes'); + command + .command('list') + .description('List all sale estimates') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-c, --customer ', 'Filter by customer ID') + .option('-s, --status ', 'Filter by status (draft, published)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading estimates...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.customer && { customerId: parseInt(options.customer, 10) }), + ...(options.status && { status: options.status }), + }; + const response = await (0, sdk_ts_1.fetchSaleEstimates)(fetcher, query); + spinner.stop(); + const estimates = response.saleEstimates; + if (!estimates || estimates.length === 0) { + console.log(chalk_1.default.yellow('No estimates found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total estimates)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Estimate #', 'Customer', 'Date', 'Expires', 'Total', 'Status']); + estimates.forEach((estimate) => { + table.push([ + estimate.id, + estimate.estimateNumber || '-', + (0, table_1.truncate)(estimate.customer?.displayName, 20), + (0, table_1.formatDate)(estimate.estimateDate), + (0, table_1.formatDate)(estimate.expirationDate), + (0, table_1.formatCurrency)(estimate.total), + (0, table_1.formatStatus)(estimate.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/expenses.d.ts b/packages/cli/dist/commands/expenses.d.ts new file mode 100644 index 000000000..83267150a --- /dev/null +++ b/packages/cli/dist/commands/expenses.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createExpensesCommand(): Command; diff --git a/packages/cli/dist/commands/expenses.js b/packages/cli/dist/commands/expenses.js new file mode 100644 index 000000000..4bc01539e --- /dev/null +++ b/packages/cli/dist/commands/expenses.js @@ -0,0 +1,70 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createExpensesCommand = createExpensesCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createExpensesCommand() { + const command = new commander_1.Command('expenses') + .description('Manage expenses'); + command + .command('list') + .description('List all expenses') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('--active-only', 'Show only active expenses', false) + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading expenses...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.activeOnly && { active: true }), + }; + const response = await (0, sdk_ts_1.fetchExpenses)(fetcher, query); + spinner.stop(); + const expenses = response.expenses; + if (!expenses || expenses.length === 0) { + console.log(chalk_1.default.yellow('No expenses found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total expenses)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Date', 'Description', 'Total', 'Status']); + expenses.forEach((expense) => { + table.push([ + expense.id, + (0, table_1.formatDate)(expense.paymentDate), + (0, table_1.truncate)(expense.description, 35), + (0, table_1.formatCurrency)(expense.total), + (0, table_1.formatStatus)(expense.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/inventory.d.ts b/packages/cli/dist/commands/inventory.d.ts new file mode 100644 index 000000000..2c363c5c2 --- /dev/null +++ b/packages/cli/dist/commands/inventory.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createInventoryCommand(): Command; diff --git a/packages/cli/dist/commands/inventory.js b/packages/cli/dist/commands/inventory.js new file mode 100644 index 000000000..b1ea6e554 --- /dev/null +++ b/packages/cli/dist/commands/inventory.js @@ -0,0 +1,119 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createInventoryCommand = createInventoryCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createInventoryCommand() { + const command = new commander_1.Command('inventory') + .description('Manage inventory'); + command + .command('adjustments') + .description('List inventory adjustments') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading inventory adjustments...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + }; + const response = await (0, sdk_ts_1.fetchInventoryAdjustments)(fetcher, query); + spinner.stop(); + const adjustments = response.inventoryAdjustments; + if (!adjustments || adjustments.length === 0) { + console.log(chalk_1.default.yellow('No inventory adjustments found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total adjustments)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Date', 'Reference', 'Reason', 'Status']); + adjustments.forEach((adj) => { + table.push([ + adj.id, + (0, table_1.formatDate)(adj.adjustmentDate), + adj.referenceNo || '-', + (0, table_1.truncate)(adj.reason, 30), + (0, table_1.formatStatus)(adj.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + command + .command('transfers') + .description('List warehouse transfers') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading warehouse transfers...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + }; + const response = await (0, sdk_ts_1.fetchWarehouseTransfers)(fetcher, query); + spinner.stop(); + const transfers = response.warehouseTransfers; + if (!transfers || transfers.length === 0) { + console.log(chalk_1.default.yellow('No warehouse transfers found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total transfers)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Date', 'Reference', 'From', 'To', 'Status']); + transfers.forEach((transfer) => { + table.push([ + transfer.id, + (0, table_1.formatDate)(transfer.date), + transfer.referenceNo || '-', + (0, table_1.truncate)(transfer.fromWarehouse?.name, 15), + (0, table_1.truncate)(transfer.toWarehouse?.name, 15), + (0, table_1.formatStatus)(transfer.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/invoices.d.ts b/packages/cli/dist/commands/invoices.d.ts new file mode 100644 index 000000000..56f18393a --- /dev/null +++ b/packages/cli/dist/commands/invoices.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createInvoicesCommand(): Command; diff --git a/packages/cli/dist/commands/invoices.js b/packages/cli/dist/commands/invoices.js new file mode 100644 index 000000000..5b1b6c9a5 --- /dev/null +++ b/packages/cli/dist/commands/invoices.js @@ -0,0 +1,77 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createInvoicesCommand = createInvoicesCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createInvoicesCommand() { + const command = new commander_1.Command('invoices') + .description('Manage sale invoices'); + command + .command('list') + .description('List all sale invoices') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-c, --customer ', 'Filter by customer ID') + .option('-s, --status ', 'Filter by status (draft, published, paid, partial, overdue)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading invoices...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.customer && { customerId: parseInt(options.customer, 10) }), + ...(options.status && { status: options.status }), + }; + const response = await (0, sdk_ts_1.fetchSaleInvoices)(fetcher, query); + spinner.stop(); + // The response has 'salesInvoices' array (camelCase from middleware), not 'data' + const invoices = response.salesInvoices; + if (!invoices || invoices.length === 0) { + console.log(chalk_1.default.yellow('No invoices found.')); + return; + } + // Pagination info + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total invoices)\n`)); + } + // Create table + const table = (0, table_1.createTable)(['ID', 'Invoice #', 'Customer', 'Date', 'Total', 'Balance', 'Status']); + invoices.forEach((invoice) => { + table.push([ + invoice.id, + invoice.invoiceNo || '-', + (0, table_1.truncate)(invoice.customer?.displayName, 25), + (0, table_1.formatDate)(invoice.invoiceDate), + (0, table_1.formatCurrency)(invoice.total), + (0, table_1.formatCurrency)(invoice.balance), + (0, table_1.formatStatus)(invoice.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/items.d.ts b/packages/cli/dist/commands/items.d.ts new file mode 100644 index 000000000..03a446923 --- /dev/null +++ b/packages/cli/dist/commands/items.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createItemsCommand(): Command; diff --git a/packages/cli/dist/commands/items.js b/packages/cli/dist/commands/items.js new file mode 100644 index 000000000..0c97340f5 --- /dev/null +++ b/packages/cli/dist/commands/items.js @@ -0,0 +1,77 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createItemsCommand = createItemsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createItemsCommand() { + const command = new commander_1.Command('items') + .description('Manage items and products'); + command + .command('list') + .description('List all items/products') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-t, --type ', 'Filter by item type (inventory, service, product)') + .option('--active-only', 'Show only active items', false) + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading items...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.type && { type: options.type }), + ...(options.activeOnly && { active: true }), + }; + const response = await (0, sdk_ts_1.fetchItems)(fetcher, query); + spinner.stop(); + // The response has 'items' array, not 'data' + const items = response.items; + if (!items || items.length === 0) { + console.log(chalk_1.default.yellow('No items found.')); + return; + } + // Pagination info + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total items)\n`)); + } + // Create table + const table = (0, table_1.createTable)(['ID', 'Name', 'Type', 'Sell Price', 'Cost Price', 'Qty', 'Status']); + items.forEach((item) => { + table.push([ + item.id, + (0, table_1.truncate)(item.name, 30), + item.type || '-', + (0, table_1.formatCurrency)(item.sellPrice), + (0, table_1.formatCurrency)(item.costPrice), + item.quantityOnHand ?? '-', + (0, table_1.formatStatus)(item.active ? 'active' : 'inactive'), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/journals.d.ts b/packages/cli/dist/commands/journals.d.ts new file mode 100644 index 000000000..1a8cbce51 --- /dev/null +++ b/packages/cli/dist/commands/journals.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createJournalsCommand(): Command; diff --git a/packages/cli/dist/commands/journals.js b/packages/cli/dist/commands/journals.js new file mode 100644 index 000000000..29f30ce2d --- /dev/null +++ b/packages/cli/dist/commands/journals.js @@ -0,0 +1,66 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createJournalsCommand = createJournalsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createJournalsCommand() { + const command = new commander_1.Command('journals') + .description('Manage manual journals'); + command + .command('list') + .description('List all manual journals') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading journals...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const response = await (0, sdk_ts_1.fetchManualJournals)(fetcher, {}); + spinner.stop(); + const journals = response.manualJournals; + if (!journals || journals.length === 0) { + console.log(chalk_1.default.yellow('No manual journals found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total journals)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Journal #', 'Date', 'Reference', 'Description', 'Amount', 'Status']); + journals.forEach((journal) => { + table.push([ + journal.id, + journal.journalNumber || '-', + (0, table_1.formatDate)(journal.date), + journal.reference || '-', + (0, table_1.truncate)(journal.description, 25), + journal.amount?.toFixed(2) || '-', + (0, table_1.formatStatus)(journal.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/payments.d.ts b/packages/cli/dist/commands/payments.d.ts new file mode 100644 index 000000000..78230b0c3 --- /dev/null +++ b/packages/cli/dist/commands/payments.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createPaymentsCommand(): Command; diff --git a/packages/cli/dist/commands/payments.js b/packages/cli/dist/commands/payments.js new file mode 100644 index 000000000..e06764ad6 --- /dev/null +++ b/packages/cli/dist/commands/payments.js @@ -0,0 +1,80 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createPaymentsCommand = createPaymentsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createPaymentsCommand() { + const command = new commander_1.Command('payments') + .description('Manage payments'); + command + .command('received') + .description('List payments received from customers') + .action(async () => { + const spinner = (0, ora_1.default)('Loading payments received...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const response = await (0, sdk_ts_1.fetchPaymentsReceived)(fetcher); + spinner.stop(); + const payments = response.paymentsReceived; + if (!payments || payments.length === 0) { + console.log(chalk_1.default.yellow('No payments received found.')); + return; + } + const table = (0, table_1.createTable)(['ID', 'Customer', 'Date', 'Amount', 'Reference']); + payments.forEach((payment) => { + table.push([ + payment.id, + (0, table_1.truncate)(payment.customer?.displayName, 25), + (0, table_1.formatDate)(payment.paymentDate), + (0, table_1.formatCurrency)(payment.amount), + payment.referenceNo || '-', + ]); + }); + console.log(table.toString()); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + command + .command('made') + .description('List bill payments made to vendors') + .action(async () => { + const spinner = (0, ora_1.default)('Loading payments made...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const response = await (0, sdk_ts_1.fetchBillPayments)(fetcher); + spinner.stop(); + const payments = response.billPayments; + if (!payments || payments.length === 0) { + console.log(chalk_1.default.yellow('No payments made found.')); + return; + } + const table = (0, table_1.createTable)(['ID', 'Vendor', 'Date', 'Amount', 'Reference']); + payments.forEach((payment) => { + table.push([ + payment.id, + (0, table_1.truncate)(payment.vendor?.displayName, 25), + (0, table_1.formatDate)(payment.paymentDate), + (0, table_1.formatCurrency)(payment.amount), + payment.referenceNo || '-', + ]); + }); + console.log(table.toString()); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/receipts.d.ts b/packages/cli/dist/commands/receipts.d.ts new file mode 100644 index 000000000..b2e710c61 --- /dev/null +++ b/packages/cli/dist/commands/receipts.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createReceiptsCommand(): Command; diff --git a/packages/cli/dist/commands/receipts.js b/packages/cli/dist/commands/receipts.js new file mode 100644 index 000000000..dfe0b8bb7 --- /dev/null +++ b/packages/cli/dist/commands/receipts.js @@ -0,0 +1,71 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createReceiptsCommand = createReceiptsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createReceiptsCommand() { + const command = new commander_1.Command('receipts') + .description('Manage sale receipts'); + command + .command('list') + .description('List all sale receipts') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-c, --customer ', 'Filter by customer ID') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading receipts...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.customer && { customerId: parseInt(options.customer, 10) }), + }; + const response = await (0, sdk_ts_1.fetchSaleReceipts)(fetcher, query); + spinner.stop(); + const receipts = response.saleReceipts; + if (!receipts || receipts.length === 0) { + console.log(chalk_1.default.yellow('No receipts found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total receipts)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Receipt #', 'Customer', 'Date', 'Total', 'Status']); + receipts.forEach((receipt) => { + table.push([ + receipt.id, + receipt.receiptNumber || '-', + (0, table_1.truncate)(receipt.customer?.displayName, 20), + (0, table_1.formatDate)(receipt.receiptDate), + (0, table_1.formatCurrency)(receipt.total), + (0, table_1.formatStatus)(receipt.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/reports.d.ts b/packages/cli/dist/commands/reports.d.ts new file mode 100644 index 000000000..ce737cc29 --- /dev/null +++ b/packages/cli/dist/commands/reports.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createReportsCommand(): Command; diff --git a/packages/cli/dist/commands/reports.js b/packages/cli/dist/commands/reports.js new file mode 100644 index 000000000..09cdfd259 --- /dev/null +++ b/packages/cli/dist/commands/reports.js @@ -0,0 +1,448 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createReportsCommand = createReportsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function getDateRange(options) { + const toDate = options.to || new Date().toISOString().split('T')[0]; + const fromDate = options.from || new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + return { fromDate, toDate }; +} +function createReportsCommand() { + const command = new commander_1.Command('reports') + .description('Financial reports'); + // Balance Sheet + command + .command('balance-sheet') + .description('Show balance sheet') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading balance sheet...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchBalanceSheetJson)(fetcher, { fromDate, toDate }); + spinner.stop(); + console.log(chalk_1.default.bold('\n📊 Balance Sheet')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + // Assets + console.log(chalk_1.default.green.bold('\nASSETS')); + const assetsTable = (0, table_1.createTable)(['Account', 'Amount']); + let totalAssets = 0; + report.assets?.forEach((item) => { + assetsTable.push([item.accountName, (0, table_1.formatCurrency)(item.total)]); + totalAssets += item.total; + }); + assetsTable.push(['', '']); + assetsTable.push([chalk_1.default.bold('Total Assets'), chalk_1.default.bold((0, table_1.formatCurrency)(totalAssets))]); + console.log(assetsTable.toString()); + // Liabilities + console.log(chalk_1.default.red.bold('\nLIABILITIES')); + const liabilitiesTable = (0, table_1.createTable)(['Account', 'Amount']); + let totalLiabilities = 0; + report.liabilities?.forEach((item) => { + liabilitiesTable.push([item.accountName, (0, table_1.formatCurrency)(item.total)]); + totalLiabilities += item.total; + }); + liabilitiesTable.push(['', '']); + liabilitiesTable.push([chalk_1.default.bold('Total Liabilities'), chalk_1.default.bold((0, table_1.formatCurrency)(totalLiabilities))]); + console.log(liabilitiesTable.toString()); + // Equity + console.log(chalk_1.default.blue.bold('\nEQUITY')); + const equityTable = (0, table_1.createTable)(['Account', 'Amount']); + let totalEquity = 0; + report.equity?.forEach((item) => { + equityTable.push([item.accountName, (0, table_1.formatCurrency)(item.total)]); + totalEquity += item.total; + }); + equityTable.push(['', '']); + equityTable.push([chalk_1.default.bold('Total Equity'), chalk_1.default.bold((0, table_1.formatCurrency)(totalEquity))]); + console.log(equityTable.toString()); + // Summary + console.log(chalk_1.default.bold('\n' + '─'.repeat(40))); + console.log(`${chalk_1.default.bold('Total Liabilities + Equity:')} ${(0, table_1.formatCurrency)(totalLiabilities + totalEquity)}`); + console.log(`${chalk_1.default.bold('Balance:')} ${totalAssets === (totalLiabilities + totalEquity) ? chalk_1.default.green('✓ Balanced') : chalk_1.default.red('✗ Unbalanced')}\n`); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Profit & Loss + command + .command('profit-loss') + .alias('pl') + .description('Show profit and loss statement') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading profit & loss...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchProfitLossJson)(fetcher, { fromDate, toDate }); + spinner.stop(); + console.log(chalk_1.default.bold('\n📈 Profit & Loss Statement')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + const income = report.income?.total || 0; + const expenses = report.expenses?.total || 0; + const netProfit = income - expenses; + const table = (0, table_1.createTable)(['', 'Amount']); + table.push([chalk_1.default.green('Total Income'), (0, table_1.formatCurrency)(income)]); + table.push([chalk_1.default.red('Total Expenses'), (0, table_1.formatCurrency)(expenses)]); + table.push(['', '']); + table.push([netProfit >= 0 ? chalk_1.default.green.bold('Net Profit') : chalk_1.default.red.bold('Net Loss'), (0, table_1.formatCurrency)(Math.abs(netProfit))]); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Cashflow + command + .command('cashflow') + .description('Show cashflow statement') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading cashflow statement...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchCashflowStatementJson)(fetcher, { fromDate, toDate }); + spinner.stop(); + console.log(chalk_1.default.bold('\n💰 Cashflow Statement')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + const operating = report.operating?.total || 0; + const investing = report.investing?.total || 0; + const financing = report.financing?.total || 0; + const netCash = operating + investing + financing; + const table = (0, table_1.createTable)(['Activity', 'Amount']); + table.push(['Operating Activities', (0, table_1.formatCurrency)(operating)]); + table.push(['Investing Activities', (0, table_1.formatCurrency)(investing)]); + table.push(['Financing Activities', (0, table_1.formatCurrency)(financing)]); + table.push(['', '']); + table.push([chalk_1.default.bold('Net Cash Change'), chalk_1.default.bold((0, table_1.formatCurrency)(netCash))]); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Trial Balance + command + .command('trial-balance') + .description('Show trial balance') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading trial balance...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchTrialBalanceJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n⚖️ Trial Balance')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + const table = (0, table_1.createTable)(['Account', 'Debit', 'Credit']); + let totalDebit = 0; + let totalCredit = 0; + report.data?.forEach((item) => { + table.push([item.accountName, (0, table_1.formatCurrency)(item.debit), (0, table_1.formatCurrency)(item.credit)]); + totalDebit += item.debit; + totalCredit += item.credit; + }); + table.push(['', '', '']); + table.push([chalk_1.default.bold('Total'), chalk_1.default.bold((0, table_1.formatCurrency)(totalDebit)), chalk_1.default.bold((0, table_1.formatCurrency)(totalCredit))]); + console.log(table.toString()); + console.log(`\n${chalk_1.default.bold('Balance:')} ${Math.abs(totalDebit - totalCredit) < 0.01 ? chalk_1.default.green('✓ Balanced') : chalk_1.default.red('✗ Unbalanced')}\n`); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // General Ledger + command + .command('general-ledger') + .description('Show general ledger') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading general ledger...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchGeneralLedgerJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n📒 General Ledger')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + console.log(JSON.stringify(report, null, 2)); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Journal + command + .command('journal') + .description('Show journal report') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading journal...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchJournalJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n📝 Journal Report')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + const table = (0, table_1.createTable)(['Date', 'Reference', 'Account', 'Debit', 'Credit']); + report.data?.forEach((item) => { + table.push([item.date, item.reference || '-', item.accountName, (0, table_1.formatCurrency)(item.debit), (0, table_1.formatCurrency)(item.credit)]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Receivable Aging + command + .command('receivable-aging') + .description('Show accounts receivable aging') + .action(async () => { + const spinner = (0, ora_1.default)('Loading receivable aging...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const report = await (0, sdk_ts_1.fetchReceivableAgingJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n📋 Accounts Receivable Aging\n')); + const table = (0, table_1.createTable)(['Customer', 'Current', '1-30', '31-60', '61-90', '90+', 'Total']); + report.data?.forEach((item) => { + table.push([ + item.customerName, + (0, table_1.formatCurrency)(item.current), + (0, table_1.formatCurrency)(item.days1to30), + (0, table_1.formatCurrency)(item.days31to60), + (0, table_1.formatCurrency)(item.days61to90), + (0, table_1.formatCurrency)(item.over90), + chalk_1.default.bold((0, table_1.formatCurrency)(item.total)), + ]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Payable Aging + command + .command('payable-aging') + .description('Show accounts payable aging') + .action(async () => { + const spinner = (0, ora_1.default)('Loading payable aging...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const report = await (0, sdk_ts_1.fetchPayableAgingJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n📋 Accounts Payable Aging\n')); + const table = (0, table_1.createTable)(['Vendor', 'Current', '1-30', '31-60', '61-90', '90+', 'Total']); + report.data?.forEach((item) => { + table.push([ + item.vendorName, + (0, table_1.formatCurrency)(item.current), + (0, table_1.formatCurrency)(item.days1to30), + (0, table_1.formatCurrency)(item.days31to60), + (0, table_1.formatCurrency)(item.days61to90), + (0, table_1.formatCurrency)(item.over90), + chalk_1.default.bold((0, table_1.formatCurrency)(item.total)), + ]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Customer Balance + command + .command('customer-balance') + .description('Show customer balance summary') + .action(async () => { + const spinner = (0, ora_1.default)('Loading customer balances...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const report = await (0, sdk_ts_1.fetchCustomerBalanceJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n👤 Customer Balance Summary\n')); + const table = (0, table_1.createTable)(['Customer', 'Total']); + report.data?.forEach((item) => { + table.push([item.customerName, (0, table_1.formatCurrency)(item.total)]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Vendor Balance + command + .command('vendor-balance') + .description('Show vendor balance summary') + .action(async () => { + const spinner = (0, ora_1.default)('Loading vendor balances...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const report = await (0, sdk_ts_1.fetchVendorBalanceJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n🏢 Vendor Balance Summary\n')); + const table = (0, table_1.createTable)(['Vendor', 'Total']); + report.data?.forEach((item) => { + table.push([item.vendorName, (0, table_1.formatCurrency)(item.total)]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Sales by Items + command + .command('sales-by-items') + .description('Show sales by items report') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading sales by items...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchSalesByItemsJson)(fetcher, { fromDate, toDate }); + spinner.stop(); + console.log(chalk_1.default.bold('\n🛍️ Sales by Items')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + const table = (0, table_1.createTable)(['Item', 'Quantity', 'Amount']); + report.data?.forEach((item) => { + table.push([item.itemName, item.quantity.toString(), (0, table_1.formatCurrency)(item.amount)]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Purchases by Items + command + .command('purchases-by-items') + .description('Show purchases by items report') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading purchases by items...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchPurchasesByItemsJson)(fetcher, { fromDate, toDate }); + spinner.stop(); + console.log(chalk_1.default.bold('\n🛒 Purchases by Items')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + const table = (0, table_1.createTable)(['Item', 'Quantity', 'Amount']); + report.data?.forEach((item) => { + table.push([item.itemName, item.quantity.toString(), (0, table_1.formatCurrency)(item.amount)]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Inventory Valuation + command + .command('inventory-valuation') + .description('Show inventory valuation summary') + .action(async () => { + const spinner = (0, ora_1.default)('Loading inventory valuation...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const report = await (0, sdk_ts_1.fetchInventoryValuationJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n📦 Inventory Valuation\n')); + const table = (0, table_1.createTable)(['Item', 'Qty on Hand', 'Avg Cost', 'Total Value']); + report.data?.forEach((item) => { + table.push([item.itemName, item.quantityOnHand.toString(), (0, table_1.formatCurrency)(item.averageCost), (0, table_1.formatCurrency)(item.totalValue)]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Inventory Details + command + .command('inventory-details') + .description('Show inventory item details') + .option('--item ', 'Filter by item ID') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading inventory details...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const report = await (0, sdk_ts_1.fetchInventoryItemDetailsJson)(fetcher, {}); + spinner.stop(); + console.log(chalk_1.default.bold('\n📦 Inventory Item Details\n')); + console.log(JSON.stringify(report, null, 2)); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + // Sales Tax Liability + command + .command('sales-tax-liability') + .description('Show sales tax liability report') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading sales tax liability...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const { fromDate, toDate } = getDateRange(options); + const report = await (0, sdk_ts_1.fetchSalesTaxLiabilityJson)(fetcher, { fromDate, toDate }); + spinner.stop(); + console.log(chalk_1.default.bold('\n💵 Sales Tax Liability')); + console.log(chalk_1.default.gray(`${fromDate} to ${toDate}\n`)); + const table = (0, table_1.createTable)(['Tax Rate', 'Taxable Amount', 'Tax Amount']); + report.data?.forEach((item) => { + table.push([item.taxRateName, (0, table_1.formatCurrency)(item.taxableAmount), (0, table_1.formatCurrency)(item.taxAmount)]); + }); + console.log(table.toString() + '\n'); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/tax-rates.d.ts b/packages/cli/dist/commands/tax-rates.d.ts new file mode 100644 index 000000000..6b221c44e --- /dev/null +++ b/packages/cli/dist/commands/tax-rates.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createTaxRatesCommand(): Command; diff --git a/packages/cli/dist/commands/tax-rates.js b/packages/cli/dist/commands/tax-rates.js new file mode 100644 index 000000000..85bd69e93 --- /dev/null +++ b/packages/cli/dist/commands/tax-rates.js @@ -0,0 +1,49 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createTaxRatesCommand = createTaxRatesCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createTaxRatesCommand() { + const command = new commander_1.Command('tax-rates') + .description('Manage tax rates'); + command + .command('list') + .description('List all tax rates') + .action(async () => { + const spinner = (0, ora_1.default)('Loading tax rates...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const response = await (0, sdk_ts_1.fetchTaxRates)(fetcher); + spinner.stop(); + const taxRates = response.taxRates; + if (!taxRates || taxRates.length === 0) { + console.log(chalk_1.default.yellow('No tax rates found.')); + return; + } + const table = (0, table_1.createTable)(['ID', 'Name', 'Code', 'Rate', 'Status']); + taxRates.forEach((tax) => { + table.push([ + tax.id, + tax.name || '-', + tax.code || '-', + `${tax.rate || 0}%`, + (0, table_1.formatStatus)(tax.active ? 'active' : 'inactive'), + ]); + }); + console.log(table.toString()); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/users.d.ts b/packages/cli/dist/commands/users.d.ts new file mode 100644 index 000000000..30adf6a28 --- /dev/null +++ b/packages/cli/dist/commands/users.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createUsersCommand(): Command; diff --git a/packages/cli/dist/commands/users.js b/packages/cli/dist/commands/users.js new file mode 100644 index 000000000..541e1d612 --- /dev/null +++ b/packages/cli/dist/commands/users.js @@ -0,0 +1,80 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createUsersCommand = createUsersCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createUsersCommand() { + const command = new commander_1.Command('users') + .description('Manage users and roles'); + command + .command('list') + .description('List all users') + .action(async () => { + const spinner = (0, ora_1.default)('Loading users...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const response = await (0, sdk_ts_1.fetchUsers)(fetcher); + spinner.stop(); + const users = response.users; + if (!users || users.length === 0) { + console.log(chalk_1.default.yellow('No users found.')); + return; + } + const table = (0, table_1.createTable)(['ID', 'Name', 'Email', 'Role', 'Status']); + users.forEach((user) => { + const name = `${user.firstName || ''} ${user.lastName || ''}`.trim() || '-'; + table.push([ + user.id, + name, + user.email || '-', + user.role?.name || '-', + (0, table_1.formatStatus)(user.active ? 'active' : 'inactive'), + ]); + }); + console.log(table.toString()); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + command + .command('roles') + .description('List all roles') + .action(async () => { + const spinner = (0, ora_1.default)('Loading roles...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const response = await (0, sdk_ts_1.fetchRoles)(fetcher); + spinner.stop(); + const roles = response.roles; + if (!roles || roles.length === 0) { + console.log(chalk_1.default.yellow('No roles found.')); + return; + } + const table = (0, table_1.createTable)(['ID', 'Name', 'Slug', 'Description']); + roles.forEach((role) => { + table.push([ + role.id, + role.name || '-', + role.slug || '-', + role.description || '-', + ]); + }); + console.log(table.toString()); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/vendor-credits.d.ts b/packages/cli/dist/commands/vendor-credits.d.ts new file mode 100644 index 000000000..a0d344454 --- /dev/null +++ b/packages/cli/dist/commands/vendor-credits.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createVendorCreditsCommand(): Command; diff --git a/packages/cli/dist/commands/vendor-credits.js b/packages/cli/dist/commands/vendor-credits.js new file mode 100644 index 000000000..ba52fa980 --- /dev/null +++ b/packages/cli/dist/commands/vendor-credits.js @@ -0,0 +1,72 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createVendorCreditsCommand = createVendorCreditsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createVendorCreditsCommand() { + const command = new commander_1.Command('vendor-credits') + .description('Manage vendor credits'); + command + .command('list') + .description('List all vendor credits') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-v, --vendor ', 'Filter by vendor ID') + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading vendor credits...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.vendor && { vendorId: parseInt(options.vendor, 10) }), + }; + const response = await (0, sdk_ts_1.fetchVendorCredits)(fetcher, query); + spinner.stop(); + const vendorCredits = response.vendorCredits; + if (!vendorCredits || vendorCredits.length === 0) { + console.log(chalk_1.default.yellow('No vendor credits found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total vendor credits)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'VC #', 'Vendor', 'Date', 'Total', 'Balance', 'Status']); + vendorCredits.forEach((vc) => { + table.push([ + vc.id, + vc.vendorCreditNumber || '-', + (0, table_1.truncate)(vc.vendor?.displayName, 20), + (0, table_1.formatDate)(vc.vendorCreditDate), + (0, table_1.formatCurrency)(vc.total), + (0, table_1.formatCurrency)(vc.balance), + (0, table_1.formatStatus)(vc.status), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/vendors.d.ts b/packages/cli/dist/commands/vendors.d.ts new file mode 100644 index 000000000..bd8059b43 --- /dev/null +++ b/packages/cli/dist/commands/vendors.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createVendorsCommand(): Command; diff --git a/packages/cli/dist/commands/vendors.js b/packages/cli/dist/commands/vendors.js new file mode 100644 index 000000000..6d24a07a5 --- /dev/null +++ b/packages/cli/dist/commands/vendors.js @@ -0,0 +1,72 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createVendorsCommand = createVendorsCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createVendorsCommand() { + const command = new commander_1.Command('vendors') + .description('Manage vendors'); + command + .command('list') + .description('List all vendors') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('--active-only', 'Show only active vendors', false) + .action(async (options) => { + const spinner = (0, ora_1.default)('Loading vendors...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.activeOnly && { active: true }), + }; + const response = await (0, sdk_ts_1.fetchVendors)(fetcher, query); + spinner.stop(); + const vendors = response.vendors; + if (!vendors || vendors.length === 0) { + console.log(chalk_1.default.yellow('No vendors found.')); + return; + } + const pagination = response.pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk_1.default.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total vendors)\n`)); + } + const table = (0, table_1.createTable)(['ID', 'Name', 'Email', 'Work Phone', 'Personal Phone', 'Balance', 'Status']); + vendors.forEach((vendor) => { + table.push([ + vendor.id, + (0, table_1.truncate)(vendor.displayName, 25), + (0, table_1.truncate)(vendor.email, 25) || '-', + vendor.workPhone || '-', + vendor.personalPhone || '-', + (0, table_1.formatCurrency)(vendor.balance), + (0, table_1.formatStatus)(vendor.active ? 'active' : 'inactive'), + ]); + }); + console.log(table.toString()); + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk_1.default.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/commands/warehouses.d.ts b/packages/cli/dist/commands/warehouses.d.ts new file mode 100644 index 000000000..660a42242 --- /dev/null +++ b/packages/cli/dist/commands/warehouses.d.ts @@ -0,0 +1,2 @@ +import { Command } from 'commander'; +export declare function createWarehousesCommand(): Command; diff --git a/packages/cli/dist/commands/warehouses.js b/packages/cli/dist/commands/warehouses.js new file mode 100644 index 000000000..26715f1e4 --- /dev/null +++ b/packages/cli/dist/commands/warehouses.js @@ -0,0 +1,49 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createWarehousesCommand = createWarehousesCommand; +const commander_1 = require("commander"); +const chalk_1 = __importDefault(require("chalk")); +const ora_1 = __importDefault(require("ora")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config_1 = require("../config"); +const errors_1 = require("../utils/errors"); +const table_1 = require("../utils/table"); +function createWarehousesCommand() { + const command = new commander_1.Command('warehouses') + .description('Manage warehouses'); + command + .command('list') + .description('List all warehouses') + .action(async () => { + const spinner = (0, ora_1.default)('Loading warehouses...').start(); + try { + const fetcher = (0, config_1.createAuthenticatedFetcher)(); + const response = await (0, sdk_ts_1.fetchWarehouses)(fetcher); + spinner.stop(); + const warehouses = response.warehouses; + if (!warehouses || warehouses.length === 0) { + console.log(chalk_1.default.yellow('No warehouses found.')); + return; + } + const table = (0, table_1.createTable)(['ID', 'Code', 'Name', 'City', 'Status']); + warehouses.forEach((warehouse) => { + table.push([ + warehouse.id, + warehouse.code || '-', + warehouse.name || '-', + warehouse.city || '-', + (0, table_1.formatStatus)(warehouse.active ? 'active' : 'inactive'), + ]); + }); + console.log(table.toString()); + } + catch (error) { + spinner.stop(); + (0, errors_1.handleError)(error); + } + }); + return command; +} diff --git a/packages/cli/dist/config.d.ts b/packages/cli/dist/config.d.ts new file mode 100644 index 000000000..cc01c767c --- /dev/null +++ b/packages/cli/dist/config.d.ts @@ -0,0 +1,13 @@ +import { ApiFetcher } from '@bigcapital/sdk-ts'; +interface ConfigSchema { + apiKey: string; + baseUrl: string; + organizationId?: string; +} +export declare function getConfig(): ConfigSchema; +export declare function setApiKey(apiKey: string): void; +export declare function setBaseUrl(baseUrl: string): void; +export declare function setOrganizationId(organizationId: string): void; +export declare function validateConfig(): ConfigSchema; +export declare function createAuthenticatedFetcher(): ApiFetcher; +export {}; diff --git a/packages/cli/dist/config.js b/packages/cli/dist/config.js new file mode 100644 index 000000000..7b8d9227e --- /dev/null +++ b/packages/cli/dist/config.js @@ -0,0 +1,75 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getConfig = getConfig; +exports.setApiKey = setApiKey; +exports.setBaseUrl = setBaseUrl; +exports.setOrganizationId = setOrganizationId; +exports.validateConfig = validateConfig; +exports.createAuthenticatedFetcher = createAuthenticatedFetcher; +const conf_1 = __importDefault(require("conf")); +const chalk_1 = __importDefault(require("chalk")); +const sdk_ts_1 = require("@bigcapital/sdk-ts"); +const config = new conf_1.default({ + projectName: 'bigcapital', + schema: { + apiKey: { + type: 'string', + }, + baseUrl: { + type: 'string', + }, + organizationId: { + type: 'string', + }, + }, +}); +function getConfig() { + return { + apiKey: config.get('apiKey', ''), + baseUrl: config.get('baseUrl', ''), + organizationId: config.get('organizationId'), + }; +} +function setApiKey(apiKey) { + config.set('apiKey', apiKey); +} +function setBaseUrl(baseUrl) { + // Remove trailing slash if present + const normalizedUrl = baseUrl.replace(/\/$/, ''); + config.set('baseUrl', normalizedUrl); +} +function setOrganizationId(organizationId) { + config.set('organizationId', organizationId); +} +function validateConfig() { + const currentConfig = getConfig(); + if (!currentConfig.apiKey) { + console.error(chalk_1.default.red('Error: API key is not configured.')); + console.error(chalk_1.default.yellow('Run: bigcapital config set api-key ')); + process.exit(1); + } + if (!currentConfig.baseUrl) { + console.error(chalk_1.default.red('Error: Base URL is not configured.')); + console.error(chalk_1.default.yellow('Run: bigcapital config set base-url ')); + process.exit(1); + } + return currentConfig; +} +function createAuthenticatedFetcher() { + const currentConfig = validateConfig(); + const headers = { + 'Authorization': `Bearer ${currentConfig.apiKey}`, + }; + if (currentConfig.organizationId) { + headers['organization-id'] = currentConfig.organizationId; + } + return (0, sdk_ts_1.createApiFetcher)({ + baseUrl: currentConfig.baseUrl, + init: { + headers, + }, + }); +} diff --git a/packages/cli/dist/index.d.ts b/packages/cli/dist/index.d.ts new file mode 100644 index 000000000..b7988016d --- /dev/null +++ b/packages/cli/dist/index.d.ts @@ -0,0 +1,2 @@ +#!/usr/bin/env node +export {}; diff --git a/packages/cli/dist/index.js b/packages/cli/dist/index.js new file mode 100755 index 000000000..70825f920 --- /dev/null +++ b/packages/cli/dist/index.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const commander_1 = require("commander"); +const items_1 = require("./commands/items"); +const invoices_1 = require("./commands/invoices"); +const config_1 = require("./commands/config"); +const customers_1 = require("./commands/customers"); +const vendors_1 = require("./commands/vendors"); +const bills_1 = require("./commands/bills"); +const accounts_1 = require("./commands/accounts"); +const expenses_1 = require("./commands/expenses"); +const credit_notes_1 = require("./commands/credit-notes"); +const vendor_credits_1 = require("./commands/vendor-credits"); +const payments_1 = require("./commands/payments"); +const estimates_1 = require("./commands/estimates"); +const receipts_1 = require("./commands/receipts"); +const journals_1 = require("./commands/journals"); +const inventory_1 = require("./commands/inventory"); +const tax_rates_1 = require("./commands/tax-rates"); +const warehouses_1 = require("./commands/warehouses"); +const users_1 = require("./commands/users"); +const reports_1 = require("./commands/reports"); +const chalk_1 = __importDefault(require("chalk")); +const program = new commander_1.Command(); +program + .name('bigcapital') + .description('Bigcapital CLI - Interact with Bigcapital API') + .version('1.0.0') + .configureOutput({ + writeErr: (str) => process.stderr.write(chalk_1.default.red(str)), + outputError: (str, write) => write(chalk_1.default.red(str)), +}); +// Core modules +program.addCommand((0, config_1.createConfigCommand)()); +program.addCommand((0, items_1.createItemsCommand)()); +program.addCommand((0, invoices_1.createInvoicesCommand)()); +program.addCommand((0, customers_1.createCustomersCommand)()); +program.addCommand((0, vendors_1.createVendorsCommand)()); +program.addCommand((0, bills_1.createBillsCommand)()); +// Additional transactional modules +program.addCommand((0, accounts_1.createAccountsCommand)()); +program.addCommand((0, expenses_1.createExpensesCommand)()); +program.addCommand((0, credit_notes_1.createCreditNotesCommand)()); +program.addCommand((0, vendor_credits_1.createVendorCreditsCommand)()); +program.addCommand((0, payments_1.createPaymentsCommand)()); +program.addCommand((0, estimates_1.createEstimatesCommand)()); +program.addCommand((0, receipts_1.createReceiptsCommand)()); +program.addCommand((0, journals_1.createJournalsCommand)()); +program.addCommand((0, inventory_1.createInventoryCommand)()); +program.addCommand((0, tax_rates_1.createTaxRatesCommand)()); +program.addCommand((0, warehouses_1.createWarehousesCommand)()); +program.addCommand((0, users_1.createUsersCommand)()); +// Financial reports +program.addCommand((0, reports_1.createReportsCommand)()); +// Global error handling +program.hook('preAction', () => { + process.on('unhandledRejection', (error) => { + console.error(chalk_1.default.red('\nUnhandled error:'), error); + process.exit(1); + }); + process.on('uncaughtException', (error) => { + console.error(chalk_1.default.red('\nUncaught exception:'), error); + process.exit(1); + }); +}); +// Show help if no command provided +if (process.argv.length <= 2) { + program.help(); +} +program.parse(); diff --git a/packages/cli/dist/utils/errors.d.ts b/packages/cli/dist/utils/errors.d.ts new file mode 100644 index 000000000..a6dd2dbf5 --- /dev/null +++ b/packages/cli/dist/utils/errors.d.ts @@ -0,0 +1,2 @@ +export declare function handleError(error: unknown): never; +export declare function assertDefined(value: T | undefined | null, name: string): T; diff --git a/packages/cli/dist/utils/errors.js b/packages/cli/dist/utils/errors.js new file mode 100644 index 000000000..0cd724cff --- /dev/null +++ b/packages/cli/dist/utils/errors.js @@ -0,0 +1,45 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handleError = handleError; +exports.assertDefined = assertDefined; +const chalk_1 = __importDefault(require("chalk")); +function handleError(error) { + if (error instanceof Error) { + // Check if it's an HTTP error from the SDK + const httpError = error; + if (httpError.status) { + console.error(chalk_1.default.red(`HTTP Error ${httpError.status}: ${httpError.statusText || error.message}`)); + if (httpError.status === 401) { + console.error(chalk_1.default.yellow('Your API key may be invalid or expired.')); + console.error(chalk_1.default.yellow('Run: bigcapital config set api-key ')); + } + else if (httpError.status === 403) { + console.error(chalk_1.default.yellow('You don\'t have permission to access this resource.')); + } + else if (httpError.status === 404) { + console.error(chalk_1.default.yellow('The requested resource was not found.')); + } + } + else { + console.error(chalk_1.default.red(`Error: ${error.message}`)); + } + if (process.env.DEBUG) { + console.error(chalk_1.default.gray(error.stack)); + } + } + else { + console.error(chalk_1.default.red('An unknown error occurred')); + console.error(error); + } + process.exit(1); +} +function assertDefined(value, name) { + if (value === undefined || value === null) { + console.error(chalk_1.default.red(`Error: ${name} is required but was not provided.`)); + process.exit(1); + } + return value; +} diff --git a/packages/cli/dist/utils/table.d.ts b/packages/cli/dist/utils/table.d.ts new file mode 100644 index 000000000..2c980c131 --- /dev/null +++ b/packages/cli/dist/utils/table.d.ts @@ -0,0 +1,6 @@ +import Table from 'cli-table3'; +export declare function createTable(headers: string[]): Table.Table; +export declare function formatCurrency(amount: number | string | undefined, currency?: string): string; +export declare function formatDate(dateStr: string | undefined): string; +export declare function truncate(str: string | undefined, maxLength: number): string; +export declare function formatStatus(status: string | undefined): string; diff --git a/packages/cli/dist/utils/table.js b/packages/cli/dist/utils/table.js new file mode 100644 index 000000000..eb9a2407b --- /dev/null +++ b/packages/cli/dist/utils/table.js @@ -0,0 +1,84 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createTable = createTable; +exports.formatCurrency = formatCurrency; +exports.formatDate = formatDate; +exports.truncate = truncate; +exports.formatStatus = formatStatus; +const cli_table3_1 = __importDefault(require("cli-table3")); +const chalk_1 = __importDefault(require("chalk")); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createTable(headers) { + return new cli_table3_1.default({ + head: headers.map(h => chalk_1.default.cyan.bold(h)), + style: { + head: [], + border: [], + }, + chars: { + top: '─', + 'top-mid': '┬', + 'top-left': '┌', + 'top-right': '┐', + bottom: '─', + 'bottom-mid': '┴', + 'bottom-left': '└', + 'bottom-right': '┘', + left: '│', + 'left-mid': '├', + mid: '─', + 'mid-mid': '┼', + right: '│', + 'right-mid': '┤', + middle: '│', + }, + }); +} +function formatCurrency(amount, currency = '$') { + if (amount === undefined || amount === null) + return '-'; + const num = typeof amount === 'string' ? parseFloat(amount) : amount; + if (isNaN(num)) + return '-'; + return `${currency}${num.toFixed(2)}`; +} +function formatDate(dateStr) { + if (!dateStr) + return '-'; + const date = new Date(dateStr); + if (isNaN(date.getTime())) + return dateStr; + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +} +function truncate(str, maxLength) { + if (!str) + return '-'; + if (str.length <= maxLength) + return str; + return str.substring(0, maxLength - 3) + '...'; +} +function formatStatus(status) { + if (!status) + return '-'; + const statusColors = { + published: chalk_1.default.green, + draft: chalk_1.default.gray, + delivered: chalk_1.default.blue, + partial: chalk_1.default.yellow, + paid: chalk_1.default.green.bold, + unpaid: chalk_1.default.red, + overdue: chalk_1.default.red.bold, + active: chalk_1.default.green, + inactive: chalk_1.default.gray, + }; + const lowerStatus = status.toLowerCase(); + const colorFn = statusColors[lowerStatus] || chalk_1.default.white; + return colorFn(status); +} diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000..1b15c675b --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,38 @@ +{ + "name": "@bigcapital/cli", + "version": "1.0.0", + "description": "Bigcapital CLI - Interact with Bigcapital API", + "main": "./dist/index.js", + "bin": { + "bigcapital": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "bigcapital", + "cli", + "accounting", + "api" + ], + "author": "", + "license": "ISC", + "dependencies": { + "@bigcapital/sdk-ts": "workspace:*", + "chalk": "^4.1.2", + "cli-table3": "^0.6.3", + "commander": "^11.1.0", + "conf": "^10.2.0", + "ora": "^5.4.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.1.3" + }, + "engines": { + "node": ">=16.0.0" + } +} diff --git a/packages/cli/src/commands/accounts.ts b/packages/cli/src/commands/accounts.ts new file mode 100644 index 000000000..2dad7746f --- /dev/null +++ b/packages/cli/src/commands/accounts.ts @@ -0,0 +1,67 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchAccounts } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, truncate, formatStatus } from '../utils/table'; + +export function createAccountsCommand(): Command { + const command = new Command('accounts') + .description('Manage chart of accounts'); + + command + .command('list') + .description('List all accounts') + .option('-t, --type ', 'Filter by account type') + .option('--active-only', 'Show only active accounts', false) + .action(async (options) => { + const spinner = ora('Loading accounts...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + ...(options.type && { type: options.type }), + ...(options.activeOnly && { active: true }), + }; + + const accounts = await fetchAccounts(fetcher, query); + + spinner.stop(); + + if (!accounts || accounts.length === 0) { + console.log(chalk.yellow('No accounts found.')); + return; + } + + const table = createTable(['ID', 'Code', 'Name', 'Type', 'Balance', 'Status']); + + accounts.forEach((account: { + id: number; + code?: string; + name?: string; + accountType?: string; + amount?: number; + active?: boolean; + }) => { + table.push([ + account.id, + account.code || '-', + truncate(account.name, 30), + account.accountType || '-', + formatCurrency(account.amount), + formatStatus(account.active ? 'active' : 'inactive'), + ]); + }); + + console.log(table.toString()); + console.log(chalk.gray(`\nTotal accounts: ${accounts.length}`)); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/bills.ts b/packages/cli/src/commands/bills.ts new file mode 100644 index 000000000..232508584 --- /dev/null +++ b/packages/cli/src/commands/bills.ts @@ -0,0 +1,93 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchBills } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table'; + +export function createBillsCommand(): Command { + const command = new Command('bills') + .description('Manage bills'); + + command + .command('list') + .description('List all bills') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-v, --vendor ', 'Filter by vendor ID') + .option('-s, --status ', 'Filter by status (draft, published, paid, partial)') + .action(async (options) => { + const spinner = ora('Loading bills...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.vendor && { vendorId: parseInt(options.vendor, 10) }), + ...(options.status && { status: options.status }), + }; + + const response = await fetchBills(fetcher, query); + + spinner.stop(); + + const bills = (response as unknown as { bills: Array<{ + id: number; + billNumber?: string; + vendor?: { displayName?: string }; + billDate?: string; + dueDate?: string; + total?: number; + balance?: number; + status?: string; + }> }).bills; + + if (!bills || bills.length === 0) { + console.log(chalk.yellow('No bills found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total bills)\n`)); + } + + const table = createTable(['ID', 'Bill #', 'Vendor', 'Date', 'Due Date', 'Total', 'Balance', 'Status']); + + bills.forEach((bill) => { + table.push([ + bill.id, + bill.billNumber || '-', + truncate(bill.vendor?.displayName, 20), + formatDate(bill.billDate), + formatDate(bill.dueDate), + formatCurrency(bill.total), + formatCurrency(bill.balance), + formatStatus(bill.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/config.ts b/packages/cli/src/commands/config.ts new file mode 100644 index 000000000..9bd9f4c58 --- /dev/null +++ b/packages/cli/src/commands/config.ts @@ -0,0 +1,68 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import { getConfig, setApiKey, setBaseUrl, setOrganizationId } from '../config'; + +export function createConfigCommand(): Command { + const command = new Command('config') + .description('Manage CLI configuration'); + + command + .command('set') + .description('Set a configuration value') + .argument('', 'Configuration key (api-key, base-url, organization-id)') + .argument('', 'Configuration value') + .action((key: string, value: string) => { + switch (key.toLowerCase()) { + case 'api-key': + setApiKey(value); + console.log(chalk.green('✓ API key configured successfully')); + break; + case 'base-url': + setBaseUrl(value); + console.log(chalk.green('✓ Base URL configured successfully')); + break; + case 'organization-id': + setOrganizationId(value); + console.log(chalk.green('✓ Organization ID configured successfully')); + break; + default: + console.error(chalk.red(`Error: Unknown configuration key "${key}"`)); + console.log(chalk.yellow('Valid keys: api-key, base-url, organization-id')); + process.exit(1); + } + }); + + command + .command('get') + .description('Show current configuration') + .action(() => { + const config = getConfig(); + + console.log(chalk.bold('\nBigcapital CLI Configuration:')); + console.log(chalk.gray('─'.repeat(50))); + + if (config.apiKey) { + const maskedKey = config.apiKey.substring(0, 4) + '...' + config.apiKey.substring(config.apiKey.length - 4); + console.log(`API Key: ${chalk.green(maskedKey)}`); + } else { + console.log(`API Key: ${chalk.yellow('Not set')}`); + } + + if (config.baseUrl) { + console.log(`Base URL: ${chalk.green(config.baseUrl)}`); + } else { + console.log(`Base URL: ${chalk.yellow('Not set')}`); + } + + if (config.organizationId) { + console.log(`Organization: ${chalk.green(config.organizationId)}`); + } else { + console.log(`Organization: ${chalk.yellow('Not set')} (optional)`); + } + + console.log(chalk.gray('─'.repeat(50))); + console.log(chalk.gray('\nConfig file location: ~/.config/bigcapital-nodejs/config.json\n')); + }); + + return command; +} diff --git a/packages/cli/src/commands/credit-notes.ts b/packages/cli/src/commands/credit-notes.ts new file mode 100644 index 000000000..24ce41989 --- /dev/null +++ b/packages/cli/src/commands/credit-notes.ts @@ -0,0 +1,89 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchCreditNotes } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table'; + +export function createCreditNotesCommand(): Command { + const command = new Command('credit-notes') + .description('Manage credit notes'); + + command + .command('list') + .description('List all credit notes') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-c, --customer ', 'Filter by customer ID') + .action(async (options) => { + const spinner = ora('Loading credit notes...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.customer && { customerId: parseInt(options.customer, 10) }), + }; + + const response = await fetchCreditNotes(fetcher, query); + + spinner.stop(); + + const creditNotes = (response as unknown as { creditNotes: Array<{ + id: number; + creditNoteNumber?: string; + customer?: { displayName?: string }; + creditNoteDate?: string; + total?: number; + balance?: number; + status?: string; + }> }).creditNotes; + + if (!creditNotes || creditNotes.length === 0) { + console.log(chalk.yellow('No credit notes found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total credit notes)\n`)); + } + + const table = createTable(['ID', 'CN #', 'Customer', 'Date', 'Total', 'Balance', 'Status']); + + creditNotes.forEach((cn) => { + table.push([ + cn.id, + cn.creditNoteNumber || '-', + truncate(cn.customer?.displayName, 20), + formatDate(cn.creditNoteDate), + formatCurrency(cn.total), + formatCurrency(cn.balance), + formatStatus(cn.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/customers.ts b/packages/cli/src/commands/customers.ts new file mode 100644 index 000000000..4657bc299 --- /dev/null +++ b/packages/cli/src/commands/customers.ts @@ -0,0 +1,89 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchCustomers } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, truncate, formatStatus } from '../utils/table'; + +export function createCustomersCommand(): Command { + const command = new Command('customers') + .description('Manage customers'); + + command + .command('list') + .description('List all customers') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('--active-only', 'Show only active customers', false) + .action(async (options) => { + const spinner = ora('Loading customers...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.activeOnly && { active: true }), + }; + + const response = await fetchCustomers(fetcher, query); + + spinner.stop(); + + const customers = (response as unknown as { customers: Array<{ + id: number; + displayName?: string; + email?: string; + workPhone?: string; + personalPhone?: string; + balance?: number; + active?: boolean; + }> }).customers; + + if (!customers || customers.length === 0) { + console.log(chalk.yellow('No customers found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total customers)\n`)); + } + + const table = createTable(['ID', 'Name', 'Email', 'Work Phone', 'Personal Phone', 'Balance', 'Status']); + + customers.forEach((customer) => { + table.push([ + customer.id, + truncate(customer.displayName, 25), + truncate(customer.email, 25) || '-', + customer.workPhone || '-', + customer.personalPhone || '-', + formatCurrency(customer.balance), + formatStatus(customer.active ? 'active' : 'inactive'), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/estimates.ts b/packages/cli/src/commands/estimates.ts new file mode 100644 index 000000000..06fe0a8c6 --- /dev/null +++ b/packages/cli/src/commands/estimates.ts @@ -0,0 +1,91 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchSaleEstimates } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table'; + +export function createEstimatesCommand(): Command { + const command = new Command('estimates') + .description('Manage sale estimates/quotes'); + + command + .command('list') + .description('List all sale estimates') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-c, --customer ', 'Filter by customer ID') + .option('-s, --status ', 'Filter by status (draft, published)') + .action(async (options) => { + const spinner = ora('Loading estimates...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.customer && { customerId: parseInt(options.customer, 10) }), + ...(options.status && { status: options.status }), + }; + + const response = await fetchSaleEstimates(fetcher, query); + + spinner.stop(); + + const estimates = (response as unknown as { saleEstimates: Array<{ + id: number; + estimateNumber?: string; + customer?: { displayName?: string }; + estimateDate?: string; + expirationDate?: string; + total?: number; + status?: string; + }> }).saleEstimates; + + if (!estimates || estimates.length === 0) { + console.log(chalk.yellow('No estimates found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total estimates)\n`)); + } + + const table = createTable(['ID', 'Estimate #', 'Customer', 'Date', 'Expires', 'Total', 'Status']); + + estimates.forEach((estimate) => { + table.push([ + estimate.id, + estimate.estimateNumber || '-', + truncate(estimate.customer?.displayName, 20), + formatDate(estimate.estimateDate), + formatDate(estimate.expirationDate), + formatCurrency(estimate.total), + formatStatus(estimate.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/expenses.ts b/packages/cli/src/commands/expenses.ts new file mode 100644 index 000000000..7a5ec178c --- /dev/null +++ b/packages/cli/src/commands/expenses.ts @@ -0,0 +1,86 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchExpenses } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table'; + +export function createExpensesCommand(): Command { + const command = new Command('expenses') + .description('Manage expenses'); + + command + .command('list') + .description('List all expenses') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('--active-only', 'Show only active expenses', false) + .action(async (options) => { + const spinner = ora('Loading expenses...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.activeOnly && { active: true }), + }; + + const response = await fetchExpenses(fetcher, query); + + spinner.stop(); + + const expenses = (response as unknown as { expenses: Array<{ + id: number; + paymentDate?: string; + description?: string; + total?: number; + status?: string; + active?: boolean; + }> }).expenses; + + if (!expenses || expenses.length === 0) { + console.log(chalk.yellow('No expenses found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total expenses)\n`)); + } + + const table = createTable(['ID', 'Date', 'Description', 'Total', 'Status']); + + expenses.forEach((expense) => { + table.push([ + expense.id, + formatDate(expense.paymentDate), + truncate(expense.description, 35), + formatCurrency(expense.total), + formatStatus(expense.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/inventory.ts b/packages/cli/src/commands/inventory.ts new file mode 100644 index 000000000..e48798314 --- /dev/null +++ b/packages/cli/src/commands/inventory.ts @@ -0,0 +1,154 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchInventoryAdjustments, fetchWarehouseTransfers } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatDate, formatStatus, truncate } from '../utils/table'; + +export function createInventoryCommand(): Command { + const command = new Command('inventory') + .description('Manage inventory'); + + command + .command('adjustments') + .description('List inventory adjustments') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .action(async (options) => { + const spinner = ora('Loading inventory adjustments...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + }; + + const response = await fetchInventoryAdjustments(fetcher, query); + + spinner.stop(); + + const adjustments = (response as unknown as { inventoryAdjustments: Array<{ + id: number; + adjustmentDate?: string; + referenceNo?: string; + reason?: string; + status?: string; + }> }).inventoryAdjustments; + + if (!adjustments || adjustments.length === 0) { + console.log(chalk.yellow('No inventory adjustments found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total adjustments)\n`)); + } + + const table = createTable(['ID', 'Date', 'Reference', 'Reason', 'Status']); + + adjustments.forEach((adj) => { + table.push([ + adj.id, + formatDate(adj.adjustmentDate), + adj.referenceNo || '-', + truncate(adj.reason, 30), + formatStatus(adj.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + command + .command('transfers') + .description('List warehouse transfers') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .action(async (options) => { + const spinner = ora('Loading warehouse transfers...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + }; + + const response = await fetchWarehouseTransfers(fetcher, query); + + spinner.stop(); + + const transfers = (response as unknown as { warehouseTransfers: Array<{ + id: number; + date?: string; + referenceNo?: string; + fromWarehouse?: { name?: string }; + toWarehouse?: { name?: string }; + status?: string; + }> }).warehouseTransfers; + + if (!transfers || transfers.length === 0) { + console.log(chalk.yellow('No warehouse transfers found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total transfers)\n`)); + } + + const table = createTable(['ID', 'Date', 'Reference', 'From', 'To', 'Status']); + + transfers.forEach((transfer) => { + table.push([ + transfer.id, + formatDate(transfer.date), + transfer.referenceNo || '-', + truncate(transfer.fromWarehouse?.name, 15), + truncate(transfer.toWarehouse?.name, 15), + formatStatus(transfer.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/invoices.ts b/packages/cli/src/commands/invoices.ts new file mode 100644 index 000000000..786a6229c --- /dev/null +++ b/packages/cli/src/commands/invoices.ts @@ -0,0 +1,94 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchSaleInvoices } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table'; + +export function createInvoicesCommand(): Command { + const command = new Command('invoices') + .description('Manage sale invoices'); + + command + .command('list') + .description('List all sale invoices') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-c, --customer ', 'Filter by customer ID') + .option('-s, --status ', 'Filter by status (draft, published, paid, partial, overdue)') + .action(async (options) => { + const spinner = ora('Loading invoices...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.customer && { customerId: parseInt(options.customer, 10) }), + ...(options.status && { status: options.status }), + }; + + const response = await fetchSaleInvoices(fetcher, query); + + spinner.stop(); + + // The response has 'salesInvoices' array (camelCase from middleware), not 'data' + const invoices = (response as unknown as { salesInvoices: Array<{ + id: number; + invoiceNo?: string; + customer?: { displayName?: string }; + invoiceDate?: string; + total?: number; + balance?: number; + status?: string; + }> }).salesInvoices; + + if (!invoices || invoices.length === 0) { + console.log(chalk.yellow('No invoices found.')); + return; + } + + // Pagination info + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total invoices)\n`)); + } + + // Create table + const table = createTable(['ID', 'Invoice #', 'Customer', 'Date', 'Total', 'Balance', 'Status']); + + invoices.forEach((invoice) => { + table.push([ + invoice.id, + invoice.invoiceNo || '-', + truncate(invoice.customer?.displayName, 25), + formatDate(invoice.invoiceDate), + formatCurrency(invoice.total), + formatCurrency(invoice.balance), + formatStatus(invoice.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/items.ts b/packages/cli/src/commands/items.ts new file mode 100644 index 000000000..7ee822aff --- /dev/null +++ b/packages/cli/src/commands/items.ts @@ -0,0 +1,94 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchItems } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, truncate, formatStatus } from '../utils/table'; + +export function createItemsCommand(): Command { + const command = new Command('items') + .description('Manage items and products'); + + command + .command('list') + .description('List all items/products') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-t, --type ', 'Filter by item type (inventory, service, product)') + .option('--active-only', 'Show only active items', false) + .action(async (options) => { + const spinner = ora('Loading items...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.type && { type: options.type }), + ...(options.activeOnly && { active: true }), + }; + + const response = await fetchItems(fetcher, query); + + spinner.stop(); + + // The response has 'items' array, not 'data' + const items = (response as unknown as { items: Array<{ + id: number; + name?: string; + type?: string; + sellPrice?: number; + costPrice?: number; + quantityOnHand?: number; + active?: boolean; + }> }).items; + + if (!items || items.length === 0) { + console.log(chalk.yellow('No items found.')); + return; + } + + // Pagination info + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total items)\n`)); + } + + // Create table + const table = createTable(['ID', 'Name', 'Type', 'Sell Price', 'Cost Price', 'Qty', 'Status']); + + items.forEach((item) => { + table.push([ + item.id, + truncate(item.name, 30), + item.type || '-', + formatCurrency(item.sellPrice), + formatCurrency(item.costPrice), + item.quantityOnHand ?? '-', + formatStatus(item.active ? 'active' : 'inactive'), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/journals.ts b/packages/cli/src/commands/journals.ts new file mode 100644 index 000000000..1c32641f4 --- /dev/null +++ b/packages/cli/src/commands/journals.ts @@ -0,0 +1,82 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchManualJournals } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatDate, truncate, formatStatus } from '../utils/table'; + +export function createJournalsCommand(): Command { + const command = new Command('journals') + .description('Manage manual journals'); + + command + .command('list') + .description('List all manual journals') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .action(async (options) => { + const spinner = ora('Loading journals...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const response = await fetchManualJournals(fetcher, {} as never); + + spinner.stop(); + + const journals = (response as unknown as { manualJournals: Array<{ + id: number; + journalNumber?: string; + date?: string; + reference?: string; + description?: string; + status?: string; + amount?: number; + }> }).manualJournals; + + if (!journals || journals.length === 0) { + console.log(chalk.yellow('No manual journals found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total journals)\n`)); + } + + const table = createTable(['ID', 'Journal #', 'Date', 'Reference', 'Description', 'Amount', 'Status']); + + journals.forEach((journal) => { + table.push([ + journal.id, + journal.journalNumber || '-', + formatDate(journal.date), + journal.reference || '-', + truncate(journal.description, 25), + journal.amount?.toFixed(2) || '-', + formatStatus(journal.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/payments.ts b/packages/cli/src/commands/payments.ts new file mode 100644 index 000000000..246243f5c --- /dev/null +++ b/packages/cli/src/commands/payments.ts @@ -0,0 +1,102 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchPaymentsReceived, fetchBillPayments } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, formatDate, truncate } from '../utils/table'; + +export function createPaymentsCommand(): Command { + const command = new Command('payments') + .description('Manage payments'); + + command + .command('received') + .description('List payments received from customers') + .action(async () => { + const spinner = ora('Loading payments received...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + const response = await fetchPaymentsReceived(fetcher); + + spinner.stop(); + + const payments = (response as unknown as { paymentsReceived: Array<{ + id: number; + customer?: { displayName?: string }; + paymentDate?: string; + amount?: number; + referenceNo?: string; + }> }).paymentsReceived; + + if (!payments || payments.length === 0) { + console.log(chalk.yellow('No payments received found.')); + return; + } + + const table = createTable(['ID', 'Customer', 'Date', 'Amount', 'Reference']); + + payments.forEach((payment) => { + table.push([ + payment.id, + truncate(payment.customer?.displayName, 25), + formatDate(payment.paymentDate), + formatCurrency(payment.amount), + payment.referenceNo || '-', + ]); + }); + + console.log(table.toString()); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + command + .command('made') + .description('List bill payments made to vendors') + .action(async () => { + const spinner = ora('Loading payments made...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + const response = await fetchBillPayments(fetcher); + + spinner.stop(); + + const payments = (response as unknown as { billPayments: Array<{ + id: number; + vendor?: { displayName?: string }; + paymentDate?: string; + amount?: number; + referenceNo?: string; + }> }).billPayments; + + if (!payments || payments.length === 0) { + console.log(chalk.yellow('No payments made found.')); + return; + } + + const table = createTable(['ID', 'Vendor', 'Date', 'Amount', 'Reference']); + + payments.forEach((payment) => { + table.push([ + payment.id, + truncate(payment.vendor?.displayName, 25), + formatDate(payment.paymentDate), + formatCurrency(payment.amount), + payment.referenceNo || '-', + ]); + }); + + console.log(table.toString()); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/receipts.ts b/packages/cli/src/commands/receipts.ts new file mode 100644 index 000000000..ee0134f5c --- /dev/null +++ b/packages/cli/src/commands/receipts.ts @@ -0,0 +1,87 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchSaleReceipts } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table'; + +export function createReceiptsCommand(): Command { + const command = new Command('receipts') + .description('Manage sale receipts'); + + command + .command('list') + .description('List all sale receipts') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-c, --customer ', 'Filter by customer ID') + .action(async (options) => { + const spinner = ora('Loading receipts...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.customer && { customerId: parseInt(options.customer, 10) }), + }; + + const response = await fetchSaleReceipts(fetcher, query); + + spinner.stop(); + + const receipts = (response as unknown as { saleReceipts: Array<{ + id: number; + receiptNumber?: string; + customer?: { displayName?: string }; + receiptDate?: string; + total?: number; + status?: string; + }> }).saleReceipts; + + if (!receipts || receipts.length === 0) { + console.log(chalk.yellow('No receipts found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total receipts)\n`)); + } + + const table = createTable(['ID', 'Receipt #', 'Customer', 'Date', 'Total', 'Status']); + + receipts.forEach((receipt) => { + table.push([ + receipt.id, + receipt.receiptNumber || '-', + truncate(receipt.customer?.displayName, 20), + formatDate(receipt.receiptDate), + formatCurrency(receipt.total), + formatStatus(receipt.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/reports.ts b/packages/cli/src/commands/reports.ts new file mode 100644 index 000000000..a3f1ace6f --- /dev/null +++ b/packages/cli/src/commands/reports.ts @@ -0,0 +1,513 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { + fetchBalanceSheetJson, + fetchProfitLossJson, + fetchCashflowStatementJson, + fetchTrialBalanceJson, + fetchGeneralLedgerJson, + fetchJournalJson, + fetchReceivableAgingJson, + fetchPayableAgingJson, + fetchCustomerBalanceJson, + fetchVendorBalanceJson, + fetchSalesByItemsJson, + fetchPurchasesByItemsJson, + fetchInventoryValuationJson, + fetchInventoryItemDetailsJson, + fetchSalesTaxLiabilityJson, +} from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency } from '../utils/table'; + +interface DateRangeOptions { + from?: string; + to?: string; +} + +function getDateRange(options: DateRangeOptions): { fromDate: string; toDate: string } { + const toDate = options.to || new Date().toISOString().split('T')[0]; + const fromDate = options.from || new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + return { fromDate, toDate }; +} + +export function createReportsCommand(): Command { + const command = new Command('reports') + .description('Financial reports'); + + // Balance Sheet + command + .command('balance-sheet') + .description('Show balance sheet') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading balance sheet...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchBalanceSheetJson(fetcher, { fromDate, toDate } as never); + spinner.stop(); + + console.log(chalk.bold('\n📊 Balance Sheet')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + + // Assets + console.log(chalk.green.bold('\nASSETS')); + const assetsTable = createTable(['Account', 'Amount']); + let totalAssets = 0; + (report as unknown as { assets?: Array<{ accountName: string; total: number }> }).assets?.forEach((item) => { + assetsTable.push([item.accountName, formatCurrency(item.total)]); + totalAssets += item.total; + }); + assetsTable.push(['', '']); + assetsTable.push([chalk.bold('Total Assets'), chalk.bold(formatCurrency(totalAssets))]); + console.log(assetsTable.toString()); + + // Liabilities + console.log(chalk.red.bold('\nLIABILITIES')); + const liabilitiesTable = createTable(['Account', 'Amount']); + let totalLiabilities = 0; + (report as unknown as { liabilities?: Array<{ accountName: string; total: number }> }).liabilities?.forEach((item) => { + liabilitiesTable.push([item.accountName, formatCurrency(item.total)]); + totalLiabilities += item.total; + }); + liabilitiesTable.push(['', '']); + liabilitiesTable.push([chalk.bold('Total Liabilities'), chalk.bold(formatCurrency(totalLiabilities))]); + console.log(liabilitiesTable.toString()); + + // Equity + console.log(chalk.blue.bold('\nEQUITY')); + const equityTable = createTable(['Account', 'Amount']); + let totalEquity = 0; + (report as unknown as { equity?: Array<{ accountName: string; total: number }> }).equity?.forEach((item) => { + equityTable.push([item.accountName, formatCurrency(item.total)]); + totalEquity += item.total; + }); + equityTable.push(['', '']); + equityTable.push([chalk.bold('Total Equity'), chalk.bold(formatCurrency(totalEquity))]); + console.log(equityTable.toString()); + + // Summary + console.log(chalk.bold('\n' + '─'.repeat(40))); + console.log(`${chalk.bold('Total Liabilities + Equity:')} ${formatCurrency(totalLiabilities + totalEquity)}`); + console.log(`${chalk.bold('Balance:')} ${totalAssets === (totalLiabilities + totalEquity) ? chalk.green('✓ Balanced') : chalk.red('✗ Unbalanced')}\n`); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Profit & Loss + command + .command('profit-loss') + .alias('pl') + .description('Show profit and loss statement') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading profit & loss...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchProfitLossJson(fetcher, { fromDate, toDate } as never); + spinner.stop(); + + console.log(chalk.bold('\n📈 Profit & Loss Statement')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + + const income = (report as unknown as { income?: { total: number } }).income?.total || 0; + const expenses = (report as unknown as { expenses?: { total: number } }).expenses?.total || 0; + const netProfit = income - expenses; + + const table = createTable(['', 'Amount']); + table.push([chalk.green('Total Income'), formatCurrency(income)]); + table.push([chalk.red('Total Expenses'), formatCurrency(expenses)]); + table.push(['', '']); + table.push([netProfit >= 0 ? chalk.green.bold('Net Profit') : chalk.red.bold('Net Loss'), formatCurrency(Math.abs(netProfit))]); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Cashflow + command + .command('cashflow') + .description('Show cashflow statement') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading cashflow statement...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchCashflowStatementJson(fetcher, { fromDate, toDate } as never); + spinner.stop(); + + console.log(chalk.bold('\n💰 Cashflow Statement')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + + const operating = (report as unknown as { operating?: { total: number } }).operating?.total || 0; + const investing = (report as unknown as { investing?: { total: number } }).investing?.total || 0; + const financing = (report as unknown as { financing?: { total: number } }).financing?.total || 0; + const netCash = operating + investing + financing; + + const table = createTable(['Activity', 'Amount']); + table.push(['Operating Activities', formatCurrency(operating)]); + table.push(['Investing Activities', formatCurrency(investing)]); + table.push(['Financing Activities', formatCurrency(financing)]); + table.push(['', '']); + table.push([chalk.bold('Net Cash Change'), chalk.bold(formatCurrency(netCash))]); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Trial Balance + command + .command('trial-balance') + .description('Show trial balance') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading trial balance...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchTrialBalanceJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n⚖️ Trial Balance')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + + const table = createTable(['Account', 'Debit', 'Credit']); + let totalDebit = 0; + let totalCredit = 0; + + (report as unknown as { data?: Array<{ accountName: string; debit: number; credit: number }> }).data?.forEach((item) => { + table.push([item.accountName, formatCurrency(item.debit), formatCurrency(item.credit)]); + totalDebit += item.debit; + totalCredit += item.credit; + }); + + table.push(['', '', '']); + table.push([chalk.bold('Total'), chalk.bold(formatCurrency(totalDebit)), chalk.bold(formatCurrency(totalCredit))]); + + console.log(table.toString()); + console.log(`\n${chalk.bold('Balance:')} ${Math.abs(totalDebit - totalCredit) < 0.01 ? chalk.green('✓ Balanced') : chalk.red('✗ Unbalanced')}\n`); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // General Ledger + command + .command('general-ledger') + .description('Show general ledger') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading general ledger...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchGeneralLedgerJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n📒 General Ledger')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + console.log(JSON.stringify(report, null, 2)); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Journal + command + .command('journal') + .description('Show journal report') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading journal...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchJournalJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n📝 Journal Report')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + + const table = createTable(['Date', 'Reference', 'Account', 'Debit', 'Credit']); + (report as unknown as { data?: Array<{ date: string; reference: string; accountName: string; debit: number; credit: number }> }).data?.forEach((item) => { + table.push([item.date, item.reference || '-', item.accountName, formatCurrency(item.debit), formatCurrency(item.credit)]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Receivable Aging + command + .command('receivable-aging') + .description('Show accounts receivable aging') + .action(async () => { + const spinner = ora('Loading receivable aging...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const report = await fetchReceivableAgingJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n📋 Accounts Receivable Aging\n')); + + const table = createTable(['Customer', 'Current', '1-30', '31-60', '61-90', '90+', 'Total']); + (report as unknown as { data?: Array<{ customerName: string; current: number; days1to30: number; days31to60: number; days61to90: number; over90: number; total: number }> }).data?.forEach((item) => { + table.push([ + item.customerName, + formatCurrency(item.current), + formatCurrency(item.days1to30), + formatCurrency(item.days31to60), + formatCurrency(item.days61to90), + formatCurrency(item.over90), + chalk.bold(formatCurrency(item.total)), + ]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Payable Aging + command + .command('payable-aging') + .description('Show accounts payable aging') + .action(async () => { + const spinner = ora('Loading payable aging...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const report = await fetchPayableAgingJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n📋 Accounts Payable Aging\n')); + + const table = createTable(['Vendor', 'Current', '1-30', '31-60', '61-90', '90+', 'Total']); + (report as unknown as { data?: Array<{ vendorName: string; current: number; days1to30: number; days31to60: number; days61to90: number; over90: number; total: number }> }).data?.forEach((item) => { + table.push([ + item.vendorName, + formatCurrency(item.current), + formatCurrency(item.days1to30), + formatCurrency(item.days31to60), + formatCurrency(item.days61to90), + formatCurrency(item.over90), + chalk.bold(formatCurrency(item.total)), + ]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Customer Balance + command + .command('customer-balance') + .description('Show customer balance summary') + .action(async () => { + const spinner = ora('Loading customer balances...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const report = await fetchCustomerBalanceJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n👤 Customer Balance Summary\n')); + + const table = createTable(['Customer', 'Total']); + (report as unknown as { data?: Array<{ customerName: string; total: number }> }).data?.forEach((item) => { + table.push([item.customerName, formatCurrency(item.total)]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Vendor Balance + command + .command('vendor-balance') + .description('Show vendor balance summary') + .action(async () => { + const spinner = ora('Loading vendor balances...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const report = await fetchVendorBalanceJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n🏢 Vendor Balance Summary\n')); + + const table = createTable(['Vendor', 'Total']); + (report as unknown as { data?: Array<{ vendorName: string; total: number }> }).data?.forEach((item) => { + table.push([item.vendorName, formatCurrency(item.total)]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Sales by Items + command + .command('sales-by-items') + .description('Show sales by items report') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading sales by items...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchSalesByItemsJson(fetcher, { fromDate, toDate } as never); + spinner.stop(); + + console.log(chalk.bold('\n🛍️ Sales by Items')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + + const table = createTable(['Item', 'Quantity', 'Amount']); + (report as unknown as { data?: Array<{ itemName: string; quantity: number; amount: number }> }).data?.forEach((item) => { + table.push([item.itemName, item.quantity.toString(), formatCurrency(item.amount)]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Purchases by Items + command + .command('purchases-by-items') + .description('Show purchases by items report') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading purchases by items...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchPurchasesByItemsJson(fetcher, { fromDate, toDate } as never); + spinner.stop(); + + console.log(chalk.bold('\n🛒 Purchases by Items')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + + const table = createTable(['Item', 'Quantity', 'Amount']); + (report as unknown as { data?: Array<{ itemName: string; quantity: number; amount: number }> }).data?.forEach((item) => { + table.push([item.itemName, item.quantity.toString(), formatCurrency(item.amount)]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Inventory Valuation + command + .command('inventory-valuation') + .description('Show inventory valuation summary') + .action(async () => { + const spinner = ora('Loading inventory valuation...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const report = await fetchInventoryValuationJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n📦 Inventory Valuation\n')); + + const table = createTable(['Item', 'Qty on Hand', 'Avg Cost', 'Total Value']); + (report as unknown as { data?: Array<{ itemName: string; quantityOnHand: number; averageCost: number; totalValue: number }> }).data?.forEach((item) => { + table.push([item.itemName, item.quantityOnHand.toString(), formatCurrency(item.averageCost), formatCurrency(item.totalValue)]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Inventory Details + command + .command('inventory-details') + .description('Show inventory item details') + .option('--item ', 'Filter by item ID') + .action(async (options: { item?: string }) => { + const spinner = ora('Loading inventory details...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const report = await fetchInventoryItemDetailsJson(fetcher, {} as never); + spinner.stop(); + + console.log(chalk.bold('\n📦 Inventory Item Details\n')); + console.log(JSON.stringify(report, null, 2)); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + // Sales Tax Liability + command + .command('sales-tax-liability') + .description('Show sales tax liability report') + .option('--from ', 'Start date (YYYY-MM-DD)') + .option('--to ', 'End date (YYYY-MM-DD)') + .action(async (options: DateRangeOptions) => { + const spinner = ora('Loading sales tax liability...').start(); + try { + const fetcher = createAuthenticatedFetcher(); + const { fromDate, toDate } = getDateRange(options); + const report = await fetchSalesTaxLiabilityJson(fetcher, { fromDate, toDate } as never); + spinner.stop(); + + console.log(chalk.bold('\n💵 Sales Tax Liability')); + console.log(chalk.gray(`${fromDate} to ${toDate}\n`)); + + const table = createTable(['Tax Rate', 'Taxable Amount', 'Tax Amount']); + (report as unknown as { data?: Array<{ taxRateName: string; taxableAmount: number; taxAmount: number }> }).data?.forEach((item) => { + table.push([item.taxRateName, formatCurrency(item.taxableAmount), formatCurrency(item.taxAmount)]); + }); + + console.log(table.toString() + '\n'); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/tax-rates.ts b/packages/cli/src/commands/tax-rates.ts new file mode 100644 index 000000000..9ce4c7b2d --- /dev/null +++ b/packages/cli/src/commands/tax-rates.ts @@ -0,0 +1,58 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchTaxRates } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatStatus } from '../utils/table'; + +export function createTaxRatesCommand(): Command { + const command = new Command('tax-rates') + .description('Manage tax rates'); + + command + .command('list') + .description('List all tax rates') + .action(async () => { + const spinner = ora('Loading tax rates...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + const response = await fetchTaxRates(fetcher); + + spinner.stop(); + + const taxRates = (response as unknown as { taxRates: Array<{ + id: number; + name?: string; + rate?: number; + code?: string; + active?: boolean; + }> }).taxRates; + + if (!taxRates || taxRates.length === 0) { + console.log(chalk.yellow('No tax rates found.')); + return; + } + + const table = createTable(['ID', 'Name', 'Code', 'Rate', 'Status']); + + taxRates.forEach((tax) => { + table.push([ + tax.id, + tax.name || '-', + tax.code || '-', + `${tax.rate || 0}%`, + formatStatus(tax.active ? 'active' : 'inactive'), + ]); + }); + + console.log(table.toString()); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/users.ts b/packages/cli/src/commands/users.ts new file mode 100644 index 000000000..890f8a30d --- /dev/null +++ b/packages/cli/src/commands/users.ts @@ -0,0 +1,102 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchUsers, fetchRoles } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatStatus } from '../utils/table'; + +export function createUsersCommand(): Command { + const command = new Command('users') + .description('Manage users and roles'); + + command + .command('list') + .description('List all users') + .action(async () => { + const spinner = ora('Loading users...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + const response = await fetchUsers(fetcher); + + spinner.stop(); + + const users = (response as unknown as { users: Array<{ + id: number; + firstName?: string; + lastName?: string; + email?: string; + role?: { name?: string }; + active?: boolean; + }> }).users; + + if (!users || users.length === 0) { + console.log(chalk.yellow('No users found.')); + return; + } + + const table = createTable(['ID', 'Name', 'Email', 'Role', 'Status']); + + users.forEach((user) => { + const name = `${user.firstName || ''} ${user.lastName || ''}`.trim() || '-'; + table.push([ + user.id, + name, + user.email || '-', + user.role?.name || '-', + formatStatus(user.active ? 'active' : 'inactive'), + ]); + }); + + console.log(table.toString()); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + command + .command('roles') + .description('List all roles') + .action(async () => { + const spinner = ora('Loading roles...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + const response = await fetchRoles(fetcher); + + spinner.stop(); + + const roles = (response as unknown as { roles: Array<{ + id: number; + name?: string; + description?: string; + slug?: string; + }> }).roles; + + if (!roles || roles.length === 0) { + console.log(chalk.yellow('No roles found.')); + return; + } + + const table = createTable(['ID', 'Name', 'Slug', 'Description']); + + roles.forEach((role) => { + table.push([ + role.id, + role.name || '-', + role.slug || '-', + role.description || '-', + ]); + }); + + console.log(table.toString()); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/vendor-credits.ts b/packages/cli/src/commands/vendor-credits.ts new file mode 100644 index 000000000..23b17643a --- /dev/null +++ b/packages/cli/src/commands/vendor-credits.ts @@ -0,0 +1,89 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchVendorCredits } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, formatDate, truncate, formatStatus } from '../utils/table'; + +export function createVendorCreditsCommand(): Command { + const command = new Command('vendor-credits') + .description('Manage vendor credits'); + + command + .command('list') + .description('List all vendor credits') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('-v, --vendor ', 'Filter by vendor ID') + .action(async (options) => { + const spinner = ora('Loading vendor credits...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.vendor && { vendorId: parseInt(options.vendor, 10) }), + }; + + const response = await fetchVendorCredits(fetcher, query); + + spinner.stop(); + + const vendorCredits = (response as unknown as { vendorCredits: Array<{ + id: number; + vendorCreditNumber?: string; + vendor?: { displayName?: string }; + vendorCreditDate?: string; + total?: number; + balance?: number; + status?: string; + }> }).vendorCredits; + + if (!vendorCredits || vendorCredits.length === 0) { + console.log(chalk.yellow('No vendor credits found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total vendor credits)\n`)); + } + + const table = createTable(['ID', 'VC #', 'Vendor', 'Date', 'Total', 'Balance', 'Status']); + + vendorCredits.forEach((vc) => { + table.push([ + vc.id, + vc.vendorCreditNumber || '-', + truncate(vc.vendor?.displayName, 20), + formatDate(vc.vendorCreditDate), + formatCurrency(vc.total), + formatCurrency(vc.balance), + formatStatus(vc.status), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/vendors.ts b/packages/cli/src/commands/vendors.ts new file mode 100644 index 000000000..e0420f60f --- /dev/null +++ b/packages/cli/src/commands/vendors.ts @@ -0,0 +1,89 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchVendors } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatCurrency, truncate, formatStatus } from '../utils/table'; + +export function createVendorsCommand(): Command { + const command = new Command('vendors') + .description('Manage vendors'); + + command + .command('list') + .description('List all vendors') + .option('-l, --limit ', 'Limit number of results per page', '50') + .option('-p, --page ', 'Page number', '1') + .option('--active-only', 'Show only active vendors', false) + .action(async (options) => { + const spinner = ora('Loading vendors...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + + const query = { + page: parseInt(options.page, 10), + pageSize: Math.min(parseInt(options.limit, 10), 100), + ...(options.activeOnly && { active: true }), + }; + + const response = await fetchVendors(fetcher, query); + + spinner.stop(); + + const vendors = (response as unknown as { vendors: Array<{ + id: number; + displayName?: string; + email?: string; + workPhone?: string; + personalPhone?: string; + balance?: number; + active?: boolean; + }> }).vendors; + + if (!vendors || vendors.length === 0) { + console.log(chalk.yellow('No vendors found.')); + return; + } + + const pagination = (response as unknown as { + pagination?: { total: number; page: number; pageSize?: number; page_size?: number } + }).pagination; + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + console.log(chalk.gray(`\nPage ${pagination.page} of ${totalPages} (${pagination.total} total vendors)\n`)); + } + + const table = createTable(['ID', 'Name', 'Email', 'Work Phone', 'Personal Phone', 'Balance', 'Status']); + + vendors.forEach((vendor) => { + table.push([ + vendor.id, + truncate(vendor.displayName, 25), + truncate(vendor.email, 25) || '-', + vendor.workPhone || '-', + vendor.personalPhone || '-', + formatCurrency(vendor.balance), + formatStatus(vendor.active ? 'active' : 'inactive'), + ]); + }); + + console.log(table.toString()); + + if (pagination) { + const pageSize = pagination.pageSize || pagination.page_size || 10; + const totalPages = Math.ceil(pagination.total / pageSize); + if (pagination.page < totalPages) { + console.log(chalk.gray(`\nUse --page ${pagination.page + 1} to see more results`)); + } + } + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/commands/warehouses.ts b/packages/cli/src/commands/warehouses.ts new file mode 100644 index 000000000..b30ca5ed8 --- /dev/null +++ b/packages/cli/src/commands/warehouses.ts @@ -0,0 +1,59 @@ +import { Command } from 'commander'; +import chalk from 'chalk'; +import ora from 'ora'; +import { fetchWarehouses } from '@bigcapital/sdk-ts'; +import { createAuthenticatedFetcher } from '../config'; +import { handleError } from '../utils/errors'; +import { createTable, formatStatus } from '../utils/table'; + +export function createWarehousesCommand(): Command { + const command = new Command('warehouses') + .description('Manage warehouses'); + + command + .command('list') + .description('List all warehouses') + .action(async () => { + const spinner = ora('Loading warehouses...').start(); + + try { + const fetcher = createAuthenticatedFetcher(); + const response = await fetchWarehouses(fetcher); + + spinner.stop(); + + const warehouses = (response as unknown as { warehouses: Array<{ + id: string; + name?: string; + code?: string; + address?: string; + city?: string; + active?: boolean; + }> }).warehouses; + + if (!warehouses || warehouses.length === 0) { + console.log(chalk.yellow('No warehouses found.')); + return; + } + + const table = createTable(['ID', 'Code', 'Name', 'City', 'Status']); + + warehouses.forEach((warehouse) => { + table.push([ + warehouse.id, + warehouse.code || '-', + warehouse.name || '-', + warehouse.city || '-', + formatStatus(warehouse.active ? 'active' : 'inactive'), + ]); + }); + + console.log(table.toString()); + } catch (error) { + spinner.stop(); + handleError(error); + } + }); + + return command; +} diff --git a/packages/cli/src/config.ts b/packages/cli/src/config.ts new file mode 100644 index 000000000..bdf3dde36 --- /dev/null +++ b/packages/cli/src/config.ts @@ -0,0 +1,83 @@ +import Conf from 'conf'; +import chalk from 'chalk'; +import { createApiFetcher, ApiFetcher } from '@bigcapital/sdk-ts'; + +interface ConfigSchema { + apiKey: string; + baseUrl: string; + organizationId?: string; +} + +const config = new Conf({ + projectName: 'bigcapital', + schema: { + apiKey: { + type: 'string', + }, + baseUrl: { + type: 'string', + }, + organizationId: { + type: 'string', + }, + }, +}); + +export function getConfig(): ConfigSchema { + return { + apiKey: config.get('apiKey', ''), + baseUrl: config.get('baseUrl', ''), + organizationId: config.get('organizationId'), + }; +} + +export function setApiKey(apiKey: string): void { + config.set('apiKey', apiKey); +} + +export function setBaseUrl(baseUrl: string): void { + // Remove trailing slash if present + const normalizedUrl = baseUrl.replace(/\/$/, ''); + config.set('baseUrl', normalizedUrl); +} + +export function setOrganizationId(organizationId: string): void { + config.set('organizationId', organizationId); +} + +export function validateConfig(): ConfigSchema { + const currentConfig = getConfig(); + + if (!currentConfig.apiKey) { + console.error(chalk.red('Error: API key is not configured.')); + console.error(chalk.yellow('Run: bigcapital config set api-key ')); + process.exit(1); + } + + if (!currentConfig.baseUrl) { + console.error(chalk.red('Error: Base URL is not configured.')); + console.error(chalk.yellow('Run: bigcapital config set base-url ')); + process.exit(1); + } + + return currentConfig; +} + +export function createAuthenticatedFetcher(): ApiFetcher { + const currentConfig = validateConfig(); + + const headers: Record = { + 'Authorization': `Bearer ${currentConfig.apiKey}`, + }; + + if (currentConfig.organizationId) { + headers['organization-id'] = currentConfig.organizationId; + } + + return createApiFetcher({ + baseUrl: currentConfig.baseUrl, + init: { + headers, + }, + }); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 000000000..4b829979e --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import { createItemsCommand } from './commands/items'; +import { createInvoicesCommand } from './commands/invoices'; +import { createConfigCommand } from './commands/config'; +import { createCustomersCommand } from './commands/customers'; +import { createVendorsCommand } from './commands/vendors'; +import { createBillsCommand } from './commands/bills'; +import { createAccountsCommand } from './commands/accounts'; +import { createExpensesCommand } from './commands/expenses'; +import { createCreditNotesCommand } from './commands/credit-notes'; +import { createVendorCreditsCommand } from './commands/vendor-credits'; +import { createPaymentsCommand } from './commands/payments'; +import { createEstimatesCommand } from './commands/estimates'; +import { createReceiptsCommand } from './commands/receipts'; +import { createJournalsCommand } from './commands/journals'; +import { createInventoryCommand } from './commands/inventory'; +import { createTaxRatesCommand } from './commands/tax-rates'; +import { createWarehousesCommand } from './commands/warehouses'; +import { createUsersCommand } from './commands/users'; +import { createReportsCommand } from './commands/reports'; +import chalk from 'chalk'; + +const program = new Command(); + +program + .name('bigcapital') + .description('Bigcapital CLI - Interact with Bigcapital API') + .version('1.0.0') + .configureOutput({ + writeErr: (str) => process.stderr.write(chalk.red(str)), + outputError: (str, write) => write(chalk.red(str)), + }); + +// Core modules +program.addCommand(createConfigCommand()); +program.addCommand(createItemsCommand()); +program.addCommand(createInvoicesCommand()); +program.addCommand(createCustomersCommand()); +program.addCommand(createVendorsCommand()); +program.addCommand(createBillsCommand()); + +// Additional transactional modules +program.addCommand(createAccountsCommand()); +program.addCommand(createExpensesCommand()); +program.addCommand(createCreditNotesCommand()); +program.addCommand(createVendorCreditsCommand()); +program.addCommand(createPaymentsCommand()); +program.addCommand(createEstimatesCommand()); +program.addCommand(createReceiptsCommand()); +program.addCommand(createJournalsCommand()); +program.addCommand(createInventoryCommand()); +program.addCommand(createTaxRatesCommand()); +program.addCommand(createWarehousesCommand()); +program.addCommand(createUsersCommand()); + +// Financial reports +program.addCommand(createReportsCommand()); + +// Global error handling +program.hook('preAction', () => { + process.on('unhandledRejection', (error) => { + console.error(chalk.red('\nUnhandled error:'), error); + process.exit(1); + }); + + process.on('uncaughtException', (error) => { + console.error(chalk.red('\nUncaught exception:'), error); + process.exit(1); + }); +}); + +// Show help if no command provided +if (process.argv.length <= 2) { + program.help(); +} + +program.parse(); diff --git a/packages/cli/src/utils/errors.ts b/packages/cli/src/utils/errors.ts new file mode 100644 index 000000000..9484d117d --- /dev/null +++ b/packages/cli/src/utils/errors.ts @@ -0,0 +1,40 @@ +import chalk from 'chalk'; + +export function handleError(error: unknown): never { + if (error instanceof Error) { + // Check if it's an HTTP error from the SDK + const httpError = error as { status?: number; statusText?: string }; + + if (httpError.status) { + console.error(chalk.red(`HTTP Error ${httpError.status}: ${httpError.statusText || error.message}`)); + + if (httpError.status === 401) { + console.error(chalk.yellow('Your API key may be invalid or expired.')); + console.error(chalk.yellow('Run: bigcapital config set api-key ')); + } else if (httpError.status === 403) { + console.error(chalk.yellow('You don\'t have permission to access this resource.')); + } else if (httpError.status === 404) { + console.error(chalk.yellow('The requested resource was not found.')); + } + } else { + console.error(chalk.red(`Error: ${error.message}`)); + } + + if (process.env.DEBUG) { + console.error(chalk.gray(error.stack)); + } + } else { + console.error(chalk.red('An unknown error occurred')); + console.error(error); + } + + process.exit(1); +} + +export function assertDefined(value: T | undefined | null, name: string): T { + if (value === undefined || value === null) { + console.error(chalk.red(`Error: ${name} is required but was not provided.`)); + process.exit(1); + } + return value; +} diff --git a/packages/cli/src/utils/table.ts b/packages/cli/src/utils/table.ts new file mode 100644 index 000000000..63c2adb82 --- /dev/null +++ b/packages/cli/src/utils/table.ts @@ -0,0 +1,74 @@ +import Table from 'cli-table3'; +import chalk from 'chalk'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function createTable(headers: string[]): Table.Table { + return new Table({ + head: headers.map(h => chalk.cyan.bold(h)), + style: { + head: [], + border: [], + }, + chars: { + top: '─', + 'top-mid': '┬', + 'top-left': '┌', + 'top-right': '┐', + bottom: '─', + 'bottom-mid': '┴', + 'bottom-left': '└', + 'bottom-right': '┘', + left: '│', + 'left-mid': '├', + mid: '─', + 'mid-mid': '┼', + right: '│', + 'right-mid': '┤', + middle: '│', + }, + }); +} + +export function formatCurrency(amount: number | string | undefined, currency = '$'): string { + if (amount === undefined || amount === null) return '-'; + const num = typeof amount === 'string' ? parseFloat(amount) : amount; + if (isNaN(num)) return '-'; + return `${currency}${num.toFixed(2)}`; +} + +export function formatDate(dateStr: string | undefined): string { + if (!dateStr) return '-'; + const date = new Date(dateStr); + if (isNaN(date.getTime())) return dateStr; + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +} + +export function truncate(str: string | undefined, maxLength: number): string { + if (!str) return '-'; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength - 3) + '...'; +} + +export function formatStatus(status: string | undefined): string { + if (!status) return '-'; + + const statusColors: Record string> = { + published: chalk.green, + draft: chalk.gray, + delivered: chalk.blue, + partial: chalk.yellow, + paid: chalk.green.bold, + unpaid: chalk.red, + overdue: chalk.red.bold, + active: chalk.green, + inactive: chalk.gray, + }; + + const lowerStatus = status.toLowerCase(); + const colorFn = statusColors[lowerStatus] || chalk.white; + return colorFn(status); +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 000000000..917df7727 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5bb4421f5..05f863c78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,34 @@ importers: specifier: ^9.0.5 version: 9.1.2 + packages/cli: + dependencies: + '@bigcapital/sdk-ts': + specifier: workspace:* + version: link:../../shared/sdk-ts + chalk: + specifier: ^4.1.2 + version: 4.1.2 + cli-table3: + specifier: ^0.6.3 + version: 0.6.5 + commander: + specifier: ^11.1.0 + version: 11.1.0 + conf: + specifier: ^10.2.0 + version: 10.2.0 + ora: + specifier: ^5.4.1 + version: 5.4.1 + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.25 + typescript: + specifier: ^5.1.3 + version: 5.6.3 + packages/server: dependencies: '@aws-sdk/client-s3': @@ -6354,6 +6382,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomically@1.7.0: + resolution: {integrity: sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==} + engines: {node: '>=10.12.0'} + attr-accept@2.2.2: resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==} engines: {node: '>=4'} @@ -7020,6 +7052,10 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + conf@10.2.0: + resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} + engines: {node: '>=12'} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -7299,6 +7335,10 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debounce-fn@4.0.0: + resolution: {integrity: sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==} + engines: {node: '>=10'} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -9406,6 +9446,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@7.0.3: + resolution: {integrity: sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -9910,6 +9953,10 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + mimic-fn@3.1.0: + resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} + engines: {node: '>=8'} + mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} @@ -10747,6 +10794,10 @@ packages: pkg-types@1.2.1: resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + plaid-threads@11.5.0: resolution: {integrity: sha512-KS3w5Ydv+aC7wS1XxiWeDUhQHHFQ/5dQOoAQwdg+3DxFHbh5nSPH1L4Zy9qHru5FVDmC5DQ6/9XhJwzZ9BhpTA==} peerDependencies: @@ -20181,7 +20232,7 @@ snapshots: '@types/nodemailer@6.4.17': dependencies: - '@types/node': 20.5.1 + '@types/node': 20.19.25 '@types/normalize-package-data@2.4.4': {} @@ -21238,6 +21289,8 @@ snapshots: asynckit@0.4.0: {} + atomically@1.7.0: {} + attr-accept@2.2.2: {} available-typed-arrays@1.0.7: @@ -22095,6 +22148,19 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + conf@10.2.0: + dependencies: + ajv: 8.17.1 + ajv-formats: 2.1.1(ajv@8.17.1) + atomically: 1.7.0 + debounce-fn: 4.0.0 + dot-prop: 6.0.1 + env-paths: 2.2.1 + json-schema-typed: 7.0.3 + onetime: 5.1.2 + pkg-up: 3.1.0 + semver: 7.6.3 + confbox@0.1.8: {} config-chain@1.1.13: @@ -22405,6 +22471,10 @@ snapshots: de-indent@1.0.2: {} + debounce-fn@4.0.0: + dependencies: + mimic-fn: 3.1.0 + debug@2.6.9: dependencies: ms: 2.0.0 @@ -25124,6 +25194,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@7.0.3: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: {} @@ -25718,6 +25790,8 @@ snapshots: mimic-fn@2.1.0: {} + mimic-fn@3.1.0: {} + mimic-fn@4.0.0: {} min-indent@1.0.1: {} @@ -26700,6 +26774,10 @@ snapshots: mlly: 1.7.2 pathe: 1.1.2 + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + plaid-threads@11.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@popperjs/core': 2.11.8 @@ -28541,7 +28619,7 @@ snapshots: stripe@16.10.0: dependencies: - '@types/node': 20.5.1 + '@types/node': 20.19.25 qs: 6.14.0 strnum@1.0.5: {}