Compare commits

..

No commits in common. "13aadc2358f623335c723fbf68b29641ef2fd054" and "df4737d0a51b13122a0da6d4265c4219f1843cae" have entirely different histories.

18 changed files with 55 additions and 473 deletions

View File

@ -9,6 +9,7 @@ import std.algorithm;
import std.array;
import std.conv;
import slf4d;
import asdf;
import profile.data;
import profile.model;

View File

@ -9,7 +9,6 @@ interface AttachmentRepository {
Attachment[] findAllByLinkedEntity(string subquery, ulong entityId);
Attachment[] findAllByTransactionId(ulong transactionId);
Attachment[] findAllByValueRecordId(ulong valueRecordId);
Attachment[] findAllByTransactionDraftId(ulong draftId);
ulong save(SysTime uploadedAt, string filename, string contentType, in ubyte[] content);
void remove(ulong id);
Optional!(ubyte[]) getContent(ulong id);

View File

@ -46,13 +46,6 @@ class SqliteAttachmentRepository : AttachmentRepository {
);
}
Attachment[] findAllByTransactionDraftId(ulong draftId) {
return findAllByLinkedEntity(
"SELECT attachment_id FROM transaction_draft_attachment WHERE draft_id = ?",
draftId
);
}
ulong save(SysTime uploadedAt, string filename, string contentType, in ubyte[] content) {
util.sqlite.update(
db,

View File

@ -224,12 +224,20 @@ immutable DEFAULT_DRAFT_PAGE = PageRequest(1, 10, [Sort("txn.id", SortDir.DESC)]
void handleGetDrafts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
ProfileDataSource ds = getProfileDataSource(request);
PageRequest pr = PageRequest.parse(request, DEFAULT_DRAFT_PAGE);
bool shouldFetchTemplates = request.getParamAs!bool("template", false);
Page!TransactionDraftListItem page = getDrafts(ds, pr, shouldFetchTemplates);
Page!TransactionDraftListItem page = getDrafts(ds, pr);
writeJsonBody(response, page);
}
@GetMapping(PROFILE_PATH ~ "/transaction-templates")
void handleGetTemplates(ref ServerHttpRequest request, ref ServerHttpResponse response) {
ProfileDataSource ds = getProfileDataSource(request);
PageRequest pr = PageRequest.parse(request, DEFAULT_DRAFT_PAGE);
Page!TransactionDraftListItem page = getTemplates(ds, pr);
writeJsonBody(response, page);
}
@GetMapping(PROFILE_PATH ~ "/transaction-drafts/:draftId:ulong")
@GetMapping(PROFILE_PATH ~ "/transaction-templates/:draftId:ulong")
void handleGetDraft(ref ServerHttpRequest request, ref ServerHttpResponse response) {
ProfileDataSource ds = getProfileDataSource(request);
TransactionDraftResponse draft = getDraft(ds, getDraftId(request));
@ -238,33 +246,6 @@ void handleGetDraft(ref ServerHttpRequest request, ref ServerHttpResponse respon
response.writeBodyString(jsonStr, "application/json");
}
@PostMapping(PROFILE_PATH ~ "/transaction-drafts")
void handleAddDraft(ref ServerHttpRequest request, ref ServerHttpResponse response) {
auto fullPayload = parseMultipartFilesAndBody!TransactionDraftPayload(request);
ProfileDataSource ds = getProfileDataSource(request);
TransactionDraftResponse draft = addDraft(ds, fullPayload.payload, fullPayload.files);
import asdf : serializeToJson;
string jsonStr = serializeToJson(draft);
response.writeBodyString(jsonStr, "application/json");
}
@PutMapping(PROFILE_PATH ~ "/transaction-drafts/:draftId:ulong")
void handleUpdateDraft(ref ServerHttpRequest request, ref ServerHttpResponse response) {
ProfileDataSource ds = getProfileDataSource(request);
auto fullPayload = parseMultipartFilesAndBody!TransactionDraftPayload(request);
TransactionDraftResponse draft = updateDraft(ds, getDraftId(request), fullPayload.payload, fullPayload.files);
import asdf : serializeToJson;
string jsonStr = serializeToJson(draft);
response.writeBodyString(jsonStr, "application/json");
}
@DeleteMapping(PROFILE_PATH ~ "/transaction-drafts/:draftId:ulong")
void handleDeleteDraft(ref ServerHttpRequest request, ref ServerHttpResponse response) {
ProfileDataSource ds = getProfileDataSource(request);
ulong draftId = getDraftId(request);
deleteDraft(ds, draftId);
}
private ulong getDraftId(in ServerHttpRequest request) {
return getPathParamOrThrow!ulong(request, "draftId");
}

View File

@ -36,7 +36,6 @@ interface TransactionCategoryRepository {
);
}
// TODO: Migrate into transaction repo, similar to drafts!
interface TransactionTagRepository {
string[] findAllByTransactionId(ulong transactionId);
void updateTags(ulong transactionId, in string[] tags);
@ -62,7 +61,4 @@ interface TransactionDraftRepository {
void linkAttachment(ulong draftId, ulong attachmentId);
TransactionDraftResponse update(ulong draftId, in TransactionDraftPayload data);
void deleteById(ulong id);
void updateTags(ulong draftId, in string[] tags);
string[] findAllTags();
}

View File

@ -659,75 +659,18 @@ class SqliteTransactionDraftRepository : TransactionDraftRepository {
}
Optional!TransactionDraftResponse findById(ulong id) {
// First fetch the draft list item (contains all basic properties).
QueryBuilder qb = getBuilderForDraftsList();
addSelectsForDraftsList(qb);
qb.groupBy("draft.id");
qb.where("draft.id = ?");
string query = qb.build();
Optional!TransactionDraftListItem li = util.sqlite.findOne(db, query, &parseDraftListItem, id);
if (li.isNull) return Optional!TransactionDraftResponse.empty();
TransactionDraftListItem draft = li.value;
// Then fetch line items.
TransactionDraftResponse response;
response.id = draft.id;
response.addedAt = draft.addedAt;
response.templateName = draft.templateName;
response.timestamp = draft.timestamp;
response.amount = draft.amount;
response.currency = draft.currency;
response.description = draft.description;
response.internalTransfer = draft.internalTransfer;
response.vendor = draft.vendor;
response.category = draft.category;
response.creditedAccount = draft.creditedAccount;
response.debitedAccount = draft.debitedAccount;
response.lineItems = util.sqlite.findAll(
db,
import("sql/query/get_line_items_draft.sql"),
(row) {
TransactionLineItemResponse item;
item.idx = row.peek!uint(0);
item.valuePerItem = row.peek!long(1);
item.quantity = row.peek!ulong(2);
item.description = row.peek!string(3);
Optional!ulong categoryId = row.parseOptional!ulong(4);
if (categoryId) {
item.category = Optional!TransactionCategory.of(
TransactionCategory(
categoryId.value,
row.parseOptional!ulong(5),
row.peek!string(6),
row.peek!string(7),
row.peek!string(8)
));
}
return item;
}
);
// Return the response, excluding attachments (they are fetched using the attachment repo).
return Optional!TransactionDraftResponse.of(response);
// return util.sqlite.findOne(db, query, &parseDraft, id);
// TODO!
return Optional!TransactionDraftResponse.empty();
}
TransactionDraftResponse insert(in TransactionDraftPayload data) {
util.sqlite.update(
db,
import("sql/insert_transaction_draft.sql"),
Clock.currTime(UTC()).toISOExtString(),
data.templateName.toNullable(),
data.timestamp.toNullable(),
data.amount.toNullable(),
data.currencyCode.toNullable(),
data.description.toNullable(),
data.internalTransfer.toNullable(),
data.vendorId.toNullable(),
data.categoryId.toNullable(),
data.creditedAccountId.toNullable(),
data.debitedAccountId.toNullable()
);
ulong draftId = db.lastInsertRowid();
insertLineItems(draftId, data);
return findById(draftId).orElseThrow();
// TODO
return TransactionDraftResponse.init;
}
void linkAttachment(ulong draftId, ulong attachmentId) {
@ -740,29 +683,8 @@ class SqliteTransactionDraftRepository : TransactionDraftRepository {
}
TransactionDraftResponse update(ulong draftId, in TransactionDraftPayload data) {
util.sqlite.update(
db,
import("sql/update_transaction_draft.sql"),
data.templateName.toNullable(),
data.timestamp.toNullable(),
data.amount.toNullable(),
data.currencyCode.toNullable(),
data.description.toNullable(),
data.internalTransfer.toNullable(),
data.vendorId.toNullable(),
data.categoryId.toNullable(),
data.creditedAccountId.toNullable(),
data.debitedAccountId.toNullable(),
draftId
);
// Re-write all line items:
util.sqlite.update(
db,
"DELETE FROM transaction_draft_line_item WHERE draft_id = ?",
draftId
);
insertLineItems(draftId, data);
return findById(draftId).orElseThrow();
// TODO
return TransactionDraftResponse.init;
}
void deleteById(ulong id) {
@ -776,31 +698,6 @@ class SqliteTransactionDraftRepository : TransactionDraftRepository {
util.sqlite.deleteById(db, "transaction_draft", id);
}
void updateTags(ulong draftId, in string[] tags) {
util.sqlite.update(
db,
"DELETE FROM transaction_draft_tag WHERE draft_id = ?",
draftId
);
foreach (tag; tags) {
util.sqlite.update(
db,
"INSERT INTO transaction_draft_tag (draft_id, tag) VALUES (?, ?)",
draftId, tag
);
}
}
string[] findAllTags() {
return util.sqlite.findAll(
db,
"SELECT DISTINCT tag FROM transaction_draft_tag ORDER BY tag",
r => r.peek!string(0)
);
}
private QueryBuilder getBuilderForDraftsList() {
return QueryBuilder("transaction_draft draft")
.join("LEFT JOIN transaction_vendor vendor ON vendor.id = draft.vendor_id")
@ -887,19 +784,4 @@ class SqliteTransactionDraftRepository : TransactionDraftRepository {
}
return item;
}
private void insertLineItems(ulong draftId, in TransactionDraftPayload payload) {
foreach (size_t idx, lineItem; payload.lineItems) {
util.sqlite.update(
db,
import("sql/insert_line_item_draft.sql"),
draftId,
idx,
lineItem.valuePerItem,
lineItem.quantity,
lineItem.description,
lineItem.categoryId.toNullable()
);
}
}
}

View File

@ -182,26 +182,5 @@ struct TransactionDraftResponse {
/// Data provided by users when creating or updating drafts.
struct TransactionDraftPayload {
Optional!string templateName;
Optional!string timestamp;
Optional!ulong amount;
Optional!string currencyCode;
Optional!string description;
Optional!bool internalTransfer;
Optional!ulong vendorId;
Optional!ulong categoryId;
Optional!ulong creditedAccountId;
Optional!ulong debitedAccountId;
string[] tags;
LineItemPayload[] lineItems;
ulong[] attachmentIdsToRemove;
static struct LineItemPayload {
long valuePerItem;
ulong quantity;
string description;
Optional!ulong categoryId;
}
// TODO.
}

View File

@ -45,7 +45,6 @@ TransactionDetail addTransaction(ProfileDataSource ds, in AddTransactionPayload
AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
validateTransactionPayload(vendorRepo, categoryRepo, accountRepo, payload);
SysTime now = Clock.currTime(UTC());
SysTime timestamp = SysTime.fromISOExtString(payload.timestamp, UTC());
// Add the transaction:
@ -75,7 +74,7 @@ TransactionDetail addTransaction(ProfileDataSource ds, in AddTransactionPayload
}
TransactionTagRepository tagRepo = ds.getTransactionTagRepository();
tagRepo.updateTags(txn.id, payload.tags);
updateAttachments(txn.id, now, payload.attachmentIdsToRemove, files, attachmentRepo, txnRepo);
updateAttachments(txn.id, timestamp, payload.attachmentIdsToRemove, files, attachmentRepo, txnRepo);
txnId = txn.id;
});
return getTransaction(ds, txnId);
@ -96,7 +95,6 @@ TransactionDetail updateTransaction(
validateTransactionPayload(vendorRepo, categoryRepo, accountRepo, payload);
SysTime timestamp = SysTime.fromISOExtString(payload.timestamp, UTC());
SysTime now = Clock.currTime(UTC());
const TransactionDetail prev = transactionRepo.findById(transactionId)
.orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
@ -106,7 +104,7 @@ TransactionDetail updateTransaction(
TransactionDetail curr = transactionRepo.update(transactionId, payload);
updateLinkedAccountJournalEntries(prev, curr, payload, ds, timestamp);
tagRepo.updateTags(transactionId, payload.tags);
updateAttachments(curr.id, now, payload.attachmentIdsToRemove, files, attachmentRepo, transactionRepo);
updateAttachments(curr.id, timestamp, payload.attachmentIdsToRemove, files, attachmentRepo, transactionRepo);
});
return getTransaction(ds, transactionId);
}
@ -501,99 +499,18 @@ void deleteCategory(ProfileDataSource ds, ulong categoryId) {
// Draft services
Page!TransactionDraftListItem getDrafts(ProfileDataSource ds, in PageRequest pr, bool shouldFetchTemplates) {
if (shouldFetchTemplates) {
return ds.getTransactionDraftRepository()
.findAllTemplates(pr);
}
Page!TransactionDraftListItem getDrafts(ProfileDataSource ds, in PageRequest pr) {
return ds.getTransactionDraftRepository()
.findAllDrafts(pr);
}
Page!TransactionDraftListItem getTemplates(ProfileDataSource ds, in PageRequest pr) {
return ds.getTransactionDraftRepository()
.findAllTemplates(pr);
}
TransactionDraftResponse getDraft(ProfileDataSource ds, ulong draftId) {
return ds.getTransactionDraftRepository()
.findById(draftId)
// Populate the list of attachments for the draft.
.mapIfPresent!((draft) {
import std.algorithm : map;
import std.array : array;
draft.attachments = ds.getAttachmentRepository()
.findAllByTransactionDraftId(draft.id)
.map!(AttachmentResponse.of)
.array;
return draft;
})
.orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
}
TransactionDraftResponse addDraft(ProfileDataSource ds, in TransactionDraftPayload payload, in MultipartFile[] files) {
TransactionDraftRepository draftRepo = ds.getTransactionDraftRepository();
AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
validateDraftPayload(payload);
SysTime now = Clock.currTime(UTC());
ulong draftId;
ds.doTransaction(() {
TransactionDraftResponse draft = draftRepo.insert(payload);
draftRepo.updateTags(draft.id, payload.tags);
updateDraftAttachments(draft.id, now, payload.attachmentIdsToRemove, files, attachmentRepo, draftRepo);
draftId = draft.id;
});
return getDraft(ds, draftId);
}
TransactionDraftResponse updateDraft(
ProfileDataSource ds,
ulong draftId,
in TransactionDraftPayload payload,
in MultipartFile[] files
) {
TransactionDraftRepository draftRepo = ds.getTransactionDraftRepository();
AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
validateDraftPayload(payload);
SysTime now = Clock.currTime(UTC());
ds.doTransaction(() {
draftRepo.update(draftId, payload);
draftRepo.updateTags(draftId, payload.tags);
updateDraftAttachments(draftId, now, payload.attachmentIdsToRemove, files, attachmentRepo, draftRepo);
});
return getDraft(ds, draftId);
}
void deleteDraft(ProfileDataSource ds, ulong draftId) {
TransactionDraftRepository draftRepo = ds.getTransactionDraftRepository();
AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
TransactionDraftResponse draft = draftRepo.findById(draftId)
.orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
ds.doTransaction(() {
// First delete all attachments.
foreach (a; attachmentRepo.findAllByTransactionDraftId(draft.id)) {
attachmentRepo.remove(a.id);
}
draftRepo.deleteById(draft.id);
});
}
private void validateDraftPayload(in TransactionDraftPayload payload) {
// TODO!
}
private void updateDraftAttachments(
ulong draftId,
SysTime timestamp,
in ulong[] attachmentIdsToRemove,
in MultipartFile[] attachmentsToAdd,
AttachmentRepository attachmentRepo,
TransactionDraftRepository draftRepo
) {
foreach (file; attachmentsToAdd) {
ulong attachmentId = attachmentRepo.save(timestamp, file.name, file.contentType, file.content);
draftRepo.linkAttachment(draftId, attachmentId);
}
foreach (idToRemove; attachmentIdsToRemove) {
attachmentRepo.remove(idToRemove);
}
}

View File

@ -1,8 +0,0 @@
INSERT INTO transaction_draft_line_item (
draft_id,
idx,
value_per_item,
quantity,
description,
category_id
) VALUES (?, ?, ?, ?, ?, ?)

View File

@ -1,13 +0,0 @@
INSERT INTO transaction_draft (
added_at,
template_name,
timestamp,
amount,
currency,
description,
internal_transfer,
vendor_id,
category_id,
credited_account_id,
debited_account_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

View File

@ -1,17 +0,0 @@
SELECT
i.idx,
i.value_per_item,
i.quantity,
i.description,
i.category_id,
category.parent_id,
category.name,
category.description,
category.color
FROM transaction_draft_line_item i
LEFT JOIN transaction_category category
ON category.id = i.category_id
WHERE i.draft_id = ?
ORDER BY idx;

View File

@ -1,13 +0,0 @@
UPDATE transaction_draft
SET
template_name = ?,
timestamp = ?,
amount = ?,
currency = ?,
description = ?,
internal_transfer = ?,
vendor_id = ?,
category_id = ?,
credited_account_id = ?,
debited_account_id = ?
WHERE id = ?

View File

@ -59,13 +59,6 @@ export interface Account {
currentBalance: number | null
}
export interface SimpleAccountResponse {
id: number
name: string
type: string
numberSuffix: string
}
export interface AccountCreationPayload {
type: string
numberSuffix: string

View File

@ -1,28 +1,8 @@
import type { SimpleAccountResponse } from './account'
import type { Attachment } from './attachment'
import { ApiClient } from './base'
import type { Currency } from './data'
import { type Page, type PageRequest } from './pagination'
export interface SimpleVendorResponse {
id: number
name: string
}
export interface SimpleCategoryResponse {
id: number
name: string
color: string
}
export interface TransactionLineItemResponse {
idx: number
valuePerItem: number
quantity: number
description: string
category: TransactionCategory | null
}
export interface TransactionVendor {
id: number
name: string
@ -67,10 +47,10 @@ export interface TransactionsListItem {
currency: Currency
description: string
internalTransfer: boolean
vendor: SimpleVendorResponse | null
category: SimpleCategoryResponse | null
creditedAccount: SimpleAccountResponse | null
debitedAccount: SimpleAccountResponse | null
vendor: TransactionsListItemVendor | null
category: TransactionsListItemCategory | null
creditedAccount: TransactionsListItemAccount | null
debitedAccount: TransactionsListItemAccount | null
tags: string[]
}
@ -102,13 +82,28 @@ export interface TransactionDetail {
internalTransfer: boolean
vendor: TransactionVendor | null
category: TransactionCategory | null
creditedAccount: SimpleAccountResponse | null
debitedAccount: SimpleAccountResponse | null
creditedAccount: TransactionDetailAccount | null
debitedAccount: TransactionDetailAccount | null
tags: string[]
lineItems: TransactionLineItemResponse[]
lineItems: TransactionDetailLineItem[]
attachments: Attachment[]
}
export interface TransactionDetailAccount {
id: number
name: string
type: string
numberSuffix: string
}
export interface TransactionDetailLineItem {
idx: number
valuePerItem: number
quantity: number
description: string
category: TransactionCategory | null
}
export interface AddTransactionPayload {
timestamp: string
amount: number
@ -149,56 +144,6 @@ export interface AggregateTransactionData {
currencies: AggregateTransactionCurrencyData[]
}
export interface TransactionDraftListItem {
id: number
addedAt: string
templateName: string | null
timestamp: string | null
amount: number | null
currency: Currency | null
description: string | null
internalTransfer: boolean | null
vendor: SimpleVendorResponse | null
category: SimpleCategoryResponse | null
creditedAccount: SimpleAccountResponse | null
debitedAccount: SimpleAccountResponse | null
tags: string[]
}
export interface TransactionDraftResponse {
id: number
addedAt: string
templateName: string | null
timestamp: string | null
amount: number | null
currency: Currency | null
description: string | null
internalTransfer: boolean | null
vendor: SimpleVendorResponse | null
category: SimpleCategoryResponse | null
creditedAccount: SimpleAccountResponse | null
debitedAccount: SimpleAccountResponse | null
tags: string[]
lineItems: TransactionLineItemResponse[]
attachments: Attachment[]
}
export interface TransactionDraftPayload {
templateName: string | null
timestamp: string | null
amount: number | null
currencyCode: string | null
description: string | null
internalTransfer: boolean | null
vendorId: number | null
categoryId: number | null
creditedAccountId: number | null
debitedAccountId: number | null
tags: string[]
lineItems: AddTransactionPayloadLineItem[]
attachmentIdsToRemove: number[]
}
export class TransactionApiClient extends ApiClient {
readonly path: string
@ -332,57 +277,4 @@ export class TransactionApiClient extends ApiClient {
getAllTags(): Promise<string[]> {
return super.getJson(this.path + '/transaction-tags')
}
// Drafts:
getDrafts(
paginationOptions: PageRequest | undefined = undefined,
): Promise<Page<TransactionDraftListItem>> {
return super.getJsonPage(this.path + '/transaction-drafts', paginationOptions)
}
getTemplateDrafts(
paginationOptions: PageRequest | undefined = undefined,
): Promise<Page<TransactionDraftListItem>> {
const params = new URLSearchParams()
params.append('template', 'true')
if (paginationOptions !== undefined) {
params.append('page', paginationOptions.page + '')
params.append('size', paginationOptions.size + '')
for (const sort of paginationOptions.sorts) {
params.append('sort', sort.attribute + ',' + sort.dir)
}
}
return super.getJson(this.path + '/transaction-drafts?' + params.toString())
}
getDraft(id: number): Promise<TransactionDraftResponse> {
return super.getJson(this.path + '/transaction-drafts/' + id)
}
addDraft(data: TransactionDraftPayload, files: File[] = []): Promise<TransactionDraftResponse> {
const formData = new FormData()
formData.append('payload', JSON.stringify(data))
for (const file of files) {
formData.append('file', file)
}
return super.postFormData(this.path + '/transaction-drafts', formData)
}
updateDraft(
id: number,
data: TransactionDraftPayload,
files: File[] = [],
): Promise<TransactionDraftResponse> {
const formData = new FormData()
formData.append('payload', JSON.stringify(data))
for (const file of files) {
formData.append('file', file)
}
return super.putFormData(this.path + '/transaction-drafts/' + id, formData)
}
deleteDraft(id: number): Promise<void> {
return super.delete(this.path + '/transaction-drafts/' + id)
}
}

View File

@ -1,10 +1,10 @@
<script setup lang="ts">
import type { TransactionLineItemResponse } from '@/api/transaction'
import type { TransactionDetailLineItem } from '@/api/transaction'
import AppButton from './common/AppButton.vue'
import { formatMoney, type Currency } from '@/api/data'
defineProps<{
lineItem: TransactionLineItemResponse
lineItem: TransactionDetailLineItem
currency: Currency
totalCount?: number
editable?: boolean

View File

@ -4,7 +4,7 @@ transaction. This editor shows a table of current line items, and includes a
modal for adding a new one.
-->
<script setup lang="ts">
import { type TransactionCategoryTree, type TransactionLineItemResponse } from '@/api/transaction'
import { type TransactionCategoryTree, type TransactionDetailLineItem } from '@/api/transaction'
import AppButton from '@/components/common/AppButton.vue'
import FormGroup from '@/components/common/form/FormGroup.vue'
import { floatMoneyToInteger, formatMoney, type Currency } from '@/api/data'
@ -15,7 +15,7 @@ import CategorySelect from './CategorySelect.vue'
import LineItemCard from './LineItemCard.vue'
import AppBadge from './common/AppBadge.vue'
const model = defineModel<TransactionLineItemResponse[]>({ required: true })
const model = defineModel<TransactionDetailLineItem[]>({ required: true })
const props = defineProps<{
transactionAmount: number
currency: Currency

View File

@ -17,7 +17,7 @@ import {
TransactionApiClient,
type AddTransactionPayload,
type TransactionDetail,
type TransactionLineItemResponse,
type TransactionDetailLineItem,
type TransactionVendor,
} from '@/api/transaction'
import AppPage from '@/components/common/AppPage.vue'
@ -144,7 +144,7 @@ const vendor: Ref<TransactionVendor | null> = ref(null)
const categoryId: Ref<number | null> = ref(null)
const creditedAccountId: Ref<number | null> = ref(null)
const debitedAccountId: Ref<number | null> = ref(null)
const lineItems: Ref<TransactionLineItemResponse[]> = ref([])
const lineItems: Ref<TransactionDetailLineItem[]> = ref([])
const tags: Ref<string[]> = ref([])
const customTagInput = ref('')
const customTagInputValid = ref(false)