Refactors reports page search and filtering functionality
Moves search and filtering logic from inline script to a dedicated TypeScript module Improves code organization by creating a ReportsController with: - Enhanced search capabilities - Advanced range query parsing - Flexible filtering mechanisms Removes inline JavaScript and integrates modular approach in the application
This commit is contained in:
parent
405dabeb34
commit
2f92b0eb7b
3 changed files with 364 additions and 182 deletions
|
|
@ -391,188 +391,6 @@
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { createChart } from '/lib/swp-charting/dist/swp-charting.js';
|
import { createChart } from '/lib/swp-charting/dist/swp-charting.js';
|
||||||
import Fuse from '/lib/fuse/fuse.mjs';
|
|
||||||
|
|
||||||
// === SEARCH FUNCTIONALITY ===
|
|
||||||
const searchInput = document.getElementById('searchInput');
|
|
||||||
const tableRows = document.querySelectorAll('swp-card.sales-table swp-data-table-row');
|
|
||||||
|
|
||||||
// Build searchable data from table rows
|
|
||||||
const salesData = Array.from(tableRows).map((row, index) => {
|
|
||||||
const cells = row.querySelectorAll('swp-data-table-cell');
|
|
||||||
return {
|
|
||||||
index,
|
|
||||||
invoice: cells[0]?.textContent?.trim() || '',
|
|
||||||
date: cells[1]?.textContent?.trim() || '',
|
|
||||||
customer: cells[2]?.textContent?.trim() || '',
|
|
||||||
employee: cells[3]?.textContent?.trim() || '',
|
|
||||||
services: cells[4]?.textContent?.trim() || '',
|
|
||||||
amount: cells[5]?.textContent?.trim() || '',
|
|
||||||
payment: cells[6]?.textContent?.trim() || '',
|
|
||||||
status: cells[7]?.textContent?.trim() || ''
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize Fuse.js
|
|
||||||
const fuse = new Fuse(salesData, {
|
|
||||||
keys: ['invoice', 'customer', 'employee', 'services', 'amount', 'payment', 'status'],
|
|
||||||
threshold: 0.3,
|
|
||||||
ignoreLocation: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse amount string "1.450 kr" -> 1450
|
|
||||||
function parseAmountFromText(text) {
|
|
||||||
const match = text.match(/-?([\d.]+)/);
|
|
||||||
if (!match) return null;
|
|
||||||
// Remove thousand separators (Danish format uses . for thousands)
|
|
||||||
return parseFloat(match[1].replace(/\./g, ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse invoice number "#1847" -> 1847
|
|
||||||
function parseInvoiceNumber(text) {
|
|
||||||
const match = text.match(/#(\d+)/);
|
|
||||||
return match ? parseInt(match[1], 10) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse range operators from query (supports both amount and invoice ranges)
|
|
||||||
function parseRangeQuery(query) {
|
|
||||||
let textQuery = query;
|
|
||||||
let minAmount = null;
|
|
||||||
let maxAmount = null;
|
|
||||||
let minInvoice = null;
|
|
||||||
let maxInvoice = null;
|
|
||||||
|
|
||||||
// === INVOICE NUMBER RANGES (with # prefix) ===
|
|
||||||
// Match #1840-1845
|
|
||||||
let match = textQuery.match(/#(\d+)-(\d+)/);
|
|
||||||
if (match) {
|
|
||||||
minInvoice = parseInt(match[1], 10);
|
|
||||||
maxInvoice = parseInt(match[2], 10);
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match #>=
|
|
||||||
match = textQuery.match(/#>=\s*(\d+)/);
|
|
||||||
if (match) {
|
|
||||||
minInvoice = parseInt(match[1], 10);
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match #> (but not #>=)
|
|
||||||
match = textQuery.match(/#>\s*(\d+)/);
|
|
||||||
if (match) {
|
|
||||||
minInvoice = parseInt(match[1], 10) + 1;
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match #<=
|
|
||||||
match = textQuery.match(/#<=\s*(\d+)/);
|
|
||||||
if (match) {
|
|
||||||
maxInvoice = parseInt(match[1], 10);
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match #< (but not #<=)
|
|
||||||
match = textQuery.match(/#<\s*(\d+)/);
|
|
||||||
if (match) {
|
|
||||||
maxInvoice = parseInt(match[1], 10) - 1;
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// === AMOUNT RANGES (no prefix) ===
|
|
||||||
// Match range syntax: 400-1000
|
|
||||||
match = textQuery.match(/(\d+)-(\d+)/);
|
|
||||||
if (match) {
|
|
||||||
minAmount = parseFloat(match[1]);
|
|
||||||
maxAmount = parseFloat(match[2]);
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match >=
|
|
||||||
match = textQuery.match(/>=\s*(\d+(?:\.\d+)?)/);
|
|
||||||
if (match) {
|
|
||||||
minAmount = parseFloat(match[1]);
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match > (but not >=)
|
|
||||||
match = textQuery.match(/>\s*(\d+(?:\.\d+)?)/);
|
|
||||||
if (match) {
|
|
||||||
minAmount = parseFloat(match[1]) + 0.01;
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match <=
|
|
||||||
match = textQuery.match(/<=\s*(\d+(?:\.\d+)?)/);
|
|
||||||
if (match) {
|
|
||||||
maxAmount = parseFloat(match[1]);
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match < (but not <=)
|
|
||||||
match = textQuery.match(/<\s*(\d+(?:\.\d+)?)/);
|
|
||||||
if (match) {
|
|
||||||
maxAmount = parseFloat(match[1]) - 0.01;
|
|
||||||
textQuery = textQuery.replace(match[0], '').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { textQuery, minAmount, maxAmount, minInvoice, maxInvoice };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search handler with range support
|
|
||||||
searchInput.addEventListener('input', (e) => {
|
|
||||||
const query = e.target.value.trim();
|
|
||||||
|
|
||||||
if (!query) {
|
|
||||||
// Show all rows
|
|
||||||
tableRows.forEach(row => row.style.display = '');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { textQuery, minAmount, maxAmount, minInvoice, maxInvoice } = parseRangeQuery(query);
|
|
||||||
|
|
||||||
// Start with all indices
|
|
||||||
let matchedIndices = new Set(salesData.map((_, i) => i));
|
|
||||||
|
|
||||||
// Apply invoice number range filter
|
|
||||||
if (minInvoice !== null || maxInvoice !== null) {
|
|
||||||
matchedIndices = new Set(
|
|
||||||
salesData
|
|
||||||
.filter((item) => {
|
|
||||||
const invoiceNum = parseInvoiceNumber(item.invoice);
|
|
||||||
if (invoiceNum === null) return false;
|
|
||||||
if (minInvoice !== null && invoiceNum < minInvoice) return false;
|
|
||||||
if (maxInvoice !== null && invoiceNum > maxInvoice) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map(item => item.index)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply amount range filter
|
|
||||||
if (minAmount !== null || maxAmount !== null) {
|
|
||||||
matchedIndices = new Set(
|
|
||||||
[...matchedIndices].filter(i => {
|
|
||||||
const amount = parseAmountFromText(salesData[i].amount);
|
|
||||||
if (amount === null) return false;
|
|
||||||
if (minAmount !== null && amount < minAmount) return false;
|
|
||||||
if (maxAmount !== null && amount > maxAmount) return false;
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply Fuse.js text search if there's remaining text
|
|
||||||
if (textQuery) {
|
|
||||||
const fuseResults = fuse.search(textQuery);
|
|
||||||
const textMatches = new Set(fuseResults.map(r => r.item.index));
|
|
||||||
matchedIndices = new Set([...matchedIndices].filter(i => textMatches.has(i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
tableRows.forEach((row, index) => {
|
|
||||||
row.style.display = matchedIndices.has(index) ? '' : 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Revenue bar chart
|
// Revenue bar chart
|
||||||
createChart(document.getElementById('revenueChart'), {
|
createChart(document.getElementById('revenueChart'), {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { ControlsController } from './modules/controls';
|
||||||
import { ServicesController } from './modules/services';
|
import { ServicesController } from './modules/services';
|
||||||
import { CustomersController } from './modules/customers';
|
import { CustomersController } from './modules/customers';
|
||||||
import { TrackingController } from './modules/tracking';
|
import { TrackingController } from './modules/tracking';
|
||||||
|
import { ReportsController } from './modules/reports';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main application class
|
* Main application class
|
||||||
|
|
@ -31,6 +32,7 @@ export class App {
|
||||||
readonly services: ServicesController;
|
readonly services: ServicesController;
|
||||||
readonly customers: CustomersController;
|
readonly customers: CustomersController;
|
||||||
readonly tracking: TrackingController;
|
readonly tracking: TrackingController;
|
||||||
|
readonly reports: ReportsController;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize controllers
|
// Initialize controllers
|
||||||
|
|
@ -45,6 +47,7 @@ export class App {
|
||||||
this.services = new ServicesController();
|
this.services = new ServicesController();
|
||||||
this.customers = new CustomersController();
|
this.customers = new CustomersController();
|
||||||
this.tracking = new TrackingController();
|
this.tracking = new TrackingController();
|
||||||
|
this.reports = new ReportsController();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
361
PlanTempus.Application/wwwroot/ts/modules/reports.ts
Normal file
361
PlanTempus.Application/wwwroot/ts/modules/reports.ts
Normal file
|
|
@ -0,0 +1,361 @@
|
||||||
|
/**
|
||||||
|
* Reports Controller
|
||||||
|
*
|
||||||
|
* Handles search, filtering, and chart rendering for the Reports page.
|
||||||
|
* Supports range queries on amounts and invoice numbers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
interface SalesDataItem {
|
||||||
|
index: number;
|
||||||
|
invoice: string;
|
||||||
|
date: string;
|
||||||
|
customer: string;
|
||||||
|
employee: string;
|
||||||
|
services: string;
|
||||||
|
amount: string;
|
||||||
|
payment: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParsedQuery {
|
||||||
|
textQuery: string;
|
||||||
|
minAmount: number | null;
|
||||||
|
maxAmount: number | null;
|
||||||
|
minInvoice: number | null;
|
||||||
|
maxInvoice: number | null;
|
||||||
|
invoicePrefix: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReportsController {
|
||||||
|
private searchInput: HTMLInputElement | null = null;
|
||||||
|
private tableRows: NodeListOf<HTMLElement> | null = null;
|
||||||
|
private salesData: SalesDataItem[] = [];
|
||||||
|
private fuse: Fuse<SalesDataItem> | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.searchInput = document.getElementById('searchInput') as HTMLInputElement | null;
|
||||||
|
this.tableRows = document.querySelectorAll<HTMLElement>('swp-card.sales-table swp-data-table-row');
|
||||||
|
|
||||||
|
if (this.searchInput && this.tableRows?.length) {
|
||||||
|
this.buildSearchData();
|
||||||
|
this.initializeFuse();
|
||||||
|
this.setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setupTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build searchable data from table rows
|
||||||
|
*/
|
||||||
|
private buildSearchData(): void {
|
||||||
|
if (!this.tableRows) return;
|
||||||
|
|
||||||
|
this.salesData = Array.from(this.tableRows).map((row, index) => {
|
||||||
|
const cells = row.querySelectorAll('swp-data-table-cell');
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
invoice: cells[0]?.textContent?.trim() || '',
|
||||||
|
date: cells[1]?.textContent?.trim() || '',
|
||||||
|
customer: cells[2]?.textContent?.trim() || '',
|
||||||
|
employee: cells[3]?.textContent?.trim() || '',
|
||||||
|
services: cells[4]?.textContent?.trim() || '',
|
||||||
|
amount: cells[5]?.textContent?.trim() || '',
|
||||||
|
payment: cells[6]?.textContent?.trim() || '',
|
||||||
|
status: cells[7]?.textContent?.trim() || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Fuse.js for fuzzy text search
|
||||||
|
*/
|
||||||
|
private initializeFuse(): void {
|
||||||
|
this.fuse = new Fuse(this.salesData, {
|
||||||
|
keys: ['invoice', 'customer', 'employee', 'services', 'amount', 'payment', 'status'],
|
||||||
|
threshold: 0.3,
|
||||||
|
ignoreLocation: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup event listeners
|
||||||
|
*/
|
||||||
|
private setupListeners(): void {
|
||||||
|
this.searchInput?.addEventListener('input', (e) => this.handleSearch(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup tab switching functionality
|
||||||
|
*/
|
||||||
|
private setupTabs(): void {
|
||||||
|
const tabs = document.querySelectorAll<HTMLElement>('swp-tab[data-tab]');
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
const targetTab = tab.dataset.tab;
|
||||||
|
if (targetTab) {
|
||||||
|
this.switchToTab(targetTab);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a specific tab by name
|
||||||
|
*/
|
||||||
|
private switchToTab(targetTab: string): void {
|
||||||
|
const tabs = document.querySelectorAll<HTMLElement>('swp-tab[data-tab]');
|
||||||
|
const contents = document.querySelectorAll<HTMLElement>('swp-tab-content[data-tab]');
|
||||||
|
|
||||||
|
tabs.forEach(t => {
|
||||||
|
t.classList.toggle('active', t.dataset.tab === targetTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
contents.forEach(content => {
|
||||||
|
content.classList.toggle('active', content.dataset.tab === targetTab);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle search input
|
||||||
|
*/
|
||||||
|
private handleSearch(e: Event): void {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
const query = target.value.trim();
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
this.showAllRows();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = this.parseRangeQuery(query);
|
||||||
|
let matchedIndices = new Set(this.salesData.map((_, i) => i));
|
||||||
|
|
||||||
|
// Apply invoice prefix filter
|
||||||
|
if (parsed.invoicePrefix !== null) {
|
||||||
|
matchedIndices = this.filterByInvoicePrefix(parsed.invoicePrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply invoice number range filter
|
||||||
|
if (parsed.minInvoice !== null || parsed.maxInvoice !== null) {
|
||||||
|
matchedIndices = this.filterByInvoiceRange(matchedIndices, parsed.minInvoice, parsed.maxInvoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply amount range filter
|
||||||
|
if (parsed.minAmount !== null || parsed.maxAmount !== null) {
|
||||||
|
matchedIndices = this.filterByAmountRange(matchedIndices, parsed.minAmount, parsed.maxAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply Fuse.js text search
|
||||||
|
if (parsed.textQuery) {
|
||||||
|
matchedIndices = this.filterByText(matchedIndices, parsed.textQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applyFilter(matchedIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show all rows
|
||||||
|
*/
|
||||||
|
private showAllRows(): void {
|
||||||
|
this.tableRows?.forEach(row => row.style.display = '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply filter to table rows
|
||||||
|
*/
|
||||||
|
private applyFilter(matchedIndices: Set<number>): void {
|
||||||
|
this.tableRows?.forEach((row, index) => {
|
||||||
|
row.style.display = matchedIndices.has(index) ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by invoice prefix (e.g., #18 matches #1847, #1846)
|
||||||
|
*/
|
||||||
|
private filterByInvoicePrefix(prefix: string): Set<number> {
|
||||||
|
return new Set(
|
||||||
|
this.salesData
|
||||||
|
.filter(item => {
|
||||||
|
const invoiceDigits = item.invoice.replace(/\D/g, '');
|
||||||
|
return invoiceDigits.startsWith(prefix);
|
||||||
|
})
|
||||||
|
.map(item => item.index)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by invoice number range
|
||||||
|
*/
|
||||||
|
private filterByInvoiceRange(
|
||||||
|
indices: Set<number>,
|
||||||
|
min: number | null,
|
||||||
|
max: number | null
|
||||||
|
): Set<number> {
|
||||||
|
return new Set(
|
||||||
|
[...indices].filter(i => {
|
||||||
|
const invoiceNum = this.parseInvoiceNumber(this.salesData[i].invoice);
|
||||||
|
if (invoiceNum === null) return false;
|
||||||
|
if (min !== null && invoiceNum < min) return false;
|
||||||
|
if (max !== null && invoiceNum > max) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by amount range
|
||||||
|
*/
|
||||||
|
private filterByAmountRange(
|
||||||
|
indices: Set<number>,
|
||||||
|
min: number | null,
|
||||||
|
max: number | null
|
||||||
|
): Set<number> {
|
||||||
|
return new Set(
|
||||||
|
[...indices].filter(i => {
|
||||||
|
const amount = this.parseAmountFromText(this.salesData[i].amount);
|
||||||
|
if (amount === null) return false;
|
||||||
|
if (min !== null && amount < min) return false;
|
||||||
|
if (max !== null && amount > max) return false;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter by text using Fuse.js
|
||||||
|
*/
|
||||||
|
private filterByText(indices: Set<number>, query: string): Set<number> {
|
||||||
|
if (!this.fuse) return indices;
|
||||||
|
|
||||||
|
const fuseResults = this.fuse.search(query);
|
||||||
|
const textMatches = new Set(fuseResults.map(r => r.item.index));
|
||||||
|
return new Set([...indices].filter(i => textMatches.has(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse amount string "1.450 kr" -> 1450
|
||||||
|
*/
|
||||||
|
private parseAmountFromText(text: string): number | null {
|
||||||
|
const match = text.match(/-?([\d.]+)/);
|
||||||
|
if (!match) return null;
|
||||||
|
return parseFloat(match[1].replace(/\./g, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse invoice number "#1847" -> 1847
|
||||||
|
*/
|
||||||
|
private parseInvoiceNumber(text: string): number | null {
|
||||||
|
const match = text.match(/#(\d+)/);
|
||||||
|
return match ? parseInt(match[1], 10) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse range operators from query
|
||||||
|
* Supports:
|
||||||
|
* - Amount ranges: >1000, <500, >=500, <=1000, 400-1000
|
||||||
|
* - Invoice ranges: #>1845, #<1845, #>=1845, #<=1845, #1840-1845
|
||||||
|
* - Invoice prefix: #1847 (matches invoices starting with "1847")
|
||||||
|
* - Combined: Maria >1000, #>1845 <500
|
||||||
|
*/
|
||||||
|
private parseRangeQuery(query: string): ParsedQuery {
|
||||||
|
let textQuery = query;
|
||||||
|
let minAmount: number | null = null;
|
||||||
|
let maxAmount: number | null = null;
|
||||||
|
let minInvoice: number | null = null;
|
||||||
|
let maxInvoice: number | null = null;
|
||||||
|
let invoicePrefix: string | null = null;
|
||||||
|
|
||||||
|
// === INVOICE NUMBER RANGES (with # prefix) ===
|
||||||
|
|
||||||
|
// Match #1840-1845 (range)
|
||||||
|
let match = textQuery.match(/#(\d+)-(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
minInvoice = parseInt(match[1], 10);
|
||||||
|
maxInvoice = parseInt(match[2], 10);
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match #>=
|
||||||
|
match = textQuery.match(/#>=\s*(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
minInvoice = parseInt(match[1], 10);
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match #> (but not #>=)
|
||||||
|
if (!match) {
|
||||||
|
match = textQuery.match(/#>\s*(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
minInvoice = parseInt(match[1], 10) + 1;
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match #<=
|
||||||
|
match = textQuery.match(/#<=\s*(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
maxInvoice = parseInt(match[1], 10);
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match #< (but not #<=)
|
||||||
|
if (!match) {
|
||||||
|
match = textQuery.match(/#<\s*(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
maxInvoice = parseInt(match[1], 10) - 1;
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match #1847 (no operator) - prefix match
|
||||||
|
match = textQuery.match(/#(\d+)(?!\d|-)/);
|
||||||
|
if (match) {
|
||||||
|
invoicePrefix = match[1];
|
||||||
|
textQuery = textQuery.replace(/#\d+/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AMOUNT RANGES (no prefix) ===
|
||||||
|
|
||||||
|
// Match range syntax: 400-1000
|
||||||
|
match = textQuery.match(/(\d+)-(\d+)/);
|
||||||
|
if (match) {
|
||||||
|
minAmount = parseFloat(match[1]);
|
||||||
|
maxAmount = parseFloat(match[2]);
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match >=
|
||||||
|
match = textQuery.match(/>=\s*(\d+(?:\.\d+)?)/);
|
||||||
|
if (match) {
|
||||||
|
minAmount = parseFloat(match[1]);
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match > (but not >=)
|
||||||
|
match = textQuery.match(/>\s*(\d+(?:\.\d+)?)/);
|
||||||
|
if (match) {
|
||||||
|
minAmount = parseFloat(match[1]) + 0.01;
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match <=
|
||||||
|
match = textQuery.match(/<=\s*(\d+(?:\.\d+)?)/);
|
||||||
|
if (match) {
|
||||||
|
maxAmount = parseFloat(match[1]);
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match < (but not <=)
|
||||||
|
match = textQuery.match(/<\s*(\d+(?:\.\d+)?)/);
|
||||||
|
if (match) {
|
||||||
|
maxAmount = parseFloat(match[1]) - 0.01;
|
||||||
|
textQuery = textQuery.replace(match[0], '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { textQuery, minAmount, maxAmount, minInvoice, maxInvoice, invoicePrefix };
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue