Compare commits
No commits in common. "115a79a5c0ab99cc26364625694fc48b3158f8e8" and "23cfe0b1a976b270fe27325285284dfd3d2e6f96" have entirely different histories.
115a79a5c0
...
23cfe0b1a9
|
|
@ -224,7 +224,8 @@ immutable DEFAULT_DRAFT_PAGE = PageRequest(1, 10, [Sort("draft.id", SortDir.DESC
|
||||||
void handleGetDrafts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
void handleGetDrafts(ref ServerHttpRequest request, ref ServerHttpResponse response) {
|
||||||
ProfileDataSource ds = getProfileDataSource(request);
|
ProfileDataSource ds = getProfileDataSource(request);
|
||||||
PageRequest pr = PageRequest.parse(request, DEFAULT_DRAFT_PAGE);
|
PageRequest pr = PageRequest.parse(request, DEFAULT_DRAFT_PAGE);
|
||||||
Page!TransactionDraftListItem page = getDrafts(ds, pr);
|
bool shouldFetchTemplates = request.getParamAs!bool("template", false);
|
||||||
|
Page!TransactionDraftListItem page = getDrafts(ds, pr, shouldFetchTemplates);
|
||||||
writeJsonBody(response, page);
|
writeJsonBody(response, page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,6 @@ interface TransactionRepository {
|
||||||
interface TransactionDraftRepository {
|
interface TransactionDraftRepository {
|
||||||
Page!TransactionDraftListItem findAllDrafts(in PageRequest pr);
|
Page!TransactionDraftListItem findAllDrafts(in PageRequest pr);
|
||||||
Page!TransactionDraftListItem findAllTemplates(in PageRequest pr);
|
Page!TransactionDraftListItem findAllTemplates(in PageRequest pr);
|
||||||
Page!TransactionDraftListItem findAll(in PageRequest pr);
|
|
||||||
Optional!TransactionDraftResponse findById(ulong id);
|
Optional!TransactionDraftResponse findById(ulong id);
|
||||||
TransactionDraftResponse insert(in TransactionDraftPayload data);
|
TransactionDraftResponse insert(in TransactionDraftPayload data);
|
||||||
void linkAttachment(ulong draftId, ulong attachmentId);
|
void linkAttachment(ulong draftId, ulong attachmentId);
|
||||||
|
|
|
||||||
|
|
@ -631,15 +631,11 @@ class SqliteTransactionDraftRepository : TransactionDraftRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
Page!TransactionDraftListItem findAllDrafts(in PageRequest pr) {
|
Page!TransactionDraftListItem findAllDrafts(in PageRequest pr) {
|
||||||
return findAllInternal(pr, DraftType.DRAFT.toOptional);
|
return findAllInternal(pr, DraftType.DRAFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Page!TransactionDraftListItem findAllTemplates(in PageRequest pr) {
|
Page!TransactionDraftListItem findAllTemplates(in PageRequest pr) {
|
||||||
return findAllInternal(pr, DraftType.TEMPLATE.toOptional);
|
return findAllInternal(pr, DraftType.TEMPLATE);
|
||||||
}
|
|
||||||
|
|
||||||
Page!TransactionDraftListItem findAll(in PageRequest pr) {
|
|
||||||
return findAllInternal(pr, Optional!DraftType.empty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static enum DraftType {
|
private static enum DraftType {
|
||||||
|
|
@ -647,16 +643,14 @@ class SqliteTransactionDraftRepository : TransactionDraftRepository {
|
||||||
TEMPLATE
|
TEMPLATE
|
||||||
}
|
}
|
||||||
|
|
||||||
private Page!TransactionDraftListItem findAllInternal(in PageRequest pr, Optional!DraftType type) {
|
private Page!TransactionDraftListItem findAllInternal(in PageRequest pr, DraftType type) {
|
||||||
QueryBuilder qb = getBuilderForDraftsList();
|
QueryBuilder qb = getBuilderForDraftsList();
|
||||||
addSelectsForDraftsList(qb);
|
addSelectsForDraftsList(qb);
|
||||||
qb.groupBy("draft.id");
|
qb.groupBy("draft.id");
|
||||||
if (type) {
|
if (type == DraftType.DRAFT) {
|
||||||
if (type.value == DraftType.DRAFT) {
|
qb.where("template_name IS NULL");
|
||||||
qb.where("template_name IS NULL");
|
} else {
|
||||||
} else {
|
qb.where("template_name IS NOT NULL");
|
||||||
qb.where("template_name IS NOT NULL");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
string query = qb.build() ~ "\n" ~ pr.toSql();
|
string query = qb.build() ~ "\n" ~ pr.toSql();
|
||||||
TransactionDraftListItem[] results = util.sqlite.findAll(db, query, &parseDraftListItem);
|
TransactionDraftListItem[] results = util.sqlite.findAll(db, query, &parseDraftListItem);
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ import account.data;
|
||||||
import util.money;
|
import util.money;
|
||||||
import util.pagination;
|
import util.pagination;
|
||||||
import util.data;
|
import util.data;
|
||||||
import util.validation.transaction;
|
|
||||||
import util.validation.draft;
|
|
||||||
import attachment.data;
|
import attachment.data;
|
||||||
import attachment.dto;
|
import attachment.dto;
|
||||||
|
|
||||||
|
|
@ -184,6 +182,76 @@ void deleteTransaction(ProfileDataSource ds, ulong transactionId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateTransactionPayload(
|
||||||
|
TransactionVendorRepository vendorRepo,
|
||||||
|
TransactionCategoryRepository categoryRepo,
|
||||||
|
AccountRepository accountRepo,
|
||||||
|
in AddTransactionPayload payload
|
||||||
|
) {
|
||||||
|
if (!payload.creditedAccountId && !payload.debitedAccountId) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "At least one account must be linked.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
payload.creditedAccountId &&
|
||||||
|
payload.debitedAccountId &&
|
||||||
|
payload.creditedAccountId.value == payload.debitedAccountId.value
|
||||||
|
) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Cannot link the same account as both credit and debit.");
|
||||||
|
}
|
||||||
|
if (payload.amount == 0) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Amount should be greater than 0.");
|
||||||
|
}
|
||||||
|
SysTime now = Clock.currTime(UTC());
|
||||||
|
SysTime timestamp;
|
||||||
|
try {
|
||||||
|
timestamp = SysTime.fromISOExtString(payload.timestamp, UTC());
|
||||||
|
} catch (TimeException e) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp format. Expected ISO-8601 datetime.");
|
||||||
|
}
|
||||||
|
if (timestamp > now) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Cannot create transaction in the future.");
|
||||||
|
}
|
||||||
|
if (payload.vendorId && !vendorRepo.existsById(payload.vendorId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Vendor doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.categoryId && !categoryRepo.existsById(payload.categoryId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Category doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.creditedAccountId && !accountRepo.existsById(payload.creditedAccountId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Credited account doesn't exist.");
|
||||||
|
}
|
||||||
|
if (payload.debitedAccountId && !accountRepo.existsById(payload.debitedAccountId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Debited account doesn't exist.");
|
||||||
|
}
|
||||||
|
foreach (tag; payload.tags) {
|
||||||
|
import std.regex;
|
||||||
|
auto r = ctRegex!(`^[a-z0-9-_]{3,32}$`);
|
||||||
|
if (!matchFirst(tag, r)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid tag: \"" ~ tag ~ "\".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.lineItems.length > 0) {
|
||||||
|
long lineItemsTotal = 0;
|
||||||
|
foreach (lineItem; payload.lineItems) {
|
||||||
|
if (lineItem.categoryId && !categoryRepo.existsById(lineItem.categoryId.value)) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's category doesn't exist.");
|
||||||
|
}
|
||||||
|
if (lineItem.quantity == 0) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's quantity should greater than zero.");
|
||||||
|
}
|
||||||
|
for (ulong i = 0; i < lineItem.quantity; i++) {
|
||||||
|
lineItemsTotal += lineItem.valuePerItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lineItemsTotal != payload.amount) {
|
||||||
|
throw new HttpStatusException(
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
"Total of all line items doesn't equal the transaction's total."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to add / remove attachments for a transaction.
|
* Helper function to add / remove attachments for a transaction.
|
||||||
*/
|
*/
|
||||||
|
|
@ -433,8 +501,13 @@ void deleteCategory(ProfileDataSource ds, ulong categoryId) {
|
||||||
|
|
||||||
// Draft services
|
// Draft services
|
||||||
|
|
||||||
Page!TransactionDraftListItem getDrafts(ProfileDataSource ds, in PageRequest pr) {
|
Page!TransactionDraftListItem getDrafts(ProfileDataSource ds, in PageRequest pr, bool shouldFetchTemplates) {
|
||||||
return ds.getTransactionDraftRepository().findAll(pr);
|
if (shouldFetchTemplates) {
|
||||||
|
return ds.getTransactionDraftRepository()
|
||||||
|
.findAllTemplates(pr);
|
||||||
|
}
|
||||||
|
return ds.getTransactionDraftRepository()
|
||||||
|
.findAllDrafts(pr);
|
||||||
}
|
}
|
||||||
|
|
||||||
TransactionDraftResponse getDraft(ProfileDataSource ds, ulong draftId) {
|
TransactionDraftResponse getDraft(ProfileDataSource ds, ulong draftId) {
|
||||||
|
|
@ -457,12 +530,7 @@ TransactionDraftResponse addDraft(ProfileDataSource ds, in TransactionDraftPaylo
|
||||||
TransactionDraftRepository draftRepo = ds.getTransactionDraftRepository();
|
TransactionDraftRepository draftRepo = ds.getTransactionDraftRepository();
|
||||||
AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
|
AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
|
||||||
|
|
||||||
validateDraftPayload(
|
validateDraftPayload(payload);
|
||||||
ds.getTransactionVendorRepository(),
|
|
||||||
ds.getTransactionCategoryRepository(),
|
|
||||||
ds.getAccountRepository(),
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
SysTime now = Clock.currTime(UTC());
|
SysTime now = Clock.currTime(UTC());
|
||||||
|
|
||||||
ulong draftId;
|
ulong draftId;
|
||||||
|
|
@ -484,12 +552,7 @@ TransactionDraftResponse updateDraft(
|
||||||
TransactionDraftRepository draftRepo = ds.getTransactionDraftRepository();
|
TransactionDraftRepository draftRepo = ds.getTransactionDraftRepository();
|
||||||
AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
|
AttachmentRepository attachmentRepo = ds.getAttachmentRepository();
|
||||||
|
|
||||||
validateDraftPayload(
|
validateDraftPayload(payload);
|
||||||
ds.getTransactionVendorRepository(),
|
|
||||||
ds.getTransactionCategoryRepository(),
|
|
||||||
ds.getAccountRepository(),
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
SysTime now = Clock.currTime(UTC());
|
SysTime now = Clock.currTime(UTC());
|
||||||
|
|
||||||
ds.doTransaction(() {
|
ds.doTransaction(() {
|
||||||
|
|
@ -514,6 +577,10 @@ void deleteDraft(ProfileDataSource ds, ulong draftId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateDraftPayload(in TransactionDraftPayload payload) {
|
||||||
|
// TODO!
|
||||||
|
}
|
||||||
|
|
||||||
private void updateDraftAttachments(
|
private void updateDraftAttachments(
|
||||||
ulong draftId,
|
ulong draftId,
|
||||||
SysTime timestamp,
|
SysTime timestamp,
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
module util.validation.common;
|
|
||||||
|
|
||||||
import handy_http_primitives.optional;
|
|
||||||
import std.datetime;
|
|
||||||
|
|
||||||
struct ValidationError {
|
|
||||||
string field;
|
|
||||||
string message;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidationRule(T) {
|
|
||||||
Optional!ValidationError validate(in T payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
ValidationError[] applyValidationRules(T)(ValidationRule!(T)[] rules, in T payload) {
|
|
||||||
import std.array;
|
|
||||||
auto app = appender!(ValidationError[]);
|
|
||||||
foreach (rule; rules) {
|
|
||||||
auto result = rule.validate(payload);
|
|
||||||
if (result) app ~= result.value;
|
|
||||||
}
|
|
||||||
return app[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions:
|
|
||||||
|
|
||||||
void validateTags(in string[] tags) {
|
|
||||||
import std.regex;
|
|
||||||
import handy_http_primitives: HttpStatus, HttpStatusException;
|
|
||||||
foreach (tag; tags) {
|
|
||||||
import std.regex;
|
|
||||||
auto r = ctRegex!(`^[a-z0-9-_]{3,32}$`);
|
|
||||||
if (!matchFirst(tag, r)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid tag: \"" ~ tag ~ "\".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SysTime validateTimestampFormat(string timestampStr) {
|
|
||||||
import handy_http_primitives: HttpStatus, HttpStatusException;
|
|
||||||
try {
|
|
||||||
return SysTime.fromISOExtString(timestampStr, UTC());
|
|
||||||
} catch (TimeException e) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid timestamp format. Expected ISO-8601 datetime.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
module util.validation.draft;
|
|
||||||
|
|
||||||
import handy_http_primitives;
|
|
||||||
import std.datetime;
|
|
||||||
|
|
||||||
import transaction.data;
|
|
||||||
import transaction.dto;
|
|
||||||
import util.validation.common;
|
|
||||||
import account.data;
|
|
||||||
|
|
||||||
void validateDraftPayload(
|
|
||||||
TransactionVendorRepository vendorRepo,
|
|
||||||
TransactionCategoryRepository categoryRepo,
|
|
||||||
AccountRepository accountRepo,
|
|
||||||
in TransactionDraftPayload payload
|
|
||||||
) {
|
|
||||||
if (payload.amount && !payload.currencyCode) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Currency is required when saving an amount.");
|
|
||||||
}
|
|
||||||
if (payload.amount && payload.amount.value == 0) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Amount should be greater than 0.");
|
|
||||||
}
|
|
||||||
if (payload.timestamp) {
|
|
||||||
validateTimestampFormat(payload.timestamp.value);
|
|
||||||
}
|
|
||||||
if (payload.vendorId && !vendorRepo.existsById(payload.vendorId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Vendor doesn't exist.");
|
|
||||||
}
|
|
||||||
if (payload.categoryId && !categoryRepo.existsById(payload.categoryId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Category doesn't exist.");
|
|
||||||
}
|
|
||||||
if (payload.creditedAccountId && !accountRepo.existsById(payload.creditedAccountId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Credited account doesn't exist.");
|
|
||||||
}
|
|
||||||
if (payload.debitedAccountId && !accountRepo.existsById(payload.debitedAccountId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Debited account doesn't exist.");
|
|
||||||
}
|
|
||||||
validateTags(payload.tags);
|
|
||||||
if (payload.lineItems.length > 0) {
|
|
||||||
long lineItemsTotal = 0;
|
|
||||||
foreach (lineItem; payload.lineItems) {
|
|
||||||
if (lineItem.categoryId && !categoryRepo.existsById(lineItem.categoryId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's category doesn't exist.");
|
|
||||||
}
|
|
||||||
if (lineItem.quantity == 0) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's quantity should greater than zero.");
|
|
||||||
}
|
|
||||||
for (ulong i = 0; i < lineItem.quantity; i++) {
|
|
||||||
lineItemsTotal += lineItem.valuePerItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (payload.amount && lineItemsTotal != payload.amount.value) {
|
|
||||||
throw new HttpStatusException(
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
"Total of all line items doesn't equal the transaction's total."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
module util.validation.transaction;
|
|
||||||
|
|
||||||
import handy_http_primitives;
|
|
||||||
import std.array;
|
|
||||||
import std.datetime;
|
|
||||||
|
|
||||||
import util.validation.common;
|
|
||||||
import transaction.dto;
|
|
||||||
import transaction.data;
|
|
||||||
import account.data;
|
|
||||||
|
|
||||||
// class AtLeastOneLinkedAccountRule : ValidationRule!AddTransactionPayload {
|
|
||||||
// Optional!ValidationError validate(in AddTransactionPayload payload) {
|
|
||||||
// if (!payload.creditedAccountId && !payload.debitedAccountId) {
|
|
||||||
// return ValidationError("creditedAccountId", "At least one account must be linked.").toOptional();
|
|
||||||
// }
|
|
||||||
// return Optional!ValidationError.empty();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
void validateTransactionPayload(
|
|
||||||
TransactionVendorRepository vendorRepo,
|
|
||||||
TransactionCategoryRepository categoryRepo,
|
|
||||||
AccountRepository accountRepo,
|
|
||||||
in AddTransactionPayload payload
|
|
||||||
) {
|
|
||||||
if (!payload.creditedAccountId && !payload.debitedAccountId) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "At least one account must be linked.");
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
payload.creditedAccountId &&
|
|
||||||
payload.debitedAccountId &&
|
|
||||||
payload.creditedAccountId.value == payload.debitedAccountId.value
|
|
||||||
) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Cannot link the same account as both credit and debit.");
|
|
||||||
}
|
|
||||||
if (payload.amount == 0) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Amount should be greater than 0.");
|
|
||||||
}
|
|
||||||
SysTime now = Clock.currTime(UTC());
|
|
||||||
SysTime timestamp = validateTimestampFormat(payload.timestamp);
|
|
||||||
if (timestamp > now) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Cannot create transaction in the future.");
|
|
||||||
}
|
|
||||||
if (payload.vendorId && !vendorRepo.existsById(payload.vendorId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Vendor doesn't exist.");
|
|
||||||
}
|
|
||||||
if (payload.categoryId && !categoryRepo.existsById(payload.categoryId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Category doesn't exist.");
|
|
||||||
}
|
|
||||||
if (payload.creditedAccountId && !accountRepo.existsById(payload.creditedAccountId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Credited account doesn't exist.");
|
|
||||||
}
|
|
||||||
if (payload.debitedAccountId && !accountRepo.existsById(payload.debitedAccountId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Debited account doesn't exist.");
|
|
||||||
}
|
|
||||||
validateTags(payload.tags);
|
|
||||||
if (payload.lineItems.length > 0) {
|
|
||||||
long lineItemsTotal = 0;
|
|
||||||
foreach (lineItem; payload.lineItems) {
|
|
||||||
if (lineItem.categoryId && !categoryRepo.existsById(lineItem.categoryId.value)) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's category doesn't exist.");
|
|
||||||
}
|
|
||||||
if (lineItem.quantity == 0) {
|
|
||||||
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Line item's quantity should greater than zero.");
|
|
||||||
}
|
|
||||||
for (ulong i = 0; i < lineItem.quantity; i++) {
|
|
||||||
lineItemsTotal += lineItem.valuePerItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (lineItemsTotal != payload.amount) {
|
|
||||||
throw new HttpStatusException(
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
"Total of all line items doesn't equal the transaction's total."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -26,15 +26,7 @@ function goToDraft() {
|
||||||
<!-- Top row contains timestamp and amount. -->
|
<!-- Top row contains timestamp and amount. -->
|
||||||
<div style="display: flex; justify-content: space-between">
|
<div style="display: flex; justify-content: space-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-mono font-size-xsmall text-normal">
|
<div class="font-mono font-size-xsmall text-normal">Draft #{{ draft.id }}</div>
|
||||||
Draft #{{ draft.id }}
|
|
||||||
<AppBadge
|
|
||||||
v-if="draft.templateName"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Template: {{ draft.templateName }}
|
|
||||||
</AppBadge>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="text-muted font-mono font-size-xsmall"
|
class="text-muted font-mono font-size-xsmall"
|
||||||
v-if="draft.timestamp"
|
v-if="draft.timestamp"
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ async function onVendorClicked() {
|
||||||
<AppButton
|
<AppButton
|
||||||
icon="wrench"
|
icon="wrench"
|
||||||
@click="
|
@click="
|
||||||
router.push(`/profiles/${getSelectedProfile(route)}/transaction-drafts/${draft?.id}/edit`)
|
router.push(`/profiles/${getSelectedProfile(route)}/transaction-drafts/${draft.id}/edit`)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,6 @@ onMounted(async () => {
|
||||||
v-model="formData.templateName"
|
v-model="formData.templateName"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
style="max-width: 200px"
|
style="max-width: 200px"
|
||||||
maxlength="32"
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,10 @@ export interface TransactionEditorContextBase {
|
||||||
export class NewTransactionEditorContext implements TransactionEditorContextBase {
|
export class NewTransactionEditorContext implements TransactionEditorContextBase {
|
||||||
isFormDataValid(formData: TransactionEditorFormFields): boolean {
|
isFormDataValid(formData: TransactionEditorFormFields): boolean {
|
||||||
return (
|
return (
|
||||||
isFormDataValidForDraftSave(formData) || isFormDataValidForTransactionSubmission(formData)
|
formData.amount !== null &&
|
||||||
|
formData.amount > 0 &&
|
||||||
|
formData.timestamp !== null &&
|
||||||
|
formData.currency !== null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,9 +126,7 @@ export class NewTransactionEditorContext implements TransactionEditorContextBase
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'Save',
|
name: 'Save',
|
||||||
disabled: !(
|
disabled: !(this.areChangesPresent(formData) && this.isFormDataValid(formData)),
|
||||||
this.areChangesPresent(formData) && isFormDataValidForTransactionSubmission(formData)
|
|
||||||
),
|
|
||||||
callback: async (formData, route, router) => {
|
callback: async (formData, route, router) => {
|
||||||
const api = new TransactionApiClient(getSelectedProfile(route))
|
const api = new TransactionApiClient(getSelectedProfile(route))
|
||||||
// Assume that form data is valid!
|
// Assume that form data is valid!
|
||||||
|
|
@ -136,7 +137,7 @@ export class NewTransactionEditorContext implements TransactionEditorContextBase
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Save Draft',
|
name: 'Save Draft',
|
||||||
disabled: !this.areChangesPresent(formData) || !isFormDataValidForDraftSave(formData),
|
disabled: !this.areChangesPresent(formData),
|
||||||
callback: async (formData, route, router) => {
|
callback: async (formData, route, router) => {
|
||||||
const api = new TransactionApiClient(getSelectedProfile(route))
|
const api = new TransactionApiClient(getSelectedProfile(route))
|
||||||
const data = toDraftPayload(formData)
|
const data = toDraftPayload(formData)
|
||||||
|
|
@ -168,7 +169,17 @@ export class TransactionEditorContext implements TransactionEditorContextBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
isFormDataValid(formData: TransactionEditorFormFields): boolean {
|
isFormDataValid(formData: TransactionEditorFormFields): boolean {
|
||||||
return isFormDataValidForTransactionSubmission(formData)
|
return (
|
||||||
|
formData.timestamp !== null &&
|
||||||
|
formData.timestamp.length > 0 &&
|
||||||
|
formData.amount !== null &&
|
||||||
|
formData.amount > 0 &&
|
||||||
|
formData.currency !== null &&
|
||||||
|
formData.description !== null &&
|
||||||
|
formData.description.length > 0 &&
|
||||||
|
(formData.creditedAccountId !== null || formData.debitedAccountId !== null) &&
|
||||||
|
formData.creditedAccountId !== formData.debitedAccountId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
areChangesPresent(formData: TransactionEditorFormFields): boolean {
|
areChangesPresent(formData: TransactionEditorFormFields): boolean {
|
||||||
|
|
@ -267,16 +278,9 @@ export class DraftEditorContext implements TransactionEditorContextBase {
|
||||||
this.existingDraft = existingDraft
|
this.existingDraft = existingDraft
|
||||||
}
|
}
|
||||||
|
|
||||||
isFormDataValid(formData: TransactionEditorFormFields): boolean {
|
isFormDataValid(): boolean {
|
||||||
const result =
|
// TODO: What validation is needed client-side for draft data?
|
||||||
(formData.amount === null || formData.currency !== null) &&
|
return true
|
||||||
(formData.amount === null || formData.amount > 0) &&
|
|
||||||
(formData.creditedAccountId === null ||
|
|
||||||
formData.debitedAccountId === null ||
|
|
||||||
formData.creditedAccountId !== formData.debitedAccountId) &&
|
|
||||||
(formData.templateName === null || formData.templateName.length <= 32)
|
|
||||||
console.log('Checking draft editor valid:', formData, result)
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
areChangesPresent(): boolean {
|
areChangesPresent(): boolean {
|
||||||
|
|
@ -321,11 +325,11 @@ export class DraftEditorContext implements TransactionEditorContextBase {
|
||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
getActions(formData: TransactionEditorFormFields): TransactionEditorAction[] {
|
getActions(): TransactionEditorAction[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'Save Draft',
|
name: 'Save',
|
||||||
disabled: !this.areChangesPresent() || !this.isFormDataValid(formData),
|
disabled: !this.areChangesPresent() || !this.isFormDataValid(),
|
||||||
callback: async (formData, route, router) => {
|
callback: async (formData, route, router) => {
|
||||||
const api = new TransactionApiClient(getSelectedProfile(route))
|
const api = new TransactionApiClient(getSelectedProfile(route))
|
||||||
const data = toDraftPayload(formData)
|
const data = toDraftPayload(formData)
|
||||||
|
|
@ -339,21 +343,6 @@ export class DraftEditorContext implements TransactionEditorContextBase {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Submit Transaction',
|
|
||||||
disabled: !this.areChangesPresent() || !isFormDataValidForTransactionSubmission(formData),
|
|
||||||
callback: async (formData, route, router) => {
|
|
||||||
const api = new TransactionApiClient(getSelectedProfile(route))
|
|
||||||
// First call the normal "Save" callback from the NewTransactionEditorContext.
|
|
||||||
const tec = new NewTransactionEditorContext()
|
|
||||||
const saveTxnAction = tec.getActions(formData).find((a) => a.name === 'Save')!
|
|
||||||
await saveTxnAction.callback(formData, route, router)
|
|
||||||
// Then if this is not a template draft, delete it.
|
|
||||||
if (formData.templateName === null || formData.templateName.length === 0) {
|
|
||||||
await api.deleteDraft(this.existingDraft.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Cancel',
|
name: 'Cancel',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
@ -365,8 +354,6 @@ export class DraftEditorContext implements TransactionEditorContextBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions below here!
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains an editor context by determining what the user is doing based on the
|
* Obtains an editor context by determining what the user is doing based on the
|
||||||
* route they've navigated to.
|
* route they've navigated to.
|
||||||
|
|
@ -468,26 +455,3 @@ async function goBackOrHome(router: Router, route: RouteLocation) {
|
||||||
await router.replace(`/profiles/${getSelectedProfile(route)}`)
|
await router.replace(`/profiles/${getSelectedProfile(route)}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFormDataValidForTransactionSubmission(formData: TransactionEditorFormFields): boolean {
|
|
||||||
return (
|
|
||||||
formData.timestamp !== null &&
|
|
||||||
formData.timestamp.length > 0 &&
|
|
||||||
formData.amount !== null &&
|
|
||||||
formData.amount > 0 &&
|
|
||||||
formData.currency !== null &&
|
|
||||||
formData.description !== null &&
|
|
||||||
formData.description.length > 0 &&
|
|
||||||
(formData.creditedAccountId !== null || formData.debitedAccountId !== null) &&
|
|
||||||
formData.creditedAccountId !== formData.debitedAccountId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFormDataValidForDraftSave(formData: TransactionEditorFormFields): boolean {
|
|
||||||
return (
|
|
||||||
formData.amount !== null &&
|
|
||||||
formData.amount > 0 &&
|
|
||||||
formData.timestamp !== null &&
|
|
||||||
formData.currency !== null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue