support filtering transactions by time zone minute offset, day of week, day of month, month of year and transaction hour in insights explorer

This commit is contained in:
MaysWind
2026-04-12 22:04:52 +08:00
parent d605a8f4ec
commit f214b7db88
27 changed files with 613 additions and 12 deletions
+3
View File
@@ -600,3 +600,6 @@ export const ALL_TIMEZONES: TimezoneInfo[] = [
timezoneName: 'Pacific/Kiritimati'
}
];
export const WESTERNMOST_TIMEZONE_UTC_OFFSET: number = -720; // Etc/GMT+12 (UTC-12:00)
export const EASTERNMOST_TIMEZONE_UTC_OFFSET: number = 840; // Pacific/Kiritimati (UTC+14:00)
+1
View File
@@ -31,6 +31,7 @@ export interface DateTime {
isLocalizedCalendarFirstDayOfMonth(options: DateTimeFormatOptions): boolean;
getGregorianCalendarYearDashMonthDashDay(): TextualYearMonthDay;
getGregorianCalendarYearDashMonth(): TextualYearMonth;
getMaxDayOfGregorianCalendarMonth(): number;
getWeekDay(): WeekDay;
getWeekDayDisplayName(options: DateTimeFormatOptions): string
getWeekDayDisplayShortName(options: DateTimeFormatOptions): string;
+14
View File
@@ -25,6 +25,11 @@ export const TransactionExplorerConditionRelationPriority: Record<TransactionExp
export enum TransactionExplorerConditionFieldType {
Undefined = 'undefined',
TransactionTimeDayOfWeek = 'transactionTimeDayOfWeek',
TransactionTimeDayOfMonth = 'transactionTimeDayOfMonth',
TransactionTimeMonthOfYear = 'transactionTimeMonthOfYear',
TransactionTimeHourOfDay = 'transactionTimeHourOfDay',
TransactionTimezone = 'transactionTimezone',
TransactionType = 'transactionType',
TransactionCategory = 'transactionCategory',
SourceAccount = 'sourceAccount',
@@ -41,6 +46,11 @@ export class TransactionExplorerConditionField implements NameValue {
private static readonly allInstances: TransactionExplorerConditionField[] = [];
private static readonly allInstancesByValue: Record<string, TransactionExplorerConditionField> = {};
public static readonly TransactionTimeDayOfWeek = new TransactionExplorerConditionField('Transaction Day of Week', TransactionExplorerConditionFieldType.TransactionTimeDayOfWeek);
public static readonly TransactionTimeDayOfMonth = new TransactionExplorerConditionField('Transaction Day of Month', TransactionExplorerConditionFieldType.TransactionTimeDayOfMonth)
public static readonly TransactionTimeMonthOfYear = new TransactionExplorerConditionField('Transaction Month of Year', TransactionExplorerConditionFieldType.TransactionTimeMonthOfYear);
public static readonly TransactionTimeHourOfDay = new TransactionExplorerConditionField('Transaction Hour of Day', TransactionExplorerConditionFieldType.TransactionTimeHourOfDay);
public static readonly TransactionTimezone = new TransactionExplorerConditionField('Transaction Timezone', TransactionExplorerConditionFieldType.TransactionTimezone);
public static readonly TransactionType = new TransactionExplorerConditionField('Transaction Type', TransactionExplorerConditionFieldType.TransactionType);
public static readonly TransactionCategory = new TransactionExplorerConditionField('Category', TransactionExplorerConditionFieldType.TransactionCategory);
public static readonly SourceAccount = new TransactionExplorerConditionField('Source Account', TransactionExplorerConditionFieldType.SourceAccount);
@@ -95,6 +105,8 @@ export enum TransactionExplorerConditionOperatorType {
NotEndsWith = 'notEndsWith',
RegexMatch = 'regexMatch',
NotRegexMatch = 'notRegexMatch',
MinuteOffsetBetween = 'minuteOffsetBetween',
MinuteOffsetNotBetween = 'minuteOffsetNotBetween',
LatitudeBetween = 'latitudeBetween',
LatitudeNotBetween = 'latitudeNotBetween',
LongitudeBetween = 'longitudeBetween',
@@ -127,6 +139,8 @@ export class TransactionExplorerConditionOperator implements NameValue {
public static readonly NotEndsWith = new TransactionExplorerConditionOperator('Does not end with', TransactionExplorerConditionOperatorType.NotEndsWith);
public static readonly RegexMatch = new TransactionExplorerConditionOperator('Matches regex', TransactionExplorerConditionOperatorType.RegexMatch);
public static readonly NotRegexMatch = new TransactionExplorerConditionOperator('Does not match regex', TransactionExplorerConditionOperatorType.NotRegexMatch);
public static readonly MinuteOffsetBetween = new TransactionExplorerConditionOperator('Minute offset is between', TransactionExplorerConditionOperatorType.MinuteOffsetBetween);
public static readonly MinuteOffsetNotBetween = new TransactionExplorerConditionOperator('Minute offset is not between', TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween);
public static readonly LatitudeBetween = new TransactionExplorerConditionOperator('Latitude is between', TransactionExplorerConditionOperatorType.LatitudeBetween);
public static readonly LatitudeNotBetween = new TransactionExplorerConditionOperator('Latitude is not between', TransactionExplorerConditionOperatorType.LatitudeNotBetween);
public static readonly LongitudeBetween = new TransactionExplorerConditionOperator('Longitude is between', TransactionExplorerConditionOperatorType.LongitudeBetween);
+10 -4
View File
@@ -51,6 +51,11 @@ import {
NumeralSystem
} from '@/core/numeral.ts';
import {
WESTERNMOST_TIMEZONE_UTC_OFFSET,
EASTERNMOST_TIMEZONE_UTC_OFFSET,
} from '@/consts/timezone.ts';
import {
isFunction,
isDefined,
@@ -74,15 +79,12 @@ interface DateTimeFormatResult {
type DateTimeTokenFormatFunction = (d: MomentDateTime, options: DateTimeFormatOptions) => DateTimeFormatResult;
const westernmostTimezoneUtcOffset: number = -720; // Etc/GMT+12 (UTC-12:00)
const easternmostTimezoneUtcOffset: number = 840; // Pacific/Kiritimati (UTC+14:00)
function getFixedTimezoneName(utcOffset: number): string {
return `Fixed/Timezone${utcOffset}`;
}
(function initFixedTimezone(): void {
for (let utcOffset = westernmostTimezoneUtcOffset; utcOffset <= easternmostTimezoneUtcOffset; utcOffset += 15) {
for (let utcOffset = WESTERNMOST_TIMEZONE_UTC_OFFSET; utcOffset <= EASTERNMOST_TIMEZONE_UTC_OFFSET; utcOffset += 15) {
const timezoneName = getFixedTimezoneName(utcOffset);
if (moment.tz.zone(timezoneName)) {
@@ -254,6 +256,10 @@ class MomentDateTime implements DateTime {
return (this.instance.year() + '-' + (this.instance.month() + 1).toString().padStart(2, NumeralSystem.WesternArabicNumerals.digitZero)) as TextualYearMonth;
}
public getMaxDayOfGregorianCalendarMonth(): number {
return this.instance.clone().endOf('month').date();
}
public getWeekDay(): WeekDay {
return WeekDay.valueOf(this.instance.day()) as WeekDay;
}
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} Stunde(n) und {minutes} Minute(n) hinter der Standardzeitzone",
"hoursMinutesAheadOfDefaultTimezone": "{hours} Stunde(n) und {minutes} Minute(n) vor der Standardzeitzone",
"monthDay": "{ordinal} Tag",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} Tage",
"everyMultiDaysOfWeek": "Jeden {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Letzte 5 Jahre",
"Previous Billing Cycle": "Vorheriger Abrechnungszeitraum",
"Current Billing Cycle": "Aktueller Abrechnungszeitraum",
"Last day": "Last day",
"Custom Date": "Benutzerdefiniertes Datum",
"Start Date": "Startdatum",
"End Date": "Enddatum",
@@ -1573,6 +1575,8 @@
"Does not end with": "Endet nicht mit",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Breitengrad zwischen",
"Latitude is not between": "Breitengrad nicht zwischen",
"Longitude is between": "Längengrad zwischen",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} hour(s) and {minutes} minutes behind default timezone",
"hoursMinutesAheadOfDefaultTimezone": "{hours} hour(s) and {minutes} minutes ahead of default timezone",
"monthDay": "{ordinal} day",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} days",
"everyMultiDaysOfWeek": "Every {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Recent 5 years",
"Previous Billing Cycle": "Previous Billing Cycle",
"Current Billing Cycle": "Current Billing Cycle",
"Last day": "Last day",
"Custom Date": "Custom Date",
"Start Date": "Start Date",
"End Date": "End Date",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} hora(s) y {minutes} minutos de retraso en la zona horaria predeterminada",
"hoursMinutesAheadOfDefaultTimezone": "{hours} hora(s) y {minutes} minutos antes de la zona horaria predeterminada",
"monthDay": "día {ordinal}",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} días",
"everyMultiDaysOfWeek": "Cada {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Últimos 5 años",
"Previous Billing Cycle": "Ciclo de facturación anterior",
"Current Billing Cycle": "Ciclo de facturación actual",
"Last day": "Last day",
"Custom Date": "Fecha personalizada",
"Start Date": "Fecha de Inicio",
"End Date": "Fecha de Finalización",
@@ -1573,6 +1575,8 @@
"Does not end with": "No termina en",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} heure(s) et {minutes} minutes de retard sur le fuseau horaire par défaut",
"hoursMinutesAheadOfDefaultTimezone": "{hours} heure(s) et {minutes} minutes d'avance sur le fuseau horaire par défaut",
"monthDay": "{ordinal}e jour",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} jours",
"everyMultiDaysOfWeek": "Tous les {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "5 dernières années",
"Previous Billing Cycle": "Cycle de facturation précédent",
"Current Billing Cycle": "Cycle de facturation actuel",
"Last day": "Last day",
"Custom Date": "Date personnalisée",
"Start Date": "Date de début",
"End Date": "Date de fin",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+69
View File
@@ -195,6 +195,7 @@ import {
} from '@/lib/common.ts';
import {
getCurrentDateTime,
formatCurrentTime,
formatGregorianCalendarYearDashMonthDashDay,
formatGregorianCalendarMonthDashDay,
@@ -1059,6 +1060,20 @@ export function useI18n() {
return getAllWeekdayNames('min');
}
function getAllMonths(): TypeAndDisplayName[] {
const ret: TypeAndDisplayName[] = [];
const allMonths = Month.values();
for (const month of allMonths) {
ret.push({
type: month.month,
displayName: t(`datetime.${month.name}.long`)
});
}
return ret;
}
function getAllWeekDays(firstDayOfWeek?: WeekDayValue): TypeAndDisplayName[] {
const ret: TypeAndDisplayName[] = [];
const allWeekDays = WeekDay.values();
@@ -1088,6 +1103,51 @@ export function useI18n() {
return ret;
}
function getAllHours(): TypeAndDisplayName[] {
const ret: TypeAndDisplayName[] = [];
const now: DateTime = getCurrentDateTime();
const format: string = getLocalizedShortTimeFormat();
const options: DateTimeFormatOptions = getDateTimeFormatOptions();
for (let i = 0; i < 24; i++) {
const dateTime = now.set({
hour: i,
minute: 0,
second: 0,
millisecond: 0
});
ret.push({
type: i,
displayName: dateTime.format(format, options)
});
}
return ret;
}
function getAvailableMonthDays(daysInMonth: number, lastDaysOfMonth?: number): TypeAndDisplayName[] {
const ret: TypeAndDisplayName[] = [];
for (let i = 1; i <= daysInMonth; i++) {
ret.push({
type: i,
displayName: getMonthdayShortName(i),
});
}
if (isNumber(lastDaysOfMonth) && lastDaysOfMonth > 0) {
for (let i = -lastDaysOfMonth; i < 0; i++) {
ret.push({
type: i,
displayName: (i === -1) ? t('Last day') : getMonthLastDayShortName(-i),
});
}
}
return ret;
}
function getLocalizedDateTimeFormats<T extends DateFormat | TimeFormat>(type: string, allFormatMap: Record<string, T>, allFormatArray: T[], languageDefaultTypeNameKey: string, systemDefaultFormatType: T, numeralSystem: NumeralSystem, calendarType?: CalendarType): LocalizedDateTimeFormat[] {
const defaultFormat = getLocalizedDateTimeFormat<T>(type, allFormatMap, allFormatArray, LANGUAGE_DEFAULT_DATE_TIME_FORMAT_VALUE, languageDefaultTypeNameKey, systemDefaultFormatType);
const ret: LocalizedDateTimeFormat[] = [];
@@ -1606,6 +1666,12 @@ export function useI18n() {
});
}
function getMonthLastDayShortName(ordinal: number): string {
return t('format.misc.lastMonthDay', {
ordinal: getMonthdayOrdinal(ordinal)
});
}
function getWeekdayShortName(weekDay: WeekDay): string {
return t(`datetime.${weekDay.name}.short`);
}
@@ -2411,7 +2477,10 @@ export function useI18n() {
getAllLongWeekdayNames,
getAllShortWeekdayNames,
getAllMinWeekdayNames,
getAllMonths,
getAllWeekDays,
getAllHours,
getAvailableMonthDays,
getAllCalendarDisplayTypes: () => getAllLocalizedCalendarTypes(CalendarDisplayType.values(), CalendarDisplayType.parse(t('default.calendarDisplayType')), CalendarDisplayType.Default, CalendarDisplayType.LanguageDefaultType),
getAllDateDisplayTypes: () => getAllLocalizedCalendarTypes(DateDisplayType.values(), DateDisplayType.parse(t('default.dateDisplayType')), DateDisplayType.Default, DateDisplayType.LanguageDefaultType),
getAllLongDateFormats: (numeralSystem: NumeralSystem, calendarType: CalendarType) => getLocalizedDateTimeFormats<LongDateFormat>('longDate', LongDateFormat.all(), LongDateFormat.values(), 'longDateFormat', LongDateFormat.Default, numeralSystem, calendarType),
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "Indietro di {hours} ore e {minutes} minuti rispetto al fuso orario standard",
"hoursMinutesAheadOfDefaultTimezone": "Avanti di {hours} ore e {minutes} minuti rispetto al fuso orario standard",
"monthDay": "{ordinal} giorno",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} giorni",
"everyMultiDaysOfWeek": "Ogni {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Ultimi 5 anni",
"Previous Billing Cycle": "Ciclo di fatturazione precedente",
"Current Billing Cycle": "Ciclo di fatturazione corrente",
"Last day": "Last day",
"Custom Date": "Data personalizzata",
"Start Date": "Data di inizio",
"End Date": "Data di fine",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "デフォルトのタイムゾーンより{hours}時間{minutes}分遅れています",
"hoursMinutesAheadOfDefaultTimezone": "デフォルトのタイムゾーンから{hours}時間{minutes}分進んでいます",
"monthDay": "{ordinal}日",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays}日間",
"everyMultiDaysOfWeek": "毎{days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "直近5年",
"Previous Billing Cycle": "以前の請求サイクル",
"Current Billing Cycle": "現在の請求サイクル",
"Last day": "Last day",
"Custom Date": "カスタム日付",
"Start Date": "開始日",
"End Date": "終了日",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "ಡೀಫಾಲ್ಟ್ ಸಮಯ ವಲಯಕ್ಕಿಂತ {hours} ಗಂಟೆ ಹಾಗೂ {minutes} ನಿಮಿಷಗಳು ಹಿಂದೆ",
"hoursMinutesAheadOfDefaultTimezone": "ಡೀಫಾಲ್ಟ್ ಸಮಯ ವಲಯಕ್ಕಿಂತ {hours} ಗಂಟೆ ಹಾಗೂ {minutes} ನಿಮಿಷಗಳು ಮುಂದೆ",
"monthDay": "{ordinal} ದಿನ",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} ದಿನಗಳು",
"everyMultiDaysOfWeek": "ಪ್ರತಿ {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "ಇತ್ತೀಚಿನ 5 ವರ್ಷಗಳು",
"Previous Billing Cycle": "ಹಿಂದಿನ ಬಿಲ್ಲಿಂಗ್ ಚಕ್ರ",
"Current Billing Cycle": "ಪ್ರಸ್ತುತ ಬಿಲ್ಲಿಂಗ್ ಚಕ್ರ",
"Last day": "Last day",
"Custom Date": "ಕಸ್ಟಮ್ ದಿನಾಂಕ",
"Start Date": "ಪ್ರಾರಂಭ ದಿನಾಂಕ",
"End Date": "ಅಂತ್ಯ ದಿನಾಂಕ",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "기본 시간대보다 {hours}시간 {minutes}분 느립니다",
"hoursMinutesAheadOfDefaultTimezone": "기본 시간대보다 {hours}시간 {minutes}분 빠릅니다",
"monthDay": "{ordinal}일",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays}일",
"everyMultiDaysOfWeek": "매주 {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "최근 5년",
"Previous Billing Cycle": "이전 청구 주기",
"Current Billing Cycle": "현재 청구 주기",
"Last day": "Last day",
"Custom Date": "사용자 지정 날짜",
"Start Date": "시작 날짜",
"End Date": "종료 날짜",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} uur en {minutes} minuten achter standaardtijdzone",
"hoursMinutesAheadOfDefaultTimezone": "{hours} uur en {minutes} minuten voor op standaardtijdzone",
"monthDay": "{ordinal} dag",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} dagen",
"everyMultiDaysOfWeek": "Elke {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Afgelopen 5 jaar",
"Previous Billing Cycle": "Vorige factureringsperiode",
"Current Billing Cycle": "Huidige factureringsperiode",
"Last day": "Last day",
"Custom Date": "Aangepaste datum",
"Start Date": "Begindatum",
"End Date": "Einddatum",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} hora(s) e {minutes} minutos atrás do fuso horário padrão",
"hoursMinutesAheadOfDefaultTimezone": "{hours} hora(s) e {minutes} minutos à frente do fuso horário padrão",
"monthDay": "{ordinal} dia",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} dias",
"everyMultiDaysOfWeek": "A cada {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Últimos 5 anos",
"Previous Billing Cycle": "Ciclo de Cobrança Anterior",
"Current Billing Cycle": "Ciclo de Cobrança Atual",
"Last day": "Last day",
"Custom Date": "Data Personalizada",
"Start Date": "Data de Início",
"End Date": "Data de Término",
@@ -1573,6 +1575,8 @@
"Does not end with": "Não termina com",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude entre",
"Latitude is not between": "Latitude não entre",
"Longitude is between": "Longitude entre",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} час(ов) и {minutes} минут позади часового пояса по умолчанию",
"hoursMinutesAheadOfDefaultTimezone": "{hours} час(ов) и {minutes} минут впереди часового пояса по умолчанию",
"monthDay": "{ordinal} день",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} дней",
"everyMultiDaysOfWeek": "Каждые {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Последние 5 лет",
"Previous Billing Cycle": "Предыдущий расчетный период",
"Current Billing Cycle": "Текущий расчетный период",
"Last day": "Last day",
"Custom Date": "Выбрать дату",
"Start Date": "Дата начала",
"End Date": "Дата конца",
@@ -1573,6 +1575,8 @@
"Does not end with": "Не заканчивается с",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} ura(ur) in {minutes} minut za privzetim časovnim pasom",
"hoursMinutesAheadOfDefaultTimezone": "{hours} ura(ur) in {minutes} minut po privzetem časovnem pasu",
"monthDay": "{ordinal} dan",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} dni",
"everyMultiDaysOfWeek": "Vsak {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Zadnjih 5 let",
"Previous Billing Cycle": "Prejšnje obračunsko obdobje",
"Current Billing Cycle": "Trenutno obračunsko obdobje",
"Last day": "Last day",
"Custom Date": "Datum po meri",
"Start Date": "Datum začetka",
"End Date": "Datum konca",
@@ -1573,6 +1575,8 @@
"Does not end with": "Se ne konča z",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "இயல்பு நேர மண்டலத்தை விட {hours} மணி நேரம் {minutes} நிமிடங்கள் பின்னால்",
"hoursMinutesAheadOfDefaultTimezone": "இயல்பு நேர மண்டலத்தை விட {hours} மணி நேரம் {minutes} நிமிடங்கள் முன்னால்",
"monthDay": "{ordinal} நாள்",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} நாட்கள்",
"everyMultiDaysOfWeek": "ஒவ்வொரு {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "சமீபத்திய 5 ஆண்டுகள்",
"Previous Billing Cycle": "முந்தைய பில்லிங் சுழற்சி",
"Current Billing Cycle": "தற்போதைய பில்லிங் சுழற்சி",
"Last day": "Last day",
"Custom Date": "தனிப்பயன் தேதி",
"Start Date": "தொடக்கம் தேதி",
"End Date": "முடிவு தேதி",
@@ -1573,6 +1575,8 @@
"Does not end with": "முடியவில்லை",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "ช้ากว่าเขตเวลาเริ่มต้น {hours} ชั่วโมง {minutes} นาที",
"hoursMinutesAheadOfDefaultTimezone": "เร็วกว่าเขตเวลาเริ่มต้น {hours} ชั่วโมง {minutes} นาที",
"monthDay": "วันที่ {ordinal}",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} วัน",
"everyMultiDaysOfWeek": "ทุกๆ {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "5 ปีที่ผ่านมา",
"Previous Billing Cycle": "รอบบิลก่อนหน้า",
"Current Billing Cycle": "รอบบิลปัจจุบัน",
"Last day": "Last day",
"Custom Date": "วันที่กำหนดเอง",
"Start Date": "วันที่เริ่มต้น",
"End Date": "วันที่สิ้นสุด",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "Varsayılan saat diliminin {hours} saat {minutes} dakika gerisinde",
"hoursMinutesAheadOfDefaultTimezone": "Varsayılan saat diliminin {hours} saat {minutes} dakika ilerisinde",
"monthDay": "{ordinal} gün",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} gün",
"everyMultiDaysOfWeek": "Her {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Son 5 yıl",
"Previous Billing Cycle": "Önceki Fatura Dönemi",
"Current Billing Cycle": "Mevcut Fatura Dönemi",
"Last day": "Last day",
"Custom Date": "Özel Tarih",
"Start Date": "Başlangıç Tarihi",
"End Date": "Bitiş Tarihi",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} год і {minutes} хв позаду часового поясу за замовчуванням",
"hoursMinutesAheadOfDefaultTimezone": "{hours} год і {minutes} хв попереду часового поясу за замовчуванням",
"monthDay": "{ordinal} день",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} днів",
"everyMultiDaysOfWeek": "Кожні {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "Останні 5 років",
"Previous Billing Cycle": "Попередній розрахунковий період",
"Current Billing Cycle": "Поточний розрахунковий період",
"Last day": "Last day",
"Custom Date": "Обрати дату",
"Start Date": "Дата початку",
"End Date": "Дата завершення",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "{hours} giờ và {minutes} phút sau múi giờ mặc định",
"hoursMinutesAheadOfDefaultTimezone": "{hours} giờ và {minutes} phút trước múi giờ mặc định",
"monthDay": "Ngày {ordinal}",
"lastMonthDay": "Last {ordinal} day",
"eachMonthDayInMonthDays": "{ordinal}",
"monthDays": "{multiMonthDays} ngày",
"everyMultiDaysOfWeek": "Mỗi {days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "5 năm gần đây",
"Previous Billing Cycle": "Previous Billing Cycle",
"Current Billing Cycle": "Current Billing Cycle",
"Last day": "Last day",
"Custom Date": "Ngày tùy chỉnh",
"Start Date": "Start Date",
"End Date": "End Date",
@@ -1573,6 +1575,8 @@
"Does not end with": "Does not end with",
"Matches regex": "Matches regex",
"Does not match regex": "Does not match regex",
"Minute offset is between": "Minute offset is between",
"Minute offset is not between": "Minute offset is not between",
"Latitude is between": "Latitude is between",
"Latitude is not between": "Latitude is not between",
"Longitude is between": "Longitude is between",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "比默认时区晚{hours}小时{minutes}分",
"hoursMinutesAheadOfDefaultTimezone": "比默认时区早{hours}小时{minutes}分",
"monthDay": "{ordinal}日",
"lastMonthDay": "倒数第{ordinal}日",
"eachMonthDayInMonthDays": "{ordinal}日",
"monthDays": "{multiMonthDays}",
"everyMultiDaysOfWeek": "每{days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "最近5年",
"Previous Billing Cycle": "上个账单周期",
"Current Billing Cycle": "当前账单周期",
"Last day": "倒数第1日",
"Custom Date": "自定义日期",
"Start Date": "开始日期",
"End Date": "结束日期",
@@ -1573,6 +1575,8 @@
"Does not end with": "结尾不是",
"Matches regex": "正则匹配",
"Does not match regex": "不匹配正则",
"Minute offset is between": "分钟偏移量介于",
"Minute offset is not between": "分钟偏移量不介于",
"Latitude is between": "纬度介于",
"Latitude is not between": "纬度不介于",
"Longitude is between": "经度介于",
+4
View File
@@ -119,6 +119,7 @@
"hoursMinutesBehindDefaultTimezone": "比預設時區晚{hours}小時{minutes}分",
"hoursMinutesAheadOfDefaultTimezone": "比預設時區早{hours}小時{minutes}分",
"monthDay": "{ordinal}日",
"lastMonthDay": "倒數第{ordinal}日",
"eachMonthDayInMonthDays": "{ordinal}日",
"monthDays": "{multiMonthDays}",
"everyMultiDaysOfWeek": "每{days}",
@@ -1540,6 +1541,7 @@
"Recent 5 years": "最近5年",
"Previous Billing Cycle": "上個帳單週期",
"Current Billing Cycle": "當前帳單週期",
"Last day": "倒數第1日",
"Custom Date": "自訂日期",
"Start Date": "開始日期",
"End Date": "結束日期",
@@ -1573,6 +1575,8 @@
"Does not end with": "結尾不是",
"Matches regex": "正則匹配",
"Does not match regex": "不匹配正則",
"Minute offset is between": "分钟偏移量介於",
"Minute offset is not between": "分钟偏移量不介於",
"Latitude is between": "緯度介於",
"Latitude is not between": "緯度不介於",
"Longitude is between": "經度介於",
+290 -3
View File
@@ -1,4 +1,5 @@
import { type PartialRecord, itemAndIndex, keysIfValueEquals } from '@/core/base.ts';
import { type DateTime } from '@/core/datetime.ts';
import { TimezoneTypeForStatistics } from '@/core/timezone.ts';
import { AccountType } from '@/core/account.ts';
import { TransactionType } from '@/core/transaction.ts';
@@ -300,6 +301,21 @@ export class TransactionExplorerQuery {
let condition: TransactionExplorerCondition;
switch (field) {
case TransactionExplorerConditionField.TransactionTimeDayOfWeek:
condition = new TransactionExplorerTransactionTimeDayOfWeekCondition(TransactionExplorerConditionOperatorType.In, []);
break;
case TransactionExplorerConditionField.TransactionTimeDayOfMonth:
condition = new TransactionExplorerTransactionTimeDayOfMonthCondition(TransactionExplorerConditionOperatorType.In, []);
break;
case TransactionExplorerConditionField.TransactionTimeMonthOfYear:
condition = new TransactionExplorerTransactionTimeMonthOfYearCondition(TransactionExplorerConditionOperatorType.In, []);
break;
case TransactionExplorerConditionField.TransactionTimeHourOfDay:
condition = new TransactionExplorerTransactionTimeHourOfDayCondition(TransactionExplorerConditionOperatorType.In, []);
break;
case TransactionExplorerConditionField.TransactionTimezone:
condition = new TransactionExplorerTransactionTimezoneCondition(TransactionExplorerConditionOperatorType.MinuteOffsetBetween, [ 0, 0 ]);
break;
case TransactionExplorerConditionField.TransactionType:
condition = new TransactionExplorerTransactionTypeCondition(TransactionExplorerConditionOperatorType.In, [ TransactionType.Expense, TransactionType.Income, TransactionType.Transfer ]);
break;
@@ -345,7 +361,7 @@ export class TransactionExplorerQuery {
return new TransactionExplorerConditionWithRelation(new TransactionExplorerUndefinedCondition(), TransactionExplorerConditionRelation.SubEnd);
}
public match(transaction: TransactionInsightDataItem): boolean {
public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean {
if (!this.conditions || this.conditions.length < 1) {
return true;
}
@@ -370,7 +386,7 @@ export class TransactionExplorerQuery {
throw new Error('invalid postfix expression');
}
} else {
stack.push(token.match(transaction));
stack.push(token.match(transaction, context));
}
}
@@ -646,6 +662,21 @@ export class TransactionExplorerConditionWithRelation {
let operatorTypes: PartialRecord<TransactionExplorerConditionOperatorType, true> = {};
switch (this.condition.field) {
case TransactionExplorerConditionField.TransactionTimeDayOfWeek.value:
operatorTypes = TransactionExplorerTransactionTimeDayOfWeekCondition.supportedOperators;
break;
case TransactionExplorerConditionField.TransactionTimeDayOfMonth.value:
operatorTypes = TransactionExplorerTransactionTimeDayOfMonthCondition.supportedOperators;
break;
case TransactionExplorerConditionField.TransactionTimeMonthOfYear.value:
operatorTypes = TransactionExplorerTransactionTimeMonthOfYearCondition.supportedOperators;
break;
case TransactionExplorerConditionField.TransactionTimeHourOfDay.value:
operatorTypes = TransactionExplorerTransactionTimeHourOfDayCondition.supportedOperators;
break;
case TransactionExplorerConditionField.TransactionTimezone.value:
operatorTypes = TransactionExplorerTransactionTimezoneCondition.supportedOperators;
break;
case TransactionExplorerConditionField.TransactionType.value:
operatorTypes = TransactionExplorerTransactionTypeCondition.supportedOperators;
break;
@@ -736,6 +767,31 @@ export class TransactionExplorerConditionWithRelation {
const conditionValue = conditionObject['value'];
switch (conditionField) {
case TransactionExplorerConditionField.TransactionTimeDayOfWeek.value:
if (TransactionExplorerTransactionTimeDayOfWeekCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
condition = new TransactionExplorerTransactionTimeDayOfWeekCondition(conditionOperator as TransactionTimeDayOfWeekConditionOperator, conditionValue as number[]);
}
break;
case TransactionExplorerConditionField.TransactionTimeDayOfMonth.value:
if (TransactionExplorerTransactionTimeDayOfMonthCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
condition = new TransactionExplorerTransactionTimeDayOfMonthCondition(conditionOperator as TransactionTimeDayOfMonthConditionOperator, conditionValue as number[]);
}
break;
case TransactionExplorerConditionField.TransactionTimeMonthOfYear.value:
if (TransactionExplorerTransactionTimeMonthOfYearCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
condition = new TransactionExplorerTransactionTimeMonthOfYearCondition(conditionOperator as TransactionTimeMonthOfYearConditionOperator, conditionValue as number[]);
}
break;
case TransactionExplorerConditionField.TransactionTimeHourOfDay.value:
if (TransactionExplorerTransactionTimeHourOfDayCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
condition = new TransactionExplorerTransactionTimeHourOfDayCondition(conditionOperator as TransactionTimeHourOfDayConditionOperator, conditionValue as number[]);
}
break;
case TransactionExplorerConditionField.TransactionTimezone.value:
if (TransactionExplorerTransactionTimezoneCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue) && conditionValue.length === 2) {
condition = new TransactionExplorerTransactionTimezoneCondition(conditionOperator as TransactionTimezoneConditionOperator, conditionValue as [number, number]);
}
break;
case TransactionExplorerConditionField.TransactionType.value:
if (TransactionExplorerTransactionTypeCondition.supportedOperators[conditionOperator] && Array.isArray(conditionValue)) {
condition = new TransactionExplorerTransactionTypeCondition(conditionOperator as TransactionTypeConditionOperator, conditionValue as number[]);
@@ -803,13 +859,17 @@ export class TransactionExplorerConditionWithRelation {
}
}
export interface InsightsExplorerMatchContext {
getTransactionDateTime(): DateTime;
}
export interface TransactionExplorerCondition<T = TransactionExplorerConditionFieldType, V = string | string[] | number[]> {
readonly field: T;
readonly operator: TransactionExplorerConditionOperatorType;
value: V;
getValueForStore(): V;
match(transaction: TransactionInsightDataItem): boolean;
match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean;
toExpression(allCategoriesMap: Record<string, TransactionCategory>, allAccountsMap: Record<string, Account>, allTagsMap: Record<string, TransactionTag>): string;
}
@@ -831,6 +891,233 @@ export class TransactionExplorerUndefinedCondition implements TransactionExplore
}
}
type TransactionTimeDayOfWeekConditionOperator = TransactionExplorerConditionOperatorType.In |
TransactionExplorerConditionOperatorType.NotIn;
export class TransactionExplorerTransactionTimeDayOfWeekCondition implements TransactionExplorerCondition<TransactionExplorerConditionFieldType.TransactionTimeDayOfWeek, number[]> {
public static readonly supportedOperators: PartialRecord<TransactionExplorerConditionOperatorType, true> = {
[TransactionExplorerConditionOperatorType.In]: true,
[TransactionExplorerConditionOperatorType.NotIn]: true
};
public readonly field = TransactionExplorerConditionFieldType.TransactionTimeDayOfWeek;
public readonly operator: TransactionTimeDayOfWeekConditionOperator = TransactionExplorerConditionOperatorType.In;
public value: number[];
constructor(operator: TransactionTimeDayOfWeekConditionOperator, value: number[]) {
this.operator = operator;
this.value = value;
}
public getValueForStore(): number[] {
return this.value;
}
public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean {
const transactionDateTime = context.getTransactionDateTime();
if (this.operator === TransactionExplorerConditionOperatorType.In) {
return this.value.includes(transactionDateTime.getWeekDay().type);
} else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) {
return !this.value.includes(transactionDateTime.getWeekDay().type);
}
return false;
}
public toExpression(): string {
const textualDayOfWeeks = this.value.join(', ');
if (this.operator === TransactionExplorerConditionOperatorType.In) {
return `DAY_OF_WEEK(transaction_time) IN (${textualDayOfWeeks})`;
} else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) {
return `DAY_OF_WEEK(transaction_time) NOT IN (${textualDayOfWeeks})`;
} else {
return '';
}
}
}
type TransactionTimeDayOfMonthConditionOperator = TransactionExplorerConditionOperatorType.In |
TransactionExplorerConditionOperatorType.NotIn;
export class TransactionExplorerTransactionTimeDayOfMonthCondition implements TransactionExplorerCondition<TransactionExplorerConditionFieldType.TransactionTimeDayOfMonth, number[]> {
public static readonly supportedOperators: PartialRecord<TransactionExplorerConditionOperatorType, true> = {
[TransactionExplorerConditionOperatorType.In]: true,
[TransactionExplorerConditionOperatorType.NotIn]: true
};
public readonly field = TransactionExplorerConditionFieldType.TransactionTimeDayOfMonth;
public readonly operator: TransactionTimeDayOfMonthConditionOperator = TransactionExplorerConditionOperatorType.In;
public value: number[];
constructor(operator: TransactionTimeDayOfMonthConditionOperator, value: number[]) {
this.operator = operator;
this.value = value;
}
public getValueForStore(): number[] {
return this.value;
}
public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean {
const transactionDateTime = context.getTransactionDateTime();
const normalizedValue: number[] = this.value.map(day => day >= 0 ? day : transactionDateTime.getMaxDayOfGregorianCalendarMonth() + day + 1);
if (this.operator === TransactionExplorerConditionOperatorType.In) {
return normalizedValue.includes(transactionDateTime.getGregorianCalendarDay());
} else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) {
return !normalizedValue.includes(transactionDateTime.getGregorianCalendarDay());
}
return false;
}
public toExpression(): string {
const textualDayOfMonths = this.value.join(', ');
if (this.operator === TransactionExplorerConditionOperatorType.In) {
return `DAY(transaction_time) IN (${textualDayOfMonths})`;
} else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) {
return `DAY(transaction_time) NOT IN (${textualDayOfMonths})`;
} else {
return '';
}
}
}
type TransactionTimeMonthOfYearConditionOperator = TransactionExplorerConditionOperatorType.In |
TransactionExplorerConditionOperatorType.NotIn;
export class TransactionExplorerTransactionTimeMonthOfYearCondition implements TransactionExplorerCondition<TransactionExplorerConditionFieldType.TransactionTimeMonthOfYear, number[]> {
public static readonly supportedOperators: PartialRecord<TransactionExplorerConditionOperatorType, true> = {
[TransactionExplorerConditionOperatorType.In]: true,
[TransactionExplorerConditionOperatorType.NotIn]: true
};
public readonly field = TransactionExplorerConditionFieldType.TransactionTimeMonthOfYear;
public readonly operator: TransactionTimeMonthOfYearConditionOperator = TransactionExplorerConditionOperatorType.In;
public value: number[];
constructor(operator: TransactionTimeMonthOfYearConditionOperator, value: number[]) {
this.operator = operator;
this.value = value;
}
public getValueForStore(): number[] {
return this.value;
}
public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean {
const transactionDateTime = context.getTransactionDateTime();
if (this.operator === TransactionExplorerConditionOperatorType.In) {
return this.value.includes(transactionDateTime.getGregorianCalendarMonth());
} else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) {
return !this.value.includes(transactionDateTime.getGregorianCalendarMonth());
}
return false;
}
public toExpression(): string {
const textualMonthOfYears = this.value.join(', ');
if (this.operator === TransactionExplorerConditionOperatorType.In) {
return `MONTH(transaction_time) IN (${textualMonthOfYears})`;
} else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) {
return `MONTH(transaction_time) NOT IN (${textualMonthOfYears})`;
} else {
return '';
}
}
}
type TransactionTimeHourOfDayConditionOperator = TransactionExplorerConditionOperatorType.In |
TransactionExplorerConditionOperatorType.NotIn;
export class TransactionExplorerTransactionTimeHourOfDayCondition implements TransactionExplorerCondition<TransactionExplorerConditionFieldType.TransactionTimeHourOfDay, number[]> {
public static readonly supportedOperators: PartialRecord<TransactionExplorerConditionOperatorType, true> = {
[TransactionExplorerConditionOperatorType.In]: true,
[TransactionExplorerConditionOperatorType.NotIn]: true
};
public readonly field = TransactionExplorerConditionFieldType.TransactionTimeHourOfDay;
public readonly operator: TransactionTimeHourOfDayConditionOperator = TransactionExplorerConditionOperatorType.In;
public value: number[];
constructor(operator: TransactionTimeHourOfDayConditionOperator, value: number[]) {
this.operator = operator;
this.value = value;
}
public getValueForStore(): number[] {
return this.value;
}
public match(transaction: TransactionInsightDataItem, context: InsightsExplorerMatchContext): boolean {
const transactionDateTime = context.getTransactionDateTime();
if (this.operator === TransactionExplorerConditionOperatorType.In) {
return this.value.includes(transactionDateTime.getHour());
} else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) {
return !this.value.includes(transactionDateTime.getHour());
}
return false;
}
public toExpression(): string {
const textualHourOfDays = this.value.join(', ');
if (this.operator === TransactionExplorerConditionOperatorType.In) {
return `HOUR(transaction_time) IN (${textualHourOfDays})`;
} else if (this.operator === TransactionExplorerConditionOperatorType.NotIn) {
return `HOUR(transaction_time) NOT IN (${textualHourOfDays})`;
} else {
return '';
}
}
}
type TransactionTimezoneConditionOperator = TransactionExplorerConditionOperatorType.MinuteOffsetBetween |
TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween;
export class TransactionExplorerTransactionTimezoneCondition implements TransactionExplorerCondition<TransactionExplorerConditionFieldType.TransactionTimezone, [number, number]> {
public static readonly supportedOperators: PartialRecord<TransactionExplorerConditionOperatorType, true> = {
[TransactionExplorerConditionOperatorType.MinuteOffsetBetween]: true,
[TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween]: true
};
public readonly field = TransactionExplorerConditionFieldType.TransactionTimezone;
public readonly operator: TransactionTimezoneConditionOperator = TransactionExplorerConditionOperatorType.MinuteOffsetBetween;
public value: [number, number];
constructor(operator: TransactionTimezoneConditionOperator, value: [number, number]) {
this.operator = operator;
this.value = value;
}
public getValueForStore(): [number, number] {
return this.value;
}
public match(transaction: TransactionInsightDataItem): boolean {
if (this.operator === TransactionExplorerConditionOperatorType.MinuteOffsetBetween) {
return transaction.utcOffset >= this.value[0] && transaction.utcOffset <= this.value[1];
} else if (this.operator === TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween) {
return transaction.utcOffset < this.value[0] || transaction.utcOffset > this.value[1];
}
return false;
}
public toExpression(): string {
if (this.operator === TransactionExplorerConditionOperatorType.MinuteOffsetBetween) {
return `(UTC_OFFSET(timezone) >= ${this.value[0]} AND UTC_OFFSET(timezone) <= ${this.value[1]})`;
} else if (this.operator === TransactionExplorerConditionOperatorType.MinuteOffsetNotBetween) {
return `(UTC_OFFSET(timezone) < ${this.value[0]} OR UTC_OFFSET(timezone) > ${this.value[1]})`;
}
return '';
}
}
type TransactionTypeConditionOperator = TransactionExplorerConditionOperatorType.In |
TransactionExplorerConditionOperatorType.NotIn;
+22 -3
View File
@@ -10,7 +10,7 @@ import { useExchangeRatesStore } from './exchangeRates.ts';
import { type BeforeResolveFunction, itemAndIndex, reversed, keys, values } from '@/core/base.ts';
import { NumeralSystem, AmountFilterType } from '@/core/numeral.ts';
import { DateRangeScene, DateRange } from '@/core/datetime.ts';
import { type DateTime, DateRangeScene, DateRange } from '@/core/datetime.ts';
import { TimezoneTypeForStatistics } from '@/core/timezone.ts';
import { AccountCategory } from '@/core/account.ts';
import { TransactionType } from '@/core/transaction.ts';
@@ -32,6 +32,7 @@ import {
import {
type InsightsExplorerNewDisplayOrderRequest,
type InsightsExplorerInfoResponse,
type InsightsExplorerMatchContext,
InsightsExplorer,
InsightsExplorerBasicInfo
} from '@/models/explorer.ts';
@@ -151,6 +152,20 @@ export const useExplorersStore = defineStore('explorers', () => {
return result;
})();
function buildInsightsExplorerMatchContext(insightsExplorer: InsightsExplorer, transaction: TransactionInsightDataItem): InsightsExplorerMatchContext {
return {
getTransactionDateTime(): DateTime {
let transactionTimeUtfOffset: number | undefined = undefined;
if (insightsExplorer.timezoneUsedForDateRange === TimezoneTypeForStatistics.TransactionTimezone.type) {
transactionTimeUtfOffset = transaction.utcOffset;
}
return isDefined(transactionTimeUtfOffset) ? parseDateTimeFromUnixTimeWithTimezoneOffset(transaction.time, transactionTimeUtfOffset) : parseDateTimeFromUnixTime(transaction.time);
}
};
}
function getDataCategoryInfo(timezoneUsedForDateRange: number, dimension: TransactionExplorerDataDimension, queryName: string, queryIndex: number, transaction: TransactionInsightDataItem): CategoriedInfo {
let transactionTimeUtfOffset: number | undefined = undefined;
@@ -600,12 +615,14 @@ export const useExplorersStore = defineStore('explorers', () => {
const result: TransactionInsightDataItem[] = [];
for (const transaction of allTransactions.value) {
const matchOptions: InsightsExplorerMatchContext = buildInsightsExplorerMatchContext(currentInsightsExplorer.value, transaction);
for (const query of currentInsightsExplorer.value.queries) {
if (currentInsightsExplorer.value.datatableQuerySource && currentInsightsExplorer.value.datatableQuerySource !== query.id) {
continue;
}
if (query.match(transaction)) {
if (query.match(transaction, matchOptions)) {
result.push(transaction);
break;
}
@@ -751,8 +768,10 @@ export const useExplorersStore = defineStore('explorers', () => {
continue;
}
const matchContext: InsightsExplorerMatchContext = buildInsightsExplorerMatchContext(currentInsightsExplorer.value, transaction);
for (const [query, index] of itemAndIndex(currentInsightsExplorer.value.queries)) {
if (query.match(transaction)) {
if (query.match(transaction, matchContext)) {
addTransactionToCategoriedDataMap(currentInsightsExplorer.value.timezoneUsedForDateRange, categoriedDataMap, categoryDimension, seriesDimension, query.name, index, transaction);
if (categoryDimension !== TransactionExplorerDataDimension.Query) {
@@ -153,6 +153,119 @@
/>
<div class="d-flex w-100 flex-1-1" style="min-width: 280px;">
<v-select
multiple chips closable-chips
density="compact"
item-title="displayName"
item-value="type"
:disabled="loading || disabled || !!editingQuery"
:placeholder="tt('None')"
:items="allAvailableDayOfWeekOptions"
v-model="conditionWithRelation.condition.value"
v-if="conditionWithRelation.condition.field === TransactionExplorerConditionField.TransactionTimeDayOfWeek.value"
>
<template #item="{ props, item }">
<v-list-item :value="item.value" v-bind="props">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">{{ item.title }}</div>
</v-list-item-title>
</template>
</v-list-item>
</template>
</v-select>
<v-select
multiple chips closable-chips
density="compact"
item-title="displayName"
item-value="type"
:disabled="loading || disabled || !!editingQuery"
:placeholder="tt('None')"
:items="allAvailableDayOfMonthOptions"
v-model="conditionWithRelation.condition.value"
v-else-if="conditionWithRelation.condition.field === TransactionExplorerConditionField.TransactionTimeDayOfMonth.value"
>
<template #item="{ props, item }">
<v-list-item :value="item.value" v-bind="props">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">{{ item.title }}</div>
</v-list-item-title>
</template>
</v-list-item>
</template>
</v-select>
<v-select
multiple chips closable-chips
density="compact"
item-title="displayName"
item-value="type"
:disabled="loading || disabled || !!editingQuery"
:placeholder="tt('None')"
:items="allAvailableMonthOfYearOptions"
v-model="conditionWithRelation.condition.value"
v-else-if="conditionWithRelation.condition.field === TransactionExplorerConditionField.TransactionTimeMonthOfYear.value"
>
<template #item="{ props, item }">
<v-list-item :value="item.value" v-bind="props">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">{{ item.title }}</div>
</v-list-item-title>
</template>
</v-list-item>
</template>
</v-select>
<v-select
multiple chips closable-chips
density="compact"
item-title="displayName"
item-value="type"
:disabled="loading || disabled || !!editingQuery"
:placeholder="tt('None')"
:items="allAvailableHourOfDayOptions"
v-model="conditionWithRelation.condition.value"
v-else-if="conditionWithRelation.condition.field === TransactionExplorerConditionField.TransactionTimeHourOfDay.value"
>
<template #item="{ props, item }">
<v-list-item :value="item.value" v-bind="props">
<template #title>
<v-list-item-title>
<div class="d-flex align-center">{{ item.title }}</div>
</v-list-item-title>
</template>
</v-list-item>
</template>
</v-select>
<div class="d-flex w-100 align-center gap-2"
v-else-if="conditionWithRelation.condition.field === TransactionExplorerConditionField.TransactionTimezone.value">
<number-input density="compact"
:disabled="loading || disabled || !!editingQuery"
:min-value="WESTERNMOST_TIMEZONE_UTC_OFFSET"
:max-value="EASTERNMOST_TIMEZONE_UTC_OFFSET"
:max-decimal-count="0"
v-model="conditionWithRelation.condition.value[0]"
v-if="conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.MinuteOffsetBetween.value ||
conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.MinuteOffsetNotBetween.value"
/>
<span class="ms-2 me-2"
v-if="conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.MinuteOffsetBetween.value ||
conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.MinuteOffsetNotBetween.value">~</span>
<number-input density="compact"
:disabled="loading || disabled || !!editingQuery"
:min-value="WESTERNMOST_TIMEZONE_UTC_OFFSET"
:max-value="EASTERNMOST_TIMEZONE_UTC_OFFSET"
:max-decimal-count="0"
v-model="conditionWithRelation.condition.value[1]"
v-if="conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.MinuteOffsetBetween.value ||
conditionWithRelation.condition.operator === TransactionExplorerConditionOperator.MinuteOffsetNotBetween.value"
/>
</div>
<v-select
multiple chips closable-chips
density="compact"
@@ -166,7 +279,7 @@
{ type: TransactionType.Transfer, displayName: tt('Transfer') }
]"
v-model="conditionWithRelation.condition.value"
v-if="conditionWithRelation.condition.field === TransactionExplorerConditionField.TransactionType.value"
v-else-if="conditionWithRelation.condition.field === TransactionExplorerConditionField.TransactionType.value"
>
<template #item="{ props, item }">
<v-list-item :value="item.value" v-bind="props">
@@ -410,7 +523,7 @@ import { useTransactionCategoriesStore } from '@/stores/transactionCategory.ts';
import { useTransactionTagsStore } from '@/stores/transactionTag.ts';
import { useExplorersStore } from '@/stores/explorer.ts';
import { type NameValue, values } from '@/core/base.ts';
import { type NameValue, type TypeAndDisplayName, values } from '@/core/base.ts';
import { AccountType } from '@/core/account.ts';
import { TransactionType } from '@/core/transaction.ts';
import {
@@ -419,6 +532,11 @@ import {
TransactionExplorerConditionOperator
} from '@/core/explorer.ts';
import {
WESTERNMOST_TIMEZONE_UTC_OFFSET,
EASTERNMOST_TIMEZONE_UTC_OFFSET,
} from '@/consts/timezone.ts';
import {
type TransactionExplorerCondition,
TransactionExplorerQuery,
@@ -457,6 +575,10 @@ const props = defineProps<ExplorerQueryTabProps>();
const {
tt,
joinMultiText,
getAllMonths,
getAllWeekDays,
getAllHours,
getAvailableMonthDays,
getAllTransactionExplorerConditionFields,
getAllTransactionExplorerConditionOperators
} = useI18n();
@@ -489,6 +611,10 @@ const hasAnyAccount = computed<boolean>(() => accountsStore.allPlainAccounts.len
const hasAnyTransactionCategory = computed<boolean>(() => !isObjectEmpty(transactionCategoriesStore.allTransactionCategoriesMap));
const allTransactionExplorerConditionFields = computed<NameValue[]>(() => getAllTransactionExplorerConditionFields());
const allAvailableDayOfWeekOptions = computed<TypeAndDisplayName[]>(() => getAllWeekDays(userStore.currentUserFirstDayOfWeek));
const allAvailableDayOfMonthOptions = computed<TypeAndDisplayName[]>(() => getAvailableMonthDays(31, 3));
const allAvailableMonthOfYearOptions = computed<TypeAndDisplayName[]>(() => getAllMonths());
const allAvailableHourOfDayOptions = computed<TypeAndDisplayName[]>(() => getAllHours());
function getFilteredAccountsDisplayContent(filterAccountIds?: Record<string, boolean>): string {
if ((props.loading && !hasAnyAccount.value) || !accountsStore.allVisiblePlainAccounts || !accountsStore.allVisiblePlainAccounts.length) {