From 377a4899b74c98b49be883de1e5d66d1a67bbb6c Mon Sep 17 00:00:00 2001 From: MaysWind Date: Fri, 28 Feb 2025 00:14:52 +0800 Subject: [PATCH] scheduled transaction supports start time and end time (#36) --- pkg/api/transaction_templates.go | 67 +++++++++++- pkg/errs/transaction_template.go | 13 +-- pkg/models/transaction_template.go | 29 ++++- pkg/services/transaction_templates.go | 2 +- pkg/services/transactions.go | 14 ++- pkg/utils/datetimes.go | 30 ++++++ pkg/utils/datetimes_test.go | 32 ++++++ src/components/desktop/DateSelect.vue | 99 +++++++++++++++++ src/components/mobile/DateSelectionSheet.vue | 100 ++++++++++++++++++ src/desktop-main.ts | 2 + src/lib/datetime.ts | 4 + src/locales/de.json | 3 + src/locales/en.json | 3 + src/locales/es.json | 3 + src/locales/helpers.ts | 6 ++ src/locales/ru.json | 3 + src/locales/vi.json | 3 + src/locales/zh_Hans.json | 3 + src/mobile-main.ts | 2 + src/models/transaction_template.ts | 22 +++- .../transactions/list/dialogs/EditDialog.vue | 16 +++ src/views/mobile/transactions/EditPage.vue | 61 ++++++++++- 22 files changed, 500 insertions(+), 17 deletions(-) create mode 100644 src/components/desktop/DateSelect.vue create mode 100644 src/components/mobile/DateSelectionSheet.vue diff --git a/pkg/api/transaction_templates.go b/pkg/api/transaction_templates.go index d49ce79a..b42894c3 100644 --- a/pkg/api/transaction_templates.go +++ b/pkg/api/transaction_templates.go @@ -156,7 +156,12 @@ func (a *TransactionTemplatesApi) TemplateCreateHandler(c *core.WebContext) (any } serverUtcOffset := utils.GetServerTimezoneOffsetMinutes() - template := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1) + template, err := a.createNewTemplateModel(uid, &templateCreateReq, maxOrderId+1) + + if err != nil { + log.Errorf(c, "[transaction_templates.TemplateCreateHandler] failed to create new template for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } if a.CurrentConfig().EnableDuplicateSubmissionsCheck && templateCreateReq.ClientSessionId != "" { found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE, uid, templateCreateReq.ClientSessionId) @@ -260,6 +265,34 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any newTemplate.ScheduledFrequency = a.getOrderedFrequencyValues(*templateModifyReq.ScheduledFrequency) newTemplate.ScheduledAt = a.getUTCScheduledAt(*templateModifyReq.ScheduledTimezoneUtcOffset) newTemplate.ScheduledTimezoneUtcOffset = *templateModifyReq.ScheduledTimezoneUtcOffset + + if templateModifyReq.ScheduledStartDate != nil { + startTime, err := utils.ParseFromLongDateFirstTime(*templateModifyReq.ScheduledStartDate, *templateModifyReq.ScheduledTimezoneUtcOffset) + + if err != nil { + log.Errorf(c, "[transaction_templates.TemplateModifyHandler] failed to parse scheduled start date for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + startUnixTime := startTime.Unix() + newTemplate.ScheduledStartTime = &startUnixTime + } + + if templateModifyReq.ScheduledEndDate != nil { + endTime, err := utils.ParseFromLongDateLastTime(*templateModifyReq.ScheduledEndDate, *templateModifyReq.ScheduledTimezoneUtcOffset) + + if err != nil { + log.Errorf(c, "[transaction_templates.TemplateModifyHandler] failed to parse scheduled end date for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + endUnixTime := endTime.Unix() + newTemplate.ScheduledEndTime = &endUnixTime + } + + if newTemplate.ScheduledStartTime != nil && newTemplate.ScheduledEndTime != nil && *newTemplate.ScheduledStartTime > *newTemplate.ScheduledEndTime { + return nil, errs.ErrScheduledTransactionTemplateStartDataLaterThanEndDate + } } if newTemplate.Name == template.Name && @@ -277,6 +310,8 @@ func (a *TransactionTemplatesApi) TemplateModifyHandler(c *core.WebContext) (any } else if template.TemplateType == models.TRANSACTION_TEMPLATE_TYPE_SCHEDULE { if newTemplate.ScheduledFrequencyType == template.ScheduledFrequencyType && newTemplate.ScheduledFrequency == template.ScheduledFrequency && + newTemplate.ScheduledStartTime == template.ScheduledStartTime && + newTemplate.ScheduledEndTime == template.ScheduledEndTime && newTemplate.ScheduledAt == template.ScheduledAt && newTemplate.ScheduledTimezoneUtcOffset == template.ScheduledTimezoneUtcOffset { return nil, errs.ErrNothingWillBeUpdated @@ -419,7 +454,7 @@ func (a *TransactionTemplatesApi) TemplateDeleteHandler(c *core.WebContext) (any return true, nil } -func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) *models.TransactionTemplate { +func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCreateReq *models.TransactionTemplateCreateRequest, order int32) (*models.TransactionTemplate, error) { template := &models.TransactionTemplate{ Uid: uid, TemplateType: templateCreateReq.TemplateType, @@ -441,9 +476,35 @@ func (a *TransactionTemplatesApi) createNewTemplateModel(uid int64, templateCrea template.ScheduledFrequency = a.getOrderedFrequencyValues(*templateCreateReq.ScheduledFrequency) template.ScheduledAt = a.getUTCScheduledAt(*templateCreateReq.ScheduledTimezoneUtcOffset) template.ScheduledTimezoneUtcOffset = *templateCreateReq.ScheduledTimezoneUtcOffset + + if templateCreateReq.ScheduledStartDate != nil { + startTime, err := utils.ParseFromLongDateFirstTime(*templateCreateReq.ScheduledStartDate, *templateCreateReq.ScheduledTimezoneUtcOffset) + + if err != nil { + return nil, err + } + + startUnixTime := startTime.Unix() + template.ScheduledStartTime = &startUnixTime + } + + if templateCreateReq.ScheduledEndDate != nil { + endTime, err := utils.ParseFromLongDateLastTime(*templateCreateReq.ScheduledEndDate, *templateCreateReq.ScheduledTimezoneUtcOffset) + + if err != nil { + return nil, err + } + + endUnixTime := endTime.Unix() + template.ScheduledEndTime = &endUnixTime + } + + if template.ScheduledStartTime != nil && template.ScheduledEndTime != nil && *template.ScheduledStartTime > *template.ScheduledEndTime { + return nil, errs.ErrScheduledTransactionTemplateStartDataLaterThanEndDate + } } - return template + return template, nil } func (a *TransactionTemplatesApi) getUTCScheduledAt(scheduledTimezoneUtcOffset int16) int16 { diff --git a/pkg/errs/transaction_template.go b/pkg/errs/transaction_template.go index 3cde13cd..b0e7641f 100644 --- a/pkg/errs/transaction_template.go +++ b/pkg/errs/transaction_template.go @@ -4,10 +4,11 @@ import "net/http" // Error codes related to transaction templates var ( - ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid") - ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found") - ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid") - ErrScheduledTransactionNotEnabled = NewNormalError(NormalSubcategoryTemplate, 3, http.StatusBadRequest, "scheduled transaction is not enabled") - ErrScheduledTransactionFrequencyInvalid = NewNormalError(NormalSubcategoryTemplate, 4, http.StatusBadRequest, "scheduled transaction frequency is invalid") - ErrTransactionTemplateHasTooManyTags = NewNormalError(NormalSubcategoryTemplate, 5, http.StatusBadRequest, "transaction template has too many tags") + ErrTransactionTemplateIdInvalid = NewNormalError(NormalSubcategoryTemplate, 0, http.StatusBadRequest, "transaction template id is invalid") + ErrTransactionTemplateNotFound = NewNormalError(NormalSubcategoryTemplate, 1, http.StatusBadRequest, "transaction template not found") + ErrTransactionTemplateTypeInvalid = NewNormalError(NormalSubcategoryTemplate, 2, http.StatusBadRequest, "transaction template type is invalid") + ErrScheduledTransactionNotEnabled = NewNormalError(NormalSubcategoryTemplate, 3, http.StatusBadRequest, "scheduled transaction is not enabled") + ErrScheduledTransactionFrequencyInvalid = NewNormalError(NormalSubcategoryTemplate, 4, http.StatusBadRequest, "scheduled transaction frequency is invalid") + ErrTransactionTemplateHasTooManyTags = NewNormalError(NormalSubcategoryTemplate, 5, http.StatusBadRequest, "transaction template has too many tags") + ErrScheduledTransactionTemplateStartDataLaterThanEndDate = NewNormalError(NormalSubcategoryTemplate, 6, http.StatusBadRequest, "scheduled transaction start date is later than end time") ) diff --git a/pkg/models/transaction_template.go b/pkg/models/transaction_template.go index b6375e10..8b3abccb 100644 --- a/pkg/models/transaction_template.go +++ b/pkg/models/transaction_template.go @@ -2,6 +2,7 @@ package models import ( "strings" + "time" "github.com/mayswind/ezbookkeeping/pkg/utils" ) @@ -29,15 +30,17 @@ const ( type TransactionTemplate struct { TemplateId int64 `xorm:"PK"` Uid int64 `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) NOT NULL"` - Deleted bool `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at) NOT NULL"` - TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at) NOT NULL"` + Deleted bool `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time) NOT NULL"` + TemplateType TransactionTemplateType `xorm:"INDEX(IDX_transaction_template_uid_deleted_template_type_order) INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time) NOT NULL"` Name string `xorm:"VARCHAR(64) NOT NULL"` Type TransactionType `xorm:"NOT NULL"` CategoryId int64 `xorm:"NOT NULL"` AccountId int64 `xorm:"NOT NULL"` - ScheduledFrequencyType TransactionScheduleFrequencyType `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at)"` + ScheduledFrequencyType TransactionScheduleFrequencyType `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time)"` ScheduledFrequency string `xorm:"VARCHAR(100)"` - ScheduledAt int16 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_at)"` + ScheduledStartTime *int64 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time)"` + ScheduledEndTime *int64 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time)"` + ScheduledAt int16 `xorm:"INDEX(IDX_transaction_template_deleted_type_freqtype_scheduled_time)"` ScheduledTimezoneUtcOffset int16 TagIds string `xorm:"VARCHAR(255) NOT NULL"` Amount int64 `xorm:"NOT NULL"` @@ -77,6 +80,8 @@ type TransactionTemplateCreateRequest struct { Comment string `json:"comment" binding:"max=255"` ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"` ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"` + ScheduledStartDate *string `json:"scheduledStartDate" binding:"omitempty"` + ScheduledEndDate *string `json:"scheduledEndDate" binding:"omitempty"` ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"` ClientSessionId string `json:"clientSessionId"` } @@ -102,6 +107,8 @@ type TransactionTemplateModifyRequest struct { Comment string `json:"comment" binding:"max=255"` ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType" binding:"omitempty"` ScheduledFrequency *string `json:"scheduledFrequency" binding:"omitempty"` + ScheduledStartDate *string `json:"scheduledStartDate" binding:"omitempty"` + ScheduledEndDate *string `json:"scheduledEndDate" binding:"omitempty"` ScheduledTimezoneUtcOffset *int16 `json:"utcOffset" binding:"omitempty,min=-720,max=840"` } @@ -133,6 +140,8 @@ type TransactionTemplateInfoResponse struct { Name string `json:"name"` ScheduledFrequencyType *TransactionScheduleFrequencyType `json:"scheduledFrequencyType,omitempty"` ScheduledFrequency *string `json:"scheduledFrequency,omitempty"` + ScheduledStartDate *string `json:"scheduledStartDate" binding:"omitempty"` + ScheduledEndDate *string `json:"scheduledEndDate" binding:"omitempty"` ScheduledAt *int16 `json:"scheduledAt,omitempty"` DisplayOrder int32 `json:"displayOrder"` Hidden bool `json:"hidden"` @@ -171,6 +180,18 @@ func (t *TransactionTemplate) ToTransactionTemplateInfoResponse(serverUtcOffset response.ScheduledFrequencyType = &t.ScheduledFrequencyType response.ScheduledFrequency = &t.ScheduledFrequency response.ScheduledAt = &t.ScheduledAt + + templateTimeZone := time.FixedZone("Template Timezone", int(t.ScheduledTimezoneUtcOffset)*60) + + if t.ScheduledStartTime != nil { + startDate := utils.FormatUnixTimeToLongDate(*t.ScheduledStartTime, templateTimeZone) + response.ScheduledStartDate = &startDate + } + + if t.ScheduledEndTime != nil { + endDate := utils.FormatUnixTimeToLongDate(*t.ScheduledEndTime, templateTimeZone) + response.ScheduledEndDate = &endDate + } } return response diff --git a/pkg/services/transaction_templates.go b/pkg/services/transaction_templates.go index ba28f565..f1c42529 100644 --- a/pkg/services/transaction_templates.go +++ b/pkg/services/transaction_templates.go @@ -136,7 +136,7 @@ func (s *TransactionTemplateService) ModifyTemplate(c core.Context, template *mo template.UpdatedUnixTime = time.Now().Unix() return s.UserDataDB(template.Uid).DoTransaction(c, func(sess *xorm.Session) error { - updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "scheduled_frequency_type", "scheduled_frequency", "scheduled_at", "scheduled_timezone_utc_offset", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template) + updatedRows, err := sess.ID(template.TemplateId).Cols("name", "type", "category_id", "account_id", "scheduled_frequency_type", "scheduled_frequency", "scheduled_start_time", "scheduled_end_time", "scheduled_at", "scheduled_timezone_utc_offset", "tag_ids", "amount", "related_account_id", "related_account_amount", "hide_amount", "comment", "updated_unix_time").Where("uid=? AND deleted=?", template.Uid, false).Update(template) if err != nil { return err diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index 9ec00869..c88a249b 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -398,7 +398,7 @@ func (s *TransactionService) CreateScheduledTransactions(c core.Context, current for i := 0; i < s.UserDataDBCount(); i++ { var templates []*models.TransactionTemplate - err := s.UserDataDBByIndex(i).NewSession(c).Where("deleted=? AND template_type=? AND (scheduled_frequency_type=? OR scheduled_frequency_type=?) AND scheduled_at>=? AND scheduled_at=?) AND scheduled_at>=? AND scheduled_at transactionUnixTime { + skipCount++ + log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" does not need to create transaction, now is earlier than the start time %d", template.TemplateId, *template.ScheduledStartTime) + continue + } + + if template.ScheduledEndTime != nil && *template.ScheduledEndTime < transactionUnixTime { + skipCount++ + log.Infof(c, "[transactions.CreateScheduledTransactions] transaction template \"id:%d\" does not need to create transaction, now is later than the end time %d", template.TemplateId, *template.ScheduledEndTime) + continue + } + var transactionDbType models.TransactionDbType if template.Type == models.TRANSACTION_TYPE_EXPENSE { diff --git a/pkg/utils/datetimes.go b/pkg/utils/datetimes.go index 85c9dba8..63d9b994 100644 --- a/pkg/utils/datetimes.go +++ b/pkg/utils/datetimes.go @@ -9,6 +9,7 @@ import ( ) const ( + longDateFormat = "2006-01-02" longDateTimeFormat = "2006-01-02 15:04:05" longDateTimeWithTimezoneFormat = "2006-01-02 15:04:05Z07:00" longDateTimeWithTimezoneFormat2 = "2006-01-02 15:04:05 Z0700" @@ -42,6 +43,17 @@ func ParseNumericYearMonth(yearMonth string) (int32, int32, error) { return year, month, nil } +// FormatUnixTimeToLongDate returns a textual representation of the unix time formatted by long date time format +func FormatUnixTimeToLongDate(unixTime int64, timezone *time.Location) string { + t := parseFromUnixTime(unixTime) + + if timezone != nil { + t = t.In(timezone) + } + + return t.Format(longDateFormat) +} + // FormatUnixTimeToLongDateTime returns a textual representation of the unix time formatted by long date time format func FormatUnixTimeToLongDateTime(unixTime int64, timezone *time.Location) string { t := parseFromUnixTime(unixTime) @@ -119,6 +131,24 @@ func GetMaxUnixTimeWithSameLocalDateTime(unixTime int64, currentUtcOffset int16) return unixTime + int64(currentUtcOffset)*60 - westernmostTimezoneUtcOffset*60 } +// ParseFromLongDateFirstTime parses a formatted string in long date format +func ParseFromLongDateFirstTime(t string, utcOffset int16) (time.Time, error) { + timezone := time.FixedZone("Timezone", int(utcOffset)*60) + return time.ParseInLocation(longDateFormat, t, timezone) +} + +// ParseFromLongDateLastTime parses a formatted string in long date format +func ParseFromLongDateLastTime(t string, utcOffset int16) (time.Time, error) { + timezone := time.FixedZone("Timezone", int(utcOffset)*60) + lastTime, err := time.ParseInLocation(longDateFormat, t, timezone) + + if err != nil { + return lastTime, err + } + + return lastTime.Add(24 * time.Hour).Add(-1 * time.Nanosecond), nil +} + // ParseFromLongDateTimeToMinUnixTime parses a formatted string in long date time format to minimal unix time (the westernmost timezone) func ParseFromLongDateTimeToMinUnixTime(t string) (time.Time, error) { timezone := time.FixedZone("Timezone", easternmostTimezoneUtcOffset*60) diff --git a/pkg/utils/datetimes_test.go b/pkg/utils/datetimes_test.go index a4c0e0ca..aa6cc404 100644 --- a/pkg/utils/datetimes_test.go +++ b/pkg/utils/datetimes_test.go @@ -18,6 +18,20 @@ func TestParseNumericYearMonth(t *testing.T) { assert.Equal(t, expectedMonth, actualMonth) } +func TestFormatUnixTimeToLongDate(t *testing.T) { + unixTime := int64(1617228083) + utcTimezone := time.FixedZone("Test Timezone", 0) // UTC + utc8Timezone := time.FixedZone("Test Timezone", 28800) // UTC+8 + + expectedValue := "2021-03-31" + actualValue := FormatUnixTimeToLongDate(unixTime, utcTimezone) + assert.Equal(t, expectedValue, actualValue) + + expectedValue = "2021-04-01" + actualValue = FormatUnixTimeToLongDate(unixTime, utc8Timezone) + assert.Equal(t, expectedValue, actualValue) +} + func TestFormatUnixTimeToLongDateTime(t *testing.T) { unixTime := int64(1617228083) utcTimezone := time.FixedZone("Test Timezone", 0) // UTC @@ -106,6 +120,24 @@ func TestGetMaxUnixTimeWithSameLocalDateTime(t *testing.T) { assert.Equal(t, expectedValue, actualValue) } +func TestParseFromLongDateFirstTime(t *testing.T) { + expectedValue := int64(1690819200) + actualTime, err := ParseFromLongDateFirstTime("2023-08-01", 480) + assert.Equal(t, nil, err) + + actualValue := actualTime.Unix() + assert.Equal(t, expectedValue, actualValue) +} + +func TestParseFromLongDateLastTime(t *testing.T) { + expectedValue := int64(1690905599) + actualTime, err := ParseFromLongDateLastTime("2023-08-01", 480) + assert.Equal(t, nil, err) + + actualValue := actualTime.Unix() + assert.Equal(t, expectedValue, actualValue) +} + func TestParseFromLongDateTimeToMinUnixTime(t *testing.T) { expectedValue := int64(1690797600) actualTime, err := ParseFromLongDateTimeToMinUnixTime("2023-08-01 00:00:00") diff --git a/src/components/desktop/DateSelect.vue b/src/components/desktop/DateSelect.vue new file mode 100644 index 00000000..c34b4b82 --- /dev/null +++ b/src/components/desktop/DateSelect.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/components/mobile/DateSelectionSheet.vue b/src/components/mobile/DateSelectionSheet.vue new file mode 100644 index 00000000..d79faca9 --- /dev/null +++ b/src/components/mobile/DateSelectionSheet.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/src/desktop-main.ts b/src/desktop-main.ts index d201474e..8a5a01d3 100644 --- a/src/desktop-main.ts +++ b/src/desktop-main.ts @@ -79,6 +79,7 @@ import ItemIcon from '@/components/desktop/ItemIcon.vue'; import BtnVerticalGroup from '@/components/desktop/BtnVerticalGroup.vue'; import AmountInput from '@/components/desktop/AmountInput.vue'; import DateTimeSelect from '@/components/desktop/DateTimeSelect.vue'; +import DateSelect from '@/components/desktop/DateSelect.vue'; import ColorSelect from '@/components/desktop/ColorSelect.vue'; import IconSelect from '@/components/desktop/IconSelect.vue'; import TwoColumnSelect from '@/components/desktop/TwoColumnSelect.vue'; @@ -450,6 +451,7 @@ app.component('ItemIcon', ItemIcon); app.component('BtnVerticalGroup', BtnVerticalGroup); app.component('AmountInput', AmountInput); app.component('DateTimeSelect', DateTimeSelect); +app.component('DateSelect', DateSelect); app.component('ColorSelect', ColorSelect); app.component('IconSelect', IconSelect); app.component('TwoColumnSelect', TwoColumnSelect); diff --git a/src/lib/datetime.ts b/src/lib/datetime.ts index c74192bc..c35924de 100644 --- a/src/lib/datetime.ts +++ b/src/lib/datetime.ts @@ -199,6 +199,10 @@ export function formatCurrentTime(format: string): string { return moment().format(format); } +export function formatDate(date: string, format: string): string { + return moment(date, 'YYYY-MM-DD').format(format); +} + export function getUnixTime(date: SupportedDate): number { return moment(date).unix(); } diff --git a/src/locales/de.json b/src/locales/de.json index b601f7fe..12ff965c 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1123,6 +1123,7 @@ "scheduled transaction is not enabled": "Geplante Transaktion ist nicht aktiviert", "scheduled transaction frequency is invalid": "Häufigkeit der geplanten Transaktion ist ungültig", "transaction template has too many tags": "Transaktionsvorlage hat zu viele Tags", + "scheduled transaction start date is later than end time": "Scheduled transaction start date is later than end time", "transaction picture id is invalid": "Transaktionsbild-ID ist ungültig", "transaction picture not found": "Transaktionsbild nicht gefunden", "no transaction picture": "Kein Transaktionsbild vorhanden", @@ -1289,6 +1290,8 @@ "Previous Billing Cycle": "Vorheriger Abrechnungszeitraum", "Current Billing Cycle": "Aktueller Abrechnungszeitraum", "Custom Date": "Benutzerdefiniertes Datum", + "Start Date": "Start Date", + "End Date": "End Date", "Start Time": "Startzeit", "End Time": "Endzeit", "Select Date": "Datum auswählen", diff --git a/src/locales/en.json b/src/locales/en.json index 5064478b..3e57f493 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1123,6 +1123,7 @@ "scheduled transaction is not enabled": "Scheduled transaction is not enabled", "scheduled transaction frequency is invalid": "Scheduled transaction frequency is invalid", "transaction template has too many tags": "There are too many tags in this transaction template", + "scheduled transaction start date is later than end time": "Scheduled transaction start date is later than end time", "transaction picture id is invalid": "Transaction picture ID is invalid", "transaction picture not found": "Transaction picture is not found", "no transaction picture": "There is no transaction picture file", @@ -1289,6 +1290,8 @@ "Previous Billing Cycle": "Previous Billing Cycle", "Current Billing Cycle": "Current Billing Cycle", "Custom Date": "Custom Date", + "Start Date": "Start Date", + "End Date": "End Date", "Start Time": "Start Time", "End Time": "End Time", "Select Date": "Select Date", diff --git a/src/locales/es.json b/src/locales/es.json index 86bde32e..6b9da5a8 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1123,6 +1123,7 @@ "scheduled transaction is not enabled": "La transacción programada no está habilitada", "scheduled transaction frequency is invalid": "La frecuencia de transacción programada no es válida", "transaction template has too many tags": "Hay demasiadas etiquetas en esta plantilla de transacción", + "scheduled transaction start date is later than end time": "Scheduled transaction start date is later than end time", "transaction picture id is invalid": "El ID de la imagen de la transacción no es válido", "transaction picture not found": "No se encuentra la imagen de la transacción", "no transaction picture": "No hay ningún archivo de imagen de transacción.", @@ -1289,6 +1290,8 @@ "Previous Billing Cycle": "Ciclo de facturación anterior", "Current Billing Cycle": "Ciclo de facturación actual", "Custom Date": "Fecha personalizada", + "Start Date": "Start Date", + "End Date": "End Date", "Start Time": "Hora de inicio", "End Time": "Hora de finalización", "Select Date": "Seleccionar fecha", diff --git a/src/locales/helpers.ts b/src/locales/helpers.ts index 80cb1ec6..8605f15a 100644 --- a/src/locales/helpers.ts +++ b/src/locales/helpers.ts @@ -117,6 +117,7 @@ import { isPM, formatUnixTime, formatCurrentTime, + formatDate, parseDateFromUnixTime, getYear, getTimezoneOffset, @@ -1297,6 +1298,10 @@ export function useI18n() { return getLocalizedDateTimeType(ShortTimeFormat.all(), ShortTimeFormat.values(), userStore.currentUserShortTimeFormat, 'shortTimeFormat', ShortTimeFormat.Default).isMeridiemIndicatorFirst || false; } + function formatDateToLongDate(date: string): string { + return formatDate(date, getLocalizedLongDateFormat()); + } + function formatYearQuarter(year: number, quarter: number): string { if (1 <= quarter && quarter <= 4) { return t('format.yearQuarter.q' + quarter, { @@ -1713,6 +1718,7 @@ export function useI18n() { formatUnixTimeToShortMonthDay: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortMonthDayFormat(), utcOffset, currentUtcOffset), formatUnixTimeToLongTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedLongTimeFormat(), utcOffset, currentUtcOffset), formatUnixTimeToShortTime: (unixTime: number, utcOffset?: number, currentUtcOffset?: number) => formatUnixTime(unixTime, getLocalizedShortTimeFormat(), utcOffset, currentUtcOffset), + formatDateToLongDate, formatYearQuarter, formatDateRange, getTimezoneDifferenceDisplayText, diff --git a/src/locales/ru.json b/src/locales/ru.json index 20b3ef0d..2cd3f27d 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1123,6 +1123,7 @@ "scheduled transaction is not enabled": "Запланированная транзакция не включена", "scheduled transaction frequency is invalid": "Частота запланированной транзакции недействительна", "transaction template has too many tags": "Слишком много тегов в этом шаблоне транзакции", + "scheduled transaction start date is later than end time": "Scheduled transaction start date is later than end time", "transaction picture id is invalid": "ID изображения транзакции недействителен", "transaction picture not found": "Изображение транзакции не найдено", "no transaction picture": "Нет файла изображения транзакции", @@ -1289,6 +1290,8 @@ "Previous Billing Cycle": "Предыдущий расчетный период", "Current Billing Cycle": "Текущий расчетный период", "Custom Date": "Выбрать дату", + "Start Date": "Start Date", + "End Date": "End Date", "Start Time": "Время начала", "End Time": "Время окончания", "Select Date": "Выбрать дату", diff --git a/src/locales/vi.json b/src/locales/vi.json index 4dcad7d3..68ad37eb 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1123,6 +1123,7 @@ "scheduled transaction is not enabled": "Giao dịch theo lịch trình chưa được bật", "scheduled transaction frequency is invalid": "Tần suất giao dịch theo lịch trình không hợp lệ", "transaction template has too many tags": "Có quá nhiều thẻ trong mẫu giao dịch này", + "scheduled transaction start date is later than end time": "Scheduled transaction start date is later than end time", "transaction picture id is invalid": "ID ảnh giao dịch không hợp lệ", "transaction picture not found": "Không tìm thấy ảnh giao dịch", "no transaction picture": "Không có tệp ảnh giao dịch", @@ -1289,6 +1290,8 @@ "Previous Billing Cycle": "Previous Billing Cycle", "Current Billing Cycle": "Current Billing Cycle", "Custom Date": "Ngày tùy chỉnh", + "Start Date": "Start Date", + "End Date": "End Date", "Start Time": "Thời gian bắt đầu", "End Time": "Thời gian kết thúc", "Select Date": "Chọn ngày", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index cdd501a1..b3b828dd 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1123,6 +1123,7 @@ "scheduled transaction is not enabled": "定时交易没有启用", "scheduled transaction frequency is invalid": "定时交易周期无效", "transaction template has too many tags": "交易模板中的标签过多", + "scheduled transaction start date is later than end time": "定时交易开始时间晚于结束时间", "transaction picture id is invalid": "交易图片ID无效", "transaction picture not found": "交易图片不存在", "no transaction picture": "没有交易图片文件", @@ -1289,6 +1290,8 @@ "Previous Billing Cycle": "上个账单周期", "Current Billing Cycle": "当前账单周期", "Custom Date": "自定义日期", + "Start Date": "开始日期", + "End Date": "结束日期", "Start Time": "开始时间", "End Time": "结束时间", "Select Date": "选择日期", diff --git a/src/mobile-main.ts b/src/mobile-main.ts index 5b7b5636..88fd6ef1 100644 --- a/src/mobile-main.ts +++ b/src/mobile-main.ts @@ -92,6 +92,7 @@ import PinCodeInputSheet from '@/components/mobile/PinCodeInputSheet.vue'; import PasswordInputSheet from '@/components/mobile/PasswordInputSheet.vue'; import PasscodeInputSheet from '@/components/mobile/PasscodeInputSheet.vue'; import DateTimeSelectionSheet from '@/components/mobile/DateTimeSelectionSheet.vue'; +import DateSelectionSheet from '@/components/mobile/DateSelectionSheet.vue'; import DateRangeSelectionSheet from '@/components/mobile/DateRangeSelectionSheet.vue'; import MonthRangeSelectionSheet from '@/components/mobile/MonthRangeSelectionSheet.vue'; import ListItemSelectionSheet from '@/components/mobile/ListItemSelectionSheet.vue'; @@ -175,6 +176,7 @@ app.component('PinCodeInputSheet', PinCodeInputSheet); app.component('PasswordInputSheet', PasswordInputSheet); app.component('PasscodeInputSheet', PasscodeInputSheet); app.component('DateTimeSelectionSheet', DateTimeSelectionSheet); +app.component('DateSelectionSheet', DateSelectionSheet); app.component('DateRangeSelectionSheet', DateRangeSelectionSheet); app.component('MonthRangeSelectionSheet', MonthRangeSelectionSheet); app.component('ListItemSelectionSheet', ListItemSelectionSheet); diff --git a/src/models/transaction_template.ts b/src/models/transaction_template.ts index a30dc8a3..9a11ed38 100644 --- a/src/models/transaction_template.ts +++ b/src/models/transaction_template.ts @@ -8,16 +8,20 @@ export class TransactionTemplate extends Transaction implements TransactionTempl public name: string; public scheduledFrequencyType?: number; public scheduledFrequency?: string; + public scheduledStartDate?: string; + public scheduledEndDate?: string; public scheduledAt?: number; public displayOrder: number; public hidden: boolean; - private constructor(id: string, templateType: number, name: string, type: number, categoryId: string, utcOffset: number, sourceAccountId: string, destinationAccountId: string, sourceAmount: number, destinationAmount: number, hideAmount: boolean, scheduledFrequencyType: number | undefined, scheduledFrequency: string | undefined, scheduledAt: number | undefined, tagIds: string[], comment: string, editable: boolean, displayOrder: number, hidden: boolean) { + private constructor(id: string, templateType: number, name: string, type: number, categoryId: string, utcOffset: number, sourceAccountId: string, destinationAccountId: string, sourceAmount: number, destinationAmount: number, hideAmount: boolean, scheduledFrequencyType: number | undefined, scheduledFrequency: string | undefined, scheduledStartDate: string | undefined, scheduledEndDate: string | undefined, scheduledAt: number | undefined, tagIds: string[], comment: string, editable: boolean, displayOrder: number, hidden: boolean) { super(id, '', type, categoryId, 0, undefined, utcOffset, sourceAccountId, destinationAccountId, sourceAmount, destinationAmount, hideAmount, tagIds, comment, editable); this.templateType = templateType; this.name = name; this.scheduledFrequencyType = scheduledFrequencyType; this.scheduledFrequency = scheduledFrequency; + this.scheduledStartDate = scheduledStartDate; + this.scheduledEndDate = scheduledEndDate; this.scheduledAt = scheduledAt; this.displayOrder = displayOrder; this.hidden = hidden; @@ -30,6 +34,8 @@ export class TransactionTemplate extends Transaction implements TransactionTempl if (this.templateType === TemplateType.Schedule.type) { this.scheduledFrequencyType = other.scheduledFrequencyType; this.scheduledFrequency = other.scheduledFrequency; + this.scheduledStartDate = other.scheduledStartDate; + this.scheduledEndDate = other.scheduledEndDate; this.utcOffset = other.utcOffset; this.timeZone = undefined; } @@ -50,6 +56,8 @@ export class TransactionTemplate extends Transaction implements TransactionTempl comment: this.comment, scheduledFrequencyType: this.templateType === TemplateType.Schedule.type ? this.scheduledFrequencyType : undefined, scheduledFrequency: this.templateType === TemplateType.Schedule.type ? this.scheduledFrequency : undefined, + scheduledStartDate: this.templateType === TemplateType.Schedule.type && this.scheduledStartDate ? this.scheduledStartDate : undefined, + scheduledEndDate: this.templateType === TemplateType.Schedule.type && this.scheduledEndDate ? this.scheduledEndDate : undefined, utcOffset: this.templateType === TemplateType.Schedule.type ? this.utcOffset : undefined, clientSessionId: clientSessionId }; @@ -70,6 +78,8 @@ export class TransactionTemplate extends Transaction implements TransactionTempl comment: this.comment, scheduledFrequencyType: this.templateType === TemplateType.Schedule.type ? this.scheduledFrequencyType : undefined, scheduledFrequency: this.templateType === TemplateType.Schedule.type ? this.scheduledFrequency : undefined, + scheduledStartDate: this.templateType === TemplateType.Schedule.type && this.scheduledStartDate ? this.scheduledStartDate : undefined, + scheduledEndDate: this.templateType === TemplateType.Schedule.type && this.scheduledEndDate ? this.scheduledEndDate : undefined, utcOffset: this.templateType === TemplateType.Schedule.type ? this.utcOffset : undefined }; } @@ -89,6 +99,8 @@ export class TransactionTemplate extends Transaction implements TransactionTempl transaction.hideAmount, undefined, // scheduledFrequencyType undefined, // scheduledFrequency + undefined, // scheduledStartDate + undefined, // scheduledEndDate undefined, // scheduledAt transaction.tagIds, transaction.comment, @@ -113,6 +125,8 @@ export class TransactionTemplate extends Transaction implements TransactionTempl templateResponse.hideAmount, templateResponse.scheduledFrequencyType, templateResponse.scheduledFrequency, + templateResponse.scheduledStartDate ?? undefined, + templateResponse.scheduledEndDate ?? undefined, templateResponse.scheduledAt, templateResponse.tagIds, templateResponse.comment, @@ -147,6 +161,8 @@ export interface TransactionTemplateCreateRequest { readonly comment: string; readonly scheduledFrequencyType?: number; readonly scheduledFrequency?: string; + readonly scheduledStartDate?: string; + readonly scheduledEndDate?: string; readonly utcOffset?: number; readonly clientSessionId: string; } @@ -165,6 +181,8 @@ export interface TransactionTemplateModifyRequest { readonly comment: string; readonly scheduledFrequencyType?: number; readonly scheduledFrequency?: string; + readonly scheduledStartDate?: string; + readonly scheduledEndDate?: string; readonly utcOffset?: number; } @@ -191,6 +209,8 @@ export interface TransactionTemplateInfoResponse extends TransactionInfoResponse readonly name: string; readonly scheduledFrequencyType?: number; readonly scheduledFrequency?: string; + readonly scheduledStartDate?: string; + readonly scheduledEndDate?: string; readonly scheduledAt?: number; readonly displayOrder: number; readonly hidden: boolean; diff --git a/src/views/desktop/transactions/list/dialogs/EditDialog.vue b/src/views/desktop/transactions/list/dialogs/EditDialog.vue index 5169afbd..b1ef0fa3 100644 --- a/src/views/desktop/transactions/list/dialogs/EditDialog.vue +++ b/src/views/desktop/transactions/list/dialogs/EditDialog.vue @@ -235,6 +235,22 @@ + + + + + + + + + + + + + + + + (false); const showDestinationAccountSheet = ref(false); const showTransactionDateTimeSheet = ref(false); const showTransactionScheduledFrequencySheet = ref(false); +const showScheduledStartDateSheet = ref(false); +const showScheduledEndDateSheet = ref(false); const showGeoLocationMapSheet = ref(false); const showTransactionTagSheet = ref(false); const showTransactionPictures = ref(false); @@ -700,6 +731,34 @@ const transactionDisplayScheduledFrequency = computed(() => { } }); +const transactionDisplayScheduledStartDate = computed(() => { + if (pageTypeAndMode?.type !== TransactionEditPageType.Template) { + return ''; + } + + const template = transaction.value as TransactionTemplate; + + if (template.scheduledStartDate) { + return formatDateToLongDate(template.scheduledStartDate); + } else { + return tt('Unspecified'); + } +}); + +const transactionDisplayScheduledEndDate = computed(() => { + if (pageTypeAndMode?.type !== TransactionEditPageType.Template) { + return ''; + } + + const template = transaction.value as TransactionTemplate; + + if (template.scheduledEndDate) { + return formatDateToLongDate(template.scheduledEndDate); + } else { + return tt('Unspecified'); + } +}); + function getPageTypeNameMode(): { type: TransactionEditPageType, mode: TransactionEditPageMode } | null { if (props.f7route.path === '/transaction/add') { return {