Skip to content

Commit

Permalink
Merge pull request #74 from ya-erm/dev
Browse files Browse the repository at this point in the history
Additional options for operation - duplicate
  • Loading branch information
ya-erm authored Oct 27, 2024
2 parents 4dddad8 + be35077 commit 630dad4
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 19 deletions.
1 change: 1 addition & 0 deletions src/lib/data/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export type CurrencyRate = {
};

// TODO: rename to Operation
// after adding new field don't forget to change function copyOperation
export type Transaction = {
id: string;
accountId: string;
Expand Down
51 changes: 51 additions & 0 deletions src/lib/data/operations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dayjs from 'dayjs';
import { derived, get, type Readable } from 'svelte/store';
import { v4 as uuid } from 'uuid';

import { translate } from '$lib/translate';
import { showErrorToast } from '$lib/ui/toasts';
Expand Down Expand Up @@ -147,3 +148,53 @@ export const operationsService = new OperationsService();

export const operationsStore = operationsService.$operations;
export const operationsCommentsStore = operationsService.$comments;

/** Delete operation and linked operation */
export function deleteOperation(id: string) {
const item = operationsService.getById(id);
if (item) {
operationsService.delete(item);
if (item.linkedTransactionId) {
const linkedOperation = operationsService.getById(item.linkedTransactionId);
if (linkedOperation) {
operationsService.delete(linkedOperation);
}
}
}
}

/** Create a copy of operation (note: don't forget to change id if needed) */
export function cloneOperation(item: Transaction): Transaction {
return {
id: item.id,
accountId: item.accountId,
categoryId: item.categoryId,
date: item.date,
timeZone: item.timeZone,
amount: item.amount,
comment: item.comment,
description: item.description,
linkedTransactionId: item.linkedTransactionId,
anotherCurrency: item.anotherCurrency,
anotherCurrencyAmount: item.anotherCurrencyAmount,
excludeFromAnalysis: item.excludeFromAnalysis,
tagIds: item.tagIds ? [...item.tagIds] : undefined,
};
}

/** Create a new copy of operation (also copy linked operation if it exists) */
export function copyOperation(operation: Transaction) {
const copiedOperation = cloneOperation(operation);
copiedOperation.id = uuid();
if (operation.linkedTransactionId) {
const linkedOperation = operationsService.getById(operation.linkedTransactionId);
if (linkedOperation) {
const copiedLinkedOperation = cloneOperation(linkedOperation);
copiedLinkedOperation.id = uuid();
copiedLinkedOperation.linkedTransactionId = copiedOperation.id;
copiedOperation.linkedTransactionId = copiedLinkedOperation.id;
operationsService.save(copiedLinkedOperation);
}
}
operationsService.save(copiedOperation);
}
2 changes: 2 additions & 0 deletions src/lib/translate/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const enDict: Dictionary = {
'common.hide': 'Hide',
'common.data_problems': 'Data storage initialization finished with {count, plural, =1 {# error} other {# errors}}',
'common.select_all': 'Select all',
'common.additional_options': 'Additional options',
'common.duplicate': 'Duplicate',
// Timezones
'timezones.select_time_zone': 'Select time zone',
'timezones.current_time_zone': 'Current time zone',
Expand Down
2 changes: 2 additions & 0 deletions src/lib/translate/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export type Messages =
| 'common.hide'
| 'common.data_problems'
| 'common.select_all'
| 'common.additional_options'
| 'common.duplicate'
// Timezones
| 'timezones.select_time_zone'
| 'timezones.current_time_zone'
Expand Down
2 changes: 2 additions & 0 deletions src/lib/translate/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const ruDict: Dictionary = {
'common.data_problems':
'{count, plural, one {Обнаружена # ошибка} few {Обнаружено # ошибки} other {Обнаружено # ошибок}} при инициализации данных',
'common.select_all': 'Выбрать все',
'common.additional_options': 'Дополнительные опции',
'common.duplicate': 'Дублировать',
// Timezones
'timezones.select_time_zone': 'Выберите часовой пояс',
'timezones.current_time_zone': 'Текущий часовой пояс',
Expand Down
57 changes: 48 additions & 9 deletions src/lib/ui/MultiSwitch.svelte
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
<script lang="ts">
export let options: { id: string; title: string }[];
export let selected: { id: string; title: string } | undefined;
import Icon from './Icon.svelte';
type Option = { id: string; icon?: string; title: string };
export let options: Option[];
export let selected: Option | undefined;
export let disabled: boolean = false;
export let sameSize: boolean = false;
export let testId: string | undefined = 'MultiSwitch';
export let ariaLabel: string | null = null;
export let onChange: (value: { id: string; title: string }) => void;
export let onChange: ((value: Option) => void) | null = null;
const handleClick = (value: { id: string; title: string }) => async () => {
const handleClick = (value: Option) => async () => {
if (disabled) return;
selected = value;
onChange(value);
onChange?.(value);
};
</script>

<div class="multi-switch" data-testId={testId}>
<div class="multi-switch" class:same-size={sameSize} data-testId={testId} role="group" aria-label={ariaLabel}>
{#each options as option (option.id)}
<button
on:click={handleClick(option)}
class:with-icon={Boolean(option.icon)}
class:active={selected?.id === option.id}
data-testId={`${testId}.Button.${option.id}`}
class="switch-item text-ellipsis"
type="button"
{disabled}
>
{#if option.icon}
<Icon name={option.icon} size={1.5} />
{/if}
{option.title}
</button>
{/each}
Expand All @@ -35,17 +45,43 @@
border-radius: 1rem;
max-width: 100%;
}
.switch-item {
border: 0;
.multi-switch.same-size {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(64px, 1fr));
}
button {
min-width: 0;
font-size: 1rem;
font-weight: normal;
display: inline-flex;
cursor: pointer;
outline: none;
border: none;
color: inherit;
background: none;
padding: 0;
margin: 0;
outline: 0;
overflow: hidden;
}
button:focus-visible {
outline: 2px solid var(--active-color);
z-index: 1;
}
.switch-item {
padding: 0.5rem 1rem;
font-size: 1rem;
cursor: pointer;
color: var(--primary-text-color);
background: var(--header-background-color);
border: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
}
.switch-item:hover {
opacity: 0.9;
}
Expand All @@ -63,6 +99,9 @@
border-bottom-right-radius: 1rem;
border-top-right-radius: 1rem;
}
.switch-item.with-icon {
padding: 0.5rem;
}
.active {
color: var(--white-color);
background-color: var(--active-color);
Expand Down
5 changes: 4 additions & 1 deletion src/lib/ui/SpoilerToggle.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Icon from '$lib/ui/Icon.svelte';
export let hidden: boolean;
export let withoutText: boolean = false;
</script>

<button
Expand All @@ -14,7 +15,9 @@
<slot />
</h3>
<div class="flex items-center">
<span>{$translate(hidden ? 'common.show' : 'common.hide')}</span>
{#if !withoutText}
<span>{$translate(hidden ? 'common.show' : 'common.hide')}</span>
{/if}
<div class="spoiler-toggle-icon" class:shown={!hidden}>
<Icon padding={0} name={'mdi:chevron-down'} />
</div>
Expand Down
12 changes: 12 additions & 0 deletions src/routes/accounts/GroupedOperationsList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import TransactionListItem from '../transactions/TransactionListItem.svelte';
import EditTransaction from '../transactions/edit/EditTransaction.svelte';
import OperationOptionsModal from './OperationOptionsModal.svelte';
$: currencyRates = $currencyRatesStore;
$: settings = $memberSettingsStore;
Expand All @@ -32,6 +33,9 @@
$: operationId = getSearchParam($page, 'operation-id');
const openOperationForm = (id: string) => setSearchParam($page, 'operation-id', id, { replace: false });
const closeOperationForm = () => history.back();
let optionsModalOpened = false;
let optionsModalOperation: TransactionViewModel | null = null;
</script>

<ul class="operations-list flex-col gap-1">
Expand All @@ -42,6 +46,10 @@
hideAccount={!!account}
currencyRate={currencyRate ?? findCurrencyRate(currencyRates, settings?.currency, transaction.account.currency)}
onClick={() => openOperationForm(transaction.id)}
onLongPress={() => {
optionsModalOpened = true;
optionsModalOperation = transaction;
}}
{transaction}
/>
{/each}
Expand All @@ -63,6 +71,10 @@
</Layout>
</Portal>

{#if optionsModalOperation}
<OperationOptionsModal bind:opened={optionsModalOpened} operation={optionsModalOperation} />
{/if}

<style>
.operations-list {
list-style: none;
Expand Down
43 changes: 43 additions & 0 deletions src/routes/accounts/OperationOptionsModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang="ts">
import type { TransactionViewModel } from '$lib/data/interfaces';
import { copyOperation, deleteOperation } from '$lib/data/operations';
import { translate } from '$lib/translate';
import Button from '$lib/ui/Button.svelte';
import Icon from '$lib/ui/Icon.svelte';
import Modal from '$lib/ui/Modal.svelte';
import { showSuccessToast } from '$lib/ui/toasts';
export let opened: boolean;
export let operation: TransactionViewModel;
const onClose = () => (opened = false);
const handleDuplicate = () => {
copyOperation(operation);
onClose();
};
const handleDelete = () => {
deleteOperation(operation.id);
showSuccessToast($translate('transactions.delete_transaction_success'), {
testId: 'DeleteTransactionSuccessToast',
});
onClose();
};
</script>

<Modal bind:opened header={$translate('common.additional_options')}>
<div class="flex-col gap-1">
<Button appearance="transparent" bordered on:click={handleDuplicate}>
<Icon name="mdi:content-copy" />
<span>{$translate('common.duplicate')}</span>
</Button>
<Button color="danger" appearance="transparent" bordered on:click={handleDelete}>
<Icon name="mdi:delete-outline" />
<span>{$translate('common.delete')}</span>
</Button>
<Button color="white" bordered on:click={onClose}>
<span>{$translate('common.cancel')}</span>
</Button>
</div>
</Modal>
3 changes: 3 additions & 0 deletions src/routes/transactions/TransactionListItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
import type { CurrencyRate, TransactionViewModel } from '$lib/data/interfaces';
import { translate, type Messages } from '$lib/translate';
import Icon from '$lib/ui/Icon.svelte';
import { longPress } from '$lib/utils';
import { replaceCalcExpressions } from '$lib/utils/calc';
import { formatMoney } from '$lib/utils/formatMoney';
export let transaction: TransactionViewModel;
export let currencyRate: CurrencyRate | null = null;
export let hideAccount: boolean = false;
export let onClick: ((transaction: TransactionViewModel) => void) | null = null;
export let onLongPress: ((transaction: TransactionViewModel) => void) | null = null;
$: incoming = transaction.category.type === 'IN';
$: outgoing = transaction.category.type === 'OUT';
Expand Down Expand Up @@ -45,6 +47,7 @@
data-testId="TransactionListItem"
data-id={transaction.id}
on:click={() => onClick?.(transaction)}
use:longPress={() => onLongPress?.(transaction)}
class="flex gap-0.5 items-center justify-between"
>
<div class="icon flex-center" class:deleted={transaction.category.deleted}>
Expand Down
4 changes: 2 additions & 2 deletions src/routes/transactions/edit/EditTransaction.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { accountsStore, categoriesStore, operationsService, operationsStore, operationTagsStore } from '$lib/data';
import type { Transaction } from '$lib/data/interfaces';
import { deleteOperation } from '$lib/data/operations';
import { translate } from '$lib/translate';
import Button from '$lib/ui/Button.svelte';
import { showSuccessToast } from '$lib/ui/toasts';
Expand All @@ -24,8 +25,7 @@
const handleDelete = () => {
if (!transaction) return;
const t = operationsService.getById(transaction.id);
if (t) operationsService.delete(t);
deleteOperation(transaction.id);
showSuccessToast($translate('transactions.delete_transaction_success'), {
testId: 'DeleteTransactionSuccessToast',
});
Expand Down
23 changes: 16 additions & 7 deletions src/routes/uikit/Switches.svelte
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
<script lang="ts">
import { type ComponentProps } from 'svelte';
import MultiSwitch from '$lib/ui/MultiSwitch.svelte';
import Checkbox from '$lib/ui/Checkbox.svelte';
let sameSize = false;
const options = [
const options: ComponentProps<MultiSwitch>['options'] = [
{ id: '1', title: 'First' },
{ id: '2', title: 'Second' },
{ id: '3', title: 'Third' },
];
let selected = options[0];
const optionsWithIcons: ComponentProps<MultiSwitch>['options'] = [
{ id: '1', icon: 'mdi:number-one-circle-outline', title: 'First' },
{ id: '2', icon: 'mdi:number-two-circle-outline', title: 'Second' },
{ id: '3', icon: 'mdi:number-three-circle-outline', title: 'Third' },
];
</script>

<h2>MultiSwitch</h2>
<div class="flex-col gap-1 items-start">
<div>
<MultiSwitch options={options.slice(0, 2)} {selected} onChange={(item) => (selected = item)} />
</div>
<div>
<MultiSwitch {options} {selected} onChange={(item) => (selected = item)} />
</div>
<Checkbox bind:checked={sameSize} label="same size" />
<MultiSwitch bind:selected options={options.slice(0, 2)} {sameSize} />
<MultiSwitch bind:selected {options} {sameSize} />
<MultiSwitch bind:selected options={optionsWithIcons} {sameSize} />
</div>

0 comments on commit 630dad4

Please sign in to comment.