diff --git a/frontend/components/accountOverview.vue b/frontend/components/accountOverview.vue index e930f62..13cc29b 100644 --- a/frontend/components/accountOverview.vue +++ b/frontend/components/accountOverview.vue @@ -8,7 +8,7 @@ />
- {{ accountIdToName[accountId] }} + {{ accountIdToName[accountId] }}
{{ formatNumber(account.balance - balanceUncleared) }} € @@ -35,28 +35,49 @@
+ + +
@@ -88,91 +109,59 @@ - - - - - - - - - - - - - - - - - - - - - - + +
@@ -304,11 +293,12 @@ /* eslint-disable sort-imports */ import { Modal } from 'bootstrap' -import { formatNumber, responseToJSON } from '../helpers' +import { formatNumber } from '../helpers' import rangeSelector from './rangeSelector.vue' +import txEditor from './txEditor.vue' export default { - components: { rangeSelector }, + components: { rangeSelector, txEditor }, computed: { account() { @@ -382,16 +372,7 @@ export default { data() { return { - form: { - amount: 0, - category: '', - cleared: false, - date: new Date().toISOString() - .split('T')[0], - - description: '', - payee: '', - }, + editedTxId: null, modals: { createTransfer: { @@ -410,36 +391,6 @@ export default { }, methods: { - createTransaction() { - return fetch('/api/transactions', { - body: JSON.stringify({ - ...this.form, - account: this.accountId, - category: this.form.category || null, - time: new Date(this.form.date), - }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - }) - .then(responseToJSON) - .then(() => { - this.$emit('update-accounts') - this.fetchTransactions() - - this.showAddTransaction = false - this.form = { - amount: 0, - category: '', - cleared: false, - date: this.form.date, - description: '', - payee: '', - } - }) - }, - deleteSelected() { const actions = [] for (const id of this.selectedTx) { @@ -455,6 +406,14 @@ export default { }) }, + editSelected() { + this.editTx(this.selectedTx[0]) + }, + + editTx(txId) { + this.editedTxId = txId + }, + fetchTransactions() { const since = this.timeRange.start.toISOString() const until = this.timeRange.end.toISOString() @@ -500,6 +459,12 @@ export default { }) }, + txSaved() { + this.editedTxId = null + this.$emit('update-accounts') + this.fetchTransactions() + }, + updateSelectAll(evt) { this.selectedTxRaw = Object.fromEntries(this.transactions.map(tx => [tx.id, evt.target.checked])) }, diff --git a/frontend/components/txEditor.vue b/frontend/components/txEditor.vue new file mode 100644 index 0000000..9cedb8b --- /dev/null +++ b/frontend/components/txEditor.vue @@ -0,0 +1,165 @@ + + + diff --git a/frontend/helpers.js b/frontend/helpers.js index b8c79cf..9c3876b 100644 --- a/frontend/helpers.js +++ b/frontend/helpers.js @@ -39,5 +39,9 @@ export function responseToJSON(resp) { throw new Error(`non-2xx status code: ${resp.status}`) } + if (resp.status === 204) { + return null + } + return resp.json() } diff --git a/pkg/api/api.go b/pkg/api/api.go index 51afaaf..2010847 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -61,6 +61,9 @@ func RegisterHandler(apiRouter *mux.Router, dbc *database.Client, logger *logrus apiRouter. HandleFunc("/transactions/{id}", as.handleUpdateTransaction). Methods(http.MethodPatch) + apiRouter. + HandleFunc("/transactions/{id}", as.handleOverwriteTransaction). + Methods(http.MethodPut) } func (a apiServer) errorResponse(w http.ResponseWriter, err error, desc string, status int) { diff --git a/pkg/api/transaction.go b/pkg/api/transaction.go index 339d611..c31fae0 100644 --- a/pkg/api/transaction.go +++ b/pkg/api/transaction.go @@ -123,6 +123,31 @@ func (a apiServer) handleListTransactionsByAccount(w http.ResponseWriter, r *htt a.jsonResponse(w, http.StatusOK, txs) } +func (a apiServer) handleOverwriteTransaction(w http.ResponseWriter, r *http.Request) { + var ( + tx database.Transaction + txID uuid.UUID + err error + ) + + if txID, err = uuid.Parse(mux.Vars(r)["id"]); err != nil { + a.errorResponse(w, err, "parsing id", http.StatusBadRequest) + return + } + + if err = json.NewDecoder(r.Body).Decode(&tx); err != nil { + a.errorResponse(w, err, "parsing body", http.StatusBadRequest) + return + } + + if err = a.dbc.UpdateTransaction(txID, tx); err != nil { + a.errorResponse(w, err, "updating transaction", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + func (a apiServer) handleUpdateTransaction(w http.ResponseWriter, r *http.Request) { var ( txID uuid.UUID diff --git a/pkg/database/database.go b/pkg/database/database.go index 3fc60c3..3603837 100644 --- a/pkg/database/database.go +++ b/pkg/database/database.go @@ -421,6 +421,33 @@ func (c *Client) UpdateAccountName(id uuid.UUID, name string) (err error) { return nil } +// UpdateTransaction takes a transaction, fetches the stored transaction +// applies some sanity actions and stores it back to the database +func (c *Client) UpdateTransaction(txID uuid.UUID, tx Transaction) (err error) { + if err = c.retryTx(func(db *gorm.DB) error { + var oldTX Transaction + if err := db.First(&oldTX, "id = ?", txID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return backoff.NewErrCannotRetry(fmt.Errorf("fetching old transaction: %w", err)) //nolint:wrapcheck + } + return fmt.Errorf("fetching old transaction: %w", err) + } + + tx.ID = txID + tx.Account = oldTX.Account // Changing that would create chaos + + if err = tx.Validate(c); err != nil { + return fmt.Errorf("validating transaction: %w", err) + } + + return db.Save(&tx).Error + }); err != nil { + return fmt.Errorf("updating transaction: %w", err) + } + + return nil +} + // UpdateTransactionCategory modifies the category of the given // transaction. (It is not possible to remove a category with this) func (c *Client) UpdateTransactionCategory(id uuid.UUID, cat uuid.UUID) (err error) {