diff --git a/.github/workflows/ember.yml b/.github/workflows/ember.yml index 20dd30a..fe331a2 100644 --- a/.github/workflows/ember.yml +++ b/.github/workflows/ember.yml @@ -8,21 +8,19 @@ on: pull_request: branches: [ main ] +env: + NODE_VERSION: 22.x + jobs: build: runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [18.x] # Build on Node.js 18 - steps: - uses: actions/checkout@v2 - - name: Setup Node.js ${{ matrix.node-version }} + - name: Setup Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v2 with: - node-version: ${{ matrix.node-version }} + node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v2.0.1 @@ -45,10 +43,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup Node.js 18.x + - name: Setup Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v2 with: - node-version: 18.x + node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v2.0.1 @@ -74,10 +72,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup Node.js 18.x + - name: Setup Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v2 with: - node-version: 18.x + node-version: ${{ env.NODE_VERSION }} - name: Setup pnpm uses: pnpm/action-setup@v2.0.1 diff --git a/README.md b/README.md index 013343a..a0cf76b 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,262 @@

-

- -

-

- Accounting & Invoicing Extension for Fleetbase -

+ + Fleetbase Ledger +

---- +

Fleetbase Ledger

+ +

+ Accounting, invoicing, wallets, payments, and financial reporting for Fleetbase. +

+ +

+ License: AGPL-3.0-or-later + PHP CI + Ember CI + Packagist version + npm version + Ledger documentation +

+ +

+ + Fleetbase Ledger dashboard + +

## Overview -This monorepo contains both the frontend and backend components of the Ledger extension for Fleetbase. The frontend is built using Ember.js and the backend is implemented in PHP. +Ledger is the finance and billing extension for Fleetbase. It adds a complete financial management layer to the Fleetbase console, including double-entry bookkeeping, customer invoicing, invoice templates, digital wallets, payment gateway processing, immutable transaction history, and standard financial reports. -* PHP 7.3.0 or above -* Ember.js v4.8 or above -* Ember CLI v4.8 or above -* Node.js v18 or above +Ledger ships as both a Laravel package and an Ember engine. The backend package provides the accounting models, services, routes, gateway drivers, events, observers, migrations, and console commands. The frontend engine provides the Ledger console experience for billing, payments, accounting, reports, and settings. -## Structure +Ledger is included with standard Fleetbase installations. See the [Fleetbase Ledger documentation](https://www.fleetbase.io/docs/ledger) for the product guide, concepts, and setup walkthroughs. -``` -├── addon -├── app -├── assets -├── translations -├── config -├── node_modules -├── server -│ ├── config -│ ├── data -│ ├── migrations -│ ├── resources -│ ├── src -│ ├── tests -│ └── vendor -├── tests -├── testem.js -├── index.js -├── package.json -├── phpstan.neon.dist -├── phpunit.xml.dist -├── pnpm-lock.yaml -├── ember-cli-build.js -├── composer.json -├── CONTRIBUTING.md -├── LICENSE.md -├── README.md -``` +## Contents + +- [Features](#features) +- [Architecture](#architecture) +- [Requirements](#requirements) +- [Installation](#installation) +- [Development](#development) +- [API and Extension Points](#api-and-extension-points) +- [Documentation](#documentation) +- [Contributing](#contributing) +- [Security](#security) +- [License](#license) + +## Features + +### Accounting + +- Chart of accounts for asset, liability, equity, revenue, and expense accounts. +- Double-entry journal entries with debit and credit accounts. +- Cached account balances with recalculation support. +- General ledger views per account and across the company. +- System-created and manual journal entries for operational accounting workflows. + +### Billing and Invoicing + +- Customer invoices with line items, tax, subtotal, total, balance, due date, notes, and terms. +- Invoice lifecycle support for draft, sent, viewed, paid, overdue, cancelled, refunded, and void states. +- Invoice templates with company branding and registered template context variables. +- Invoice previews, rendered PDFs, invoice emails, and public customer invoice pages. +- Manual payment recording and invoice transaction history. +- Fleet-Ops purchase-rate integration for automatically generating draft invoices from orders. + +### Wallets and Transactions + +- Digital wallets for companies, users, customers, drivers, and other Fleetbase subjects. +- Wallet operations for top-ups, credits, transfers, payouts, freezes, unfreezes, and recalculation. +- Atomic balance changes through `WalletService`. +- Immutable transaction records for wallet activity, payment activity, and operational money movement. +- Direction-aware transaction history for credits, debits, deposits, payouts, transfers, refunds, and reversals. + +### Payment Gateways + +- Built-in gateway drivers for Stripe, QPay, and Cash/manual payments. +- Gateway configuration with encrypted credentials at rest. +- Sandbox and live environments. +- Purchases, refunds, setup intents, tokenization where supported, and gateway transaction history. +- Public gateway webhook endpoint with driver-level signature verification. +- Idempotent gateway processing through `GatewayTransaction` records. + +### Reports and Dashboard + +- Financial dashboard with KPIs, revenue trends, cash flow summaries, invoice status, AR aging, wallet balances, and activity. +- Standard financial reports for balance sheet, income statement, cash flow statement, trial balance, AR aging, wallet summary, and general ledger. +- Report services built around double-entry accounting data and Fleetbase transaction records. + +### Fleetbase Integrations + +- Fleet-Ops integration for purchase-rate invoice creation and order accounting. +- Storefront integration for direct storefront sale journal entries. +- Company and user observers that provision default accounts and wallets automatically. +- Invoice, payment, and accounting settings inside the Fleetbase console. + +## Architecture + +Ledger is split into two distributable packages: + +| Package | Runtime | Description | +| --- | --- | --- | +| [`fleetbase/ledger-api`](https://packagist.org/packages/fleetbase/ledger-api) | Laravel / PHP | Backend models, routes, services, migrations, gateway drivers, observers, events, resources, reports, and console commands. | +| [`@fleetbase/ledger-engine`](https://www.npmjs.com/package/@fleetbase/ledger-engine) | Ember | Fleetbase console engine for the Ledger dashboard, billing, payments, accounting, reports, and settings screens. | + +Backend routes are mounted under the configured Ledger API prefix, which defaults to `ledger`. + +| Route group | Authentication | Purpose | +| --- | --- | --- | +| `POST /ledger/webhooks/{driver}` | Public, driver verified | Payment gateway webhook callbacks. | +| `/ledger/public/invoices/{public_id}` | Public | Customer invoice view, gateway list, and payment flow. | +| `/ledger/v1/wallet/*` | API key | Customer and driver wallet API endpoints. | +| `/ledger/int/v1/*` | Fleetbase session | Console APIs for accounts, invoices, journals, wallets, transactions, gateways, settings, and reports. | + +The Ember engine mounts at the Fleetbase extension route `ledger` and exposes console sections for billing, payments, accounting, reports, and settings. + +## Requirements + +- PHP `^8.0` +- Composer +- Fleetbase Core API +- Fleetbase FleetOps API +- Node.js `>=18` +- pnpm +- Ember CLI compatible with the workspace ## Installation -### Backend +Ledger comes pre-installed with Fleetbase. In a standard Fleetbase instance, open the console sidebar and navigate to Ledger to begin. Default accounts and wallets are provisioned automatically for new companies and users. -Install the PHP packages using Composer: +For package-level installation: ```bash -composer require fleetbase/core-api -composer require fleetbase/fleetops-api composer require fleetbase/ledger-api ``` -### Frontend - -Install the Ember.js Engine/Addon: ```bash pnpm install @fleetbase/ledger-engine ``` -## Usage +If you are adding Ledger to an existing Fleetbase installation, run migrations through your normal Fleetbase deployment flow, then provision defaults for existing records: + +```bash +php artisan ledger:provision +``` + +## Development + +### Fleetbase workspace linking -### Backend +When working on Ledger inside a full Fleetbase checkout, use Fleetbase's package linker from the repository root instead of hand-editing `console/package.json`, `api/composer.json`, or `console/pnpm-workspace.yaml`. + +Install the linker once from the Fleetbase repository root: -🧹 Keep a modern codebase with **PHP CS Fixer**: ```bash -composer lint +npm link ``` -⚗️ Run static analysis using **PHPStan**: +Enable Ledger as a local development package: + ```bash -composer test:types +flb-package-linker enable ledger +flb-package-linker install ledger ``` -✅ Run unit tests using **PEST** +Use `--install` to let the linker run the required package-manager commands immediately: + ```bash -composer test:unit +flb-package-linker enable ledger --install ``` -🚀 Run the entire test suite: +Check link state with: + ```bash -composer test +flb-package-linker status +flb-package-linker doctor ``` -### Frontend +See the [Fleetbase development setup guide](https://www.fleetbase.io/docs/platform/quickstart/development-setup) for Docker mounts, local Ember dev server setup, package-linker details, and unlink/reset commands. Fleetbase runs Laravel Octane, so reload the API worker after PHP changes: -🧹 Keep a modern codebase with **ESLint**: ```bash -pnpm lint +docker compose exec application php artisan octane:reload ``` -✅ Run unit tests using **Ember/QUnit** +### Package-level development + +Install dependencies: + ```bash -pnpm test -pnpm test:ember -pnpm test:ember-compatibility +composer install +pnpm install ``` -🚀 Start the Ember Addon/Engine +Run the Ember engine locally: + ```bash pnpm start ``` -🔨 Build the Ember Addon/Engine +Frontend checks: + ```bash +pnpm lint +pnpm test pnpm build ``` +Backend checks: + +```bash +composer test:lint +composer test:types +composer test:unit +composer test +``` + +Ledger Artisan commands: + +```bash +php artisan ledger:provision +php artisan ledger:backfill-direction +php artisan ledger:update-overdue-invoices +``` + +`ledger:provision` is idempotent and can target all companies, one company, accounts only, or wallets only. `ledger:backfill-direction` fills missing transaction directions on older transaction rows. `ledger:update-overdue-invoices` marks sent or viewed invoices as overdue when their due date has passed. + +## API and Extension Points + +Ledger exposes backend services for accounting, wallets, invoices, and payments: + +- `LedgerService` creates double-entry journal entries and powers financial reports. +- `WalletService` manages wallet provisioning and balance-changing operations. +- `InvoiceService` creates and manages invoices, including order-based invoice creation. +- `PaymentService` coordinates gateway charges, refunds, setup intents, events, and gateway transaction persistence. +- `PaymentGatewayManager` resolves and initializes configured payment gateway drivers. + +Custom payment gateways can extend `AbstractGatewayDriver` and implement the `GatewayDriverInterface` contract. Gateway drivers provide a code, name, capability list, configuration schema, purchase/refund behavior, and optional webhook or tokenization support. + +Ledger also registers invoice template context variables with Fleetbase's template rendering system so invoice templates can reference invoice, transaction, account, and wallet data during rendering. + +## Documentation + +- [Ledger documentation](https://www.fleetbase.io/docs/ledger) +- [Core concepts](https://www.fleetbase.io/docs/ledger/getting-started/core-concepts) +- [Payment gateways](https://www.fleetbase.io/docs/ledger/payments/gateways) +- [Adding a payment gateway driver](https://www.fleetbase.io/docs/extension-development/recipes/adding-a-payment-gateway-driver) +- [Fleetbase development setup](https://www.fleetbase.io/docs/platform/quickstart/development-setup) + ## Contributing -See the Contributing Guide for details on how to contribute to this project. + +Contributions are welcome. Please read the [contributing guide](CONTRIBUTING.md) before opening a pull request. + +For local changes, keep frontend and backend checks focused on the area you touched and include relevant test output in your pull request. + +## Security + +Please do not report security issues in public GitHub issues. Contact Fleetbase at [hello@fleetbase.io](mailto:hello@fleetbase.io) with details so the team can coordinate a responsible fix. ## License -This project is licensed under the MIT License. + +Fleetbase Ledger is open-source software licensed under the [AGPL-3.0-or-later](LICENSE.md). diff --git a/addon/components/customer-invoice.hbs b/addon/components/customer-invoice.hbs index 91a6779..47a3fd7 100644 --- a/addon/components/customer-invoice.hbs +++ b/addon/components/customer-invoice.hbs @@ -5,11 +5,6 @@
- {{! Back to console }} -
-
-
{{! ── Loading state ─────────────────────────────────────────────────── }} @@ -205,7 +200,7 @@ @icon={{if this.submitPayment.isRunning "spinner" "lock"}} @iconSpin={{this.submitPayment.isRunning}} @text={{if this.submitPayment.isRunning "Processing…" (if this.isStripeGateway "Pay with Stripe" "Confirm Payment")}} - @disabled={{this.submitPayment.isRunning}} + @disabled={{or this.submitPayment.isRunning (not this.selectedGatewayId)}} @onClick={{perform this.submitPayment}} />
@@ -214,6 +209,17 @@ {{/if}}
+ {{else if this.cannotAcceptOnlinePayment}} +
+ +
+

Online Payment Unavailable

+

+ Online payment is not available for this invoice. Please contact the sender for payment instructions. +

+
+
+ {{! ── Paid state ───────────────────────────────────────────────── }} {{else if this.isPaid}}
@@ -251,4 +257,4 @@ {{/if}}
- \ No newline at end of file + diff --git a/addon/components/customer-invoice.js b/addon/components/customer-invoice.js index 48fe6b0..663ff0c 100644 --- a/addon/components/customer-invoice.js +++ b/addon/components/customer-invoice.js @@ -22,10 +22,10 @@ export default class CustomerInvoiceComponent extends Component { @service urlSearchParams; @service notifications; @service fetch; - @service router; @tracked invoice = null; @tracked gateways = []; + @tracked gatewaysLoaded = false; @tracked showPaymentForm = false; @tracked selectedGatewayId = null; @tracked paymentReference = ''; @@ -53,7 +53,11 @@ export default class CustomerInvoiceComponent extends Component { } get canAcceptPayment() { - return this.invoice && !this.isPaid && !this.isVoid; + return this.invoice && !this.isPaid && !this.isVoid && this.gatewaysLoaded && this.hasGateways; + } + + get cannotAcceptOnlinePayment() { + return this.invoice && !this.isPaid && !this.isVoid && this.gatewaysLoaded && !this.hasGateways; } get hasGateways() { @@ -79,6 +83,7 @@ export default class CustomerInvoiceComponent extends Component { @task({ restartable: true }) *loadInvoice() { this.error = null; + this.gatewaysLoaded = false; const id = this.invoiceId; // Detect Stripe redirect-back @@ -108,6 +113,8 @@ export default class CustomerInvoiceComponent extends Component { } catch { // Gateways are optional — do not block the invoice view if unavailable this.gateways = []; + } finally { + this.gatewaysLoaded = true; } } catch (err) { const status = err?.status ?? err?.response?.status; @@ -137,6 +144,11 @@ export default class CustomerInvoiceComponent extends Component { *submitPayment() { this.error = null; + if (!this.selectedGatewayId) { + this.error = 'Online payment is not available for this invoice.'; + return; + } + try { const data = yield this.fetch.post( `invoices/${this.invoiceId}/pay`, @@ -178,8 +190,4 @@ export default class CustomerInvoiceComponent extends Component { @action updateReference(event) { this.paymentReference = event.target.value; } - - @action transitionToConsole() { - return this.router.transitionTo('console'); - } } diff --git a/addon/components/invoice/details.hbs b/addon/components/invoice/details.hbs index e70f5b4..692b779 100644 --- a/addon/components/invoice/details.hbs +++ b/addon/components/invoice/details.hbs @@ -38,6 +38,17 @@
{{n-a @resource.currency}}
+ {{#if this.orderIdentifier}} +
+
Order
+
+ +
+
+ {{/if}} + {{#if @resource.templateName}}
Template
@@ -240,4 +251,4 @@ -
\ No newline at end of file + diff --git a/addon/components/invoice/details.js b/addon/components/invoice/details.js index 187777f..bf4d517 100644 --- a/addon/components/invoice/details.js +++ b/addon/components/invoice/details.js @@ -1,8 +1,10 @@ import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; export default class InvoiceDetailsComponent extends Component { @service invoiceActions; + @service hostRouter; /** * The public customer-facing invoice URL. @@ -11,4 +13,20 @@ export default class InvoiceDetailsComponent extends Component { get invoiceUrl() { return this.invoiceActions.getInvoiceUrl(this.args.resource); } + + get orderLabel() { + return this.args.resource?.orderTrackingLabel; + } + + get orderIdentifier() { + return this.args.resource?.orderRouteIdentifier; + } + + @action transitionToOrder() { + if (!this.orderIdentifier) { + return; + } + + return this.hostRouter.transitionTo('console.fleet-ops.operations.orders.index.details', this.orderIdentifier); + } } diff --git a/addon/components/invoice/transactions.js b/addon/components/invoice/transactions.js index a62b5b9..d608e14 100644 --- a/addon/components/invoice/transactions.js +++ b/addon/components/invoice/transactions.js @@ -6,10 +6,9 @@ import { task } from 'ember-concurrency'; /** * Invoice::Transactions component. * - * Displays all ledger-transaction records whose `context_uuid` matches the - * invoice's UUID. Uses Ember Data `store.query` so every record is properly - * normalised, cached, and available as a first-class `LedgerTransactionModel` - * instance — identical to how the global Payments → Transactions page works. + * Displays all ledger-transaction records related to the invoice. The backend + * owns the linkage rules so invoice payments, direct invoice transactions, and + * legacy context-linked records are all returned consistently. * * The invoice is passed in as `@invoice` (a `LedgerInvoiceModel` instance). * We filter by `context: invoice.uuid` because `InvoiceService::recordPayment` @@ -21,6 +20,7 @@ import { task } from 'ember-concurrency'; */ export default class InvoiceTransactionsComponent extends Component { @service store; + @service fetch; @service intl; @service transactionActions; @@ -149,11 +149,6 @@ export default class InvoiceTransactionsComponent extends Component { /** * Query ledger-transaction records scoped to this invoice. - * - * Filter param `context` maps to `TransactionFilter::context()` which - * applies `WHERE context_uuid = ?`. The invoice UUID is used (not the - * integer primary key) because that is what InvoiceService stores in - * `context_uuid` when it creates the transaction. */ @task *loadTransactions() { const invoice = this.args.invoice; @@ -165,15 +160,11 @@ export default class InvoiceTransactionsComponent extends Component { } try { - const records = yield this.store.query('ledger-transaction', { - context: invoice.uuid, - sort: '-created_at', - limit: 50, - page: this.page, - }); + const result = yield this.fetch.get(`invoices/${invoice.id}/transactions`, { sort: '-created_at', limit: 50, page: this.page }, { namespace: 'ledger/int/v1' }); + const records = (result?.transactions ?? []).map((transaction) => this.store.push(this.store.normalize('ledger-transaction', transaction))); - this.transactions = records.toArray(); - this.meta = records.meta ?? null; + this.transactions = records; + this.meta = result?.meta ?? null; } catch { this.transactions = []; this.meta = null; diff --git a/addon/components/journal/panel-header.hbs b/addon/components/journal/panel-header.hbs index 96a867b..6211e19 100644 --- a/addon/components/journal/panel-header.hbs +++ b/addon/components/journal/panel-header.hbs @@ -3,7 +3,7 @@

{{n-a @resource.number}}

- {{titleize @resource.type}} + {{titleize (humanize @resource.type)}} {{#if @resource.is_system_entry}} System {{/if}} diff --git a/addon/components/ledger-dashboard/date-range-control.hbs b/addon/components/ledger-dashboard/date-range-control.hbs new file mode 100644 index 0000000..af3df5a --- /dev/null +++ b/addon/components/ledger-dashboard/date-range-control.hbs @@ -0,0 +1,11 @@ +
+ +
diff --git a/addon/components/ledger-dashboard/date-range-control.js b/addon/components/ledger-dashboard/date-range-control.js new file mode 100644 index 0000000..3f648e6 --- /dev/null +++ b/addon/components/ledger-dashboard/date-range-control.js @@ -0,0 +1,30 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; + +export default class LedgerDashboardDateRangeControlComponent extends Component { + @service ledgerDashboard; + + @tracked dateRange; + + constructor() { + super(...arguments); + + this.dateRange = this.ledgerDashboard.dateRangeValue; + this.unsubscribeDashboard = this.ledgerDashboard.subscribe(() => { + this.dateRange = this.ledgerDashboard.dateRangeValue; + }); + } + + @action + onDateRangeChanged(selection) { + this.ledgerDashboard.setDateRange(selection); + this.dateRange = this.ledgerDashboard.dateRangeValue; + } + + willDestroy() { + super.willDestroy(...arguments); + this.unsubscribeDashboard?.(); + } +} diff --git a/addon/components/order-invoice.hbs b/addon/components/order-invoice.hbs index 3704c5c..922a0b4 100644 --- a/addon/components/order-invoice.hbs +++ b/addon/components/order-invoice.hbs @@ -35,9 +35,7 @@ View {{/if}} - +
diff --git a/addon/components/widget/activity-feed.hbs b/addon/components/widget/activity-feed.hbs index 1da3956..f137dcf 100644 --- a/addon/components/widget/activity-feed.hbs +++ b/addon/components/widget/activity-feed.hbs @@ -1,18 +1,24 @@ -
-
- Recent Journal Entries +
+
+
+
Recent Financial Activity
+
Latest journal entries posted to the ledger
+
+
-
- {{#if this.loadData.isRunning}} -
+
+ {{#if this.error}} +
{{this.error}}
+ {{else if this.loadData.isRunning}} +
{{else if this.entries.length}} -
+
{{#each this.entries as |entry|}} -
- +
+

{{n-a entry.description}}

-

{{format-date-fns entry.created_at "MMM d, yyyy"}}

+

{{format-date-fns entry.created_at "MMM d, yyyy"}} · {{n-a entry.debit}} / {{n-a entry.credit}}

{{format-currency entry.amount (or entry.currency this.companyCurrency)}} @@ -21,7 +27,7 @@ {{/each}}
{{else}} -
No recent journal entries.
+
No recent journal entries.
{{/if}}
-
\ No newline at end of file +
diff --git a/addon/components/widget/activity-feed.js b/addon/components/widget/activity-feed.js index ad6ac90..140c735 100644 --- a/addon/components/widget/activity-feed.js +++ b/addon/components/widget/activity-feed.js @@ -7,6 +7,7 @@ export default class WidgetActivityFeedComponent extends Component { @service fetch; @service currentUser; @tracked entries = []; + @tracked error = null; get companyCurrency() { return this.currentUser.company?.currency ?? this.currentUser.whoisData?.currency?.code ?? 'USD'; @@ -19,10 +20,12 @@ export default class WidgetActivityFeedComponent extends Component { @task *loadData() { try { - const response = yield this.fetch.get('reports/dashboard', {}, { namespace: 'ledger/int/v1' }); - this.entries = response?.data?.recent_journals ?? []; - } catch { + const response = yield this.fetch.get('reports/dashboard/activity', {}, { namespace: 'ledger/int/v1' }); + this.entries = response?.data?.items ?? response?.items ?? []; + this.error = null; + } catch (error) { this.entries = []; + this.error = error?.message ?? 'Unable to load activity'; } } } diff --git a/addon/components/widget/ar-aging-summary.hbs b/addon/components/widget/ar-aging-summary.hbs new file mode 100644 index 0000000..b5506e1 --- /dev/null +++ b/addon/components/widget/ar-aging-summary.hbs @@ -0,0 +1,33 @@ +
+
+
+
AR Aging Risk
+
{{or this.data.total_invoices 0}} open invoices
+
+ +
+ +
+ {{#if this.error}} +
{{this.error}}
+ {{else if this.load.isRunning}} +
+ {{else if this.buckets.length}} +
+ {{#each this.bucketRows as |bucket|}} +
+
+ {{bucket.label}} + {{bucket.invoice_count}} invoices +
+
+
+
+
+ {{/each}} +
+ {{else}} +
No outstanding receivables.
+ {{/if}} +
+
diff --git a/addon/components/widget/ar-aging-summary.js b/addon/components/widget/ar-aging-summary.js new file mode 100644 index 0000000..037ddd4 --- /dev/null +++ b/addon/components/widget/ar-aging-summary.js @@ -0,0 +1,49 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; + +export default class WidgetArAgingSummaryComponent extends Component { + @service fetch; + @service ledgerDashboard; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.unsubscribeDashboard = this.ledgerDashboard.subscribe(() => this.load.perform()); + this.load.perform(); + } + + get buckets() { + return this.data?.buckets ?? []; + } + + get bucketRows() { + return this.buckets.map((bucket) => ({ + ...bucket, + agingClass: `ledger-aging-${bucket.key?.replace(/_/g, '-')}`, + pct: Math.round(((bucket.total ?? 0) / this.maxTotal) * 100), + })); + } + + get maxTotal() { + return Math.max(...this.buckets.map((bucket) => bucket.total), 1); + } + + @task *load() { + try { + const response = yield this.fetch.get('reports/dashboard/ar-aging-summary', this.ledgerDashboard.asOfParams, { namespace: 'ledger/int/v1' }); + this.data = response?.data ?? response; + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load AR aging'; + } + } + + willDestroy() { + super.willDestroy(...arguments); + this.unsubscribeDashboard?.(); + } +} diff --git a/addon/components/widget/cash-flow-summary.hbs b/addon/components/widget/cash-flow-summary.hbs new file mode 100644 index 0000000..8931aa3 --- /dev/null +++ b/addon/components/widget/cash-flow-summary.hbs @@ -0,0 +1,23 @@ +
+
+
+
Cash Flow Summary
+
{{this.formattedNetChange}} net cash change
+
+ +
+ +
+ {{#if this.error}} +
{{this.error}}
+ {{else if this.load.isRunning}} +
+ {{else if this.data}} +
+ +
+ {{else}} +
No cash flow data yet.
+ {{/if}} +
+
diff --git a/addon/components/widget/cash-flow-summary.js b/addon/components/widget/cash-flow-summary.js new file mode 100644 index 0000000..363d766 --- /dev/null +++ b/addon/components/widget/cash-flow-summary.js @@ -0,0 +1,66 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; +import formatCurrency from '@fleetbase/ember-ui/utils/format-currency'; + +export default class WidgetCashFlowSummaryComponent extends Component { + @service fetch; + @service ledgerDashboard; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.unsubscribeDashboard = this.ledgerDashboard.subscribe(() => this.load.perform()); + this.load.perform(); + } + + get formattedNetChange() { + return formatCurrency(this.data?.net_cash_change ?? 0, this.data?.currency ?? 'USD'); + } + + get labels() { + return ['Operating', 'Financing', 'Investing']; + } + + get datasets() { + return [ + { + label: 'Net cash flow', + data: [this.data?.operating ?? 0, this.data?.financing ?? 0, this.data?.investing ?? 0], + backgroundColor: ['rgba(5, 150, 105, 0.72)', 'rgba(59, 130, 246, 0.72)', 'rgba(245, 158, 11, 0.72)'], + borderColor: ['#059669', '#2563eb', '#d97706'], + borderWidth: 1, + }, + ]; + } + + get chartOptions() { + return { + responsive: true, + maintainAspectRatio: false, + plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } }, + scales: { + x: { grid: { display: false }, ticks: { font: { size: 10 } } }, + y: { ticks: { precision: 0, font: { size: 10 } } }, + }, + }; + } + + @task *load() { + try { + const response = yield this.fetch.get('reports/dashboard/cash-flow-summary', this.ledgerDashboard.periodParams, { namespace: 'ledger/int/v1' }); + this.data = response?.data ?? response; + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load cash flow'; + } + } + + willDestroy() { + super.willDestroy(...arguments); + this.unsubscribeDashboard?.(); + } +} diff --git a/addon/components/widget/invoice-status.hbs b/addon/components/widget/invoice-status.hbs new file mode 100644 index 0000000..253eeca --- /dev/null +++ b/addon/components/widget/invoice-status.hbs @@ -0,0 +1,33 @@ +
+
+
+
Invoice Pipeline
+
{{or this.data.total_count 0}} invoices · open balance tracked by status
+
+ +
+ +
+ {{#if this.error}} +
{{this.error}}
+ {{else if this.load.isRunning}} +
+ {{else if this.hasData}} +
+ {{#each this.statusRows as |row|}} +
+
+ {{row.status}} + {{row.count}} +
+
+
+
+
+ {{/each}} +
+ {{else}} +
No invoices yet.
+ {{/if}} +
+
diff --git a/addon/components/widget/invoice-status.js b/addon/components/widget/invoice-status.js new file mode 100644 index 0000000..e181b70 --- /dev/null +++ b/addon/components/widget/invoice-status.js @@ -0,0 +1,45 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; + +export default class WidgetInvoiceStatusComponent extends Component { + @service fetch; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.load.perform(); + } + + get statuses() { + return this.data?.summary ?? []; + } + + get statusRows() { + return this.statuses.map((row) => ({ + ...row, + pct: Math.round(((row.count ?? 0) / this.maxCount) * 100), + })); + } + + get hasData() { + return (this.data?.total_count ?? 0) > 0; + } + + get maxCount() { + return Math.max(...this.statuses.map((row) => row.count), 1); + } + + @task *load() { + try { + const response = yield this.fetch.get('reports/dashboard/invoice-status', {}, { namespace: 'ledger/int/v1' }); + this.data = response?.data ?? response; + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load invoice status'; + } + } +} diff --git a/addon/components/widget/kpi-active-wallets.hbs b/addon/components/widget/kpi-active-wallets.hbs new file mode 100644 index 0000000..70463de --- /dev/null +++ b/addon/components/widget/kpi-active-wallets.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-active-wallets.js b/addon/components/widget/kpi-active-wallets.js new file mode 100644 index 0000000..ec80ad1 --- /dev/null +++ b/addon/components/widget/kpi-active-wallets.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiActiveWalletsComponent extends Component {} diff --git a/addon/components/widget/kpi-expenses.hbs b/addon/components/widget/kpi-expenses.hbs new file mode 100644 index 0000000..7e33882 --- /dev/null +++ b/addon/components/widget/kpi-expenses.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-expenses.js b/addon/components/widget/kpi-expenses.js new file mode 100644 index 0000000..e39ff60 --- /dev/null +++ b/addon/components/widget/kpi-expenses.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiExpensesComponent extends Component {} diff --git a/addon/components/widget/kpi-net-income.hbs b/addon/components/widget/kpi-net-income.hbs new file mode 100644 index 0000000..d310853 --- /dev/null +++ b/addon/components/widget/kpi-net-income.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-net-income.js b/addon/components/widget/kpi-net-income.js new file mode 100644 index 0000000..c80c148 --- /dev/null +++ b/addon/components/widget/kpi-net-income.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiNetIncomeComponent extends Component {} diff --git a/addon/components/widget/kpi-open-invoices.hbs b/addon/components/widget/kpi-open-invoices.hbs new file mode 100644 index 0000000..78f078d --- /dev/null +++ b/addon/components/widget/kpi-open-invoices.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-open-invoices.js b/addon/components/widget/kpi-open-invoices.js new file mode 100644 index 0000000..953c781 --- /dev/null +++ b/addon/components/widget/kpi-open-invoices.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiOpenInvoicesComponent extends Component {} diff --git a/addon/components/widget/kpi-outstanding-ar.hbs b/addon/components/widget/kpi-outstanding-ar.hbs new file mode 100644 index 0000000..b76cc50 --- /dev/null +++ b/addon/components/widget/kpi-outstanding-ar.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-outstanding-ar.js b/addon/components/widget/kpi-outstanding-ar.js new file mode 100644 index 0000000..621e714 --- /dev/null +++ b/addon/components/widget/kpi-outstanding-ar.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiOutstandingArComponent extends Component {} diff --git a/addon/components/widget/kpi-overdue-ar.hbs b/addon/components/widget/kpi-overdue-ar.hbs new file mode 100644 index 0000000..40a9a40 --- /dev/null +++ b/addon/components/widget/kpi-overdue-ar.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-overdue-ar.js b/addon/components/widget/kpi-overdue-ar.js new file mode 100644 index 0000000..643474c --- /dev/null +++ b/addon/components/widget/kpi-overdue-ar.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiOverdueArComponent extends Component {} diff --git a/addon/components/widget/kpi-revenue.hbs b/addon/components/widget/kpi-revenue.hbs new file mode 100644 index 0000000..1780374 --- /dev/null +++ b/addon/components/widget/kpi-revenue.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-revenue.js b/addon/components/widget/kpi-revenue.js new file mode 100644 index 0000000..1b63f2a --- /dev/null +++ b/addon/components/widget/kpi-revenue.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiRevenueComponent extends Component {} diff --git a/addon/components/widget/kpi-wallet-balance.hbs b/addon/components/widget/kpi-wallet-balance.hbs new file mode 100644 index 0000000..3d06ec2 --- /dev/null +++ b/addon/components/widget/kpi-wallet-balance.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/widget/kpi-wallet-balance.js b/addon/components/widget/kpi-wallet-balance.js new file mode 100644 index 0000000..2a4549d --- /dev/null +++ b/addon/components/widget/kpi-wallet-balance.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetKpiWalletBalanceComponent extends Component {} diff --git a/addon/components/widget/ledger-kpi-tile.hbs b/addon/components/widget/ledger-kpi-tile.hbs new file mode 100644 index 0000000..692ee5b --- /dev/null +++ b/addon/components/widget/ledger-kpi-tile.hbs @@ -0,0 +1,27 @@ +
+
+
+
+
{{this.title}}
+ {{#if this.error}} +
{{this.error}}
+ {{else if this.load.isRunning}} +
+ {{else}} +
{{this.formattedValue}}
+ {{/if}} +
+
+ +
+
+ + +
+
diff --git a/addon/components/widget/ledger-kpi-tile.js b/addon/components/widget/ledger-kpi-tile.js new file mode 100644 index 0000000..28b465e --- /dev/null +++ b/addon/components/widget/ledger-kpi-tile.js @@ -0,0 +1,100 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; +import formatCurrency from '@fleetbase/ember-ui/utils/format-currency'; + +export default class WidgetLedgerKpiTileComponent extends Component { + @service fetch; + @service ledgerDashboard; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.unsubscribeDashboard = this.ledgerDashboard.subscribe(() => this.load.perform()); + this.load.perform(); + } + + get metric() { + return this.data?.metrics?.[this.args.metric] ?? {}; + } + + get title() { + return this.args.title ?? this.metric.label ?? 'Metric'; + } + + get formattedValue() { + if (this.metric.multi_currency) { + return 'Multi'; + } + + const value = this.metric.value ?? 0; + + if (this.metric.format === 'money') { + return formatCurrency(value, this.metric.currency ?? this.data?.currency ?? 'USD'); + } + + if (this.metric.format === 'percent') { + return `${value}%`; + } + + return Number(value).toLocaleString(); + } + + get deltaText() { + const delta = this.metric.delta_percent; + if (typeof delta !== 'number') { + return 'Current'; + } + + return `${delta > 0 ? '+' : ''}${delta}%`; + } + + get deltaDirection() { + const delta = this.metric.delta_percent; + if (typeof delta !== 'number' || delta === 0) { + return 'neutral'; + } + + const isGood = this.metric.inverse ? delta < 0 : delta > 0; + + return isGood ? 'good' : 'bad'; + } + + get deltaIcon() { + if (this.deltaDirection === 'neutral') { + return 'minus'; + } + + return (this.metric.delta_percent ?? 0) > 0 ? 'arrow-up' : 'arrow-down'; + } + + get accentClass() { + return `ledger-kpi-accent-${this.args.accent ?? 'blue'} ledger-kpi-trend-${this.deltaDirection}`; + } + + get footnote() { + if (this.metric.multi_currency) { + return 'Grouped by currency'; + } + + return this.args.period ?? 'vs previous period'; + } + + @task *load() { + try { + const response = yield this.fetch.get('reports/dashboard/summary', this.ledgerDashboard.periodParams, { namespace: 'ledger/int/v1' }); + this.data = response?.data ?? response; + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load metric'; + } + } + + willDestroy() { + super.willDestroy(...arguments); + this.unsubscribeDashboard?.(); + } +} diff --git a/addon/components/widget/report-shortcuts.hbs b/addon/components/widget/report-shortcuts.hbs new file mode 100644 index 0000000..8dc8e8b --- /dev/null +++ b/addon/components/widget/report-shortcuts.hbs @@ -0,0 +1,34 @@ +
+
+
+
Financial Reports
+
Jump into the source reports behind the dashboard
+
+ +
+ +
+
+ + + Income Statement + + + + Cash Flow + + + + AR Aging + + + + Wallet Summary + + + + General Ledger + +
+
+
diff --git a/addon/components/widget/report-shortcuts.js b/addon/components/widget/report-shortcuts.js new file mode 100644 index 0000000..a8fbaeb --- /dev/null +++ b/addon/components/widget/report-shortcuts.js @@ -0,0 +1,3 @@ +import Component from '@glimmer/component'; + +export default class WidgetReportShortcutsComponent extends Component {} diff --git a/addon/components/widget/revenue-trend.hbs b/addon/components/widget/revenue-trend.hbs new file mode 100644 index 0000000..1049319 --- /dev/null +++ b/addon/components/widget/revenue-trend.hbs @@ -0,0 +1,23 @@ +
+
+
+
Revenue Trend
+
{{this.formattedRevenue}} revenue · {{this.formattedNet}} net
+
+ +
+ +
+ {{#if this.error}} +
{{this.error}}
+ {{else if this.load.isRunning}} +
+ {{else if this.data}} +
+ +
+ {{else}} +
No revenue data yet.
+ {{/if}} +
+
diff --git a/addon/components/widget/revenue-trend.js b/addon/components/widget/revenue-trend.js new file mode 100644 index 0000000..6c73d47 --- /dev/null +++ b/addon/components/widget/revenue-trend.js @@ -0,0 +1,144 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { task } from 'ember-concurrency'; +import getCurrency from '@fleetbase/ember-ui/utils/get-currency'; + +export default class WidgetRevenueTrendComponent extends Component { + @service fetch; + @service ledgerDashboard; + + @tracked data = null; + @tracked error = null; + + constructor() { + super(...arguments); + this.unsubscribeDashboard = this.ledgerDashboard.subscribe(() => this.load.perform()); + this.load.perform(); + } + + get queryParams() { + return this.ledgerDashboard.periodParams; + } + + get formattedRevenue() { + return this.formatMinorCurrency(this.data?.summary?.revenue ?? 0); + } + + get formattedNet() { + return this.formatMinorCurrency(this.data?.summary?.net ?? 0); + } + + get currencyCode() { + return this.data?.currency ?? 'USD'; + } + + get currency() { + return getCurrency(this.currencyCode) ?? getCurrency('USD'); + } + + get currencyDivisor() { + const precision = Number(this.currency?.precision ?? 2); + + return precision > 0 ? 10 ** precision : 1; + } + + get chartDatasets() { + return ( + this.data?.datasets?.map((dataset) => ({ + ...dataset, + data: dataset.data?.map((value) => this.normalizeMoneyValue(value)) ?? [], + })) ?? [] + ); + } + + normalizeMoneyValue(value) { + const numericValue = Number(value); + + if (!Number.isFinite(numericValue)) { + return value; + } + + return numericValue / this.currencyDivisor; + } + + formatChartCurrency(value) { + const numericValue = Number(value); + + if (!Number.isFinite(numericValue)) { + return value; + } + + const precision = Number(this.currency?.precision ?? 2); + + try { + return new Intl.NumberFormat(undefined, { + style: 'currency', + currency: this.currencyCode, + minimumFractionDigits: precision, + maximumFractionDigits: precision, + }).format(numericValue); + } catch { + return `${this.currency?.symbol ?? this.currencyCode} ${numericValue.toLocaleString()}`; + } + } + + formatMinorCurrency(value) { + return this.formatChartCurrency(this.normalizeMoneyValue(value)); + } + + get chartOptions() { + return { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'index', intersect: false }, + plugins: { + legend: { + position: 'bottom', + labels: { + usePointStyle: true, + pointStyle: 'circle', + boxWidth: 6, + boxHeight: 6, + padding: 10, + font: { size: 10, weight: '600' }, + }, + }, + tooltip: { + mode: 'index', + intersect: false, + callbacks: { + label: (context) => `${context.dataset?.label ?? 'Value'}: ${this.formatChartCurrency(context.parsed?.y ?? context.raw)}`, + }, + }, + }, + scales: { + x: { grid: { display: false }, ticks: { autoSkip: true, maxTicksLimit: 6, maxRotation: 0, minRotation: 0, font: { size: 10 } } }, + y: { + beginAtZero: true, + ticks: { + precision: this.currency?.precision ?? 2, + font: { size: 10 }, + callback: (value) => this.formatChartCurrency(value), + }, + }, + }, + elements: { point: { radius: 0, hoverRadius: 4 } }, + }; + } + + @task *load() { + try { + const response = yield this.fetch.get('reports/dashboard/revenue-trend', this.queryParams, { namespace: 'ledger/int/v1' }); + this.data = response?.data ?? response; + this.error = null; + } catch (error) { + this.error = error?.message ?? 'Unable to load revenue trend'; + } + } + + willDestroy() { + super.willDestroy(...arguments); + this.unsubscribeDashboard?.(); + } +} diff --git a/addon/components/widget/wallet-balances.hbs b/addon/components/widget/wallet-balances.hbs index b30df59..3fa50e6 100644 --- a/addon/components/widget/wallet-balances.hbs +++ b/addon/components/widget/wallet-balances.hbs @@ -1,22 +1,45 @@ -
-
- Wallet Balances +
+
+
+
Wallet Balances
+
Grouped by currency with top active wallets
+
+
-
- {{#if this.loadData.isRunning}} -
+ +
+ {{#if this.error}} +
{{this.error}}
+ {{else if this.loadData.isRunning}} +
{{else if this.totals.length}} - {{#each this.totals as |row|}} -
-
- {{row.currency}} - ({{row.count}} wallet{{if (gt row.count 1) "s"}}) +
+ {{#each this.totals as |row|}} +
+
+
{{row.currency}}
+
{{row.count}} wallet{{if (gt row.count 1) "s"}}
+
+
{{format-currency row.total (or row.currency this.companyCurrency)}}
+
+ {{/each}} +
+ {{#if this.topWallets.length}} +
+
Top Wallets
+
+ {{#each this.topWallets as |wallet index|}} +
+ {{add index 1}} + {{n-a (or wallet.subject.name wallet.name wallet.wallet_public_id)}} + {{format-currency wallet.balance (or wallet.currency this.companyCurrency)}} +
+ {{/each}}
- {{format-currency row.total (or row.currency this.companyCurrency)}}
- {{/each}} + {{/if}} {{else}} -
No wallet data.
+
No wallet data.
{{/if}}
-
\ No newline at end of file +
diff --git a/addon/components/widget/wallet-balances.js b/addon/components/widget/wallet-balances.js index 7bdbd5f..00ba60b 100644 --- a/addon/components/widget/wallet-balances.js +++ b/addon/components/widget/wallet-balances.js @@ -6,7 +6,10 @@ import { task } from 'ember-concurrency'; export default class WidgetWalletBalancesComponent extends Component { @service fetch; @service currentUser; + @service ledgerDashboard; @tracked totals = null; + @tracked topWallets = []; + @tracked error = null; get companyCurrency() { return this.currentUser.company?.currency ?? this.currentUser.whoisData?.currency?.code ?? 'USD'; @@ -14,15 +17,26 @@ export default class WidgetWalletBalancesComponent extends Component { constructor() { super(...arguments); + this.unsubscribeDashboard = this.ledgerDashboard.subscribe(() => this.loadData.perform()); this.loadData.perform(); } @task *loadData() { try { - const response = yield this.fetch.get('reports/dashboard', {}, { namespace: 'ledger/int/v1' }); - this.totals = response?.data?.kpis?.wallet_totals ?? null; - } catch { + const response = yield this.fetch.get('reports/dashboard/wallet-balances', this.ledgerDashboard.walletPeriodParams, { namespace: 'ledger/int/v1' }); + const data = response?.data ?? response; + this.totals = data?.totals ?? []; + this.topWallets = data?.top_wallets ?? []; + this.error = null; + } catch (error) { this.totals = null; + this.topWallets = []; + this.error = error?.message ?? 'Unable to load wallet balances'; } } + + willDestroy() { + super.willDestroy(...arguments); + this.unsubscribeDashboard?.(); + } } diff --git a/addon/controllers/billing/invoices/index.js b/addon/controllers/billing/invoices/index.js index 0f5c2eb..b4344a4 100644 --- a/addon/controllers/billing/invoices/index.js +++ b/addon/controllers/billing/invoices/index.js @@ -1,10 +1,12 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class BillingInvoicesIndexController extends Controller { @service invoiceActions; @service tableContext; + @service hostRouter; @service intl; @tracked queryParams = ['page', 'limit', 'sort', 'query', 'status', 'customer_uuid']; @@ -63,6 +65,14 @@ export default class BillingInvoicesIndexController extends Controller { resizable: true, sortable: false, }, + { + label: 'Order', + valuePath: 'orderTrackingLabel', + cellComponent: 'table/cell/anchor', + action: this.viewOrder, + resizable: true, + sortable: false, + }, { label: this.intl.t('column.status'), valuePath: 'status', @@ -174,4 +184,13 @@ export default class BillingInvoicesIndexController extends Controller { }, ]; } + + @action viewOrder(invoice) { + const orderId = invoice?.orderRouteIdentifier; + if (!orderId) { + return; + } + + return this.hostRouter.transitionTo('console.fleet-ops.operations.orders.index.details', orderId); + } } diff --git a/addon/extension.js b/addon/extension.js index 1b00b28..9e93eb7 100644 --- a/addon/extension.js +++ b/addon/extension.js @@ -100,6 +100,20 @@ export default { }) ); + // ── Storefront order details tab: Invoice ──────────────────────────── + // Reuses the same order-invoice component inside the Storefront order + // details panel so commerce orders expose their generated invoice. + menuService.registerMenuItem( + 'storefront:component:order:details', + new MenuItem({ + title: 'Invoice', + route: 'orders.index.view.virtual', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'order-invoice'), + icon: 'file-invoice-dollar', + slug: 'invoice', + }) + ); + // Register dashboard and widgets this.registerWidgets(widgetService); }, @@ -107,86 +121,211 @@ export default { registerWidgets(widgetService) { const widgets = [ new Widget({ - id: 'ledger-overview', - name: 'Financial Overview', - description: 'Key financial KPIs: revenue, expenses, net income, and outstanding AR for the current period.', - icon: 'gauge-high', - component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/overview'), - grid_options: { w: 12, h: 4, minW: 8, minH: 4 }, - options: { title: 'Financial Overview' }, + id: 'ledger-kpi-revenue', + name: 'Revenue', + description: 'Total revenue for the current period with comparison.', + icon: 'sack-dollar', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/kpi-revenue'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', default: true, }), - new Widget({ - id: 'ledger-revenue-chart', - name: 'Revenue Chart', - description: 'Daily revenue trend chart for the current period.', + id: 'ledger-kpi-expenses', + name: 'Expenses', + description: 'Total expenses for the current period with comparison.', + icon: 'receipt', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/kpi-expenses'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'ledger-kpi-net-income', + name: 'Net Income', + description: 'Revenue less expenses for the current period.', icon: 'chart-line', - component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/revenue-chart'), - grid_options: { w: 8, h: 7, minW: 6, minH: 6 }, - options: { title: 'Revenue Chart' }, + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/kpi-net-income'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', default: true, }), - new Widget({ - id: 'ledger-invoice-summary', - name: 'Invoice Summary', - description: 'Breakdown of invoices by status: draft, sent, paid, overdue, and cancelled.', + id: 'ledger-kpi-outstanding-ar', + name: 'Outstanding AR', + description: 'Unpaid receivables balance across open invoices.', icon: 'file-invoice-dollar', - component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/invoice-summary'), - grid_options: { w: 4, h: 6, minW: 3, minH: 5 }, - options: { title: 'Invoice Summary' }, + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/kpi-outstanding-ar'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'ledger-kpi-overdue-ar', + name: 'Overdue AR', + description: 'Overdue receivables requiring collection attention.', + icon: 'clock', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/kpi-overdue-ar'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'ledger-kpi-open-invoices', + name: 'Open Invoices', + description: 'Draft, sent, and overdue invoices still requiring action.', + icon: 'file-circle-exclamation', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/kpi-open-invoices'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'ledger-kpi-wallet-balance', + name: 'Wallet Balance', + description: 'Active wallet balances, grouped by currency when needed.', + icon: 'wallet', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/kpi-wallet-balance'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', + default: true, + }), + new Widget({ + id: 'ledger-kpi-active-wallets', + name: 'Active Wallets', + description: 'Count of active wallets with current balances.', + icon: 'wallet', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/kpi-active-wallets'), + grid_options: { w: 3, h: 4, minW: 3, minH: 4 }, + category: 'KPI Tiles', default: true, }), + new Widget({ + id: 'ledger-revenue-trend', + name: 'Revenue Trend', + description: 'Revenue and expense trend over selectable periods.', + icon: 'chart-line', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/revenue-trend'), + grid_options: { w: 6, h: 9, minW: 5, minH: 8 }, + category: 'Analytics', + default: true, + }), + new Widget({ + id: 'ledger-cash-flow-summary', + name: 'Cash Flow Summary', + description: 'Operating, financing, and investing cash movement.', + icon: 'money-bill-transfer', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/cash-flow-summary'), + grid_options: { w: 6, h: 9, minW: 5, minH: 8 }, + category: 'Analytics', + default: true, + }), + new Widget({ + id: 'ledger-invoice-status', + name: 'Invoice Pipeline', + description: 'Counts and balances by invoice status.', + icon: 'file-invoice-dollar', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/invoice-status'), + grid_options: { w: 4, h: 8, minW: 4, minH: 7 }, + category: 'Operations', + default: true, + }), + new Widget({ + id: 'ledger-ar-aging-summary', + name: 'AR Aging Risk', + description: 'Condensed accounts receivable aging buckets.', + icon: 'clock', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/ar-aging-summary'), + grid_options: { w: 4, h: 8, minW: 4, minH: 7 }, + category: 'Operations', + default: true, + }), new Widget({ id: 'ledger-wallet-balances', name: 'Wallet Balances', description: 'Total wallet balances grouped by currency across all driver and customer wallets.', icon: 'wallet', component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/wallet-balances'), - grid_options: { w: 4, h: 5, minW: 3, minH: 4 }, - options: { title: 'Wallet Balances' }, + grid_options: { w: 4, h: 8, minW: 4, minH: 7 }, + category: 'Operations', default: true, }), - new Widget({ id: 'ledger-activity-feed', - name: 'Recent Journal Entries', + name: 'Recent Financial Activity', description: 'Live feed of the most recent double-entry journal entries in the ledger.', icon: 'book', component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/activity-feed'), - grid_options: { w: 8, h: 11, minW: 6, minH: 8 }, - options: { title: 'Recent Journal Entries' }, + grid_options: { w: 8, h: 10, minW: 6, minH: 8 }, + category: 'Operations', default: true, }), - new Widget({ - id: 'ledger-ar-aging', - name: 'AR Aging Summary', - description: 'Accounts receivable aging buckets: current, 1–30, 31–60, 61–90, and 90+ days overdue.', - icon: 'clock', - component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/ar-aging'), - grid_options: { w: 6, h: 5, minW: 5, minH: 4 }, - options: { title: 'AR Aging Summary' }, + id: 'ledger-report-shortcuts', + name: 'Financial Reports', + description: 'Shortcuts into Ledger financial reports.', + icon: 'file-lines', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/report-shortcuts'), + grid_options: { w: 4, h: 10, minW: 4, minH: 7 }, + category: 'Reports', + default: true, }), new Widget({ - id: 'ledger-top-wallets', - name: 'Top Driver Wallets', - description: 'Leaderboard of the top 10 driver wallets by current balance.', - icon: 'ranking-star', - component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/top-wallets'), - grid_options: { w: 6, h: 6, minW: 4, minH: 5 }, - options: { title: 'Top Driver Wallets' }, + id: 'ledger-overview', + name: 'Financial Overview (Legacy)', + description: 'Legacy grouped KPI widget. Replaced by individual Ledger KPI tiles.', + icon: 'gauge-high', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/overview'), + grid_options: { w: 12, h: 4, minW: 8, minH: 4 }, + options: { title: 'Financial Overview' }, + category: 'Legacy', + default: false, + }), + new Widget({ + id: 'ledger-revenue-chart', + name: 'Revenue Chart (Legacy)', + description: 'Legacy daily revenue bar widget. Replaced by Revenue Trend.', + icon: 'chart-line', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/revenue-chart'), + grid_options: { w: 8, h: 6, minW: 6, minH: 6 }, + options: { title: 'Revenue Chart' }, + category: 'Legacy', + default: false, + }), + new Widget({ + id: 'ledger-invoice-summary', + name: 'Invoice Summary (Legacy)', + description: 'Legacy invoice counts widget. Replaced by Invoice Pipeline.', + icon: 'file-invoice-dollar', + component: new ExtensionComponent('@fleetbase/ledger-engine', 'widget/invoice-summary'), + grid_options: { w: 4, h: 6, minW: 3, minH: 5 }, + options: { title: 'Invoice Summary' }, + category: 'Legacy', + default: false, }), ]; + const getWidgetById = (id = null, mutate = null) => { + if (!id) return null; + const widget = widgets.find((w) => w.id === id); + if (typeof mutate === 'function') { + mutate(widget); + } + return widget; + }; + widgetService.registerDashboard('ledger'); widgetService.registerWidgets('ledger', widgets); - widgetService.registerWidgets( - 'dashboard', - widgets.filter((w) => ['ledger-overview', 'ledger-revenue-chart', 'ledger-invoice-summary'].includes(w.id)) - ); + widgetService.registerWidgets('dashboard', [ + getWidgetById('ledger-activity-feed', (widget) => { + widget.withGridOptions({ w: 6, minW: 6, h: 8, minH: 8 }); + }), + getWidgetById('ledger-kpi-revenue'), + getWidgetById('ledger-kpi-net-income'), + getWidgetById('ledger-kpi-outstanding-ar'), + getWidgetById('ledger-kpi-expenses'), + ]); }, }; diff --git a/addon/models/ledger-invoice.js b/addon/models/ledger-invoice.js index a7b417d..f89c23f 100644 --- a/addon/models/ledger-invoice.js +++ b/addon/models/ledger-invoice.js @@ -16,6 +16,8 @@ export default class LedgerInvoiceModel extends Model { @attr('string') customer_uuid; @attr('string') customer_type; @attr('string') order_uuid; + @attr('string') order_public_id; + @attr('string') order_tracking_number; @attr('string') transaction_uuid; @attr('string') template_uuid; @@ -64,6 +66,14 @@ export default class LedgerInvoiceModel extends Model { return this.total_amount; } + @computed('order_tracking_number', 'order_public_id', 'order_uuid') get orderTrackingLabel() { + return this.order_tracking_number ?? this.order_public_id ?? this.order_uuid; + } + + @computed('order_public_id', 'order_uuid') get orderRouteIdentifier() { + return this.order_public_id ?? this.order_uuid; + } + // ------------------------------------------------------------------------- // Customer convenience accessors // ------------------------------------------------------------------------- diff --git a/addon/services/ledger-dashboard.js b/addon/services/ledger-dashboard.js new file mode 100644 index 0000000..6c525f0 --- /dev/null +++ b/addon/services/ledger-dashboard.js @@ -0,0 +1,94 @@ +import Service from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +function formatDate(date) { + const year = date.getFullYear(); + const month = `${date.getMonth() + 1}`.padStart(2, '0'); + const day = `${date.getDate()}`.padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +export default class LedgerDashboardService extends Service { + @tracked startDate = null; + @tracked endDate = null; + @tracked dateRange = null; + @tracked version = 0; + + subscribers = new Set(); + + constructor() { + super(...arguments); + this.resetPeriod({ notify: false }); + } + + get periodParams() { + return { + start_date: this.startDate, + end_date: this.endDate, + }; + } + + get walletPeriodParams() { + return { + date_from: this.startDate, + date_to: this.endDate, + }; + } + + get asOfParams() { + return { + as_of_date: this.endDate, + }; + } + + get periodLabel() { + return this.startDate && this.endDate ? `${this.startDate} - ${this.endDate}` : 'Month to date'; + } + + get dateRangeValue() { + return this.startDate && this.endDate ? `${this.startDate},${this.endDate}` : null; + } + + setDateRange({ formattedDate } = {}) { + if (Array.isArray(formattedDate) && formattedDate.length === 2) { + this.setPeriod(formattedDate[0], formattedDate[1]); + return; + } + + this.resetPeriod(); + } + + setPeriod(startDate, endDate) { + this.startDate = startDate; + this.endDate = endDate; + this.dateRange = [startDate, endDate]; + this.notify(); + } + + resetPeriod({ notify = true } = {}) { + const today = new Date(); + const start = new Date(today.getFullYear(), today.getMonth(), 1); + + this.startDate = formatDate(start); + this.endDate = formatDate(today); + this.dateRange = [this.startDate, this.endDate]; + + if (notify) { + this.notify(); + } + } + + subscribe(callback) { + this.subscribers.add(callback); + + return () => { + this.subscribers.delete(callback); + }; + } + + notify() { + this.version++; + this.subscribers.forEach((callback) => callback(this)); + } +} diff --git a/addon/styles/ledger-engine.css b/addon/styles/ledger-engine.css index dcfb8d4..33f6ee9 100644 --- a/addon/styles/ledger-engine.css +++ b/addon/styles/ledger-engine.css @@ -46,3 +46,581 @@ .ledger-template-builder-outlet:empty { display: none; } + +.ledger-dashboard-widget { + animation: ledger-widget-fade-up 360ms cubic-bezier(0.16, 1, 0.3, 1) both; + border-color: #e5e7eb; + background-color: #fff; + transition: + border-color 180ms ease, + box-shadow 180ms ease, + transform 180ms ease; +} + +.ledger-dashboard-widget:hover { + border-color: #cbd5e1; + box-shadow: 0 10px 28px -22px rgb(15 23 42 / 45%); +} + +@keyframes ledger-widget-fade-up { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.ledger-dashboard-page { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + gap: 1rem; + border-bottom: 1px solid #e5e7eb; + background-color: #fff; + padding: 1rem; + box-shadow: 0 1px 2px rgb(15 23 42 / 6%); +} + +body[data-theme='dark'] .ledger-dashboard-page { + border-bottom-color: #1f2937; + background-color: #111827; + box-shadow: 0 1px 2px rgb(2 6 23 / 30%); +} + +.ledger-dashboard-title h1 { + color: #111827; + font-size: 1.05rem; + font-weight: 800; + line-height: 1.2; +} + +body[data-theme='dark'] .ledger-dashboard-title h1 { + color: #f9fafb; +} + +.ledger-dashboard-actions { + justify-content: flex-end; +} + +.ledger-dashboard-period-control { + display: flex; + min-width: 0; +} + +.ledger-dashboard-period-control .fleetbase-date-picker, +.ledger-dashboard-period-control .form-input, +.ledger-dashboard-period-control button.btn { + min-height: 30px !important; + height: 30px !important; +} + +.ledger-dashboard-create-wrapper { + padding: 1rem; +} + +body[data-theme='dark'] .ledger-dashboard-widget { + border-color: #374151; + background-color: #1f2937; +} + +.ledger-widget-header { + display: flex; + min-height: 3.25rem; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + border-bottom: 1px solid #e5e7eb; + padding: 0.75rem; +} + +body[data-theme='dark'] .ledger-widget-header { + border-bottom-color: #374151; +} + +.ledger-widget-title { + overflow: hidden; + color: #111827; + font-size: 0.82rem; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + white-space: nowrap; +} + +body[data-theme='dark'] .ledger-widget-title { + color: #f9fafb; +} + +.ledger-widget-subtitle { + margin-top: 0.15rem; + overflow: hidden; + color: #6b7280; + font-size: 0.69rem; + line-height: 1.25; + text-overflow: ellipsis; + white-space: nowrap; +} + +body[data-theme='dark'] .ledger-widget-subtitle { + color: #9ca3af; +} + +.ledger-widget-body { + min-height: 0; + flex: 1 1 auto; + overflow: auto; + padding: 0.9rem; +} + +.ledger-widget-empty { + display: flex; + min-height: 7rem; + flex: 1 1 auto; + align-items: center; + justify-content: center; + padding: 1rem; + text-align: center; + color: #9ca3af; + font-size: 0.8rem; +} + +.ledger-icon-button { + display: inline-flex; + height: 1.75rem; + width: 1.75rem; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + color: #64748b; +} + +.ledger-icon-button:hover { + background-color: #f1f5f9; + color: #334155; +} + +body[data-theme='dark'] .ledger-icon-button:hover { + background-color: #374151; + color: #e5e7eb; +} + +.ledger-kpi-tile { + transition: + transform 160ms ease-out, + box-shadow 160ms ease-out, + border-color 160ms ease-out; +} + +.ledger-kpi-tile:hover { + transform: translateY(-1px); + box-shadow: 0 8px 24px -12px rgb(15 23 42 / 35%); +} + +.ledger-kpi-value-loaded { + animation: ledger-kpi-pop 380ms cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.ledger-kpi-footer { + animation: ledger-row-fade-in 320ms ease-out 80ms both; +} + +@keyframes ledger-kpi-pop { + 0% { + opacity: 0; + transform: translateY(4px) scale(0.98); + } + + 70% { + transform: translateY(0) scale(1.012); + } + + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.ledger-kpi-value { + color: #111827; + font-size: 1.55rem; + font-weight: 800; + line-height: 1.05; +} + +body[data-theme='dark'] .ledger-kpi-value { + color: #f9fafb; +} + +.ledger-kpi-icon { + width: 2rem; + height: 2rem; + color: #64748b; + background-color: rgb(100 116 139 / 12%); +} + +.ledger-kpi-delta { + color: #475569; + background-color: rgb(100 116 139 / 12%); +} + +.ledger-kpi-trend-good .ledger-kpi-delta { + color: #047857; + background-color: rgb(16 185 129 / 12%); +} + +.ledger-kpi-trend-bad .ledger-kpi-delta { + color: #be123c; + background-color: rgb(244 63 94 / 12%); +} + +.ledger-kpi-accent-green { + border-color: rgb(16 185 129 / 28%); + background-image: linear-gradient(135deg, rgb(16 185 129 / 10%) 0%, rgb(16 185 129 / 0%) 62%); +} + +.ledger-kpi-accent-blue { + border-color: rgb(59 130 246 / 28%); + background-image: linear-gradient(135deg, rgb(59 130 246 / 10%) 0%, rgb(59 130 246 / 0%) 62%); +} + +.ledger-kpi-accent-amber { + border-color: rgb(245 158 11 / 32%); + background-image: linear-gradient(135deg, rgb(245 158 11 / 12%) 0%, rgb(245 158 11 / 0%) 62%); +} + +.ledger-kpi-accent-rose { + border-color: rgb(244 63 94 / 28%); + background-image: linear-gradient(135deg, rgb(244 63 94 / 10%) 0%, rgb(244 63 94 / 0%) 62%); +} + +.ledger-kpi-accent-violet { + border-color: rgb(139 92 246 / 26%); + background-image: linear-gradient(135deg, rgb(139 92 246 / 10%) 0%, rgb(139 92 246 / 0%) 62%); +} + +.ledger-chart-widget { + overflow: hidden; +} + +.ledger-dashboard-widget .ui-chart { + position: relative; + width: 100%; + height: 100%; + min-height: 0; +} + +.ledger-chart-body { + display: flex; + min-height: 0; + min-width: 0; + flex: 1 1 auto; + align-items: stretch; + justify-content: stretch; + padding: 0.75rem; +} + +.ledger-chart-frame { + position: relative; + min-height: 0; + width: 100%; + flex: 1 1 auto; + animation: ledger-chart-reveal 420ms cubic-bezier(0.16, 1, 0.3, 1) both; +} + +@keyframes ledger-chart-reveal { + from { + opacity: 0; + transform: scale(0.985); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +.ledger-dashboard-widget .ui-chart > canvas { + position: absolute; + inset: 0; + width: 100% !important; + height: 100% !important; +} + +.ledger-period-switcher { + display: inline-flex; + gap: 0.15rem; + border-radius: 0.45rem; + background-color: #f1f5f9; + padding: 0.15rem; +} + +body[data-theme='dark'] .ledger-period-switcher { + background-color: #111827; +} + +.ledger-period-switcher button { + border-radius: 0.35rem; + padding: 0.2rem 0.45rem; + color: #64748b; + font-size: 0.62rem; + font-weight: 800; +} + +.ledger-period-switcher button.active { + background-color: #2563eb; + color: #fff; +} + +.ledger-progress-track { + height: 0.45rem; + overflow: hidden; + border-radius: 999px; + background-color: #eef2f7; +} + +body[data-theme='dark'] .ledger-progress-track { + background-color: #111827; +} + +.ledger-progress-bar { + animation: ledger-progress-fill 520ms cubic-bezier(0.16, 1, 0.3, 1) both; + height: 100%; + min-width: 0.35rem; + border-radius: inherit; + background-color: #2563eb; + transform-origin: left center; +} + +@keyframes ledger-progress-fill { + from { + transform: scaleX(0); + } + + to { + transform: scaleX(1); + } +} + +.ledger-status-paid, +.ledger-aging-current { + background-color: #059669; +} + +.ledger-status-overdue, +.ledger-aging-61-90, +.ledger-aging-over-90 { + background-color: #dc2626; +} + +.ledger-status-sent, +.ledger-aging-1-30 { + background-color: #2563eb; +} + +.ledger-status-draft, +.ledger-status-cancelled, +.ledger-status-void { + background-color: #94a3b8; +} + +.ledger-aging-31-60 { + background-color: #d97706; +} + +.ledger-balance-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + border-radius: 0.5rem; + border: 1px solid #e5e7eb; + padding: 0.65rem; + transition: + border-color 160ms ease, + background-color 160ms ease, + transform 160ms ease; +} + +.ledger-balance-row:hover { + border-color: #bfdbfe; + background-color: #f8fafc; + transform: translateX(2px); +} + +body[data-theme='dark'] .ledger-balance-row { + border-color: #374151; +} + +body[data-theme='dark'] .ledger-balance-row:hover { + border-color: #1d4ed8; + background-color: rgb(17 24 39 / 58%); +} + +.ledger-activity-icon { + display: flex; + height: 1.65rem; + width: 1.65rem; + flex-shrink: 0; + align-items: center; + justify-content: center; + border-radius: 0.45rem; + color: #2563eb; + background-color: rgb(37 99 235 / 10%); +} + +.ledger-report-grid { + display: grid; + grid-template-columns: repeat(1, minmax(0, 1fr)); + gap: 0.5rem; +} + +.ledger-report-body { + overflow-x: hidden; +} + +.ledger-report-link { + display: flex; + min-width: 0; + align-items: center; + gap: 0.65rem; + border-radius: 0.5rem; + border: 1px solid #e5e7eb; + padding: 0.7rem; + color: #374151; + font-size: 0.8rem; + font-weight: 700; + transition: + border-color 160ms ease, + background-color 160ms ease, + color 160ms ease, + transform 160ms ease; +} + +.ledger-report-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.ledger-report-link:hover { + border-color: #93c5fd; + background-color: #eff6ff; + color: #1d4ed8; + transform: translateX(2px); +} + +body[data-theme='dark'] .ledger-report-link { + border-color: #374151; + color: #d1d5db; +} + +body[data-theme='dark'] .ledger-report-link:hover { + border-color: #2563eb; + background-color: rgb(37 99 235 / 12%); + color: #bfdbfe; +} + +.ledger-animated-row { + animation: ledger-row-fade-in 340ms cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.ledger-animated-row:nth-child(1) { + animation-delay: 40ms; +} + +.ledger-animated-row:nth-child(2) { + animation-delay: 80ms; +} + +.ledger-animated-row:nth-child(3) { + animation-delay: 120ms; +} + +.ledger-animated-row:nth-child(4) { + animation-delay: 160ms; +} + +.ledger-animated-row:nth-child(5) { + animation-delay: 200ms; +} + +.ledger-animated-row:nth-child(6) { + animation-delay: 240ms; +} + +.ledger-animated-row:nth-child(7) { + animation-delay: 280ms; +} + +.ledger-animated-row:nth-child(8) { + animation-delay: 320ms; +} + +.ledger-animated-row:nth-child(9) { + animation-delay: 360ms; +} + +.ledger-animated-row:nth-child(10) { + animation-delay: 400ms; +} + +@keyframes ledger-row-fade-in { + from { + opacity: 0; + transform: translateY(7px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .ledger-dashboard-widget, + .ledger-kpi-value-loaded, + .ledger-kpi-footer, + .ledger-chart-frame, + .ledger-progress-bar, + .ledger-animated-row { + animation: none !important; + } + + .ledger-dashboard-widget, + .ledger-kpi-tile, + .ledger-balance-row, + .ledger-report-link { + transition: none !important; + } + + .ledger-dashboard-widget, + .ledger-dashboard-widget:hover, + .ledger-kpi-tile:hover, + .ledger-balance-row:hover, + .ledger-report-link:hover { + transform: none !important; + } +} + +@media (width <= 767px) { + .ledger-dashboard-page { + align-items: flex-start; + flex-direction: column; + } + + .ledger-dashboard-actions { + width: 100%; + justify-content: flex-start; + } + + .ledger-dashboard-period-control, + .ledger-dashboard-period-control .form-input { + width: 100%; + } +} diff --git a/addon/templates/billing/invoices/index/details.hbs b/addon/templates/billing/invoices/index/details.hbs index 1484ea8..a53eb07 100644 --- a/addon/templates/billing/invoices/index/details.hbs +++ b/addon/templates/billing/invoices/index/details.hbs @@ -7,7 +7,7 @@ @onOverlayReady={{fn (mut this.overlay)}} @headerClass="no-bottom-border" @bodyClass="no-scroll" - @width="900px" + @width="700px" > {{outlet}} diff --git a/addon/templates/billing/invoices/index/edit.hbs b/addon/templates/billing/invoices/index/edit.hbs index ea490fc..b49e2f3 100644 --- a/addon/templates/billing/invoices/index/edit.hbs +++ b/addon/templates/billing/invoices/index/edit.hbs @@ -6,7 +6,7 @@ @saveTask={{this.save}} @onPressCancel={{this.cancel}} @onOverlayReady={{fn (mut this.overlay)}} - @width="900px" + @width="700px" > diff --git a/addon/templates/billing/invoices/index/new.hbs b/addon/templates/billing/invoices/index/new.hbs index 3628831..55ae7ed 100644 --- a/addon/templates/billing/invoices/index/new.hbs +++ b/addon/templates/billing/invoices/index/new.hbs @@ -7,7 +7,7 @@ @saveTask={{this.save}} @onPressCancel={{transition-to "billing.invoices.index"}} @onOverlayReady={{fn (mut this.overlay)}} - @width="900px" + @width="700px" > diff --git a/addon/templates/home.hbs b/addon/templates/home.hbs index d3a8ccf..2c8c8e4 100644 --- a/addon/templates/home.hbs +++ b/addon/templates/home.hbs @@ -1,5 +1,16 @@ - + + + {{outlet}} - \ No newline at end of file + diff --git a/app/components/ledger-dashboard/date-range-control.js b/app/components/ledger-dashboard/date-range-control.js new file mode 100644 index 0000000..16f5e2c --- /dev/null +++ b/app/components/ledger-dashboard/date-range-control.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/ledger-dashboard/date-range-control'; diff --git a/app/components/ledger/dashboard/activity-feed.js b/app/components/ledger/dashboard/activity-feed.js index 939b4ae..fef087f 100644 --- a/app/components/ledger/dashboard/activity-feed.js +++ b/app/components/ledger/dashboard/activity-feed.js @@ -1 +1 @@ -export { default } from '@fleetbase/ledger-engine/components/dashboard/activity-feed'; +export { default } from '@fleetbase/ledger-engine/components/widget/activity-feed'; diff --git a/app/components/ledger/dashboard/invoice-summary.js b/app/components/ledger/dashboard/invoice-summary.js index 9c2afc3..2e61b2a 100644 --- a/app/components/ledger/dashboard/invoice-summary.js +++ b/app/components/ledger/dashboard/invoice-summary.js @@ -1 +1 @@ -export { default } from '@fleetbase/ledger-engine/components/dashboard/invoice-summary'; +export { default } from '@fleetbase/ledger-engine/components/widget/invoice-status'; diff --git a/app/components/ledger/dashboard/kpi-metric.js b/app/components/ledger/dashboard/kpi-metric.js index 205cf87..93cc533 100644 --- a/app/components/ledger/dashboard/kpi-metric.js +++ b/app/components/ledger/dashboard/kpi-metric.js @@ -1 +1 @@ -export { default } from '@fleetbase/ledger-engine/components/dashboard/kpi-metric'; +export { default } from '@fleetbase/ledger-engine/components/widget/ledger-kpi-tile'; diff --git a/app/components/ledger/dashboard/revenue-chart.js b/app/components/ledger/dashboard/revenue-chart.js index c293e99..e4c95f4 100644 --- a/app/components/ledger/dashboard/revenue-chart.js +++ b/app/components/ledger/dashboard/revenue-chart.js @@ -1 +1 @@ -export { default } from '@fleetbase/ledger-engine/components/dashboard/revenue-chart'; +export { default } from '@fleetbase/ledger-engine/components/widget/revenue-trend'; diff --git a/app/components/ledger/dashboard/wallet-balances.js b/app/components/ledger/dashboard/wallet-balances.js index 9bba3e9..627ebc3 100644 --- a/app/components/ledger/dashboard/wallet-balances.js +++ b/app/components/ledger/dashboard/wallet-balances.js @@ -1 +1 @@ -export { default } from '@fleetbase/ledger-engine/components/dashboard/wallet-balances'; +export { default } from '@fleetbase/ledger-engine/components/widget/wallet-balances'; diff --git a/app/components/widget/ar-aging-summary.js b/app/components/widget/ar-aging-summary.js new file mode 100644 index 0000000..0ab26a5 --- /dev/null +++ b/app/components/widget/ar-aging-summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/ar-aging-summary'; diff --git a/app/components/widget/cash-flow-summary.js b/app/components/widget/cash-flow-summary.js new file mode 100644 index 0000000..ff4d347 --- /dev/null +++ b/app/components/widget/cash-flow-summary.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/cash-flow-summary'; diff --git a/app/components/widget/invoice-status.js b/app/components/widget/invoice-status.js new file mode 100644 index 0000000..2e61b2a --- /dev/null +++ b/app/components/widget/invoice-status.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/invoice-status'; diff --git a/app/components/widget/kpi-active-wallets.js b/app/components/widget/kpi-active-wallets.js new file mode 100644 index 0000000..182cfa5 --- /dev/null +++ b/app/components/widget/kpi-active-wallets.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/kpi-active-wallets'; diff --git a/app/components/widget/kpi-expenses.js b/app/components/widget/kpi-expenses.js new file mode 100644 index 0000000..0f99a83 --- /dev/null +++ b/app/components/widget/kpi-expenses.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/kpi-expenses'; diff --git a/app/components/widget/kpi-net-income.js b/app/components/widget/kpi-net-income.js new file mode 100644 index 0000000..13a070c --- /dev/null +++ b/app/components/widget/kpi-net-income.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/kpi-net-income'; diff --git a/app/components/widget/kpi-open-invoices.js b/app/components/widget/kpi-open-invoices.js new file mode 100644 index 0000000..91a9d83 --- /dev/null +++ b/app/components/widget/kpi-open-invoices.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/kpi-open-invoices'; diff --git a/app/components/widget/kpi-outstanding-ar.js b/app/components/widget/kpi-outstanding-ar.js new file mode 100644 index 0000000..63857a7 --- /dev/null +++ b/app/components/widget/kpi-outstanding-ar.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/kpi-outstanding-ar'; diff --git a/app/components/widget/kpi-overdue-ar.js b/app/components/widget/kpi-overdue-ar.js new file mode 100644 index 0000000..6f9e992 --- /dev/null +++ b/app/components/widget/kpi-overdue-ar.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/kpi-overdue-ar'; diff --git a/app/components/widget/kpi-revenue.js b/app/components/widget/kpi-revenue.js new file mode 100644 index 0000000..a14f08d --- /dev/null +++ b/app/components/widget/kpi-revenue.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/kpi-revenue'; diff --git a/app/components/widget/kpi-wallet-balance.js b/app/components/widget/kpi-wallet-balance.js new file mode 100644 index 0000000..edc2eb9 --- /dev/null +++ b/app/components/widget/kpi-wallet-balance.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/kpi-wallet-balance'; diff --git a/app/components/widget/ledger-kpi-tile.js b/app/components/widget/ledger-kpi-tile.js new file mode 100644 index 0000000..93cc533 --- /dev/null +++ b/app/components/widget/ledger-kpi-tile.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/ledger-kpi-tile'; diff --git a/app/components/widget/report-shortcuts.js b/app/components/widget/report-shortcuts.js new file mode 100644 index 0000000..04c8987 --- /dev/null +++ b/app/components/widget/report-shortcuts.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/report-shortcuts'; diff --git a/app/components/widget/revenue-trend.js b/app/components/widget/revenue-trend.js new file mode 100644 index 0000000..e4c95f4 --- /dev/null +++ b/app/components/widget/revenue-trend.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/components/widget/revenue-trend'; diff --git a/app/services/ledger-dashboard.js b/app/services/ledger-dashboard.js new file mode 100644 index 0000000..164025c --- /dev/null +++ b/app/services/ledger-dashboard.js @@ -0,0 +1 @@ +export { default } from '@fleetbase/ledger-engine/services/ledger-dashboard'; diff --git a/composer.json b/composer.json index e2d8bb2..e6e2bb4 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "fleetbase/ledger-api", - "version": "0.0.3", + "version": "0.0.4", "description": "Accounting & Invoicing Extension for Fleetbase", "keywords": [ "fleetbase", diff --git a/extension.json b/extension.json index a1b8743..2b4a680 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "Ledger", - "version": "0.0.3", + "version": "0.0.4", "description": "Accounting & Invoicing Extension for Fleetbase", "repository": "https://github.com/fleetbase/ledger", "license": "AGPL-3.0-or-later", diff --git a/package.json b/package.json index 9431c60..65c3ef4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/ledger-engine", - "version": "0.0.3", + "version": "0.0.4", "description": "Accounting & Invoicing Extension for Fleetbase", "keywords": [ "fleetbase-extension", @@ -44,9 +44,9 @@ }, "dependencies": { "@babel/core": "^7.23.2", - "@fleetbase/ember-core": "^0.3.17", - "@fleetbase/ember-ui": "^0.3.25", - "@fleetbase/fleetops-data": "^0.1.30", + "@fleetbase/ember-core": "^0.3.21", + "@fleetbase/ember-ui": "^0.3.33", + "@fleetbase/fleetops-data": "^0.1.36", "@fortawesome/ember-fontawesome": "^2.0.0", "@fortawesome/fontawesome-svg-core": "6.4.0", "@fortawesome/free-brands-svg-icons": "6.4.0", @@ -78,8 +78,7 @@ "ember-cli-sri": "^2.1.1", "ember-cli-terser": "^4.0.2", "ember-composable-helpers": "^5.0.0", - "ember-concurrency": "^3.1.1", - "ember-concurrency-decorators": "^2.0.3", + "ember-concurrency": "^4.0.6", "ember-data": "^4.12.5", "ember-engines": "^0.9.0", "ember-load-initializers": "^2.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67b73c2..07e4e1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,16 +10,16 @@ importers: dependencies: '@babel/core': specifier: ^7.23.2 - version: 7.23.2 + version: 7.29.0 '@fleetbase/ember-core': - specifier: ^0.3.17 - version: 0.3.17(@ember/string@3.1.1)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0) + specifier: ^0.3.21 + version: 0.3.21(@ember/string@3.1.1)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0) '@fleetbase/ember-ui': - specifier: ^0.3.25 - version: 0.3.25(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(@glimmer/component@1.1.2(@babel/core@7.23.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(postcss@8.4.35)(rollup@2.79.1)(tracked-built-ins@3.4.0(@babel/core@7.23.2))(webpack@5.89.0) + specifier: ^0.3.33 + version: 0.3.33(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(@glimmer/component@1.1.2(@babel/core@7.23.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(postcss@8.5.8)(rollup@2.79.1)(tracked-built-ins@3.4.0(@babel/core@7.23.2))(webpack@5.89.0) '@fleetbase/fleetops-data': - specifier: ^0.1.30 - version: 0.1.30(@ember/string@3.1.1)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0) + specifier: ^0.1.36 + version: 0.1.36(@ember/string@3.1.1)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0) '@fortawesome/ember-fontawesome': specifier: ^2.0.0 version: 2.0.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(rollup@2.79.1)(webpack@5.89.0) @@ -77,7 +77,7 @@ importers: version: 3.0.2 '@glimmer/component': specifier: ^1.1.2 - version: 1.1.2(@babel/core@7.23.2) + version: 1.1.2(@babel/core@7.29.0) '@glimmer/tracking': specifier: ^1.1.2 version: 1.1.2 @@ -109,11 +109,8 @@ importers: specifier: ^5.0.0 version: 5.0.0 ember-concurrency: - specifier: ^3.1.1 - version: 3.1.1(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) - ember-concurrency-decorators: - specifier: ^2.0.3 - version: 2.0.3(@babel/core@7.23.2) + specifier: ^4.0.6 + version: 4.0.6(@babel/core@7.29.0) ember-data: specifier: ^4.12.5 version: 4.12.5(@babel/core@7.23.2)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0) @@ -134,10 +131,10 @@ importers: version: 8.0.1(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(qunit@2.20.0) ember-resolver: specifier: ^11.0.1 - version: 11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) + version: 11.0.1(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)) ember-source: specifier: ~5.4.0 - version: 5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0) + version: 5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0) ember-source-channel-url: specifier: ^3.0.0 version: 3.0.0 @@ -212,6 +209,10 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.23.5': resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} engines: {node: '>=6.9.0'} @@ -220,6 +221,10 @@ packages: resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + '@babel/core@7.23.2': resolution: {integrity: sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==} engines: {node: '>=6.9.0'} @@ -250,6 +255,10 @@ packages: resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.22.5': resolution: {integrity: sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==} engines: {node: '>=6.9.0'} @@ -270,6 +279,10 @@ packages: resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + '@babel/helper-create-class-features-plugin@7.23.10': resolution: {integrity: sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==} engines: {node: '>=6.9.0'} @@ -316,6 +329,10 @@ packages: resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + '@babel/helper-hoist-variables@7.22.5': resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} @@ -336,6 +353,10 @@ packages: resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.23.3': resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} engines: {node: '>=6.9.0'} @@ -348,6 +369,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.22.5': resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} @@ -364,6 +391,10 @@ packages: resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + '@babel/helper-remap-async-to-generator@7.22.20': resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} engines: {node: '>=6.9.0'} @@ -412,6 +443,10 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.22.20': resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} @@ -420,6 +455,10 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.23.5': resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} engines: {node: '>=6.9.0'} @@ -428,6 +467,10 @@ packages: resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + '@babel/helper-wrap-function@7.22.20': resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} engines: {node: '>=6.9.0'} @@ -440,8 +483,8 @@ packages: resolution: {integrity: sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} '@babel/highlight@7.23.4': @@ -458,6 +501,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} engines: {node: '>=6.9.0'} @@ -519,20 +567,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': - resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-chaining@7.21.0': - resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-proposal-private-methods@7.18.6': resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} engines: {node: '>=6.9.0'} @@ -581,6 +615,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-decorators@7.29.7': + resolution: {integrity: sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-dynamic-import@7.8.3': resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} peerDependencies: @@ -1265,11 +1305,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.8.7': - resolution: {integrity: sha512-7O0UsPQVNKqpHeHLpfvOG4uXmlw+MOxYvUv6Otc9uH5SYMIxvF6eBdjkWvC3f9G+VXe0RsNExyAQBeTRug/wqQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-escapes@7.23.3': resolution: {integrity: sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==} engines: {node: '>=6.9.0'} @@ -1361,6 +1396,10 @@ packages: resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.23.9': resolution: {integrity: sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==} engines: {node: '>=6.9.0'} @@ -1369,6 +1408,10 @@ packages: resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + '@babel/types@7.23.9': resolution: {integrity: sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==} engines: {node: '>=6.9.0'} @@ -1377,6 +1420,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + '@cnakazawa/watch@1.0.4': resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} engines: {node: '>=0.1.95'} @@ -1905,26 +1952,29 @@ packages: resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@event-calendar/core@5.7.1': + resolution: {integrity: sha512-ms9MQagthrmRO+ytZD5dDQHjBb+r/6mwld4/psZRaBI6bciIEsmphCrxFaF9HnGvLW0htSa973TVbYMbDsDtVA==} + '@fleetbase/ember-accounting@0.0.1': resolution: {integrity: sha512-61WGQ/VtmkEloBfdNEd83C9E57axiBXbBPdXAbaS3dsCBpKmqwPo1CkrYUN7vVa3oUP9ZRouVVVffbE0YDnAng==} engines: {node: '>= 18'} peerDependencies: ember-source: '>= 4.0.0' - '@fleetbase/ember-core@0.3.17': - resolution: {integrity: sha512-fFyorS6Ir/lW2u1y/d46U/0PoIhz4JKSVJZJddveIPK3v/0shpHRRsI4gnW+EtIzE3Dgq6Z7p6pQvrPBpPw/YQ==} + '@fleetbase/ember-core@0.3.20': + resolution: {integrity: sha512-eqY15urfqFkC26TJO/irdCxke9VE7ywcLodM3K6iDEXR+FFk50YIWnXzvU8PvEQ1EZCRJdyEERvCc9FnypXFQA==} engines: {node: '>= 18'} - '@fleetbase/ember-core@0.3.18': - resolution: {integrity: sha512-XA/Ysn3NlM37qK/xJCY+Uo2sZ8JTwcDaGruPi8dSVyGfYHO55m96TepCChEU18GdwosbsBBfL8C2R+fUvPsqIg==} + '@fleetbase/ember-core@0.3.21': + resolution: {integrity: sha512-hBIisQfGuuWolyzUnIx+l6M7JtiLMXxhOM1UlAITJ3kIXK0gLclHX3JxF9zoonSnkzNtOe9/TbzIFuKlJVbG9w==} engines: {node: '>= 18'} - '@fleetbase/ember-ui@0.3.25': - resolution: {integrity: sha512-CM0dXMlFe3VyIGFgmbRDEK+e/Y79Ezu4T57geJF2cHr+/1f3oLvhLqPaZIs1J1TTSgcvCl3E+owcYWw2mWxLlg==} + '@fleetbase/ember-ui@0.3.33': + resolution: {integrity: sha512-XUkRFR/hPfXq1pGadPvEZVTGU7f1D4IHzXcyKVmppY5AxPjgk8PeteoFnNdwZmbGdkTFzifydeG313DltBWlUg==} engines: {node: '>= 18'} - '@fleetbase/fleetops-data@0.1.30': - resolution: {integrity: sha512-n+jCGT2tFnhduHDEkBPENNn9UTaWNSygW9ZmNYiRs6fnGFaRsrMfvBrW+N9galB2dpvA7M8bJO+bl/exkb4dnA==} + '@fleetbase/fleetops-data@0.1.36': + resolution: {integrity: sha512-w8FmeoCYcWHIoaN5Oxk8XDkxI+va1dwUZCeFDJ2/6oaJ/n6sCFknki67AmYJ0cFJEqHPskUgA8uaQlqw9ZGJXA==} engines: {node: '>= 18'} '@floating-ui/core@1.7.5': @@ -2222,6 +2272,11 @@ packages: '@socket.io/component-emitter@3.1.0': resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + '@sveltejs/acorn-typescript@1.0.10': + resolution: {integrity: sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==} + peerDependencies: + acorn: ^8.9.0 + '@szmarczak/http-timer@1.1.2': resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} engines: {node: '>=6'} @@ -2447,6 +2502,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@4.17.43': resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} @@ -2528,6 +2586,9 @@ packages: '@types/symlink-or-copy@1.2.2': resolution: {integrity: sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -2672,6 +2733,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + ag-channel@5.0.0: resolution: {integrity: sha512-bArHkdqQxynim981t8FLZM5TfA0v7p081OlFdOxs6clB79GSGcGlOQMDa31DT9F5VMjzqNiJmhfGwinvfU/3Zg==} @@ -2810,6 +2876,10 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + arr-diff@4.0.0: resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} engines: {node: '>=0.10.0'} @@ -2927,6 +2997,10 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + babel-code-frame@6.26.0: resolution: {integrity: sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==} @@ -3107,6 +3181,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + basic-auth@2.0.1: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} @@ -3406,6 +3485,11 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -3508,6 +3592,9 @@ packages: caniuse-lite@1.0.30001774: resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + capture-exit@2.0.0: resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} engines: {node: 6.* || 8.* || >= 10.*} @@ -3649,6 +3736,10 @@ packages: resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + collection-visit@1.0.0: resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} engines: {node: '>=0.10.0'} @@ -4148,6 +4239,9 @@ packages: decorator-transforms@2.3.1: resolution: {integrity: sha512-PDOk74Zqqy0946Lx4ckXxbgG6uhPScOICtrxL/pXmfznxchqNee0TaJISClGJQe6FeT8ohGqsOgdjfahm4FwEw==} + decorator-transforms@2.3.2: + resolution: {integrity: sha512-XcErcjlmCzG5ODgYjt6ZTXwd6S8fPKln/sJmw15ZXkWG2JpoQNwszis+AwF6XSGlOoG7g8MCEO97g+Yw3fk5OQ==} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -4223,6 +4317,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -4285,6 +4382,9 @@ packages: electron-to-chromium@1.5.302: resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} + electron-to-chromium@1.5.368: + resolution: {integrity: sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==} + element-closest@3.0.2: resolution: {integrity: sha512-JxKQiJKX0Zr5Q2/bCaTx8P+UbfyMET1OQd61qu5xQFeWr1km3fGaxelSJtnfT27XQ5Uoztn2yIyeamAc/VX13g==} engines: {node: '>=0.12.0'} @@ -4468,10 +4568,6 @@ packages: resolution: {integrity: sha512-lo5YArbJzJi5ssvaGqTt6+FnhTALnSvYVuxM7lfyL1UCMudyNJ94ovH5C7n5il7ATd6WsNiAPRUO/v+s5Jq/aA==} engines: {node: 8.* || >= 10.*} - ember-cli-typescript@3.1.4: - resolution: {integrity: sha512-HJ73kL45OGRmIkPhBNFt31I1SGUvdZND+LCH21+qpq3pPlFpJG8GORyXpP+2ze8PbnITNLzwe5AwUrpyuRswdQ==} - engines: {node: 8.* || >= 10.*} - ember-cli-typescript@4.2.1: resolution: {integrity: sha512-0iKTZ+/wH6UB/VTWKvGuXlmwiE8HSIGcxHamwNhEC5x1mN3z8RfvsFZdQWYUzIWFN2Tek0gmepGRPTwWdBYl/A==} engines: {node: 10.* || >= 12.*} @@ -4521,10 +4617,6 @@ packages: peerDependencies: ember-concurrency: ^2.0.0-rc.1 - ember-concurrency-decorators@2.0.3: - resolution: {integrity: sha512-r6O34YKI/slyYapVsuOPnmaKC4AsmBSwvgcadbdy+jHNj+mnryXPkm+3hhhRnFdlsKUKdEuXvl43lhjhYRLhhA==} - engines: {node: 10.* || >= 12} - ember-concurrency-ts@0.3.1: resolution: {integrity: sha512-lE9uqPgK1Y9PN/0BJ5zE2a+h95izRCn6FCyt7qVV3012TlblTynsBaoUuAbN1T3KfzFsrJaXwsxzRbDjEde2Sw==} engines: {node: 10.* || >= 12} @@ -4535,12 +4627,6 @@ packages: resolution: {integrity: sha512-sz6sTIXN/CuLb5wdpauFa+rWXuvXXSnSHS4kuNzU5GSMDX1pLBWSuovoUk61FUe6CYRqBmT1/UushObwBGickQ==} engines: {node: 10.* || 12.* || 14.* || >= 16} - ember-concurrency@3.1.1: - resolution: {integrity: sha512-doXFYYfy1C7jez+jDDlfahTp03QdjXeSY/W3Zbnx/q3UNJ9g10Shf2d7M/HvWo/TC22eU+6dPLIpqd/6q4pR+Q==} - engines: {node: 16.* || >= 18} - peerDependencies: - ember-source: ^3.28.0 || ^4.0.0 || >=5.0.0 - ember-concurrency@4.0.6: resolution: {integrity: sha512-Ikwl2YwXVe8aBwrT1deWTcUVxVu6KxS1qeU1ks3EML1Q/nxwKgxCkGqTJavxczawO8H/SIW45dV4r7z5Yqd2Xg==} engines: {node: 16.* || >= 18} @@ -4831,8 +4917,8 @@ packages: engines: {node: 12.* || 14.* || >= 16.*} hasBin: true - ember-tracked-storage-polyfill@1.0.0: - resolution: {integrity: sha512-eL7lZat68E6P/D7b9UoTB5bB5Oh/0aju0Z7PCMi3aTwhaydRaxloE7TGrTRYU+NdJuyNVZXeGyxFxn2frvd3TA==} + ember-tracked-storage-polyfill@1.0.1: + resolution: {integrity: sha512-lr66R+1H9qMXIUXxwzpixS/qTwsMEpJXS5s2nOdvQP9U/JYuZT9MexpvLktSUQ1uWEhGQA8DDeeVh4R1CvLDFQ==} engines: {node: 12.* || >= 14} ember-truth-helpers@3.1.1: @@ -5055,6 +5141,9 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + esm@3.2.25: resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} engines: {node: '>=6'} @@ -5077,6 +5166,14 @@ packages: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} + esrap@2.2.11: + resolution: {integrity: sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -5130,10 +5227,6 @@ packages: resolution: {integrity: sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==} engines: {node: ^8.12.0 || >=9.7.0} - execa@3.4.0: - resolution: {integrity: sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==} - engines: {node: ^8.12.0 || >=9.7.0} - execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} @@ -6080,6 +6173,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -6357,6 +6453,9 @@ packages: loader.js@4.7.0: resolution: {integrity: sha512-9M2KvGT6duzGMgkOcTkWb+PR/Q2Oe54df/tLgHGVmFpAmtqJ553xJh6N63iFYI2yjo2PeJXbS5skHi/QpJq4vA==} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@2.0.0: resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} engines: {node: '>=4'} @@ -6499,6 +6598,9 @@ packages: magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@0.30.7: resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} engines: {node: '>=12'} @@ -6850,6 +6952,10 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.47: + resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==} + engines: {node: '>=18'} + node-watch@0.7.3: resolution: {integrity: sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==} engines: {node: '>=6'} @@ -8513,6 +8619,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte@5.56.1: + resolution: {integrity: sha512-eArsJmvl3xZVuTYD852PzIEdg2wgDdIZ1NEsIPbzAukHwi284B18No4nK2rCO9AwsWUDza4Cjvmoa4HaojTl5g==} + engines: {node: '>=18'} + svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} @@ -9147,6 +9257,9 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} @@ -9169,10 +9282,18 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.23.5': {} '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.29.7': {} + '@babel/core@7.23.2': dependencies: '@ampproject/remapping': 2.2.1 @@ -9195,15 +9316,15 @@ snapshots: '@babel/core@7.29.0': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.0) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -9244,6 +9365,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.22.5': dependencies: '@babel/types': 7.23.9 @@ -9272,6 +9401,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + '@babel/helper-create-class-features-plugin@7.23.10(@babel/core@7.23.2)': dependencies: '@babel/core': 7.23.2 @@ -9405,6 +9542,8 @@ snapshots: '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} + '@babel/helper-hoist-variables@7.22.5': dependencies: '@babel/types': 7.23.9 @@ -9431,6 +9570,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.23.3(@babel/core@7.23.2)': dependencies: '@babel/core': 7.23.2 @@ -9467,6 +9613,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.22.5': dependencies: '@babel/types': 7.23.9 @@ -9479,6 +9634,8 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.23.2)': dependencies: '@babel/core': 7.23.2 @@ -9566,14 +9723,20 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.22.20': {} '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.23.5': {} '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-option@7.29.7': {} + '@babel/helper-wrap-function@7.22.20': dependencies: '@babel/helper-function-name': 7.23.0 @@ -9596,10 +9759,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helpers@7.28.6': + '@babel/helpers@7.29.7': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 '@babel/highlight@7.23.4': dependencies: @@ -9615,6 +9778,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.23.2)': dependencies: '@babel/core': 7.23.2 @@ -9751,19 +9918,6 @@ snapshots: '@babel/helper-split-export-declaration': 7.22.6 '@babel/plugin-syntax-decorators': 7.23.3(@babel/core@7.29.0) - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.23.2)': - dependencies: - '@babel/core': 7.23.2 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.2) - - '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.23.2)': - dependencies: - '@babel/core': 7.23.2 - '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.2) - '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.23.2)': dependencies: '@babel/core': 7.23.2 @@ -9850,6 +10004,16 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.23.2)': + dependencies: + '@babel/core': 7.23.2 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.23.2)': dependencies: '@babel/core': 7.23.2 @@ -11299,12 +11463,12 @@ snapshots: '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.2) - '@babel/plugin-transform-typescript@7.8.7(@babel/core@7.23.2)': + '@babel/plugin-transform-typescript@7.5.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.2 - '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.23.2) - '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.2) + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.23.10(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.29.0) '@babel/plugin-transform-unicode-escapes@7.23.3(@babel/core@7.23.2)': dependencies: @@ -11765,6 +11929,12 @@ snapshots: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@babel/traverse@7.23.9': dependencies: '@babel/code-frame': 7.23.5 @@ -11792,6 +11962,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.23.9': dependencies: '@babel/helper-string-parser': 7.23.4 @@ -11803,6 +11985,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@cnakazawa/watch@1.0.4': dependencies: exec-sh: 0.3.6 @@ -11852,201 +12039,201 @@ snapshots: '@csstools/css-parser-algorithms': 2.5.0(@csstools/css-tokenizer@2.2.3) '@csstools/css-tokenizer': 2.2.3 - '@csstools/postcss-cascade-layers@4.0.6(postcss@8.4.35)': + '@csstools/postcss-cascade-layers@4.0.6(postcss@8.5.8)': dependencies: '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.0.15) - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 - '@csstools/postcss-color-function@3.0.19(postcss@8.4.35)': + '@csstools/postcss-color-function@3.0.19(postcss@8.5.8)': dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-color-mix-function@2.0.19(postcss@8.4.35)': + '@csstools/postcss-color-mix-function@2.0.19(postcss@8.5.8)': dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-content-alt-text@1.0.0(postcss@8.4.35)': + '@csstools/postcss-content-alt-text@1.0.0(postcss@8.5.8)': dependencies: '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-exponential-functions@1.0.9(postcss@8.4.35)': + '@csstools/postcss-exponential-functions@1.0.9(postcss@8.5.8)': dependencies: '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-font-format-keywords@3.0.2(postcss@8.4.35)': + '@csstools/postcss-font-format-keywords@3.0.2(postcss@8.5.8)': dependencies: - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@1.0.11(postcss@8.4.35)': + '@csstools/postcss-gamut-mapping@1.0.11(postcss@8.5.8)': dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-gradients-interpolation-method@4.0.20(postcss@8.4.35)': + '@csstools/postcss-gradients-interpolation-method@4.0.20(postcss@8.5.8)': dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-hwb-function@3.0.18(postcss@8.4.35)': + '@csstools/postcss-hwb-function@3.0.18(postcss@8.5.8)': dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-ic-unit@3.0.7(postcss@8.4.35)': + '@csstools/postcss-ic-unit@3.0.7(postcss@8.5.8)': dependencies: - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - '@csstools/postcss-initial@1.0.1(postcss@8.4.35)': + '@csstools/postcss-initial@1.0.1(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-is-pseudo-class@4.0.8(postcss@8.4.35)': + '@csstools/postcss-is-pseudo-class@4.0.8(postcss@8.5.8)': dependencies: '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.0.15) - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 - '@csstools/postcss-light-dark-function@1.0.8(postcss@8.4.35)': + '@csstools/postcss-light-dark-function@1.0.8(postcss@8.5.8)': dependencies: '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-logical-float-and-clear@2.0.1(postcss@8.4.35)': + '@csstools/postcss-logical-float-and-clear@2.0.1(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-logical-overflow@1.0.1(postcss@8.4.35)': + '@csstools/postcss-logical-overflow@1.0.1(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-logical-overscroll-behavior@1.0.1(postcss@8.4.35)': + '@csstools/postcss-logical-overscroll-behavior@1.0.1(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-logical-resize@2.0.1(postcss@8.4.35)': + '@csstools/postcss-logical-resize@2.0.1(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@2.0.11(postcss@8.4.35)': + '@csstools/postcss-logical-viewport-units@2.0.11(postcss@8.5.8)': dependencies: '@csstools/css-tokenizer': 2.4.1 - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-media-minmax@1.1.8(postcss@8.4.35)': + '@csstools/postcss-media-minmax@1.1.8(postcss@8.5.8)': dependencies: '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-media-queries-aspect-ratio-number-values@2.0.11(postcss@8.4.35)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@2.0.11(postcss@8.5.8)': dependencies: '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-nested-calc@3.0.2(postcss@8.4.35)': + '@csstools/postcss-nested-calc@3.0.2(postcss@8.5.8)': dependencies: - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@3.0.2(postcss@8.4.35)': + '@csstools/postcss-normalize-display-values@3.0.2(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@3.0.19(postcss@8.4.35)': + '@csstools/postcss-oklab-function@3.0.19(postcss@8.5.8)': dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-progressive-custom-properties@3.3.0(postcss@8.4.35)': + '@csstools/postcss-progressive-custom-properties@3.3.0(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - '@csstools/postcss-relative-color-syntax@2.0.19(postcss@8.4.35)': + '@csstools/postcss-relative-color-syntax@2.0.19(postcss@8.5.8)': dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - '@csstools/postcss-scope-pseudo-class@3.0.1(postcss@8.4.35)': + '@csstools/postcss-scope-pseudo-class@3.0.1(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 - '@csstools/postcss-stepped-value-functions@3.0.10(postcss@8.4.35)': + '@csstools/postcss-stepped-value-functions@3.0.10(postcss@8.5.8)': dependencies: '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-text-decoration-shorthand@3.0.7(postcss@8.4.35)': + '@csstools/postcss-text-decoration-shorthand@3.0.7(postcss@8.5.8)': dependencies: '@csstools/color-helpers': 4.2.1 - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@3.0.10(postcss@8.4.35)': + '@csstools/postcss-trigonometric-functions@3.0.10(postcss@8.5.8)': dependencies: '@csstools/css-calc': 1.2.4(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - postcss: 8.4.35 + postcss: 8.5.8 - '@csstools/postcss-unset-value@3.0.1(postcss@8.4.35)': + '@csstools/postcss-unset-value@3.0.1(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 '@csstools/selector-resolve-nested@1.1.0(postcss-selector-parser@6.1.2)': dependencies: @@ -12064,11 +12251,11 @@ snapshots: dependencies: postcss-selector-parser: 6.1.2 - '@csstools/utilities@1.0.0(postcss@8.4.35)': + '@csstools/utilities@1.0.0(postcss@8.5.8)': dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - '@ember-data/adapter@4.12.5(@ember-data/store@4.12.5(@babel/core@7.23.2)(@ember-data/graph@4.12.5)(@ember-data/json-api@4.12.5)(@ember-data/legacy-compat@4.12.5)(@ember-data/model@4.12.5)(@ember-data/tracking@4.12.5)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2)': + '@ember-data/adapter@4.12.5(@ember-data/store@4.12.5)(@ember/string@3.1.1)(ember-inflector@4.0.2)': dependencies: '@ember-data/private-build-infra': 4.12.5 '@ember-data/store': 4.12.5(@babel/core@7.23.2)(@ember-data/graph@4.12.5)(@ember-data/json-api@4.12.5)(@ember-data/legacy-compat@4.12.5)(@ember-data/model@4.12.5)(@ember-data/tracking@4.12.5)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) @@ -12198,7 +12385,7 @@ snapshots: '@ember-data/rfc395-data@0.0.4': {} - '@ember-data/serializer@4.12.5(@ember-data/store@4.12.5(@babel/core@7.23.2)(@ember-data/graph@4.12.5)(@ember-data/json-api@4.12.5)(@ember-data/legacy-compat@4.12.5)(@ember-data/model@4.12.5)(@ember-data/tracking@4.12.5)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2)': + '@ember-data/serializer@4.12.5(@ember-data/store@4.12.5)(@ember/string@3.1.1)(ember-inflector@4.0.2)': dependencies: '@ember-data/private-build-infra': 4.12.5 '@ember-data/store': 4.12.5(@babel/core@7.23.2)(@ember-data/graph@4.12.5)(@ember-data/json-api@4.12.5)(@ember-data/legacy-compat@4.12.5)(@ember-data/model@4.12.5)(@ember-data/tracking@4.12.5)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) @@ -12476,7 +12663,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.4.3 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -12489,6 +12676,12 @@ snapshots: '@eslint/js@8.52.0': {} + '@event-calendar/core@5.7.1': + dependencies: + svelte: 5.56.1 + transitivePeerDependencies: + - '@typescript-eslint/types' + '@fleetbase/ember-accounting@0.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))': dependencies: '@babel/core': 7.23.2 @@ -12498,7 +12691,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@fleetbase/ember-core@0.3.17(@ember/string@3.1.1)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0)': + '@fleetbase/ember-core@0.3.20(@ember/string@3.1.1)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0)': dependencies: '@babel/core': 7.23.2 compress-json: 3.4.0 @@ -12515,12 +12708,11 @@ snapshots: ember-intl: 6.3.2(@babel/core@7.23.2)(webpack@5.89.0) ember-loading: 2.0.0(@babel/core@7.23.2) ember-local-storage: 2.0.7(@babel/core@7.23.2) - ember-simple-auth: 6.1.0(@babel/core@7.23.2)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0) + ember-simple-auth: 6.1.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0) ember-wormhole: 0.6.0 socketcluster-client: 17.2.2 transitivePeerDependencies: - '@ember/string' - - '@ember/test-helpers' - '@glint/template' - bufferutil - ember-resolver @@ -12531,29 +12723,28 @@ snapshots: - utf-8-validate - webpack - '@fleetbase/ember-core@0.3.18(@ember/string@3.1.1)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0)': + '@fleetbase/ember-core@0.3.21(@ember/string@3.1.1)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0)': dependencies: '@babel/core': 7.23.2 compress-json: 3.4.0 date-fns: 2.30.0 ember-auto-import: 2.8.1(webpack@5.89.0) - ember-can: 6.0.0(@babel/core@7.23.2)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) + ember-can: 6.0.0(@babel/core@7.23.2)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)) ember-cli-babel: 8.2.0(@babel/core@7.23.2) ember-cli-htmlbars: 6.3.0 - ember-cli-notifications: 9.1.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) + ember-cli-notifications: 9.1.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)) ember-concurrency: 4.0.6(@babel/core@7.23.2) ember-decorators: 6.1.1 ember-get-config: 2.1.1(@babel/core@7.23.2) - ember-inflector: 4.0.3(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) + ember-inflector: 4.0.3(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)) ember-intl: 6.3.2(@babel/core@7.23.2)(webpack@5.89.0) ember-loading: 2.0.0(@babel/core@7.23.2) ember-local-storage: 2.0.7(@babel/core@7.23.2) - ember-simple-auth: 6.1.0(@babel/core@7.23.2)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0) + ember-simple-auth: 6.1.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0) ember-wormhole: 0.6.0 socketcluster-client: 17.2.2 transitivePeerDependencies: - '@ember/string' - - '@ember/test-helpers' - '@glint/template' - bufferutil - ember-resolver @@ -12564,13 +12755,14 @@ snapshots: - utf-8-validate - webpack - '@fleetbase/ember-ui@0.3.25(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(@glimmer/component@1.1.2(@babel/core@7.23.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(postcss@8.4.35)(rollup@2.79.1)(tracked-built-ins@3.4.0(@babel/core@7.23.2))(webpack@5.89.0)': + '@fleetbase/ember-ui@0.3.33(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(@glimmer/component@1.1.2(@babel/core@7.23.2))(@glimmer/tracking@1.1.2)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(postcss@8.5.8)(rollup@2.79.1)(tracked-built-ins@3.4.0(@babel/core@7.23.2))(webpack@5.89.0)': dependencies: '@babel/core': 7.23.2 '@ember/render-modifiers': 2.1.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) '@ember/string': 3.1.1 '@embroider/addon': 0.30.0 '@embroider/macros': 1.20.0(@babel/core@7.23.2) + '@event-calendar/core': 5.7.1 '@fleetbase/ember-accounting': 0.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) '@floating-ui/dom': 1.7.6 '@fortawesome/ember-fontawesome': 2.0.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(rollup@2.79.1)(webpack@5.89.0) @@ -12602,7 +12794,7 @@ snapshots: '@tiptap/starter-kit': 2.27.2 air-datepicker: 3.6.0 autonumeric: 4.10.9 - autoprefixer: 10.4.27(postcss@8.4.35) + autoprefixer: 10.4.27(postcss@8.5.8) chart.js: 4.5.1 chartjs-adapter-date-fns: 3.0.0(chart.js@4.5.1)(date-fns@2.30.0) date-fns: 2.30.0 @@ -12641,12 +12833,12 @@ snapshots: interactjs: 1.10.27 intl-tel-input: 22.0.2 leaflet: 1.9.4 - postcss-at-rules-variables: 0.3.0(postcss@8.4.35) - postcss-conditionals-renewed: 1.0.0(postcss@8.4.35) - postcss-each: 1.1.0(postcss@8.4.35) - postcss-import: 15.1.0(postcss@8.4.35) - postcss-mixins: 9.0.4(postcss@8.4.35) - postcss-preset-env: 9.6.0(postcss@8.4.35) + postcss-at-rules-variables: 0.3.0(postcss@8.5.8) + postcss-conditionals-renewed: 1.0.0(postcss@8.5.8) + postcss-each: 1.1.0(postcss@8.5.8) + postcss-import: 15.1.0(postcss@8.5.8) + postcss-mixins: 9.0.4(postcss@8.5.8) + postcss-preset-env: 9.6.0(postcss@8.5.8) tailwindcss: 3.4.19 transitivePeerDependencies: - '@ember/test-helpers' @@ -12654,6 +12846,7 @@ snapshots: - '@glimmer/tracking' - '@glint/environment-ember-loose' - '@glint/template' + - '@typescript-eslint/types' - ember-cli-mirage - ember-resolver - ember-source @@ -12668,16 +12861,15 @@ snapshots: - webpack-command - yaml - '@fleetbase/fleetops-data@0.1.30(@ember/string@3.1.1)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0)': + '@fleetbase/fleetops-data@0.1.36(@ember/string@3.1.1)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0)': dependencies: '@babel/core': 7.23.2 - '@fleetbase/ember-core': 0.3.18(@ember/string@3.1.1)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0) + '@fleetbase/ember-core': 0.3.20(@ember/string@3.1.1)(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0)(webpack@5.89.0) date-fns: 2.30.0 ember-cli-babel: 8.2.0(@babel/core@7.23.2) ember-cli-htmlbars: 6.3.0 transitivePeerDependencies: - '@ember/string' - - '@ember/test-helpers' - '@glint/template' - bufferutil - ember-resolver @@ -12825,21 +13017,41 @@ snapshots: - '@babel/core' - supports-color - '@glimmer/destroyable@0.84.3': - dependencies: - '@glimmer/env': 0.1.7 - '@glimmer/global-context': 0.84.3 - '@glimmer/interfaces': 0.84.3 - '@glimmer/util': 0.84.3 - - '@glimmer/di@0.1.11': {} - - '@glimmer/encoder@0.84.3': + '@glimmer/component@1.1.2(@babel/core@7.29.0)': dependencies: + '@glimmer/di': 0.1.11 '@glimmer/env': 0.1.7 - '@glimmer/interfaces': 0.84.3 - '@glimmer/vm': 0.84.3 - + '@glimmer/util': 0.44.0 + broccoli-file-creator: 2.1.1 + broccoli-merge-trees: 3.0.2 + ember-cli-babel: 7.26.11 + ember-cli-get-component-path-option: 1.0.0 + ember-cli-is-package-missing: 1.0.0 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript: 3.0.0(@babel/core@7.29.0) + ember-cli-version-checker: 3.1.3 + ember-compatibility-helpers: 1.2.7(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@glimmer/destroyable@0.84.3': + dependencies: + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.84.3 + '@glimmer/interfaces': 0.84.3 + '@glimmer/util': 0.84.3 + + '@glimmer/di@0.1.11': {} + + '@glimmer/encoder@0.84.3': + dependencies: + '@glimmer/env': 0.1.7 + '@glimmer/interfaces': 0.84.3 + '@glimmer/vm': 0.84.3 + '@glimmer/env@0.1.7': {} '@glimmer/global-context@0.84.3': @@ -12950,6 +13162,12 @@ snapshots: transitivePeerDependencies: - '@babel/core' + '@glimmer/vm-babel-plugins@0.84.3(@babel/core@7.29.0)': + dependencies: + babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.0) + transitivePeerDependencies: + - '@babel/core' + '@glimmer/vm@0.84.3': dependencies: '@glimmer/interfaces': 0.84.3 @@ -12965,7 +13183,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.4 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -13007,8 +13225,8 @@ snapshots: '@jridgewell/source-map@0.3.5': dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.22 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.4.15': {} @@ -13123,6 +13341,10 @@ snapshots: '@socket.io/component-emitter@3.1.0': {} + '@sveltejs/acorn-typescript@1.0.10(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + '@szmarczak/http-timer@1.1.2': dependencies: defer-to-connect: 1.1.3 @@ -13361,6 +13583,8 @@ snapshots: '@types/estree@1.0.5': {} + '@types/estree@1.0.9': {} + '@types/express-serve-static-core@4.17.43': dependencies: '@types/node': 20.11.19 @@ -13454,6 +13678,8 @@ snapshots: '@types/symlink-or-copy@1.2.2': {} + '@types/trusted-types@2.0.7': {} + '@ungap/structured-clone@1.2.0': {} '@webassemblyjs/ast@1.11.6': @@ -13650,6 +13876,8 @@ snapshots: acorn@8.11.3: {} + acorn@8.16.0: {} + ag-channel@5.0.0: dependencies: consumable-stream: 2.0.0 @@ -13773,6 +14001,8 @@ snapshots: dependencies: dequal: 2.0.3 + aria-query@5.3.1: {} + arr-diff@4.0.0: {} arr-flatten@1.1.0: {} @@ -13882,13 +14112,13 @@ snapshots: autonumeric@4.10.9: {} - autoprefixer@10.4.27(postcss@8.4.35): + autoprefixer@10.4.27(postcss@8.5.8): dependencies: browserslist: 4.28.1 caniuse-lite: 1.0.30001774 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.6: {} @@ -13897,6 +14127,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + axobject-query@4.1.0: {} + babel-code-frame@6.26.0: dependencies: chalk: 1.1.3 @@ -13984,6 +14216,11 @@ snapshots: '@babel/core': 7.23.2 semver: 5.7.2 + babel-plugin-debug-macros@0.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + semver: 5.7.2 + babel-plugin-debug-macros@0.3.4(@babel/core@7.23.2): dependencies: '@babel/core': 7.23.2 @@ -14009,7 +14246,7 @@ snapshots: babel-plugin-filter-imports@4.0.0: dependencies: - '@babel/types': 7.23.9 + '@babel/types': 7.29.7 lodash: 4.17.21 babel-plugin-htmlbars-inline-precompile@3.2.0: {} @@ -14238,6 +14475,8 @@ snapshots: baseline-browser-mapping@2.10.0: {} + baseline-browser-mapping@2.10.33: {} + basic-auth@2.0.1: dependencies: safe-buffer: 5.1.2 @@ -14970,6 +15209,14 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.368 + node-releases: 2.0.47 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -15107,6 +15354,8 @@ snapshots: caniuse-lite@1.0.30001774: {} + caniuse-lite@1.0.30001793: {} + capture-exit@2.0.0: dependencies: rsvp: 4.8.5 @@ -15271,6 +15520,8 @@ snapshots: clone@2.1.2: {} + clsx@2.1.1: {} + collection-visit@1.0.0: dependencies: map-visit: 1.0.0 @@ -15517,9 +15768,9 @@ snapshots: crypto-random-string@2.0.0: {} - css-blank-pseudo@6.0.2(postcss@8.4.35): + css-blank-pseudo@6.0.2(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 css-color-converter@2.0.0: @@ -15530,10 +15781,10 @@ snapshots: css-functions-list@3.2.1: {} - css-has-pseudo@6.0.5(postcss@8.4.35): + css-has-pseudo@6.0.5(postcss@8.5.8): dependencies: '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.0.15) - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 postcss-value-parser: 4.2.0 @@ -15551,9 +15802,9 @@ snapshots: semver: 7.6.0 webpack: 5.89.0 - css-prefers-color-scheme@9.0.1(postcss@8.4.35): + css-prefers-color-scheme@9.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 css-tree@2.3.1: dependencies: @@ -15609,7 +15860,14 @@ snapshots: decorator-transforms@1.2.1(@babel/core@7.23.2): dependencies: - '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.23.2) + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.23.2) + babel-import-util: 2.1.1 + transitivePeerDependencies: + - '@babel/core' + + decorator-transforms@1.2.1(@babel/core@7.29.0): + dependencies: + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.0) babel-import-util: 2.1.1 transitivePeerDependencies: - '@babel/core' @@ -15628,6 +15886,13 @@ snapshots: transitivePeerDependencies: - '@babel/core' + decorator-transforms@2.3.2(@babel/core@7.23.2): + dependencies: + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.23.2) + babel-import-util: 3.0.1 + transitivePeerDependencies: + - '@babel/core' + deep-extend@0.6.0: {} deep-is@0.1.4: {} @@ -15690,6 +15955,8 @@ snapshots: detect-newline@3.1.0: {} + devalue@5.8.1: {} + didyoumean@1.2.2: {} diff@5.2.0: {} @@ -15751,6 +16018,8 @@ snapshots: electron-to-chromium@1.5.302: {} + electron-to-chromium@1.5.368: {} + element-closest@3.0.2: {} elliptic@6.6.1: @@ -15986,6 +16255,18 @@ snapshots: - '@babel/core' - supports-color + ember-can@6.0.0(@babel/core@7.23.2)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)))(ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)): + dependencies: + '@ember/string': 3.1.1 + '@embroider/addon-shim': 1.10.2 + decorator-transforms: 2.3.1(@babel/core@7.23.2) + ember-inflector: 4.0.3(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)) + ember-resolver: 11.0.1(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)) + ember-source: 5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + ember-cli-babel-plugin-helpers@1.1.1: {} ember-cli-babel@7.26.11: @@ -16269,6 +16550,15 @@ snapshots: - '@babel/core' - supports-color + ember-cli-notifications@9.1.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)): + dependencies: + '@embroider/addon-shim': 1.10.2 + decorator-transforms: 2.3.1(@babel/core@7.23.2) + ember-source: 5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + ember-cli-path-utils@1.0.0: {} ember-cli-postcss@8.2.0: @@ -16372,16 +16662,13 @@ snapshots: - '@babel/core' - supports-color - ember-cli-typescript@3.1.4(@babel/core@7.23.2): + ember-cli-typescript@3.0.0(@babel/core@7.29.0): dependencies: - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.23.2) - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.23.2) - '@babel/plugin-transform-typescript': 7.8.7(@babel/core@7.23.2) + '@babel/plugin-transform-typescript': 7.5.5(@babel/core@7.29.0) ansi-to-html: 0.6.15 - broccoli-stew: 3.0.0 - debug: 4.3.4 + debug: 4.4.3 ember-cli-babel-plugin-helpers: 1.1.1 - execa: 3.4.0 + execa: 2.1.0 fs-extra: 8.1.0 resolve: 1.22.8 rsvp: 4.8.5 @@ -16602,6 +16889,17 @@ snapshots: - '@babel/core' - supports-color + ember-compatibility-helpers@1.2.7(@babel/core@7.29.0): + dependencies: + babel-plugin-debug-macros: 0.2.0(@babel/core@7.29.0) + ember-cli-version-checker: 5.1.2 + find-up: 5.0.0 + fs-extra: 9.1.0 + semver: 5.7.2 + transitivePeerDependencies: + - '@babel/core' + - supports-color + ember-composability-tools@1.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0): dependencies: '@babel/core': 7.23.2 @@ -16638,16 +16936,6 @@ snapshots: transitivePeerDependencies: - supports-color - ember-concurrency-decorators@2.0.3(@babel/core@7.23.2): - dependencies: - '@ember-decorators/utils': 6.1.1 - ember-cli-babel: 7.26.11 - ember-cli-htmlbars: 4.5.0 - ember-cli-typescript: 3.1.4(@babel/core@7.23.2) - transitivePeerDependencies: - - '@babel/core' - - supports-color - ember-concurrency-ts@0.3.1(ember-concurrency@2.3.7(@babel/core@7.23.2)): dependencies: ember-cli-babel: 7.26.11 @@ -16670,27 +16958,24 @@ snapshots: - '@babel/core' - supports-color - ember-concurrency@3.1.1(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)): + ember-concurrency@4.0.6(@babel/core@7.23.2): dependencies: - '@babel/helper-plugin-utils': 7.22.5 - '@babel/types': 7.23.9 - '@glimmer/tracking': 1.1.2 - ember-cli-babel: 7.26.11 - ember-cli-babel-plugin-helpers: 1.1.1 - ember-cli-htmlbars: 6.3.0 - ember-compatibility-helpers: 1.2.7(@babel/core@7.23.2) - ember-source: 5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0) + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/types': 7.29.7 + '@embroider/addon-shim': 1.10.2 + decorator-transforms: 1.2.1(@babel/core@7.23.2) transitivePeerDependencies: - '@babel/core' - supports-color - ember-concurrency@4.0.6(@babel/core@7.23.2): + ember-concurrency@4.0.6(@babel/core@7.29.0): dependencies: - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/types': 7.29.0 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/types': 7.29.7 '@embroider/addon-shim': 1.10.2 - decorator-transforms: 1.2.1(@babel/core@7.23.2) + decorator-transforms: 1.2.1(@babel/core@7.29.0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -16702,6 +16987,13 @@ snapshots: transitivePeerDependencies: - supports-color + ember-cookies@1.3.0(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)): + dependencies: + '@embroider/addon-shim': 1.10.2 + ember-source: 5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0) + transitivePeerDependencies: + - supports-color + ember-copy@2.0.1: dependencies: ember-cli-babel: 7.26.11 @@ -16710,7 +17002,7 @@ snapshots: ember-data@4.12.5(@babel/core@7.23.2)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0): dependencies: - '@ember-data/adapter': 4.12.5(@ember-data/store@4.12.5(@babel/core@7.23.2)(@ember-data/graph@4.12.5)(@ember-data/json-api@4.12.5)(@ember-data/legacy-compat@4.12.5)(@ember-data/model@4.12.5)(@ember-data/tracking@4.12.5)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2) + '@ember-data/adapter': 4.12.5(@ember-data/store@4.12.5)(@ember/string@3.1.1)(ember-inflector@4.0.2) '@ember-data/debug': 4.12.5(@ember-data/store@4.12.5)(@ember/string@3.1.1)(webpack@5.89.0) '@ember-data/graph': 4.12.5(@ember-data/store@4.12.5) '@ember-data/json-api': 4.12.5(@ember-data/graph@4.12.5)(@ember-data/store@4.12.5) @@ -16718,7 +17010,7 @@ snapshots: '@ember-data/model': 4.12.5(@babel/core@7.23.2)(@ember-data/debug@4.12.5)(@ember-data/graph@4.12.5)(@ember-data/json-api@4.12.5)(@ember-data/legacy-compat@4.12.5)(@ember-data/store@4.12.5)(@ember-data/tracking@4.12.5)(@ember/string@3.1.1)(ember-inflector@4.0.3(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) '@ember-data/private-build-infra': 4.12.5 '@ember-data/request': 4.12.5 - '@ember-data/serializer': 4.12.5(@ember-data/store@4.12.5(@babel/core@7.23.2)(@ember-data/graph@4.12.5)(@ember-data/json-api@4.12.5)(@ember-data/legacy-compat@4.12.5)(@ember-data/model@4.12.5)(@ember-data/tracking@4.12.5)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)))(@ember/string@3.1.1)(ember-inflector@4.0.2) + '@ember-data/serializer': 4.12.5(@ember-data/store@4.12.5)(@ember/string@3.1.1)(ember-inflector@4.0.2) '@ember-data/store': 4.12.5(@babel/core@7.23.2)(@ember-data/graph@4.12.5)(@ember-data/json-api@4.12.5)(@ember-data/legacy-compat@4.12.5)(@ember-data/model@4.12.5)(@ember-data/tracking@4.12.5)(@ember/string@3.1.1)(@glimmer/tracking@1.1.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) '@ember-data/tracking': 4.12.5 '@ember/edition-utils': 1.2.0 @@ -16902,6 +17194,13 @@ snapshots: transitivePeerDependencies: - supports-color + ember-inflector@4.0.3(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)): + dependencies: + ember-cli-babel: 7.26.11 + ember-source: 5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0) + transitivePeerDependencies: + - supports-color + ember-intl@6.3.2(@babel/core@7.23.2)(webpack@5.89.0): dependencies: '@formatjs/icu-messageformat-parser': 2.7.6 @@ -17158,6 +17457,14 @@ snapshots: transitivePeerDependencies: - supports-color + ember-resolver@11.0.1(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)): + dependencies: + ember-cli-babel: 7.26.11 + optionalDependencies: + ember-source: 5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0) + transitivePeerDependencies: + - supports-color + ember-responsive@5.0.0: dependencies: ember-cli-babel: 7.26.11 @@ -17168,13 +17475,13 @@ snapshots: ember-router-generator@2.0.0: dependencies: - '@babel/parser': 7.23.9 - '@babel/traverse': 7.23.9 + '@babel/parser': 7.29.7 + '@babel/traverse': 7.29.7 recast: 0.18.10 transitivePeerDependencies: - supports-color - ember-simple-auth@6.1.0(@babel/core@7.23.2)(@ember/test-helpers@3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0))(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0): + ember-simple-auth@6.1.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0): dependencies: '@babel/eslint-parser': 7.28.6(@babel/core@7.23.2)(eslint@8.52.0) '@ember/test-waiters': 3.1.0 @@ -17183,8 +17490,22 @@ snapshots: ember-cli-is-package-missing: 1.0.0 ember-cookies: 1.3.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0)) silent-error: 1.1.1 - optionalDependencies: - '@ember/test-helpers': 3.2.0(ember-source@5.4.0(@babel/core@7.23.2)(@glimmer/component@1.1.2(@babel/core@7.23.2))(rsvp@4.8.5)(webpack@5.89.0))(webpack@5.89.0) + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - ember-source + - eslint + - supports-color + + ember-simple-auth@6.1.0(@babel/core@7.23.2)(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0))(eslint@8.52.0): + dependencies: + '@babel/eslint-parser': 7.28.6(@babel/core@7.23.2)(eslint@8.52.0) + '@ember/test-waiters': 3.1.0 + '@embroider/addon-shim': 1.10.2 + '@embroider/macros': 1.20.0(@babel/core@7.23.2) + ember-cli-is-package-missing: 1.0.0 + ember-cookies: 1.3.0(ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0)) + silent-error: 1.1.1 transitivePeerDependencies: - '@babel/core' - '@glint/template' @@ -17253,6 +17574,61 @@ snapshots: - supports-color - webpack + ember-source@5.4.0(@babel/core@7.29.0)(@glimmer/component@1.1.2(@babel/core@7.29.0))(rsvp@4.8.5)(webpack@5.89.0): + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/plugin-transform-block-scoping': 7.23.4(@babel/core@7.29.0) + '@ember/edition-utils': 1.2.0 + '@glimmer/compiler': 0.84.3 + '@glimmer/component': 1.1.2(@babel/core@7.29.0) + '@glimmer/destroyable': 0.84.3 + '@glimmer/env': 0.1.7 + '@glimmer/global-context': 0.84.3 + '@glimmer/interfaces': 0.84.3 + '@glimmer/manager': 0.84.3 + '@glimmer/node': 0.84.3 + '@glimmer/opcode-compiler': 0.84.3 + '@glimmer/owner': 0.84.3 + '@glimmer/program': 0.84.3 + '@glimmer/reference': 0.84.3 + '@glimmer/runtime': 0.84.3 + '@glimmer/syntax': 0.84.3 + '@glimmer/util': 0.84.3 + '@glimmer/validator': 0.84.3 + '@glimmer/vm-babel-plugins': 0.84.3(@babel/core@7.29.0) + '@simple-dom/interface': 1.4.0 + babel-plugin-debug-macros: 0.3.4(@babel/core@7.29.0) + babel-plugin-filter-imports: 4.0.0 + backburner.js: 2.8.0 + broccoli-concat: 4.2.5 + broccoli-debug: 0.6.5 + broccoli-file-creator: 2.1.1 + broccoli-funnel: 3.0.8 + broccoli-merge-trees: 4.2.0 + chalk: 4.1.2 + ember-auto-import: 2.8.1(webpack@5.89.0) + ember-cli-babel: 7.26.11 + ember-cli-get-component-path-option: 1.0.0 + ember-cli-is-package-missing: 1.0.0 + ember-cli-normalize-entity-name: 1.0.0 + ember-cli-path-utils: 1.0.0 + ember-cli-string-utils: 1.1.0 + ember-cli-typescript-blueprint-polyfill: 0.1.0 + ember-cli-version-checker: 5.1.2 + ember-router-generator: 2.0.0 + inflection: 2.0.1 + resolve: 1.22.8 + route-recognizer: 0.3.4 + router_js: 8.0.3(route-recognizer@0.3.4)(rsvp@4.8.5) + semver: 7.6.0 + silent-error: 1.1.1 + transitivePeerDependencies: + - '@babel/core' + - '@glint/template' + - rsvp + - supports-color + - webpack + ember-style-modifier@3.1.1(@babel/core@7.23.2)(@ember/string@3.1.1)(webpack@5.89.0): dependencies: '@ember/string': 3.1.1 @@ -17344,10 +17720,9 @@ snapshots: transitivePeerDependencies: - supports-color - ember-tracked-storage-polyfill@1.0.0: + ember-tracked-storage-polyfill@1.0.1: dependencies: ember-cli-babel: 7.26.11 - ember-cli-htmlbars: 5.7.2 transitivePeerDependencies: - supports-color @@ -17654,7 +18029,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.4.3 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -17684,6 +18059,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + esm@3.2.25: {} espree@9.6.1: @@ -17700,6 +18077,10 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@2.2.11: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -17753,19 +18134,6 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - execa@3.4.0: - dependencies: - cross-spawn: 7.0.3 - get-stream: 5.2.0 - human-signals: 1.1.1 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - p-finally: 2.0.1 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - execa@4.1.0: dependencies: cross-spawn: 7.0.3 @@ -18964,6 +19332,10 @@ snapshots: is-plain-object@5.0.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.9 + is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -19215,6 +19587,8 @@ snapshots: loader.js@4.7.0: {} + locate-character@3.0.0: {} + locate-path@2.0.0: dependencies: p-locate: 2.0.0 @@ -19341,6 +19715,10 @@ snapshots: dependencies: sourcemap-codec: 1.4.8 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.7: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -19752,6 +20130,8 @@ snapshots: node-releases@2.0.27: {} + node-releases@2.0.47: {} + node-watch@0.7.3: {} nopt@3.0.6: @@ -20105,119 +20485,112 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-at-rules-variables@0.3.0(postcss@8.4.35): + postcss-at-rules-variables@0.3.0(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - postcss-attribute-case-insensitive@6.0.3(postcss@8.4.35): + postcss-attribute-case-insensitive@6.0.3(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 - postcss-clamp@4.1.0(postcss@8.4.35): + postcss-clamp@4.1.0(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@6.0.14(postcss@8.4.35): + postcss-color-functional-notation@6.0.14(postcss@8.5.8): dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 - postcss-color-hex-alpha@9.0.4(postcss@8.4.35): + postcss-color-hex-alpha@9.0.4(postcss@8.5.8): dependencies: - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@9.0.3(postcss@8.4.35): + postcss-color-rebeccapurple@9.0.3(postcss@8.5.8): dependencies: - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-conditionals-renewed@1.0.0(postcss@8.4.35): + postcss-conditionals-renewed@1.0.0(postcss@8.5.8): dependencies: css-color-converter: 2.0.0 css-unit-converter: 1.1.2 - postcss: 8.4.35 + postcss: 8.5.8 - postcss-custom-media@10.0.8(postcss@8.4.35): + postcss-custom-media@10.0.8(postcss@8.5.8): dependencies: '@csstools/cascade-layer-name-parser': 1.0.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 '@csstools/media-query-list-parser': 2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) - postcss: 8.4.35 + postcss: 8.5.8 - postcss-custom-properties@13.3.12(postcss@8.4.35): + postcss-custom-properties@13.3.12(postcss@8.5.8): dependencies: '@csstools/cascade-layer-name-parser': 1.0.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-custom-selectors@7.1.12(postcss@8.4.35): + postcss-custom-selectors@7.1.12(postcss@8.5.8): dependencies: '@csstools/cascade-layer-name-parser': 1.0.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.1.2 - postcss-dir-pseudo-class@8.0.1(postcss@8.4.35): + postcss-dir-pseudo-class@8.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 - postcss-double-position-gradients@5.0.7(postcss@8.4.35): + postcss-double-position-gradients@5.0.7(postcss@8.5.8): dependencies: - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-each@1.1.0(postcss@8.4.35): + postcss-each@1.1.0(postcss@8.5.8): dependencies: - postcss: 8.4.35 - postcss-simple-vars: 6.0.3(postcss@8.4.35) + postcss: 8.5.8 + postcss-simple-vars: 6.0.3(postcss@8.5.8) - postcss-focus-visible@9.0.1(postcss@8.4.35): + postcss-focus-visible@9.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 - postcss-focus-within@8.0.1(postcss@8.4.35): + postcss-focus-within@8.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 - postcss-font-variant@5.0.0(postcss@8.4.35): - dependencies: - postcss: 8.4.35 - - postcss-gap-properties@5.0.1(postcss@8.4.35): + postcss-font-variant@5.0.0(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - postcss-image-set-function@6.0.3(postcss@8.4.35): + postcss-gap-properties@5.0.1(postcss@8.5.8): dependencies: - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 - postcss-value-parser: 4.2.0 + postcss: 8.5.8 - postcss-import@15.1.0(postcss@8.4.35): + postcss-image-set-function@6.0.3(postcss@8.5.8): dependencies: - postcss: 8.4.35 + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.11 postcss-import@15.1.0(postcss@8.5.8): dependencies: @@ -20226,24 +20599,19 @@ snapshots: read-cache: 1.0.0 resolve: 1.22.11 - postcss-js@4.1.0(postcss@8.4.35): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.4.35 - postcss-js@4.1.0(postcss@8.5.8): dependencies: camelcase-css: 2.0.1 postcss: 8.5.8 - postcss-lab-function@6.0.19(postcss@8.4.35): + postcss-lab-function@6.0.19(postcss@8.5.8): dependencies: '@csstools/css-color-parser': 2.0.5(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1) '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) '@csstools/css-tokenizer': 2.4.1 - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/utilities': 1.0.0(postcss@8.4.35) - postcss: 8.4.35 + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/utilities': 1.0.0(postcss@8.5.8) + postcss: 8.5.8 postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8): dependencies: @@ -20252,18 +20620,18 @@ snapshots: jiti: 1.21.7 postcss: 8.5.8 - postcss-logical@7.0.1(postcss@8.4.35): + postcss-logical@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-mixins@9.0.4(postcss@8.4.35): + postcss-mixins@9.0.4(postcss@8.5.8): dependencies: fast-glob: 3.3.2 - postcss: 8.4.35 - postcss-js: 4.1.0(postcss@8.4.35) - postcss-simple-vars: 7.0.1(postcss@8.4.35) - sugarss: 4.0.1(postcss@8.4.35) + postcss: 8.5.8 + postcss-js: 4.1.0(postcss@8.5.8) + postcss-simple-vars: 7.0.1(postcss@8.5.8) + sugarss: 4.0.1(postcss@8.5.8) postcss-modules-extract-imports@3.0.0(postcss@8.4.35): dependencies: @@ -20291,104 +20659,104 @@ snapshots: postcss: 8.5.8 postcss-selector-parser: 6.1.2 - postcss-nesting@12.1.5(postcss@8.4.35): + postcss-nesting@12.1.5(postcss@8.5.8): dependencies: '@csstools/selector-resolve-nested': 1.1.0(postcss-selector-parser@6.1.2) '@csstools/selector-specificity': 3.1.1(postcss-selector-parser@6.1.2) - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.1.2 - postcss-opacity-percentage@2.0.0(postcss@8.4.35): + postcss-opacity-percentage@2.0.0(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - postcss-overflow-shorthand@5.0.1(postcss@8.4.35): + postcss-overflow-shorthand@5.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.4.35): + postcss-page-break@3.0.4(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - postcss-place@9.0.1(postcss@8.4.35): + postcss-place@9.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser: 4.2.0 - postcss-preset-env@9.6.0(postcss@8.4.35): - dependencies: - '@csstools/postcss-cascade-layers': 4.0.6(postcss@8.4.35) - '@csstools/postcss-color-function': 3.0.19(postcss@8.4.35) - '@csstools/postcss-color-mix-function': 2.0.19(postcss@8.4.35) - '@csstools/postcss-content-alt-text': 1.0.0(postcss@8.4.35) - '@csstools/postcss-exponential-functions': 1.0.9(postcss@8.4.35) - '@csstools/postcss-font-format-keywords': 3.0.2(postcss@8.4.35) - '@csstools/postcss-gamut-mapping': 1.0.11(postcss@8.4.35) - '@csstools/postcss-gradients-interpolation-method': 4.0.20(postcss@8.4.35) - '@csstools/postcss-hwb-function': 3.0.18(postcss@8.4.35) - '@csstools/postcss-ic-unit': 3.0.7(postcss@8.4.35) - '@csstools/postcss-initial': 1.0.1(postcss@8.4.35) - '@csstools/postcss-is-pseudo-class': 4.0.8(postcss@8.4.35) - '@csstools/postcss-light-dark-function': 1.0.8(postcss@8.4.35) - '@csstools/postcss-logical-float-and-clear': 2.0.1(postcss@8.4.35) - '@csstools/postcss-logical-overflow': 1.0.1(postcss@8.4.35) - '@csstools/postcss-logical-overscroll-behavior': 1.0.1(postcss@8.4.35) - '@csstools/postcss-logical-resize': 2.0.1(postcss@8.4.35) - '@csstools/postcss-logical-viewport-units': 2.0.11(postcss@8.4.35) - '@csstools/postcss-media-minmax': 1.1.8(postcss@8.4.35) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 2.0.11(postcss@8.4.35) - '@csstools/postcss-nested-calc': 3.0.2(postcss@8.4.35) - '@csstools/postcss-normalize-display-values': 3.0.2(postcss@8.4.35) - '@csstools/postcss-oklab-function': 3.0.19(postcss@8.4.35) - '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.4.35) - '@csstools/postcss-relative-color-syntax': 2.0.19(postcss@8.4.35) - '@csstools/postcss-scope-pseudo-class': 3.0.1(postcss@8.4.35) - '@csstools/postcss-stepped-value-functions': 3.0.10(postcss@8.4.35) - '@csstools/postcss-text-decoration-shorthand': 3.0.7(postcss@8.4.35) - '@csstools/postcss-trigonometric-functions': 3.0.10(postcss@8.4.35) - '@csstools/postcss-unset-value': 3.0.1(postcss@8.4.35) - autoprefixer: 10.4.27(postcss@8.4.35) + postcss-preset-env@9.6.0(postcss@8.5.8): + dependencies: + '@csstools/postcss-cascade-layers': 4.0.6(postcss@8.5.8) + '@csstools/postcss-color-function': 3.0.19(postcss@8.5.8) + '@csstools/postcss-color-mix-function': 2.0.19(postcss@8.5.8) + '@csstools/postcss-content-alt-text': 1.0.0(postcss@8.5.8) + '@csstools/postcss-exponential-functions': 1.0.9(postcss@8.5.8) + '@csstools/postcss-font-format-keywords': 3.0.2(postcss@8.5.8) + '@csstools/postcss-gamut-mapping': 1.0.11(postcss@8.5.8) + '@csstools/postcss-gradients-interpolation-method': 4.0.20(postcss@8.5.8) + '@csstools/postcss-hwb-function': 3.0.18(postcss@8.5.8) + '@csstools/postcss-ic-unit': 3.0.7(postcss@8.5.8) + '@csstools/postcss-initial': 1.0.1(postcss@8.5.8) + '@csstools/postcss-is-pseudo-class': 4.0.8(postcss@8.5.8) + '@csstools/postcss-light-dark-function': 1.0.8(postcss@8.5.8) + '@csstools/postcss-logical-float-and-clear': 2.0.1(postcss@8.5.8) + '@csstools/postcss-logical-overflow': 1.0.1(postcss@8.5.8) + '@csstools/postcss-logical-overscroll-behavior': 1.0.1(postcss@8.5.8) + '@csstools/postcss-logical-resize': 2.0.1(postcss@8.5.8) + '@csstools/postcss-logical-viewport-units': 2.0.11(postcss@8.5.8) + '@csstools/postcss-media-minmax': 1.1.8(postcss@8.5.8) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 2.0.11(postcss@8.5.8) + '@csstools/postcss-nested-calc': 3.0.2(postcss@8.5.8) + '@csstools/postcss-normalize-display-values': 3.0.2(postcss@8.5.8) + '@csstools/postcss-oklab-function': 3.0.19(postcss@8.5.8) + '@csstools/postcss-progressive-custom-properties': 3.3.0(postcss@8.5.8) + '@csstools/postcss-relative-color-syntax': 2.0.19(postcss@8.5.8) + '@csstools/postcss-scope-pseudo-class': 3.0.1(postcss@8.5.8) + '@csstools/postcss-stepped-value-functions': 3.0.10(postcss@8.5.8) + '@csstools/postcss-text-decoration-shorthand': 3.0.7(postcss@8.5.8) + '@csstools/postcss-trigonometric-functions': 3.0.10(postcss@8.5.8) + '@csstools/postcss-unset-value': 3.0.1(postcss@8.5.8) + autoprefixer: 10.4.27(postcss@8.5.8) browserslist: 4.28.1 - css-blank-pseudo: 6.0.2(postcss@8.4.35) - css-has-pseudo: 6.0.5(postcss@8.4.35) - css-prefers-color-scheme: 9.0.1(postcss@8.4.35) + css-blank-pseudo: 6.0.2(postcss@8.5.8) + css-has-pseudo: 6.0.5(postcss@8.5.8) + css-prefers-color-scheme: 9.0.1(postcss@8.5.8) cssdb: 8.8.0 - postcss: 8.4.35 - postcss-attribute-case-insensitive: 6.0.3(postcss@8.4.35) - postcss-clamp: 4.1.0(postcss@8.4.35) - postcss-color-functional-notation: 6.0.14(postcss@8.4.35) - postcss-color-hex-alpha: 9.0.4(postcss@8.4.35) - postcss-color-rebeccapurple: 9.0.3(postcss@8.4.35) - postcss-custom-media: 10.0.8(postcss@8.4.35) - postcss-custom-properties: 13.3.12(postcss@8.4.35) - postcss-custom-selectors: 7.1.12(postcss@8.4.35) - postcss-dir-pseudo-class: 8.0.1(postcss@8.4.35) - postcss-double-position-gradients: 5.0.7(postcss@8.4.35) - postcss-focus-visible: 9.0.1(postcss@8.4.35) - postcss-focus-within: 8.0.1(postcss@8.4.35) - postcss-font-variant: 5.0.0(postcss@8.4.35) - postcss-gap-properties: 5.0.1(postcss@8.4.35) - postcss-image-set-function: 6.0.3(postcss@8.4.35) - postcss-lab-function: 6.0.19(postcss@8.4.35) - postcss-logical: 7.0.1(postcss@8.4.35) - postcss-nesting: 12.1.5(postcss@8.4.35) - postcss-opacity-percentage: 2.0.0(postcss@8.4.35) - postcss-overflow-shorthand: 5.0.1(postcss@8.4.35) - postcss-page-break: 3.0.4(postcss@8.4.35) - postcss-place: 9.0.1(postcss@8.4.35) - postcss-pseudo-class-any-link: 9.0.2(postcss@8.4.35) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.4.35) - postcss-selector-not: 7.0.2(postcss@8.4.35) - - postcss-pseudo-class-any-link@9.0.2(postcss@8.4.35): + postcss: 8.5.8 + postcss-attribute-case-insensitive: 6.0.3(postcss@8.5.8) + postcss-clamp: 4.1.0(postcss@8.5.8) + postcss-color-functional-notation: 6.0.14(postcss@8.5.8) + postcss-color-hex-alpha: 9.0.4(postcss@8.5.8) + postcss-color-rebeccapurple: 9.0.3(postcss@8.5.8) + postcss-custom-media: 10.0.8(postcss@8.5.8) + postcss-custom-properties: 13.3.12(postcss@8.5.8) + postcss-custom-selectors: 7.1.12(postcss@8.5.8) + postcss-dir-pseudo-class: 8.0.1(postcss@8.5.8) + postcss-double-position-gradients: 5.0.7(postcss@8.5.8) + postcss-focus-visible: 9.0.1(postcss@8.5.8) + postcss-focus-within: 8.0.1(postcss@8.5.8) + postcss-font-variant: 5.0.0(postcss@8.5.8) + postcss-gap-properties: 5.0.1(postcss@8.5.8) + postcss-image-set-function: 6.0.3(postcss@8.5.8) + postcss-lab-function: 6.0.19(postcss@8.5.8) + postcss-logical: 7.0.1(postcss@8.5.8) + postcss-nesting: 12.1.5(postcss@8.5.8) + postcss-opacity-percentage: 2.0.0(postcss@8.5.8) + postcss-overflow-shorthand: 5.0.1(postcss@8.5.8) + postcss-page-break: 3.0.4(postcss@8.5.8) + postcss-place: 9.0.1(postcss@8.5.8) + postcss-pseudo-class-any-link: 9.0.2(postcss@8.5.8) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.8) + postcss-selector-not: 7.0.2(postcss@8.5.8) + + postcss-pseudo-class-any-link@9.0.2(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 - postcss-replace-overflow-wrap@4.0.0(postcss@8.4.35): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-resolve-nested-selector@0.1.1: {} @@ -20396,9 +20764,9 @@ snapshots: dependencies: postcss: 8.4.35 - postcss-selector-not@7.0.2(postcss@8.4.35): + postcss-selector-not@7.0.2(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-selector-parser: 6.0.15 postcss-selector-parser@6.0.15: @@ -20411,13 +20779,13 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-simple-vars@6.0.3(postcss@8.4.35): + postcss-simple-vars@6.0.3(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 - postcss-simple-vars@7.0.1(postcss@8.4.35): + postcss-simple-vars@7.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 postcss-value-parser@4.2.0: {} @@ -20832,9 +21200,9 @@ snapshots: remove-types@1.0.0: dependencies: - '@babel/core': 7.23.2 - '@babel/plugin-syntax-decorators': 7.23.3(@babel/core@7.23.2) - '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.23.2) + '@babel/core': 7.29.0 + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.23.6(@babel/core@7.29.0) prettier: 2.8.8 transitivePeerDependencies: - supports-color @@ -21612,9 +21980,9 @@ snapshots: tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 - sugarss@4.0.1(postcss@8.4.35): + sugarss@4.0.1(postcss@8.5.8): dependencies: - postcss: 8.4.35 + postcss: 8.5.8 sum-up@1.0.3: dependencies: @@ -21641,6 +22009,27 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte@5.56.1: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) + '@types/estree': 1.0.9 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.8.1 + esm-env: 1.2.2 + esrap: 2.2.11 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + svg-tags@1.0.0: {} symlink-or-copy@1.3.1: {} @@ -21738,7 +22127,7 @@ snapshots: terser-webpack-plugin@5.3.10(webpack@5.89.0): dependencies: - '@jridgewell/trace-mapping': 0.3.22 + '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 @@ -21956,8 +22345,8 @@ snapshots: tracked-built-ins@3.4.0(@babel/core@7.23.2): dependencies: '@embroider/addon-shim': 1.10.2 - decorator-transforms: 2.3.1(@babel/core@7.23.2) - ember-tracked-storage-polyfill: 1.0.0 + decorator-transforms: 2.3.2(@babel/core@7.23.2) + ember-tracked-storage-polyfill: 1.0.1 transitivePeerDependencies: - '@babel/core' - supports-color @@ -22138,6 +22527,12 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -22475,3 +22870,5 @@ snapshots: yocto-queue@0.1.0: {} yocto-queue@1.0.0: {} + + zimmerframe@1.1.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..bf39b1a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +allowBuilds: + '@fortawesome/fontawesome-common-types': false + '@fortawesome/fontawesome-svg-core': false + '@fortawesome/free-brands-svg-icons': false + '@fortawesome/free-solid-svg-icons': false + core-js: false + fsevents: false +minimumReleaseAge: 0 \ No newline at end of file diff --git a/server/migrations/2024_01_01_000023_backfill_revenue_recognition_journal_entries.php b/server/migrations/2024_01_01_000023_backfill_revenue_recognition_journal_entries.php index b837ee6..36c4327 100644 --- a/server/migrations/2024_01_01_000023_backfill_revenue_recognition_journal_entries.php +++ b/server/migrations/2024_01_01_000023_backfill_revenue_recognition_journal_entries.php @@ -4,8 +4,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; -return new class extends Migration -{ +return new class extends Migration { /** * Backfill revenue-recognition journal entries for invoices that were * created before InvoiceController::onAfterCreate started calling @@ -43,14 +42,20 @@ public function up(): void $query->select(DB::raw(1)) ->from('ledger_journals') ->whereNull('ledger_journals.deleted_at') - ->where('ledger_journals.type', 'revenue_recognition') - ->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(ledger_journals.meta, '$.invoice_uuid')) = ledger_invoices.uuid"); + ->whereColumn('ledger_journals.company_uuid', 'ledger_invoices.company_uuid') + ->whereColumn('ledger_journals.amount', 'ledger_invoices.total_amount') + ->where(function ($query) { + $query->where(function ($query) { + $query->where('ledger_journals.type', 'revenue_recognition') + ->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(ledger_journals.meta, '$.invoice_uuid')) = ledger_invoices.uuid"); + })->orWhereRaw("ledger_journals.description = CONCAT('Revenue recognition for invoice ', ledger_invoices.number)"); + }); }) ->get(['uuid', 'public_id', 'company_uuid', 'total_amount', 'currency', 'number', 'created_at']); foreach ($invoices as $invoice) { // Resolve or create the AR and Revenue accounts for this company - $arAccount = $this->resolveAccount($invoice->company_uuid, 'AR-DEFAULT', 'asset', 'Accounts Receivable'); + $arAccount = $this->resolveAccount($invoice->company_uuid, 'AR-DEFAULT', 'asset', 'Accounts Receivable'); $revAccount = $this->resolveAccount($invoice->company_uuid, 'REV-DEFAULT', 'revenue', 'Sales Revenue'); if (!$arAccount || !$revAccount) { @@ -129,6 +134,7 @@ private function resolveAccount(string $companyUuid, string $code, string $type, ->update(['status' => 'active']); $account->status = 'active'; } + return $account; } diff --git a/server/resources/views/mail/invoice-sent.blade.php b/server/resources/views/mail/invoice-sent.blade.php new file mode 100644 index 0000000..687bd9a --- /dev/null +++ b/server/resources/views/mail/invoice-sent.blade.php @@ -0,0 +1,156 @@ + + + + + + Invoice {{ $invoiceNumber }} + + + + + + +
+ + + + + + + + + + +
+ @if ($companyLogoUrl) + {{ $companyName }} + @else +
{{ $companyName }}
+ @endif +
+ + + + + + + + + + + + + + + + +
+
Invoice
+

{{ $invoiceNumber }}

+

+ @if ($orderLabel) + {{ $companyName }} sent you an invoice for order {{ $orderLabel }}. + @else + {{ $companyName }} sent you an invoice. + @endif +

+
+ + + + + +
+
Bill To
+
{{ $customerName ?: 'Customer' }}
+ @if ($customerEmail) +
{{ $customerEmail }}
+ @endif +
+ + @if ($invoiceDate) + + + + + @endif + @if ($dueDate) + + + + + @endif + @if ($orderLabel) + + + + + @endif +
Invoice date{{ $invoiceDate }}
Due date{{ $dueDate }}
Order{{ $orderLabel }}
+
+
+ + + + + + + + + + + @forelse ($items as $item) + + + + + + + @empty + + + + @endforelse + +
DescriptionQtyUnit PriceAmount
{{ $item['description'] }}{{ $item['quantity'] }}{{ $item['unitPrice'] }}{{ $item['amount'] }}
No line items were recorded.
+
+ + + + + +
+ + + + + + + + + + + + + + @if ($hasAmountPaid) + + + + + @endif + + + + +
Subtotal{{ $subtotal }}
Tax{{ $tax }}
Total{{ $total }}
Amount paid{{ $amountPaid }}
Balance due{{ $balance }}
+
+
+ View Invoice +
+
+ This invoice was sent by {{ $companyName }}. +
+
+ + diff --git a/server/src/Http/Controllers/Internal/v1/InvoiceController.php b/server/src/Http/Controllers/Internal/v1/InvoiceController.php index 9fe0306..ca8816f 100644 --- a/server/src/Http/Controllers/Internal/v1/InvoiceController.php +++ b/server/src/Http/Controllers/Internal/v1/InvoiceController.php @@ -4,8 +4,10 @@ use Fleetbase\Ledger\Http\Controllers\LedgerResourceController; use Fleetbase\Ledger\Http\Resources\v1\Invoice as InvoiceResource; +use Fleetbase\Ledger\Http\Resources\v1\Transaction as TransactionResource; use Fleetbase\Ledger\Models\Invoice; use Fleetbase\Ledger\Models\InvoiceItem; +use Fleetbase\Ledger\Models\Transaction; use Fleetbase\Ledger\Services\InvoiceService; use Fleetbase\Services\TemplateRenderService; use Illuminate\Http\JsonResponse; @@ -104,6 +106,42 @@ public function recordPayment(string $id, Request $request): InvoiceResource return new InvoiceResource($invoice->load(['customer', 'items', 'template'])); } + /** + * List transactions related to an invoice. + */ + public function transactions(string $id, Request $request) + { + $invoice = Invoice::where('company_uuid', session('company')) + ->where(fn ($q) => $q->where('uuid', $id)->orWhere('public_id', $id)) + ->firstOrFail(); + + $transactions = Transaction::where('company_uuid', session('company')) + ->where(function ($query) use ($invoice) { + $query->where('subject_uuid', $invoice->uuid) + ->orWhere('context_uuid', $invoice->uuid); + + if ($invoice->transaction_uuid) { + $query->orWhere('uuid', $invoice->transaction_uuid); + } + }) + ->with([ + 'items', + 'journal.debitAccount', + 'journal.creditAccount', + 'subject', + 'payer', + 'payee', + 'initiator', + 'context', + ]) + ->orderBy('created_at', $request->input('sort') === 'created_at' ? 'asc' : 'desc') + ->paginate($request->integer('limit', 50)); + + TransactionResource::wrap('transactions'); + + return TransactionResource::collection($transactions); + } + /** * Mark an invoice as sent (without dispatching a notification). */ @@ -128,14 +166,12 @@ public function send(string $id, Request $request): InvoiceResource ->with('customer') ->firstOrFail(); - if (!$invoice->customer || !$invoice->customer->email) { - abort(422, 'Invoice customer does not have a valid email address.'); + try { + $invoice = app(InvoiceService::class)->send($invoice); + } catch (\InvalidArgumentException $e) { + abort(422, $e->getMessage()); } - $invoice->markAsSent(); - - // TODO (M5): Dispatch InvoiceSentNotification to $invoice->customer->email - return new InvoiceResource($invoice->load(['customer', 'items', 'template'])); } diff --git a/server/src/Http/Controllers/Internal/v1/ReportController.php b/server/src/Http/Controllers/Internal/v1/ReportController.php index f3e8536..d1b2a3a 100644 --- a/server/src/Http/Controllers/Internal/v1/ReportController.php +++ b/server/src/Http/Controllers/Internal/v1/ReportController.php @@ -82,6 +82,90 @@ public function dashboard(Request $request): JsonResponse ]); } + public function dashboardSummary(Request $request): JsonResponse + { + $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date', + ]); + + return response()->json([ + 'status' => 'ok', + 'data' => $this->ledgerService->getDashboardSummary(session('company'), $request->input('start_date'), $request->input('end_date')), + ]); + } + + public function dashboardRevenueTrend(Request $request): JsonResponse + { + $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date', + ]); + + return response()->json([ + 'status' => 'ok', + 'data' => $this->ledgerService->getDashboardRevenueTrend(session('company'), $request->input('start_date'), $request->input('end_date')), + ]); + } + + public function dashboardCashFlowSummary(Request $request): JsonResponse + { + $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date', + ]); + + return response()->json([ + 'status' => 'ok', + 'data' => $this->ledgerService->getDashboardCashFlowSummary(session('company'), $request->input('start_date'), $request->input('end_date')), + ]); + } + + public function dashboardInvoiceStatus(): JsonResponse + { + return response()->json([ + 'status' => 'ok', + 'data' => $this->ledgerService->getDashboardInvoiceStatus(session('company')), + ]); + } + + public function dashboardArAgingSummary(Request $request): JsonResponse + { + $request->validate([ + 'as_of_date' => 'nullable|date', + ]); + + return response()->json([ + 'status' => 'ok', + 'data' => $this->ledgerService->getDashboardArAgingSummary(session('company'), $request->input('as_of_date')), + ]); + } + + public function dashboardWalletBalances(Request $request): JsonResponse + { + $request->validate([ + 'date_from' => 'nullable|date', + 'date_to' => 'nullable|date', + ]); + + return response()->json([ + 'status' => 'ok', + 'data' => $this->ledgerService->getDashboardWalletBalances(session('company'), $request->input('date_from'), $request->input('date_to')), + ]); + } + + public function dashboardActivity(Request $request): JsonResponse + { + $request->validate([ + 'limit' => 'nullable|integer|min:1|max:25', + ]); + + return response()->json([ + 'status' => 'ok', + 'data' => $this->ledgerService->getDashboardActivity(session('company'), (int) $request->input('limit', 10)), + ]); + } + // ========================================================================= // Trial Balance // ========================================================================= diff --git a/server/src/Http/Controllers/Public/PublicInvoiceController.php b/server/src/Http/Controllers/Public/PublicInvoiceController.php index aa35793..2cb3f33 100644 --- a/server/src/Http/Controllers/Public/PublicInvoiceController.php +++ b/server/src/Http/Controllers/Public/PublicInvoiceController.php @@ -132,13 +132,24 @@ public function pay(Request $request, string $publicId): JsonResponse ], 422); } - // Resolve the gateway driver - try { - $driver = $this->gatewayManager->gateway($request->input('gateway_id')); - } catch (\Exception $e) { + $gateway = Gateway::query() + ->where('company_uuid', $invoice->company_uuid) + ->where('status', 'active') + ->where(function ($query) use ($request) { + $gatewayId = $request->input('gateway_id'); + + $query->where('uuid', $gatewayId) + ->orWhere('public_id', $gatewayId); + }) + ->first(); + + if (!$gateway) { return response()->json(['error' => 'Payment gateway not found or unavailable.'], 422); } + $driver = $this->gatewayManager->driver($gateway->driver) + ->initialize($gateway->decryptedConfig(), $gateway->is_sandbox); + // ── Stripe: hosted Checkout Session ─────────────────────────────────── if ($driver instanceof StripeDriver) { return $this->initiateStripeCheckout($driver, $invoice, $request); diff --git a/server/src/Http/Resources/v1/Invoice.php b/server/src/Http/Resources/v1/Invoice.php index 20f14f8..9f4570c 100644 --- a/server/src/Http/Resources/v1/Invoice.php +++ b/server/src/Http/Resources/v1/Invoice.php @@ -35,12 +35,14 @@ public function toArray($request) return $this->setCustomerType($this->transformMorphResource($this->customer)); }), // ── Related records ──────────────────────────────────────────── - 'order_uuid' => $this->when($isInternal, $this->order_uuid), - 'order' => $this->whenLoaded('order'), - 'transaction_uuid' => $this->when($isInternal, $this->transaction_uuid), - 'transaction' => $this->whenLoaded('transaction'), - 'template_uuid' => $this->when($isInternal, $this->template_uuid), - 'template' => $this->whenLoaded('template'), + 'order_uuid' => $this->when($this->order_uuid, $this->order_uuid), + 'order_public_id' => $this->when($this->order_uuid, fn () => data_get($this->order, 'public_id')), + 'order_tracking_number' => $this->when($this->order_uuid, fn () => data_get($this->order, 'trackingNumber.tracking_number')), + 'order' => $this->whenLoaded('order'), + 'transaction_uuid' => $this->when($isInternal, $this->transaction_uuid), + 'transaction' => $this->whenLoaded('transaction'), + 'template_uuid' => $this->when($isInternal, $this->template_uuid), + 'template' => $this->whenLoaded('template'), // ── Invoice details ──────────────────────────────────────────── 'number' => $this->number, 'status' => $this->status, diff --git a/server/src/Listeners/HandleSuccessfulPayment.php b/server/src/Listeners/HandleSuccessfulPayment.php index ca57858..b0c8178 100644 --- a/server/src/Listeners/HandleSuccessfulPayment.php +++ b/server/src/Listeners/HandleSuccessfulPayment.php @@ -135,7 +135,7 @@ public function handle(PaymentSucceeded $event): void [ 'company_uuid' => $companyUuid, 'currency' => $currency, - 'type' => 'gateway_payment', + 'journal_type' => 'gateway_payment', 'gateway_transaction_uuid' => $gatewayTransaction->uuid, 'meta' => [ 'gateway_driver' => $gateway->driver, diff --git a/server/src/Models/Invoice.php b/server/src/Models/Invoice.php index f93c2a1..b6ac32a 100644 --- a/server/src/Models/Invoice.php +++ b/server/src/Models/Invoice.php @@ -137,7 +137,7 @@ class Invoice extends Model * * @var array */ - protected $with = ['customer', 'items', 'template']; + protected $with = ['customer', 'items', 'template', 'order.trackingNumber']; /** * The attributes excluded from the model's JSON form. diff --git a/server/src/Notifications/InvoiceSent.php b/server/src/Notifications/InvoiceSent.php new file mode 100644 index 0000000..714fe3e --- /dev/null +++ b/server/src/Notifications/InvoiceSent.php @@ -0,0 +1,167 @@ +invoice->loadMissing(['customer', 'items', 'order.trackingNumber']); + + $number = $this->invoiceNumber(); + $companyName = $this->companyName(); + $currency = strtoupper((string) ($this->invoice->currency ?: 'USD')); + + return (new MailMessage()) + ->from(config('mail.from.address'), $companyName) + ->subject("Invoice {$number} from {$companyName}") + ->view('ledger::mail.invoice-sent', [ + 'companyName' => $companyName, + 'companyLogoUrl' => $this->companyLogoUrl(), + 'customerName' => $this->customerName(), + 'customerEmail' => $this->customerEmail(), + 'invoiceNumber' => $number, + 'invoiceDate' => $this->formatDate($this->invoice->date), + 'dueDate' => $this->formatDate($this->invoice->due_date), + 'orderLabel' => $this->orderLabel(), + 'items' => $this->lineItems(), + 'subtotal' => $this->formatMoney($this->invoice->subtotal, $currency), + 'tax' => $this->formatMoney($this->invoice->tax, $currency), + 'total' => $this->formatMoney($this->invoice->total_amount, $currency), + 'amountPaid' => $this->formatMoney($this->invoice->amount_paid, $currency), + 'balance' => $this->formatMoney($this->invoice->balance, $currency), + 'hasAmountPaid' => (int) $this->invoice->amount_paid > 0, + 'invoiceUrl' => $this->invoiceUrl(), + ]); + } + + protected function invoiceNumber(): string + { + return $this->invoice->number ?: $this->invoice->public_id; + } + + protected function companyName(): string + { + $company = $this->company(); + + return $company?->name ?: 'Your service provider'; + } + + protected function companyLogoUrl(): ?string + { + return $this->company()?->logo_url; + } + + protected function orderLabel(): ?string + { + $order = $this->invoice->order; + if (!$order) { + return null; + } + + return $order->tracking_number + ?? $order->trackingNumber?->tracking_number + ?? $order->public_id + ?? $order->uuid; + } + + protected function lineItems(): array + { + if (!$this->invoice->items || $this->invoice->items->isEmpty()) { + return []; + } + + return $this->invoice->items + ->map(function ($item) { + $currency = strtoupper((string) ($this->invoice->currency ?: 'USD')); + $quantity = (float) ($item->quantity ?: 1); + $unitPrice = $item->unit_price ?? ($quantity > 0 ? ((int) $item->amount / $quantity) : $item->amount); + + return [ + 'description' => Str::limit((string) ($item->description ?: 'Invoice item'), 140), + 'quantity' => $this->formatQuantity($quantity), + 'unitPrice' => $this->formatMoney($unitPrice, $currency), + 'amount' => $this->formatMoney($item->amount, $currency), + ]; + }) + ->values() + ->all(); + } + + protected function formatMoney($amount, string $currency): string + { + return $currency . ' ' . number_format(((int) $amount) / 100, 2); + } + + protected function invoiceUrl(): string + { + return Utils::consoleUrl('~/invoice', [ + 'id' => $this->invoice->public_id, + ]); + } + + protected function customerName(): ?string + { + $customer = $this->invoice->customer; + + return $customer?->name + ?? $customer?->display_name + ?? $customer?->email + ?? null; + } + + protected function customerEmail(): ?string + { + $customer = $this->invoice->customer; + + return $customer?->email + ?? $customer?->contact_email + ?? null; + } + + protected function company(): ?Company + { + if ($this->company === null) { + $this->company = Company::where('uuid', $this->invoice->company_uuid)->first(); + } + + return $this->company; + } + + protected function formatDate($date): ?string + { + if (!$date) { + return null; + } + + return Carbon::parse($date)->format('M j, Y'); + } + + protected function formatQuantity(float $quantity): string + { + return fmod($quantity, 1.0) === 0.0 ? (string) (int) $quantity : rtrim(rtrim(number_format($quantity, 2), '0'), '.'); + } +} diff --git a/server/src/Observers/OrderAccountingObserver.php b/server/src/Observers/OrderAccountingObserver.php new file mode 100644 index 0000000..f9536fa --- /dev/null +++ b/server/src/Observers/OrderAccountingObserver.php @@ -0,0 +1,440 @@ +type !== 'storefront') { + return; + } + + try { + DB::transaction(function () use ($order) { + $companyUuid = $order->company_uuid; + $currency = $order->getMeta('currency', 'USD'); + $total = (int) $order->getMeta('total', 0); + + if ($total <= 0) { + Log::channel('ledger')->info('[Ledger] OrderAccountingObserver: skipping Storefront order with zero total.', [ + 'order_uuid' => $order->uuid, + ]); + + return; + } + + $alreadyRecorded = Journal::where('type', 'storefront_sale') + ->where('meta->order_uuid', $order->uuid) + ->exists(); + + if ($alreadyRecorded) { + return; + } + + $cashAccount = Account::updateOrCreate( + ['company_uuid' => $companyUuid, 'code' => 'CASH-DEFAULT'], + [ + 'name' => 'Cash', + 'type' => Account::TYPE_ASSET, + 'description' => 'Default cash account', + 'is_system_account' => true, + 'status' => 'active', + ] + ); + + $revenueAccount = Account::updateOrCreate( + ['company_uuid' => $companyUuid, 'code' => 'REV-DEFAULT'], + [ + 'name' => 'Sales Revenue', + 'type' => Account::TYPE_REVENUE, + 'description' => 'Default sales revenue account', + 'is_system_account' => true, + 'status' => 'active', + ] + ); + + $meta = [ + 'order_uuid' => $order->uuid, + 'order_id' => $order->public_id, + 'subject_uuid' => $order->uuid, + 'subject_type' => get_class($order), + ]; + + foreach (['seed', 'seed_id'] as $seedMetaKey) { + if ($order->hasMeta($seedMetaKey)) { + $meta[$seedMetaKey] = $order->getMeta($seedMetaKey); + } + } + + $this->ledgerService->createJournalEntry( + $cashAccount, + $revenueAccount, + $total, + "Storefront sale - Order {$order->public_id}", + [ + 'company_uuid' => $companyUuid, + 'currency' => $currency, + 'journal_type' => 'storefront_sale', + 'entry_date' => now(), + 'meta' => $meta, + ] + ); + + Log::channel('ledger')->info('[Ledger] OrderAccountingObserver: Storefront sale journal entry created.', [ + 'order_uuid' => $order->uuid, + 'total' => $total, + 'currency' => $currency, + ]); + }); + } catch (\Throwable $e) { + // Never let a Ledger failure abort the order creation flow. + Log::channel('ledger')->error('[Ledger] OrderAccountingObserver: failed to create Storefront sale journal.', [ + 'error' => $e->getMessage(), + 'order_uuid' => $order->uuid ?? null, + ]); + } + } + + /** + * Handle the Order "updated" event. + */ + public function updated($order): void + { + if (!$order->wasChanged('status')) { + return; + } + + $previousStatus = $this->normalizeStatus((string) $order->getOriginal('status')); + $currentStatus = $this->normalizeStatus((string) $order->status); + + if (!$this->isCanceledStatus($previousStatus) && $this->isCanceledStatus($currentStatus)) { + $this->handleOrderCanceled($order, $previousStatus, $currentStatus); + + return; + } + + if ($this->isCanceledStatus($previousStatus) && !$this->isCanceledStatus($currentStatus)) { + $this->handleOrderRestored($order, $previousStatus, $currentStatus); + } + } + + private function handleOrderCanceled($order, string $previousStatus, string $currentStatus): void + { + try { + DB::transaction(function () use ($order, $previousStatus, $currentStatus) { + $reversedStorefrontSales = $this->reverseStorefrontSaleJournals($order, $previousStatus, $currentStatus); + $reversedInvoiceRevenue = $this->reverseOrderInvoiceRevenue($order, $previousStatus, $currentStatus); + + if ($reversedStorefrontSales === 0 && $reversedInvoiceRevenue === 0) { + Log::channel('ledger')->info('[Ledger] OrderAccountingObserver: order canceled with no linked Ledger revenue to reverse.', [ + 'order_uuid' => $order->uuid, + ]); + } + }); + } catch (\Throwable $e) { + // Never let a Ledger failure abort the order cancellation flow. + Log::channel('ledger')->error('[Ledger] OrderAccountingObserver: failed to reverse order revenue.', [ + 'error' => $e->getMessage(), + 'order_uuid' => $order->uuid ?? null, + ]); + } + } + + private function handleOrderRestored($order, string $previousStatus, string $currentStatus): void + { + try { + DB::transaction(function () use ($order, $previousStatus, $currentStatus) { + $reinstatedStorefrontSales = $this->reinstateReversedJournals( + $order, + 'storefront_sale_reversal', + 'storefront_sale_reinstatement', + "Storefront sale reinstatement - Order {$order->public_id}", + $previousStatus, + $currentStatus + ); + + $reinstatedInvoiceRevenue = $this->reinstateReversedJournals( + $order, + 'revenue_recognition_reversal', + 'revenue_recognition_reinstatement', + "Invoice revenue reinstatement - Order {$order->public_id}", + $previousStatus, + $currentStatus + ); + + $this->restoreOrderInvoices($order); + + if ($reinstatedStorefrontSales === 0 && $reinstatedInvoiceRevenue === 0) { + Log::channel('ledger')->info('[Ledger] OrderAccountingObserver: order restored with no reversed Ledger revenue to reinstate.', [ + 'order_uuid' => $order->uuid, + ]); + } + }); + } catch (\Throwable $e) { + // Never let a Ledger failure abort the order restoration flow. + Log::channel('ledger')->error('[Ledger] OrderAccountingObserver: failed to reinstate order revenue.', [ + 'error' => $e->getMessage(), + 'order_uuid' => $order->uuid ?? null, + ]); + } + } + + private function reverseStorefrontSaleJournals($order, string $previousStatus, string $currentStatus): int + { + $journals = Journal::with(['debitAccount', 'creditAccount']) + ->where('type', 'storefront_sale') + ->where('status', 'posted') + ->where('meta->order_uuid', $order->uuid) + ->get(); + + return $this->reverseJournals( + $journals, + $order, + 'storefront_sale_reversal', + "Storefront sale reversal - Order {$order->public_id} canceled", + $previousStatus, + $currentStatus + ); + } + + private function reverseOrderInvoiceRevenue($order, string $previousStatus, string $currentStatus): int + { + $invoices = Invoice::where('order_uuid', $order->uuid)->get(); + + foreach ($invoices as $invoice) { + $this->cancelOpenInvoice($invoice); + } + + if ($invoices->isEmpty()) { + return 0; + } + + $journals = Journal::with(['debitAccount', 'creditAccount']) + ->where('type', 'revenue_recognition') + ->where('status', 'posted') + ->whereIn('meta->invoice_uuid', $invoices->pluck('uuid')->all()) + ->get(); + + return $this->reverseJournals( + $journals, + $order, + 'revenue_recognition_reversal', + "Invoice revenue reversal - Order {$order->public_id} canceled", + $previousStatus, + $currentStatus + ); + } + + private function reverseJournals($journals, $order, string $reversalType, string $description, string $previousStatus, string $currentStatus): int + { + $created = 0; + + foreach ($journals as $journal) { + if (!$journal->debitAccount || !$journal->creditAccount || $journal->amount <= 0) { + continue; + } + + if ($this->journalIsCurrentlyReversed($journal->uuid, $reversalType)) { + continue; + } + + $meta = $this->correctionMeta($order, $journal, [ + 'reverses_journal_uuid' => $journal->uuid, + 'reverses_journal_id' => $journal->public_id, + 'original_journal_type' => $journal->type, + 'original_status' => $previousStatus, + 'canceled_status' => $currentStatus, + ]); + + $this->ledgerService->createJournalEntry( + $journal->creditAccount, + $journal->debitAccount, + (int) $journal->amount, + $description, + [ + 'company_uuid' => $journal->company_uuid, + 'currency' => $journal->currency, + 'journal_type' => $reversalType, + 'entry_date' => now(), + 'meta' => $meta, + ] + ); + + $created++; + } + + return $created; + } + + private function reinstateReversedJournals($order, string $reversalType, string $reinstatementType, string $description, string $previousStatus, string $currentStatus): int + { + $reversals = Journal::with(['debitAccount', 'creditAccount']) + ->where('type', $reversalType) + ->where('status', 'posted') + ->where('meta->order_uuid', $order->uuid) + ->get(); + + $created = 0; + + foreach ($reversals as $reversal) { + if (!$reversal->debitAccount || !$reversal->creditAccount || $reversal->amount <= 0) { + continue; + } + + if ($this->journalExists($reinstatementType, 'reinstates_journal_uuid', $reversal->uuid)) { + continue; + } + + $meta = $this->correctionMeta($order, $reversal, [ + 'reinstates_journal_uuid' => $reversal->uuid, + 'reinstates_journal_id' => $reversal->public_id, + 'reverses_journal_uuid' => $reversal->getMeta('reverses_journal_uuid'), + 'reverses_journal_id' => $reversal->getMeta('reverses_journal_id'), + 'reversal_journal_type' => $reversal->type, + 'restored_from_status' => $previousStatus, + 'restored_status' => $currentStatus, + ]); + + $this->ledgerService->createJournalEntry( + $reversal->creditAccount, + $reversal->debitAccount, + (int) $reversal->amount, + $description, + [ + 'company_uuid' => $reversal->company_uuid, + 'currency' => $reversal->currency, + 'journal_type' => $reinstatementType, + 'entry_date' => now(), + 'meta' => $meta, + ] + ); + + $created++; + } + + return $created; + } + + private function cancelOpenInvoice(Invoice $invoice): void + { + if (in_array($invoice->status, ['paid', 'void', 'cancelled'], true)) { + return; + } + + $invoice->updateMeta([ + 'order_cancellation_previous_status' => $invoice->status, + 'order_cancellation_status_changed' => true, + ]); + + $invoice->updateQuietly([ + 'status' => $invoice->status === 'draft' ? 'void' : 'cancelled', + ]); + } + + private function restoreOrderInvoices($order): void + { + Invoice::where('order_uuid', $order->uuid) + ->whereIn('status', ['void', 'cancelled']) + ->get() + ->each(function (Invoice $invoice) { + if (!$invoice->getMeta('order_cancellation_status_changed', false)) { + return; + } + + $previousInvoiceStatus = $invoice->getMeta('order_cancellation_previous_status', 'draft'); + + $invoice->updateMeta([ + 'order_cancellation_status_changed' => false, + 'order_cancellation_restored_at' => now()->toIso8601String(), + 'order_cancellation_restored_status' => $previousInvoiceStatus, + ]); + + $invoice->updateQuietly([ + 'status' => $previousInvoiceStatus, + ]); + }); + } + + private function correctionMeta($order, Journal $journal, array $meta): array + { + $journalMeta = $journal->getMeta(); + + $baseMeta = [ + 'order_uuid' => $order->uuid, + 'order_id' => $order->public_id, + 'subject_uuid' => $order->uuid, + 'subject_type' => get_class($order), + ]; + + foreach (['invoice_uuid', 'seed', 'seed_id'] as $metaKey) { + if (isset($journalMeta[$metaKey])) { + $baseMeta[$metaKey] = $journalMeta[$metaKey]; + } elseif ($order->hasMeta($metaKey)) { + $baseMeta[$metaKey] = $order->getMeta($metaKey); + } + } + + return array_merge($baseMeta, $meta); + } + + private function journalExists(string $journalType, string $metaKey, string $journalUuid): bool + { + return Journal::where('type', $journalType) + ->where("meta->{$metaKey}", $journalUuid) + ->exists(); + } + + private function journalIsCurrentlyReversed(string $journalUuid, string $reversalType): bool + { + $latestReversal = Journal::where('type', $reversalType) + ->where('meta->reverses_journal_uuid', $journalUuid) + ->orderBy('created_at', 'desc') + ->first(); + + if (!$latestReversal) { + return false; + } + + $reinstatementType = str_replace('_reversal', '_reinstatement', $reversalType); + + return !$this->journalExists($reinstatementType, 'reinstates_journal_uuid', $latestReversal->uuid); + } + + private function normalizeStatus(string $status): string + { + return strtolower(trim($status)); + } + + private function isCanceledStatus(string $status): bool + { + return in_array($status, self::CANCELED_STATUSES, true); + } +} diff --git a/server/src/Observers/PurchaseRateObserver.php b/server/src/Observers/PurchaseRateObserver.php index 83070c7..66799eb 100644 --- a/server/src/Observers/PurchaseRateObserver.php +++ b/server/src/Observers/PurchaseRateObserver.php @@ -2,8 +2,8 @@ namespace Fleetbase\Ledger\Observers; -use Fleetbase\Ledger\Services\InvoiceService; use Fleetbase\Ledger\Models\Invoice; +use Fleetbase\Ledger\Services\InvoiceService; use Illuminate\Support\Facades\Log; /** diff --git a/server/src/Observers/StorefrontOrderObserver.php b/server/src/Observers/StorefrontOrderObserver.php deleted file mode 100644 index fff6fc8..0000000 --- a/server/src/Observers/StorefrontOrderObserver.php +++ /dev/null @@ -1,129 +0,0 @@ -type === 'storefront'`. - * This is a first-class column on the orders table, always set by the - * Storefront package, and is the authoritative signal. - * - * Accounting flow - * --------------- - * DEBIT CASH-DEFAULT (asset ↑ — cash received from customer) - * CREDIT REV-DEFAULT (revenue ↑ — earned at point of sale) - * - * No invoice is created. Storefront orders are point-of-sale transactions; - * the order record itself is the receipt. Creating a Ledger invoice would - * be confusing and redundant. - * - * This observer is registered in LedgerServiceProvider only when the - * Fleet-Ops Order class is present. No hard dependency is introduced. - */ -class StorefrontOrderObserver -{ - public function __construct(protected LedgerService $ledgerService) - { - } - - /** - * Handle the Order "created" event. - */ - public function created($order): void - { - if ($order->type !== 'storefront') { - return; - } - - try { - DB::transaction(function () use ($order) { - $companyUuid = $order->company_uuid; - $currency = $order->getMeta('currency', 'USD'); - $total = (int) $order->getMeta('total', 0); - - if ($total <= 0) { - Log::channel('ledger')->info('[Ledger] StorefrontOrderObserver: skipping order with zero total.', [ - 'order_uuid' => $order->uuid, - ]); - - return; - } - - // Idempotency: skip if a journal entry already exists for this order. - $alreadyRecorded = \Fleetbase\Ledger\Models\Journal::where( - 'meta->order_uuid', $order->uuid - )->exists(); - - if ($alreadyRecorded) { - return; - } - - $cashAccount = Account::updateOrCreate( - ['company_uuid' => $companyUuid, 'code' => 'CASH-DEFAULT'], - [ - 'name' => 'Cash', - 'type' => 'asset', - 'description' => 'Default cash account', - 'is_system_account' => true, - 'status' => 'active', - ] - ); - - $revenueAccount = Account::updateOrCreate( - ['company_uuid' => $companyUuid, 'code' => 'REV-DEFAULT'], - [ - 'name' => 'Sales Revenue', - 'type' => Account::TYPE_REVENUE, - 'description' => 'Default sales revenue account', - 'is_system_account' => true, - 'status' => 'active', - ] - ); - - $this->ledgerService->createJournalEntry( - $cashAccount, - $revenueAccount, - $total, - "Storefront sale — Order {$order->public_id}", - [ - 'company_uuid' => $companyUuid, - 'currency' => $currency, - 'type' => 'storefront_sale', - 'subject_uuid' => $order->uuid, - 'subject_type' => get_class($order), - 'entry_date' => now(), - 'meta' => [ - 'order_uuid' => $order->uuid, - 'order_id' => $order->public_id, - ], - ] - ); - - Log::channel('ledger')->info('[Ledger] StorefrontOrderObserver: journal entry created.', [ - 'order_uuid' => $order->uuid, - 'total' => $total, - 'currency' => $currency, - ]); - }); - } catch (\Throwable $e) { - // Never let a Ledger failure abort the Storefront order creation flow. - Log::channel('ledger')->error('[Ledger] StorefrontOrderObserver: failed.', [ - 'error' => $e->getMessage(), - 'order_uuid' => $order->uuid ?? null, - ]); - } - } -} diff --git a/server/src/Providers/LedgerServiceProvider.php b/server/src/Providers/LedgerServiceProvider.php index deaf4eb..c435340 100644 --- a/server/src/Providers/LedgerServiceProvider.php +++ b/server/src/Providers/LedgerServiceProvider.php @@ -41,7 +41,7 @@ class LedgerServiceProvider extends CoreServiceProvider // Optional integrations — silently skipped when the package is not installed. // CoreServiceProvider::registerObservers() guards each entry with Utils::classExists(). 'Fleetbase\\FleetOps\\Models\\PurchaseRate' => \Fleetbase\Ledger\Observers\PurchaseRateObserver::class, - 'Fleetbase\\FleetOps\\Models\\Order' => \Fleetbase\Ledger\Observers\StorefrontOrderObserver::class, + 'Fleetbase\\FleetOps\\Models\\Order' => \Fleetbase\Ledger\Observers\OrderAccountingObserver::class, ]; /** @@ -94,6 +94,7 @@ public function boot() $this->registerExpansionsFrom(__DIR__ . '/../Expansions'); $this->loadRoutesFrom(__DIR__ . '/../routes.php'); $this->loadMigrationsFrom(__DIR__ . '/../../migrations'); + $this->loadViewsFrom(__DIR__ . '/../../resources/views', 'ledger'); // Register event-listener bindings for the payment gateway system $this->registerPaymentEvents(); diff --git a/server/src/Services/InvoiceService.php b/server/src/Services/InvoiceService.php index f4294ba..082f8f5 100644 --- a/server/src/Services/InvoiceService.php +++ b/server/src/Services/InvoiceService.php @@ -7,7 +7,9 @@ use Fleetbase\Ledger\Models\Invoice; use Fleetbase\Ledger\Models\InvoiceItem; use Fleetbase\Ledger\Models\Transaction; +use Fleetbase\Ledger\Notifications\InvoiceSent; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Notification; class InvoiceService { @@ -89,6 +91,47 @@ public function createFromOrder(Order $order, array $options = [], ?object $purc }); } + /** + * Send an invoice to its customer using the standard Ledger sent flow. + */ + public function send(Invoice $invoice): Invoice + { + $invoice->loadMissing(['customer']); + + if (in_array($invoice->status, ['paid', 'void', 'cancelled'], true)) { + throw new \InvalidArgumentException('Invoice cannot be sent in its current status.'); + } + + $email = $this->customerEmail($invoice); + if (!$email) { + throw new \InvalidArgumentException('Invoice customer does not have a valid email address.'); + } + + if ($invoice->status === 'draft') { + $invoice->markAsSent(); + } + + Notification::route('mail', $email)->notify(new InvoiceSent($invoice->fresh(['customer', 'items', 'template']))); + + return $invoice->fresh(['customer', 'items', 'template']); + } + + /** + * Resolve the best customer email available on the invoice customer model. + */ + protected function customerEmail(Invoice $invoice): ?string + { + $customer = $invoice->customer; + if (!$customer) { + return null; + } + + return $customer->email + ?? $customer->contact_email + ?? $customer->billing_email + ?? null; + } + /** * Create invoice line items from a FleetOps order. * @@ -446,9 +489,12 @@ public function recogniseRevenue(Invoice $invoice): void [ 'company_uuid' => $invoice->company_uuid, 'currency' => $invoice->currency, - 'type' => 'revenue_recognition', - 'subject_uuid' => $invoice->uuid, - 'subject_type' => Invoice::class, + 'journal_type' => 'revenue_recognition', + 'meta' => [ + 'invoice_uuid' => $invoice->uuid, + 'subject_uuid' => $invoice->uuid, + 'subject_type' => Invoice::class, + ], ] ); } diff --git a/server/src/Services/LedgerService.php b/server/src/Services/LedgerService.php index 978b75a..f5c8a3f 100644 --- a/server/src/Services/LedgerService.php +++ b/server/src/Services/LedgerService.php @@ -76,6 +76,11 @@ public function createJournalEntry( $companyUuid = $options['company_uuid'] ?? session('company'); $currency = $options['currency'] ?? $debitAccount->currency ?? 'USD'; $meta = $options['meta'] ?? []; + foreach (['subject_uuid', 'subject_type', 'gateway_transaction_uuid'] as $metaKey) { + if (array_key_exists($metaKey, $options) && !array_key_exists($metaKey, $meta)) { + $meta[$metaKey] = $options[$metaKey]; + } + } // Create the double-entry Journal record, linked to the caller's Transaction if provided $journal = Journal::create([ @@ -86,7 +91,7 @@ public function createJournalEntry( 'amount' => $amount, 'currency' => $currency, 'description' => $description, - 'type' => $options['journal_type'] ?? 'general', + 'type' => $options['journal_type'] ?? $options['type'] ?? 'general', 'status' => 'posted', 'reference' => $options['reference'] ?? null, 'memo' => $options['memo'] ?? $description, @@ -352,67 +357,256 @@ public function getIncomeStatement(string $companyUuid, ?string $startDate = nul $startDate = $startDate ?? now()->startOfMonth()->toDateString(); $endDate = $endDate ?? now()->toDateString(); - $accounts = Account::where('company_uuid', $companyUuid) - ->where('status', 'active') - ->whereIn('type', [Account::TYPE_REVENUE, Account::TYPE_EXPENSE]) - ->orderBy('code') + $activity = $this->getProfitAndLossActivity($companyUuid, $startDate, $endDate); + $revenues = $activity['revenues']; + $expenses = $activity['expenses']; + $totalRevenue = $activity['total_revenue']; + $totalExpenses = $activity['total_expenses']; + $netIncome = $totalRevenue - $totalExpenses; + + return [ + 'period' => [ + 'from' => $startDate, + 'to' => $endDate, + ], + 'revenues' => $revenues, + 'expenses' => $expenses, + 'total_revenue' => (int) $totalRevenue, + 'total_expenses' => (int) $totalExpenses, + 'net_income' => (int) $netIncome, + 'profitable' => $netIncome >= 0, + 'currency' => $activity['currency'], + 'daily' => $activity['daily'], + 'audit' => $activity['audit'], + ]; + } + + /** + * Return normalized profit and loss journal activity for reports and dashboard widgets. + */ + protected function getProfitAndLossActivity(string $companyUuid, string $startDate, string $endDate): array + { + $rows = Journal::where('company_uuid', $companyUuid) + ->whereBetween('entry_date', [$startDate, $endDate]) + ->where(function ($query) { + $query->whereHas('creditAccount', fn ($q) => $q->whereIn('type', [Account::TYPE_REVENUE, Account::TYPE_EXPENSE])) + ->orWhereHas('debitAccount', fn ($q) => $q->whereIn('type', [Account::TYPE_REVENUE, Account::TYPE_EXPENSE])); + }) + ->with(['creditAccount', 'debitAccount']) + ->orderBy('entry_date') + ->orderBy('created_at') ->get(); - $revenues = []; - $expenses = []; + [$journals, $duplicates] = $this->deduplicateProfitAndLossJournals($rows); + $revenues = []; + $expenses = []; + $daily = collect(); + $revenueByType = []; + $expenseByType = []; - foreach ($accounts as $account) { - // Income statement uses only activity within the period - $debits = Journal::where('debit_account_uuid', $account->uuid) - ->whereBetween('entry_date', [$startDate, $endDate]) - ->sum('amount'); + $cursor = now()->parse($startDate); + $end = now()->parse($endDate); + + while ($cursor->lte($end)) { + $key = $cursor->toDateString(); + $daily->put($key, ['date' => $key, 'revenue' => 0, 'expenses' => 0]); + $cursor->addDay(); + } - $credits = Journal::where('credit_account_uuid', $account->uuid) - ->whereBetween('entry_date', [$startDate, $endDate]) - ->sum('amount'); + foreach ($journals as $journal) { + $date = $journal->entry_date instanceof \DateTimeInterface ? $journal->entry_date->format('Y-m-d') : (string) $journal->entry_date; - if ($account->type === Account::TYPE_REVENUE) { - $balance = (int) ($credits - $debits); // credit-normal - } else { - $balance = (int) ($debits - $credits); // debit-normal + if (!$daily->has($date)) { + $daily->put($date, ['date' => $date, 'revenue' => 0, 'expenses' => 0]); } - if ($balance === 0) { - continue; + $dailyEntry = $daily->get($date); + + if ($journal->creditAccount?->type === Account::TYPE_REVENUE) { + $amount = (int) $journal->amount; + $accountKey = $journal->creditAccount->uuid; + $typeKey = $journal->type ?: 'general'; + + $revenues[$accountKey] ??= $this->makeProfitAndLossAccountRow($journal->creditAccount); + $revenues[$accountKey]['balance'] += $amount; + $dailyEntry['revenue'] += $amount; + $revenueByType[$typeKey] = ($revenueByType[$typeKey] ?? 0) + $amount; } - $row = [ - 'uuid' => $account->uuid, - 'code' => $account->code, - 'name' => $account->name, - 'balance' => $balance, - ]; + if ($journal->debitAccount?->type === Account::TYPE_REVENUE) { + $amount = (int) $journal->amount; + $accountKey = $journal->debitAccount->uuid; + $typeKey = $journal->type ?: 'general'; - if ($account->type === Account::TYPE_REVENUE) { - $revenues[] = $row; - } else { - $expenses[] = $row; + $revenues[$accountKey] ??= $this->makeProfitAndLossAccountRow($journal->debitAccount); + $revenues[$accountKey]['balance'] -= $amount; + $dailyEntry['revenue'] -= $amount; + $revenueByType[$typeKey] = ($revenueByType[$typeKey] ?? 0) - $amount; } + + if ($journal->debitAccount?->type === Account::TYPE_EXPENSE) { + $amount = (int) $journal->amount; + $accountKey = $journal->debitAccount->uuid; + $typeKey = $journal->type ?: 'general'; + + $expenses[$accountKey] ??= $this->makeProfitAndLossAccountRow($journal->debitAccount); + $expenses[$accountKey]['balance'] += $amount; + $dailyEntry['expenses'] += $amount; + $expenseByType[$typeKey] = ($expenseByType[$typeKey] ?? 0) + $amount; + } + + if ($journal->creditAccount?->type === Account::TYPE_EXPENSE) { + $amount = (int) $journal->amount; + $accountKey = $journal->creditAccount->uuid; + $typeKey = $journal->type ?: 'general'; + + $expenses[$accountKey] ??= $this->makeProfitAndLossAccountRow($journal->creditAccount); + $expenses[$accountKey]['balance'] -= $amount; + $dailyEntry['expenses'] -= $amount; + $expenseByType[$typeKey] = ($expenseByType[$typeKey] ?? 0) - $amount; + } + + $daily->put($date, $dailyEntry); } - $totalRevenue = array_sum(array_column($revenues, 'balance')); - $totalExpenses = array_sum(array_column($expenses, 'balance')); - $netIncome = $totalRevenue - $totalExpenses; + $revenues = collect($revenues)->filter(fn ($row) => $row['balance'] !== 0)->sortBy('code')->values(); + $expenses = collect($expenses)->filter(fn ($row) => $row['balance'] !== 0)->sortBy('code')->values(); + $currency = $this->resolveCurrencyFromJournals($journals) ?? $this->resolveDashboardCurrency(); return [ - 'period' => [ - 'from' => $startDate, - 'to' => $endDate, + 'revenues' => $revenues->all(), + 'expenses' => $expenses->all(), + 'total_revenue' => (int) $revenues->sum('balance'), + 'total_expenses' => (int) $expenses->sum('balance'), + 'daily' => $daily->sortKeys()->values(), + 'currency' => $currency, + 'audit' => [ + 'source' => 'ledger_journals', + 'amount_unit' => 'minor', + 'journal_rows' => $rows->count(), + 'counted_rows' => $journals->count(), + 'deduplicated_rows' => $duplicates, + 'revenue_by_type' => $revenueByType, + 'expense_by_type' => $expenseByType, + 'duplicate_strategy' => 'source_record_fingerprint', ], - 'revenues' => $revenues, - 'expenses' => $expenses, - 'total_revenue' => (int) $totalRevenue, - 'total_expenses' => (int) $totalExpenses, - 'net_income' => (int) $netIncome, - 'profitable' => $netIncome >= 0, ]; } + /** + * Keep one accounting journal per authoritative invoice/order/gateway source. + */ + protected function deduplicateProfitAndLossJournals(Collection $rows): array + { + $seen = []; + $duplicates = 0; + $journals = collect(); + + foreach ($rows as $journal) { + $key = $this->profitAndLossJournalFingerprint($journal); + + if ($key && array_key_exists($key, $seen)) { + $duplicates++; + continue; + } + + if ($key) { + $seen[$key] = true; + } + + $journals->push($journal); + } + + return [$journals, $duplicates]; + } + + /** + * Build a stable source fingerprint for revenue/expense rows that are backed by another record. + */ + protected function profitAndLossJournalFingerprint(Journal $journal): ?string + { + $meta = $journal->meta ?? []; + $companyUuid = $journal->company_uuid; + $description = (string) $journal->description; + + if (str_ends_with((string) $journal->type, '_reversal')) { + $reversesJournalUuid = data_get($meta, 'reverses_journal_uuid'); + + return implode('|', [ + 'journal-reversal', + $companyUuid, + (string) $journal->type, + $reversesJournalUuid ?: $journal->uuid, + ]); + } + + if (str_ends_with((string) $journal->type, '_reinstatement')) { + $reinstatesJournalUuid = data_get($meta, 'reinstates_journal_uuid'); + + return implode('|', [ + 'journal-reinstatement', + $companyUuid, + (string) $journal->type, + $reinstatesJournalUuid ?: $journal->uuid, + ]); + } + + if (preg_match('/Revenue recognition for invoice\s+([^\s\[]+)/', $description, $matches)) { + return implode('|', [ + 'invoice-number', + $companyUuid, + $matches[1], + (string) $journal->amount, + (string) $journal->currency, + ]); + } + + $invoiceUuid = data_get($meta, 'invoice_uuid'); + if ($invoiceUuid) { + return implode('|', ['invoice', $companyUuid, $invoiceUuid]); + } + + $orderUuid = data_get($meta, 'order_uuid'); + if ($orderUuid) { + return implode('|', ['order', $companyUuid, $orderUuid]); + } + + $gatewayTransactionUuid = data_get($meta, 'gateway_transaction_uuid'); + if ($gatewayTransactionUuid) { + return implode('|', ['gateway-transaction', $companyUuid, $gatewayTransactionUuid]); + } + + $transactionUuid = $journal->transaction_uuid; + if ($transactionUuid && in_array($journal->type, ['revenue', 'expense', 'gateway_payment', 'wallet_fee', 'refund'], true)) { + return implode('|', ['transaction', $companyUuid, $transactionUuid, (string) $journal->type]); + } + + return null; + } + + /** + * Build a report row from an account model. + */ + protected function makeProfitAndLossAccountRow(Account $account): array + { + return [ + 'uuid' => $account->uuid, + 'code' => $account->code, + 'name' => $account->name, + 'balance' => 0, + ]; + } + + /** + * Resolve the currency used by counted journal rows when all rows agree. + */ + protected function resolveCurrencyFromJournals(Collection $journals): ?string + { + $currencies = $journals->pluck('currency')->filter()->unique()->values(); + + return $currencies->count() === 1 ? $currencies->first() : null; + } + // ========================================================================= // Cash Flow Summary // ========================================================================= @@ -533,6 +727,277 @@ protected function computeNetFlow(array $items): int return $net; } + /** + * Return owner/operator KPI tiles for the redesigned dashboard. + */ + public function getDashboardSummary(string $companyUuid, ?string $startDate = null, ?string $endDate = null): array + { + $dashboard = $this->getDashboardMetrics($companyUuid, $startDate, $endDate); + $currency = $dashboard['currency'] ?? $this->resolveDashboardCurrency($dashboard['kpis']['wallet_totals'] ?? []); + + $outstandingAr = $dashboard['kpis']['outstanding_ar'] ?? ['total' => 0, 'overdue' => 0]; + $invoiceCounts = collect($dashboard['invoice_counts'] ?? []); + $walletTotals = collect($dashboard['kpis']['wallet_totals'] ?? []); + $walletMetric = $walletTotals->count() === 1 ? (int) ($walletTotals->first()['total'] ?? 0) : null; + + return [ + 'period' => $dashboard['period'], + 'currency' => $currency, + 'audit' => $dashboard['audit'] ?? [], + 'metrics' => [ + 'total_revenue' => $this->makeDashboardMetric('Total Revenue', $dashboard['kpis']['total_revenue'] ?? [], 'money', $currency), + 'total_expenses' => $this->makeDashboardMetric('Total Expenses', $dashboard['kpis']['total_expenses'] ?? [], 'money', $currency, true), + 'net_income' => $this->makeDashboardMetric('Net Income', $dashboard['kpis']['net_income'] ?? [], 'money', $currency), + 'outstanding_ar' => [ + 'label' => 'Outstanding AR', + 'value' => (int) ($outstandingAr['total'] ?? 0), + 'previous' => null, + 'delta_percent' => null, + 'format' => 'money', + 'currency' => $currency, + 'inverse' => true, + ], + 'overdue_ar' => [ + 'label' => 'Overdue AR', + 'value' => (int) ($outstandingAr['overdue'] ?? 0), + 'previous' => null, + 'delta_percent' => null, + 'format' => 'money', + 'currency' => $currency, + 'inverse' => true, + ], + 'open_invoices' => [ + 'label' => 'Open Invoices', + 'value' => (int) $invoiceCounts->except(['paid', 'cancelled', 'void'])->sum(), + 'previous' => null, + 'delta_percent' => null, + 'format' => 'count', + 'currency' => $currency, + 'inverse' => true, + ], + 'wallet_balance' => [ + 'label' => 'Wallet Balance', + 'value' => $walletMetric, + 'previous' => null, + 'delta_percent' => null, + 'format' => 'money', + 'currency' => $walletTotals->count() === 1 ? $walletTotals->first()['currency'] : $currency, + 'inverse' => false, + 'multi_currency' => $walletTotals->count() > 1, + 'currencies' => $walletTotals->values(), + ], + 'active_wallets' => [ + 'label' => 'Active Wallets', + 'value' => (int) $walletTotals->sum('count'), + 'previous' => null, + 'delta_percent' => null, + 'format' => 'count', + 'currency' => $currency, + 'inverse' => false, + ], + ], + ]; + } + + /** + * Return revenue and expense trend rows for chart widgets. + */ + public function getDashboardRevenueTrend(string $companyUuid, ?string $startDate = null, ?string $endDate = null): array + { + $startDate = $startDate ?? now()->subDays(29)->toDateString(); + $endDate = $endDate ?? now()->toDateString(); + $activity = $this->getProfitAndLossActivity($companyUuid, $startDate, $endDate); + $points = $activity['daily']; + $revenue = (int) $points->sum('revenue'); + $expenses = (int) $points->sum('expenses'); + + return [ + 'period' => [ + 'from' => $startDate, + 'to' => $endDate, + ], + 'currency' => $activity['currency'], + 'audit' => $activity['audit'], + 'summary' => [ + 'revenue' => $revenue, + 'expenses' => $expenses, + 'net' => $revenue - $expenses, + ], + 'labels' => $points->pluck('date')->values(), + 'datasets' => [ + [ + 'label' => 'Revenue', + 'data' => $points->pluck('revenue')->values(), + 'borderColor' => '#059669', + 'backgroundColor' => 'rgba(5, 150, 105, 0.12)', + 'tension' => 0.35, + 'fill' => true, + ], + [ + 'label' => 'Expenses', + 'data' => $points->pluck('expenses')->values(), + 'borderColor' => '#dc2626', + 'backgroundColor' => 'rgba(220, 38, 38, 0.08)', + 'tension' => 0.35, + 'fill' => true, + ], + ], + ]; + } + + /** + * Return a condensed cash flow summary for the dashboard. + */ + public function getDashboardCashFlowSummary(string $companyUuid, ?string $startDate = null, ?string $endDate = null): array + { + $cashFlow = $this->getCashFlowSummary($companyUuid, $startDate, $endDate); + + return [ + 'period' => $cashFlow['period'], + 'currency' => $this->resolveDashboardCurrency(), + 'operating' => (int) ($cashFlow['operating_activities']['net_flow'] ?? 0), + 'financing' => (int) ($cashFlow['financing_activities']['net_flow'] ?? 0), + 'investing' => (int) ($cashFlow['investing_activities']['net_flow'] ?? 0), + 'net_cash_change' => (int) ($cashFlow['net_cash_change'] ?? 0), + 'cash_account' => $cashFlow['cash_account'], + ]; + } + + /** + * Return invoice counts and balances by status. + */ + public function getDashboardInvoiceStatus(string $companyUuid): array + { + $statuses = ['draft', 'sent', 'paid', 'overdue', 'cancelled', 'void']; + $rows = Invoice::where('company_uuid', $companyUuid) + ->select('status', 'currency', DB::raw('count(*) as count'), DB::raw('sum(total_amount) as total'), DB::raw('sum(balance) as balance')) + ->groupBy('status', 'currency') + ->get(); + + $summary = collect($statuses)->map(function ($status) use ($rows) { + $matches = $rows->where('status', $status); + + return [ + 'status' => $status, + 'count' => (int) $matches->sum('count'), + 'total' => (int) $matches->sum('total'), + 'balance' => (int) $matches->sum('balance'), + 'currency' => $matches->pluck('currency')->filter()->unique()->count() === 1 ? $matches->first()?->currency : null, + ]; + })->values(); + + return [ + 'total_count' => (int) $summary->sum('count'), + 'total_open' => (int) $summary->whereNotIn('status', ['paid', 'cancelled', 'void'])->sum('balance'), + 'summary' => $summary, + ]; + } + + /** + * Return compact AR aging buckets for dashboard widgets. + */ + public function getDashboardArAgingSummary(string $companyUuid, ?string $asOfDate = null): array + { + $aging = $this->getArAging($companyUuid, $asOfDate); + + return [ + 'as_of_date' => $aging['as_of_date'], + 'grand_total' => $aging['grand_total'], + 'total_invoices' => $aging['total_invoices'], + 'buckets' => collect($aging['buckets'])->map(fn ($bucket, $key) => [ + 'key' => $key, + 'label' => $bucket['label'], + 'days_range' => $bucket['days_range'], + 'total' => (int) $bucket['total'], + 'invoice_count' => count($bucket['invoices'] ?? []), + ])->values(), + ]; + } + + /** + * Return dashboard-ready wallet totals and top wallet rows. + */ + public function getDashboardWalletBalances(string $companyUuid, ?string $dateFrom = null, ?string $dateTo = null): array + { + $dashboard = $this->getDashboardMetrics($companyUuid, $dateFrom, $dateTo); + + $topWallets = Wallet::where('company_uuid', $companyUuid) + ->where('status', Wallet::STATUS_ACTIVE) + ->with('subject') + ->orderBy('balance', 'desc') + ->limit(10) + ->get() + ->map(fn ($wallet) => [ + 'wallet_public_id' => $wallet->public_id, + 'name' => $wallet->name, + 'type' => $wallet->type, + 'balance' => (int) $wallet->balance, + 'formatted_balance' => $wallet->formatted_balance, + 'currency' => $wallet->currency, + 'subject' => $wallet->subject ? [ + 'name' => $wallet->subject->name ?? $wallet->subject->email ?? $wallet->subject->public_id ?? $wallet->subject->uuid, + ] : null, + ]); + + return [ + 'period' => $dashboard['period'], + 'totals' => $dashboard['kpis']['wallet_totals'] ?? [], + 'top_wallets' => $topWallets, + ]; + } + + /** + * Return recent financial activity for the dashboard feed. + */ + public function getDashboardActivity(string $companyUuid, int $limit = 10): array + { + $journals = Journal::where('company_uuid', $companyUuid) + ->with(['debitAccount', 'creditAccount']) + ->orderBy('created_at', 'desc') + ->limit($limit) + ->get() + ->map(fn ($journal) => [ + 'type' => 'journal', + 'public_id' => $journal->public_id, + 'description' => $journal->description, + 'amount' => (int) $journal->amount, + 'currency' => $journal->currency, + 'status' => $journal->status, + 'created_at' => $journal->created_at, + 'debit' => $journal->debitAccount?->name, + 'credit' => $journal->creditAccount?->name, + ]); + + return [ + 'items' => $journals, + ]; + } + + protected function makeDashboardMetric(string $label, array $source, string $format, string $currency, bool $inverse = false): array + { + return [ + 'label' => $label, + 'value' => (int) ($source['current'] ?? 0), + 'previous' => isset($source['previous']) ? (int) $source['previous'] : null, + 'delta_percent' => $source['change_pct'] ?? null, + 'format' => $format, + 'currency' => $source['currency'] ?? $currency, + 'inverse' => $inverse, + 'audit' => $source['audit'] ?? null, + ]; + } + + protected function resolveDashboardCurrency(iterable $walletTotals = []): string + { + foreach ($walletTotals as $row) { + if (!empty($row['currency'])) { + return $row['currency']; + } + } + + return 'USD'; + } + // ========================================================================= // Accounts Receivable Aging // ========================================================================= @@ -687,17 +1152,15 @@ public function getDashboardMetrics(string $companyUuid, ?string $startDate = nu ]); // Revenue trend — daily breakdown for the current period - $revenueTrend = Journal::where('company_uuid', $companyUuid) - ->whereHas('creditAccount', fn ($q) => $q->where('type', Account::TYPE_REVENUE)) - ->whereBetween('entry_date', [$startDate, $endDate]) - ->select('entry_date', DB::raw('sum(amount) as daily_revenue')) - ->groupBy('entry_date') - ->orderBy('entry_date') - ->get() - ->map(fn ($r) => [ - 'date' => $r->entry_date, - 'daily_revenue' => (int) $r->daily_revenue, - ]); + $revenueTrend = collect($currentIncome['daily'] ?? []); + if ($revenueTrend->isEmpty()) { + $revenueTrend = $this->getProfitAndLossActivity($companyUuid, $startDate, $endDate)['daily']; + } + + $revenueTrend = $revenueTrend->map(fn ($r) => [ + 'date' => $r['date'], + 'daily_revenue' => (int) $r['revenue'], + ]); // Recent journal entries $recentJournals = Journal::where('company_uuid', $companyUuid) @@ -713,22 +1176,29 @@ public function getDashboardMetrics(string $companyUuid, ?string $startDate = nu 'previous_from' => $prevStartDate, 'previous_to' => $prevEndDate, ], - 'kpis' => [ + 'currency' => $currentIncome['currency'] ?? $this->resolveDashboardCurrency($walletTotals), + 'kpis' => [ 'total_revenue' => [ 'current' => $currentIncome['total_revenue'], 'previous' => $previousIncome['total_revenue'], 'change_pct' => $this->percentageChange($previousIncome['total_revenue'], $currentIncome['total_revenue']), + 'currency' => $currentIncome['currency'], + 'audit' => $currentIncome['audit'], ], 'total_expenses' => [ 'current' => $currentIncome['total_expenses'], 'previous' => $previousIncome['total_expenses'], 'change_pct' => $this->percentageChange($previousIncome['total_expenses'], $currentIncome['total_expenses']), + 'currency' => $currentIncome['currency'], + 'audit' => $currentIncome['audit'], ], 'net_income' => [ 'current' => $currentIncome['net_income'], 'previous' => $previousIncome['net_income'], 'change_pct' => $this->percentageChange($previousIncome['net_income'], $currentIncome['net_income']), 'profitable' => $currentIncome['net_income'] >= 0, + 'currency' => $currentIncome['currency'], + 'audit' => $currentIncome['audit'], ], 'outstanding_ar' => [ 'total' => (int) $outstandingAr, @@ -739,6 +1209,10 @@ public function getDashboardMetrics(string $companyUuid, ?string $startDate = nu 'invoice_counts' => $invoiceCounts, 'revenue_trend' => $revenueTrend, 'recent_journals' => $recentJournals, + 'audit' => [ + 'income_statement' => $currentIncome['audit'], + 'previous_period' => $previousIncome['audit'], + ], ]; } diff --git a/server/src/routes.php b/server/src/routes.php index 81e7893..dbe9266 100644 --- a/server/src/routes.php +++ b/server/src/routes.php @@ -95,6 +95,7 @@ function ($router, $controller) { function ($router, $controller) { $router->post('create-from-order', $controller('createFromOrder')); $router->post('{id}/record-payment', $controller('recordPayment')); + $router->get('{id}/transactions', $controller('transactions')); $router->post('{id}/mark-as-sent', $controller('markAsSent')); $router->post('{id}/send', $controller('send')); $router->post('{id}/preview', $controller('preview')); @@ -173,6 +174,13 @@ function ($router, $controller) { // ---------------------------------------------------------------- $router->get('reports/general-ledger', 'ReportController@generalLedger'); $router->get('reports/dashboard', 'ReportController@dashboard'); + $router->get('reports/dashboard/summary', 'ReportController@dashboardSummary'); + $router->get('reports/dashboard/revenue-trend', 'ReportController@dashboardRevenueTrend'); + $router->get('reports/dashboard/cash-flow-summary', 'ReportController@dashboardCashFlowSummary'); + $router->get('reports/dashboard/invoice-status', 'ReportController@dashboardInvoiceStatus'); + $router->get('reports/dashboard/ar-aging-summary', 'ReportController@dashboardArAgingSummary'); + $router->get('reports/dashboard/wallet-balances', 'ReportController@dashboardWalletBalances'); + $router->get('reports/dashboard/activity', 'ReportController@dashboardActivity'); $router->get('reports/trial-balance', 'ReportController@trialBalance'); $router->get('reports/balance-sheet', 'ReportController@balanceSheet'); $router->get('reports/income-statement', 'ReportController@incomeStatement'); diff --git a/server/tests/Feature.php b/server/tests/Feature.php index 61cd84c..ac5c80b 100644 --- a/server/tests/Feature.php +++ b/server/tests/Feature.php @@ -3,3 +3,55 @@ test('example', function () { expect(true)->toBeTrue(); }); + +test('ledger dashboard report endpoints are registered', function () { + $routes = file_get_contents(__DIR__ . '/../src/routes.php'); + + expect($routes) + ->toContain('reports/dashboard/summary') + ->toContain('reports/dashboard/revenue-trend') + ->toContain('reports/dashboard/cash-flow-summary') + ->toContain('reports/dashboard/invoice-status') + ->toContain('reports/dashboard/ar-aging-summary') + ->toContain('reports/dashboard/wallet-balances') + ->toContain('reports/dashboard/activity'); +}); + +test('order accounting observer preserves seed metadata on storefront sale journal entries', function () { + $observer = file_get_contents(__DIR__ . '/../src/Observers/OrderAccountingObserver.php'); + + expect($observer) + ->toContain("foreach (['seed', 'seed_id'] as \$seedMetaKey)") + ->toContain('$meta[$seedMetaKey] = $order->getMeta($seedMetaKey)') + ->toContain("'meta' => \$meta"); +}); + +test('order accounting observer handles cancellation and reinstatement journal corrections', function () { + $observer = file_get_contents(__DIR__ . '/../src/Observers/OrderAccountingObserver.php'); + $provider = file_get_contents(__DIR__ . '/../src/Providers/LedgerServiceProvider.php'); + + expect($provider) + ->toContain('Fleetbase\\\\FleetOps\\\\Models\\\\Order') + ->toContain('OrderAccountingObserver::class'); + + expect($observer) + ->toContain('storefront_sale_reversal') + ->toContain('storefront_sale_reinstatement') + ->toContain('revenue_recognition_reversal') + ->toContain('revenue_recognition_reinstatement') + ->toContain('reverses_journal_uuid') + ->toContain('reinstates_journal_uuid') + ->toContain('order_cancellation_previous_status'); +}); + +test('profit and loss deduplication preserves reversal and reinstatement journals', function () { + $service = file_get_contents(__DIR__ . '/../src/Services/LedgerService.php'); + + expect($service) + ->toContain("str_ends_with((string) \$journal->type, '_reversal')") + ->toContain('journal-reversal') + ->toContain('reverses_journal_uuid') + ->toContain("str_ends_with((string) \$journal->type, '_reinstatement')") + ->toContain('journal-reinstatement') + ->toContain('reinstates_journal_uuid'); +}); diff --git a/tests/integration/components/ledger-dashboard/date-range-control-test.js b/tests/integration/components/ledger-dashboard/date-range-control-test.js new file mode 100644 index 0000000..715e66a --- /dev/null +++ b/tests/integration/components/ledger-dashboard/date-range-control-test.js @@ -0,0 +1,15 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'fleetbase-ledger-engine/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | ledger-dashboard/date-range-control', function (hooks) { + setupRenderingTest(hooks); + + test('it renders the dashboard period DatePicker wrapper', async function (assert) { + await render(hbs``); + + assert.dom('.ledger-dashboard-period-control').exists(); + assert.dom('.ledger-dashboard-period-control .form-input').exists(); + }); +}); diff --git a/tests/unit/extension-test.js b/tests/unit/extension-test.js new file mode 100644 index 0000000..cf7fe35 --- /dev/null +++ b/tests/unit/extension-test.js @@ -0,0 +1,57 @@ +import { module, test } from 'qunit'; +import extension from '@fleetbase/ledger-engine/extension'; + +module('Unit | extension', function () { + test('it registers invoice tabs for FleetOps and Storefront order details', function (assert) { + assert.expect(11); + + const registrations = []; + const dashboards = []; + const widgetRegistrations = []; + const universe = { + getService(name) { + if (name === 'universe/menu-service') { + return { + registerHeaderMenuItem() {}, + registerMenuItem(registry, menuItem) { + registrations.push({ registry, menuItem }); + }, + }; + } + + if (name === 'universe/widget-service') { + return { + registerDashboard(id) { + dashboards.push(id); + }, + registerWidgets(id, widgets) { + widgetRegistrations.push({ id, widgets }); + }, + }; + } + }, + }; + + extension.setupExtension(null, universe); + + const fleetOpsTab = registrations.find(({ registry, menuItem }) => registry === 'fleet-ops:component:order:details' && menuItem.slug === 'invoice'); + const storefrontTab = registrations.find(({ registry, menuItem }) => registry === 'storefront:component:order:details' && menuItem.slug === 'invoice'); + + assert.ok(fleetOpsTab); + assert.strictEqual(fleetOpsTab.menuItem.route, 'operations.orders.index.details.virtual'); + assert.strictEqual(fleetOpsTab.menuItem.icon, 'file-invoice-dollar'); + assert.strictEqual(fleetOpsTab.menuItem.component.name, 'order-invoice'); + + assert.ok(storefrontTab); + assert.strictEqual(storefrontTab.menuItem.route, 'orders.index.view.virtual'); + assert.strictEqual(storefrontTab.menuItem.icon, 'file-invoice-dollar'); + assert.strictEqual(storefrontTab.menuItem.component.name, 'order-invoice'); + + assert.deepEqual(dashboards, ['ledger']); + + const ledgerWidgets = widgetRegistrations.find((registration) => registration.id === 'ledger')?.widgets ?? []; + assert.ok(ledgerWidgets.find((widget) => widget.id === 'ledger-kpi-revenue')); + assert.ok(ledgerWidgets.find((widget) => widget.id === 'ledger-report-shortcuts')); + assert.notOk(ledgerWidgets.find((widget) => widget.id === 'ledger-overview')?.default); + }); +}); diff --git a/tests/unit/services/ledger-dashboard-test.js b/tests/unit/services/ledger-dashboard-test.js new file mode 100644 index 0000000..72d20f7 --- /dev/null +++ b/tests/unit/services/ledger-dashboard-test.js @@ -0,0 +1,57 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'fleetbase-ledger-engine/tests/helpers'; + +function formatDate(date) { + const year = date.getFullYear(); + const month = `${date.getMonth() + 1}`.padStart(2, '0'); + const day = `${date.getDate()}`.padStart(2, '0'); + + return `${year}-${month}-${day}`; +} + +module('Unit | Service | ledger-dashboard', function (hooks) { + setupTest(hooks); + + test('it defaults to month-to-date params', function (assert) { + const service = this.owner.lookup('service:ledger-dashboard'); + const today = new Date(); + const start = new Date(today.getFullYear(), today.getMonth(), 1); + + assert.deepEqual(service.dateRange, [formatDate(start), formatDate(today)]); + assert.deepEqual(service.periodParams, { + start_date: formatDate(start), + end_date: formatDate(today), + }); + assert.deepEqual(service.walletPeriodParams, { + date_from: formatDate(start), + date_to: formatDate(today), + }); + assert.deepEqual(service.asOfParams, { + as_of_date: formatDate(today), + }); + }); + + test('it updates subscribers when the period changes', function (assert) { + assert.expect(4); + + const service = this.owner.lookup('service:ledger-dashboard'); + const unsubscribe = service.subscribe((dashboard) => { + assert.strictEqual(dashboard.startDate, '2026-05-01'); + assert.strictEqual(dashboard.endDate, '2026-05-31'); + }); + + service.setDateRange({ formattedDate: ['2026-05-01', '2026-05-31'] }); + + assert.deepEqual(service.periodParams, { + start_date: '2026-05-01', + end_date: '2026-05-31', + }); + + unsubscribe(); + service.setDateRange({ formattedDate: ['2026-04-01', '2026-04-30'] }); + assert.deepEqual(service.periodParams, { + start_date: '2026-04-01', + end_date: '2026-04-30', + }); + }); +});