Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dcf50fb8af | |||
| cc4d56ae4c | |||
| 697a08e481 | |||
| 16078cc62b | |||
| 3a35d2181d | |||
| 6d2b5926bb | |||
| cc3bdccd9a | |||
| 3348b9f668 | |||
| d0c198118d | |||
| 2ab3f01d89 | |||
| 3981475c55 | |||
| acde42b406 | |||
| 7ca38aee70 | |||
| dd031551ca | |||
| f3cb4798d5 | |||
| 9cd0d5fb5e | |||
| 5ac369a643 | |||
| b2c97c1327 | |||
| 4ae044dace | |||
| c444f51423 | |||
| a201a95756 | |||
| b32169f433 | |||
| 023693c4c8 | |||
| 47bd728530 | |||
| 7e0ea10d68 | |||
| cd1965acc4 | |||
| 653f7dbeeb | |||
| 5506125b2e | |||
| e6ee697508 | |||
| bef14bcdcc | |||
| 8d0e5ffc1a | |||
| 680e8d8b95 | |||
| 13ba8556a4 | |||
| 46a2538bef | |||
| 57b9ec17de | |||
| af3b26a273 | |||
| 5da9003b14 | |||
| ba548583ba | |||
| 5c12a8af43 | |||
| 05210fea4b | |||
| f28201e711 | |||
| c23ec0cb53 | |||
| 42c7e59d63 | |||
| 192cf7d9cb |
+691
-169
File diff suppressed because it is too large
Load Diff
Generated
+366
-348
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -14,11 +14,12 @@
|
||||
"@vueuse/components": "^14.3.0",
|
||||
"@vueuse/core": "^14.3.0",
|
||||
"@vueuse/integrations": "^14.3.0",
|
||||
"@vueuse/router": "^14.3.0",
|
||||
"axios": "^1.4.0",
|
||||
"axios-rate-limit": "^1.3.1",
|
||||
"gemory": "file:",
|
||||
"loglevel": "^1.8.1",
|
||||
"loglevel-plugin-prefix": "^0.8.4",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"pinia": "^3.0.4",
|
||||
"sortablejs": "^1.15.7",
|
||||
"vue": "^3.3.4",
|
||||
|
||||
+7
-5
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RouterView, useRoute } from 'vue-router';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { routeNames } from '@/routes';
|
||||
import {computed} from 'vue';
|
||||
import {RouterView, useRoute} from 'vue-router';
|
||||
import {Sidebar} from './sidebar';
|
||||
import {ConfirmModal} from '@/confirm';
|
||||
import {routeNames} from '@/routes';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
@@ -21,10 +22,11 @@ const hideSidebar = computed(() => {
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
<ConfirmModal />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
|
||||
div.main-container {
|
||||
@apply px-4 sm:ml-64;
|
||||
|
||||
+37
-10
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/vue/24/outline';
|
||||
import {vOnClickOutside} from '@vueuse/components';
|
||||
import {useEventListener} from '@vueuse/core';
|
||||
import {ref} from 'vue';
|
||||
import {useElementBounding, useEventListener} from '@vueuse/core';
|
||||
import {computed, ref} from 'vue';
|
||||
|
||||
interface Props {
|
||||
inline?: boolean;
|
||||
@@ -15,6 +15,16 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
})
|
||||
|
||||
const isOpen = ref(false);
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
const floating = ref<HTMLElement | null>(null);
|
||||
|
||||
const { left, bottom, width } = useElementBounding(root);
|
||||
|
||||
const floatingStyle = computed(() => ({
|
||||
left: `${left.value}px`,
|
||||
top: `${bottom.value}px`,
|
||||
minWidth: `${width.value}px`,
|
||||
}));
|
||||
|
||||
const doAutoClose = () => {
|
||||
if (props.autoClose) {
|
||||
@@ -30,8 +40,8 @@ useEventListener('keyup', e => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown" :class="{'dropdown-open': isOpen, 'dropdown-close': !isOpen}" v-on-click-outside="doAutoClose">
|
||||
<button @click="isOpen = !isOpen">
|
||||
<div ref="root" class="dropdown" :class="{'dropdown-open': isOpen, 'dropdown-close': !isOpen}" v-on-click-outside="[doAutoClose, { ignore: [floating] }]">
|
||||
<button @click="isOpen = !isOpen" class="cursor-pointer">
|
||||
<Transition
|
||||
enter-active-class="transition-transform"
|
||||
enter-from-class="rotate-180"
|
||||
@@ -51,19 +61,36 @@ useEventListener('keyup', e => {
|
||||
<div v-if="inline && isOpen">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-else-if="isOpen" class="relative">
|
||||
<div class="z-10 divide-y rounded-b-md absolute">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition-opacity"
|
||||
enter-from-class="opacity-0"
|
||||
leave-from-class="transition-opacity"
|
||||
leave-to-class="opacity-0">
|
||||
<div v-if="!inline && isOpen" ref="floating" class="dropdown-floating" :style="floatingStyle">
|
||||
<div class="divide-y rounded-b-md">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
|
||||
.chevron {
|
||||
@apply w-4 h-4 me-1;
|
||||
}
|
||||
|
||||
.dropdown-floating {
|
||||
@apply fixed z-10;
|
||||
}
|
||||
|
||||
.dropdown-floating > div {
|
||||
@apply bg-slate-800 rounded-b-md shadow-lg;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { watch } from 'vue';
|
||||
import {vOnClickOutside} from '@vueuse/components';
|
||||
import {useEventListener} from '@vueuse/core';
|
||||
import {watch} from 'vue';
|
||||
|
||||
const open = defineModel('open', { default: false });
|
||||
|
||||
@@ -35,7 +35,7 @@ useEventListener('keyup', e => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ const submit = () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
.fake-input {
|
||||
@apply flex border bg-slate-500 rounded px-1 py-0.5;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ const modelValue = defineModel({ default: false });
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
input:checked ~ span:last-child {
|
||||
--tw-translate-x: 1.25rem;
|
||||
}
|
||||
@reference "@/style.css";
|
||||
|
||||
input:checked ~ span:last-child {
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { HeaderComponent, SortDirection } from './sort';
|
||||
import {HeaderComponent, SortDirection} from './sort';
|
||||
|
||||
interface Props {
|
||||
currentSortKey: string | null;
|
||||
@@ -33,7 +33,7 @@ const emit = defineEmits<Emit>();
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
.sort-header {
|
||||
@apply relative h-8 pe-3;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementBounding, useVirtualList } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
import {useElementBounding, useVirtualList} from '@vueuse/core';
|
||||
import {computed, ref} from 'vue';
|
||||
|
||||
interface Props {
|
||||
list?: any[];
|
||||
@@ -68,29 +68,31 @@ const itemHeightStyle = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
|
||||
div.table-container {
|
||||
@apply bg-slate-600;
|
||||
max-height: calc(100vh - v-bind(ypx));
|
||||
:deep(>div) {
|
||||
@apply bg-slate-800;
|
||||
>table {
|
||||
>thead {
|
||||
@apply sticky z-10;
|
||||
top: -1px;
|
||||
}
|
||||
>tfoot {
|
||||
@apply bg-slate-600 sticky z-10;
|
||||
bottom: -1px;
|
||||
}
|
||||
>*>tr, >*>tr>td {
|
||||
height: v-bind(itemHeightStyle);
|
||||
}
|
||||
@apply bg-slate-600;
|
||||
max-height: calc(100vh - v-bind(ypx));
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
margin-top: v-bind(computedHeaderHeight);
|
||||
margin-bottom: v-bind(computedFooterHeight);
|
||||
}
|
||||
}
|
||||
div.table-container:deep(>div) {
|
||||
@apply bg-slate-800;
|
||||
>table {
|
||||
>thead {
|
||||
@apply sticky z-10;
|
||||
top: -1px;
|
||||
}
|
||||
>tfoot {
|
||||
@apply bg-slate-600 sticky z-10;
|
||||
bottom: -1px;
|
||||
}
|
||||
>*>tr, >*>tr>td {
|
||||
height: v-bind(itemHeightStyle);
|
||||
}
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
margin-top: v-bind(computedHeaderHeight);
|
||||
margin-bottom: v-bind(computedFooterHeight);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { vElementHover } from '@vueuse/components';
|
||||
import { useElementBounding } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useSharedWindowSize } from './tooltip';
|
||||
import {vElementHover} from '@vueuse/components';
|
||||
import {useElementBounding} from '@vueuse/core';
|
||||
import {computed, ref} from 'vue';
|
||||
import {useSharedWindowSize} from './tooltip';
|
||||
|
||||
const open = defineModel('open', { default: false });
|
||||
|
||||
@@ -25,7 +25,7 @@ const positions = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="mainDiv" clas="flex flex-col items-center justify-center" :class="{
|
||||
<div ref="mainDiv" class="flex flex-col items-center justify-center" :class="{
|
||||
'open': open,
|
||||
'tooltip-top': positions.includes('top'),
|
||||
'tooltip-bottom': positions.includes('bottom'),
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue";
|
||||
import {Modal} from "@/components";
|
||||
import {useConfirmStore} from "./useConfirm";
|
||||
|
||||
const confirmStore = useConfirmStore();
|
||||
|
||||
const modalOpen = computed({
|
||||
get: () => confirmStore.open,
|
||||
set: value => {
|
||||
if (!value) {
|
||||
confirmStore.cancel();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:open="modalOpen">
|
||||
<div class="bg-slate-800 rounded pb-4 w-96">
|
||||
<span class="m-2">{{ confirmStore.options.title ?? "Confirm" }}</span>
|
||||
<hr />
|
||||
<div class="m-4">{{ confirmStore.options.message }}</div>
|
||||
<div class="flex justify-end">
|
||||
<button class="me-2" @click="confirmStore.cancel">{{ confirmStore.options.cancelLabel ?? "Cancel" }}</button>
|
||||
<button class="confirm me-4" :class="confirmStore.options.danger ? 'danger' : ''" @click="confirmStore.accept">{{ confirmStore.options.confirmLabel ?? "Confirm" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/style.css";
|
||||
|
||||
button.confirm {
|
||||
@apply border-emerald-500 bg-emerald-500 hover:bg-emerald-600;
|
||||
|
||||
&.danger {
|
||||
@apply border-amber-900 bg-amber-900 hover:bg-amber-800;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as ConfirmModal } from './ConfirmModal.vue';
|
||||
export { confirm, useConfirmStore } from './useConfirm';
|
||||
export type { ConfirmOptions } from './useConfirm';
|
||||
@@ -0,0 +1,37 @@
|
||||
import {defineStore} from "pinia";
|
||||
import {ref} from "vue";
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title?: string;
|
||||
message: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
export const useConfirmStore = defineStore('confirm', () => {
|
||||
const open = ref(false);
|
||||
const options = ref<ConfirmOptions>({message: ""});
|
||||
let resolver: ((value: boolean) => void) | undefined;
|
||||
|
||||
const settle = (value: boolean) => {
|
||||
open.value = false;
|
||||
resolver?.(value);
|
||||
resolver = undefined;
|
||||
};
|
||||
|
||||
const confirm = (opts: ConfirmOptions | string): Promise<boolean> => {
|
||||
options.value = typeof opts === "string" ? {message: opts} : opts;
|
||||
open.value = true;
|
||||
return new Promise<boolean>(resolve => {
|
||||
resolver = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
const accept = () => settle(true);
|
||||
const cancel = () => settle(false);
|
||||
|
||||
return {open, options, confirm, accept, cancel};
|
||||
});
|
||||
|
||||
export const confirm = (opts: ConfirmOptions | string): Promise<boolean> => useConfirmStore().confirm(opts);
|
||||
+1133
-152
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {computed, ref} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {isCombined, Ledger, LedgerType, LedgerTypes, useLedgersStore} from "./ledger";
|
||||
import {Modal} from "@/components";
|
||||
import LedgerLabel from "./LedgerLabel.vue";
|
||||
@@ -15,8 +14,6 @@ interface Props {
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const ledgersStore = useLedgersStore();
|
||||
const {ledgers} = storeToRefs(ledgersStore);
|
||||
const {findById, findAllById, createMain, createCombined, updateMain, updateCombined} = ledgersStore;
|
||||
|
||||
const modalOpen = ref<boolean>(false);
|
||||
|
||||
@@ -24,8 +21,8 @@ const type = ref<LedgerType>(LedgerTypes.Main);
|
||||
const name = ref("");
|
||||
const members = ref<Ledger[]>([]);
|
||||
const selectedLedger = ref<Ledger>();
|
||||
const availableLedgers = computed(() => ledgers.value
|
||||
.filter((ledger) => selectedLedger.value === ledger)
|
||||
const availableLedgers = computed(() => ledgersStore.ledgers
|
||||
.filter(l => l.ledgerId !== props.ledgerId)
|
||||
.filter(l => !members.value.includes(l)));
|
||||
|
||||
|
||||
@@ -37,12 +34,12 @@ const addMember = () => {
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
const ledger = isCreating.value ? undefined : findById(props.ledgerId);
|
||||
const ledger = isCreating.value ? undefined : ledgersStore.findById(props.ledgerId);
|
||||
|
||||
if (ledger) {
|
||||
type.value = ledger.type;
|
||||
name.value = ledger.name;
|
||||
members.value = isCombined(ledger) ? findAllById(ledger.memberLedgerIds) : [];
|
||||
members.value = isCombined(ledger) ? ledgersStore.findAllById(ledger.memberLedgerIds) : [];
|
||||
} else {
|
||||
type.value = LedgerTypes.Main;
|
||||
name.value = "";
|
||||
@@ -62,17 +59,17 @@ const title = computed(() => {
|
||||
|
||||
const create = () => {
|
||||
if (type.value === LedgerTypes.Main) {
|
||||
createMain({name: name.value})
|
||||
ledgersStore.createMain({name: name.value})
|
||||
} else {
|
||||
createCombined({name: name.value, memberLedgerIds: members.value.map(l => l.ledgerId)})
|
||||
ledgersStore.createCombined({name: name.value, memberLedgerIds: members.value.map(l => l.ledgerId)})
|
||||
}
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
if (type.value === LedgerTypes.Main) {
|
||||
updateMain(props.ledgerId, {name: name.value})
|
||||
ledgersStore.updateMain(props.ledgerId, {name: name.value})
|
||||
} else {
|
||||
updateCombined(props.ledgerId, {name: name.value, memberLedgerIds: members.value.map(l => l.ledgerId)})
|
||||
ledgersStore.updateCombined(props.ledgerId, {name: name.value, memberLedgerIds: members.value.map(l => l.ledgerId)})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +95,7 @@ defineExpose({ open });
|
||||
<span class="m-2">{{ title }}</span>
|
||||
<hr />
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-center">
|
||||
<div v-if="!ledgerId" class="flex justify-center">
|
||||
<div class="flex bg-slate-600 rounded-s-md p-1">
|
||||
<button class="switch" :class="{active: type === LedgerTypes.Main}" @click="type = LedgerTypes.Main">Main</button>
|
||||
</div>
|
||||
@@ -136,7 +133,7 @@ defineExpose({ open });
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
|
||||
button.switch {
|
||||
@apply flex items-center px-4 rounded-md bg-slate-600;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {isCombined, Ledger} from "@/ledger/ledger.ts";
|
||||
import {isCombined, Ledger, systemLedger} from "@/ledger/ledger.ts";
|
||||
import {FolderOpenIcon} from '@heroicons/vue/24/outline';
|
||||
import {RouterLink} from "vue-router";
|
||||
import {routeNames} from "@/routes.ts";
|
||||
import {routeNames} from "@/routes";
|
||||
|
||||
interface Props {
|
||||
ledger: Ledger;
|
||||
@@ -17,7 +17,15 @@ const props = defineProps<Props>();
|
||||
<div class="flex">
|
||||
<FolderOpenIcon v-if="isCombined(ledger)" class="w-4 me-1" />
|
||||
<div v-else class="w-4 me-1"/>
|
||||
<RouterLink v-if="link" :to="{name: routeNames.listLedgerTransactions, params: {ledgerId: ledger.ledgerId}}">{{ ledger.name }}</RouterLink>
|
||||
<span v-else>{{ ledger.name }}</span>
|
||||
<RouterLink v-if="link" :to="{name: routeNames.viewLedger, params: {ledgerId: ledger.ledgerId}}">{{ ledger.name }}</RouterLink>
|
||||
<span v-else :class="{'system-ledger': ledger === systemLedger}">{{ ledger.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/style.css";
|
||||
|
||||
.system-ledger {
|
||||
@apply text-emerald-400;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import {Ledger, systemLedger, useLedgersStore} from "@/ledger/ledger.ts";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface Props {
|
||||
@@ -9,9 +8,9 @@ interface Props {
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const ledger = defineModel<Ledger>();
|
||||
const {ledgers: allLedgers} = storeToRefs(useLedgersStore());
|
||||
const ledgersStore = useLedgersStore();
|
||||
|
||||
const ledgersToUse = computed(() => props.ledgers || allLedgers);
|
||||
const ledgersToUse = computed(() => props.ledgers || ledgersStore.ledgers);
|
||||
const ledgerId = computed({
|
||||
get: () => ledger.value?.ledgerId,
|
||||
set: value => ledger.value = ledgersToUse.value.find(l => l.ledgerId === value)
|
||||
@@ -26,7 +25,7 @@ const ledgerId = computed({
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
|
||||
.system-ledger {
|
||||
@apply text-emerald-400;
|
||||
|
||||
+23
-13
@@ -1,33 +1,33 @@
|
||||
import {
|
||||
BalanceResponse,
|
||||
CombinedLedgerResponse,
|
||||
CombinedLedgerResponseTypeEnum,
|
||||
CreateCombinedLedgerRequest,
|
||||
CreateMainLedgerRequest,
|
||||
LedgerResponseTypeEnum,
|
||||
LedgerResponse,
|
||||
MainLedgerResponse,
|
||||
MainLedgerResponseTypeEnum,
|
||||
TransactionResponse,
|
||||
TransferResponseTypeEnum,
|
||||
UpdateCombinedLedgerRequest,
|
||||
UpdateMainLedgerRequest
|
||||
} from "@/generated/mammon";
|
||||
import {defineStore} from "pinia";
|
||||
import {ref, triggerRef} from "vue";
|
||||
import {computed, ref, triggerRef} from "vue";
|
||||
import {ledgerApi, transactionApi} from "@/mammon";
|
||||
import {useRouteParams} from "@vueuse/router";
|
||||
|
||||
export const LedgerTypes = LedgerResponseTypeEnum;
|
||||
export const LedgerTypes = {
|
||||
Main: 'MAIN',
|
||||
Combined: 'COMBINED',
|
||||
};
|
||||
|
||||
export type LedgerType = LedgerResponseTypeEnum;
|
||||
export type MainLedger = MainLedgerResponse & {type: MainLedgerResponseTypeEnum}
|
||||
export type CombinedLedger = CombinedLedgerResponse & {type: CombinedLedgerResponseTypeEnum}
|
||||
export type LedgerType = LedgerResponse['type'];
|
||||
export type MainLedger = MainLedgerResponse
|
||||
export type CombinedLedger = CombinedLedgerResponse
|
||||
export type Ledger = MainLedger | CombinedLedger;
|
||||
|
||||
export const TransferTypes = TransferResponseTypeEnum;
|
||||
|
||||
export const systemLedgerRef = 'system';
|
||||
export const systemLedger = {
|
||||
type: LedgerTypes.Main,
|
||||
ledgerId: "",
|
||||
ledgerId: "00000000-0000-0000-0000-000000000001",
|
||||
name: "Eve Economy",
|
||||
balance: 0,
|
||||
_system: true
|
||||
@@ -74,5 +74,15 @@ export const useLedgersStore = defineStore('ledgers', () => {
|
||||
return {ledgers, findById, findAllById, createMain, createCombined, updateMain, updateCombined, refresh};
|
||||
})
|
||||
|
||||
export const findAllTransactionInLeger = (ledger: Ledger | string): Promise<TransactionResponse[]> => transactionApi.finAllTransactionsInLedger(typeof ledger == 'string' ? ledger : ledger.ledgerId).then(response => response.data)
|
||||
const getLedgerId = (ledger: Ledger | string): string => typeof ledger == 'string' ? ledger : ledger.ledgerId;
|
||||
|
||||
export const findAllTransactionInLeger = (ledger: Ledger | string): Promise<TransactionResponse[]> => transactionApi.finAllTransactionsInLedger(getLedgerId(ledger)).then(response => response.data)
|
||||
export const getLedgerBalance = (ledger: Ledger | string): Promise<BalanceResponse> => ledgerApi.findBalanceByLedgerId(getLedgerId(ledger)).then(response => response.data)
|
||||
|
||||
export const useLedgerParam = () => {
|
||||
const ledgersStore = useLedgersStore();
|
||||
const ledgerId = useRouteParams<string, string>('ledgerId', '', { transform: v => typeof v === 'string' ? v : v[0]});
|
||||
const ledger = computed(() => ledgersStore.findById(ledgerId.value))
|
||||
|
||||
return {ledgerId, ledger};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import {logResource} from "@/service";
|
||||
import axios from "axios";
|
||||
import {
|
||||
AcquisitionApi,
|
||||
ActivityApi,
|
||||
CharacterApi,
|
||||
CharacterRuleBookApi,
|
||||
LedgerApi,
|
||||
MarketApi,
|
||||
ProcessingApi,
|
||||
RuleBookApi,
|
||||
TransactionApi
|
||||
@@ -29,3 +31,5 @@ export const ruleBookApi = new RuleBookApi(undefined, mammonUrl, mammonAxiosInst
|
||||
export const characterRuleBookApi = new CharacterRuleBookApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const activityApi = new ActivityApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const processingApi = new ProcessingApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance);
|
||||
|
||||
+14
-2
@@ -1,13 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import {formatIsk} from "@/formaters";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface Props {
|
||||
amount: number;
|
||||
colored?: boolean;
|
||||
}
|
||||
|
||||
const { amount } = defineProps<Props>();
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
colored: true,
|
||||
});
|
||||
|
||||
const computedClass = computed(() => {
|
||||
if (!props.colored) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return props.amount >= 0 ? 'text-emerald-400' : 'text-amber-700';
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="amount >= 0 ? 'text-emerald-400' : 'text-amber-700'">{{ formatIsk(amount) }}</span>
|
||||
<span :class="computedClass">{{ formatIsk(amount) }}</span>
|
||||
</template>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MarketType } from "..";
|
||||
import { MarbasAcquiredType } from "./acquisition";
|
||||
import { RawAcquiredType } from "./acquisition";
|
||||
|
||||
export type AcquiredType = Omit<MarbasAcquiredType, 'type'> & {
|
||||
export type AcquiredType = Omit<RawAcquiredType, 'type'> & {
|
||||
type: MarketType,
|
||||
buy: number,
|
||||
sell: number
|
||||
|
||||
@@ -75,7 +75,7 @@ watchEffect(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
|
||||
.tooltip {
|
||||
@apply ms-auto;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { SortableHeader, useSort, VirtualScrollTable } from '@/components/table';
|
||||
import { formatEveDate, formatIsk, percentFormater } from '@/formaters';
|
||||
import { MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore } from "@/market";
|
||||
import { MinusIcon, PlusIcon } from '@heroicons/vue/24/outline';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
import { AcquiredType } from './AcquiredType';
|
||||
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||
import {MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore} from "@/market";
|
||||
import {MinusIcon, PlusIcon} from '@heroicons/vue/24/outline';
|
||||
import {useStorage} from '@vueuse/core';
|
||||
import {computed, ref} from 'vue';
|
||||
import {AcquiredType} from './AcquiredType';
|
||||
import AcquisitionQuantilsTooltip from './AcquisitionQuantilsTooltip.vue';
|
||||
import {formatEveDate, formatIsk, percentFormater} from "@/formaters.ts";
|
||||
|
||||
type Result = {
|
||||
id: number;
|
||||
id: string;
|
||||
type: MarketType;
|
||||
name: string;
|
||||
buy: number;
|
||||
@@ -98,7 +98,7 @@ const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() =>
|
||||
const precentProfit = marketTaxStore.calculateProfit(price, first.sell);
|
||||
|
||||
list.push({
|
||||
id: typeID,
|
||||
id: typeID.toString(),
|
||||
type: first.type,
|
||||
name: first.type.name,
|
||||
buy: first.buy,
|
||||
@@ -246,7 +246,7 @@ const total = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
div.end {
|
||||
@apply justify-self-end ms-2;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,43 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import {defineStore} from "pinia";
|
||||
import {computed, ref} from "vue";
|
||||
import {acquisitionApi} from "@/mammon";
|
||||
import {AcquisitionResponse, AcquisitionResponseSourceEnum} from "@/generated/mammon";
|
||||
|
||||
export type AcquiredTypeSource = 'bo' | 'so' | 'prod' | 'misc';
|
||||
|
||||
export type RawAcquiredType = {
|
||||
id: string;
|
||||
type: number;
|
||||
quantity: number;
|
||||
remaining: number;
|
||||
price: number;
|
||||
date: Date;
|
||||
source: AcquisitionResponseSourceEnum;
|
||||
}
|
||||
|
||||
const toAcquiredType = (a: AcquisitionResponse): RawAcquiredType => ({
|
||||
id: a.acquisitionId,
|
||||
type: a.marketTypeId,
|
||||
quantity: a.quantity,
|
||||
remaining: a.remaining,
|
||||
price: a.unitCost,
|
||||
date: new Date(a.datetime),
|
||||
source: a.source,
|
||||
});
|
||||
|
||||
export const useAcquiredTypesStore = defineStore('market-acquisition', () => {
|
||||
const acquiredTypes = ref<any[]>([]); // TODO
|
||||
const acquiredTypes = ref<RawAcquiredType[]>([]);
|
||||
|
||||
const types = computed(() => acquiredTypes.value.filter(item => item.remaining > 0));
|
||||
const addAcquiredType = async (type: number, quantity: number, price: number, source?: AcquiredTypeSource) => {
|
||||
const newItem = [];
|
||||
|
||||
acquiredTypes.value = [...acquiredTypes.value, newItem];
|
||||
};
|
||||
const removeAcquiredType = async (id: number, quantity: number) => {
|
||||
const found = acquiredTypes.value.find(t => t.id === id);
|
||||
// Display-only: the backend exposes no write endpoint yet, so buy/sell are no-ops.
|
||||
const addAcquiredType = async (_type: number, _quantity: number, _price: number, _source?: AcquiredTypeSource) => {};
|
||||
const removeAcquiredType = async (_id: string, _quantity: number) => {};
|
||||
|
||||
if (!found) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const item = {
|
||||
...found,
|
||||
remaining: Math.max(0, found.remaining - quantity)
|
||||
};
|
||||
|
||||
acquiredTypes.value = acquiredTypes.value.map(i => {
|
||||
if (i.id === item.id) {
|
||||
return item;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = () => {}
|
||||
const refresh = () => acquisitionApi.findAllAcquisitions()
|
||||
.then(response => acquiredTypes.value = response.data.map(toAcquiredType));
|
||||
|
||||
refresh();
|
||||
|
||||
return { acquiredTypes: types, addAcquiredType, removeAcquiredType, refresh };
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { RegionalMarketCache } from '../RegionalMarketCache';
|
||||
import { jitaId } from '../market';
|
||||
import { MarketType } from "../type";
|
||||
import { MarketTypePrice } from './MarketTypePrice';
|
||||
import { getEvepraisalPrices } from './evepraisal';
|
||||
import { getfuzzworkPrices } from './fuzzwork';
|
||||
import {defineStore} from 'pinia';
|
||||
import {RegionalMarketCache} from '../RegionalMarketCache';
|
||||
import {jitaId} from '../market';
|
||||
import {MarketType} from "../type";
|
||||
import {MarketTypePrice} from './MarketTypePrice';
|
||||
import {getMammonPrices} from './mammon';
|
||||
|
||||
const cacheDuration = 1000 * 60 * 5; // 5 minutes
|
||||
const priceGetters = {
|
||||
evepraisal: getEvepraisalPrices,
|
||||
fuzzwork: getfuzzworkPrices
|
||||
}
|
||||
const CACHE_DURATION = 1000 * 60 * 5; // 5 minutes
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
export const useApraisalStore = defineStore('appraisal', () => {
|
||||
const cache: RegionalMarketCache<MarketTypePrice> = new RegionalMarketCache(cacheDuration);
|
||||
const cache: RegionalMarketCache<MarketTypePrice> = new RegionalMarketCache(CACHE_DURATION);
|
||||
|
||||
const getPricesUncached = priceGetters.fuzzwork;
|
||||
const getPricesUncached = getMammonPrices;
|
||||
|
||||
const getPrice = async (type: MarketType, regionId?: number): Promise<MarketTypePrice> => (await getPrices([type], regionId))[0];
|
||||
const getPrices = async (types: MarketType[], regionId?: number): Promise<MarketTypePrice[]> => {
|
||||
@@ -34,7 +30,13 @@ export const useApraisalStore = defineStore('appraisal', () => {
|
||||
});
|
||||
|
||||
if (uncached.length > 0) {
|
||||
const prices = await getPricesUncached(uncached);
|
||||
const batches: Promise<MarketTypePrice[]>[] = [];
|
||||
|
||||
for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
|
||||
batches.push(getPricesUncached(uncached.slice(i, i + BATCH_SIZE)));
|
||||
}
|
||||
|
||||
const prices = (await Promise.all(batches)).flat();
|
||||
|
||||
prices.forEach(p => cache.set(rId, p.type.id, p));
|
||||
return [ ...cached, ...prices ];
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { logResource } from '@/service';
|
||||
import {logResource} from '@/service';
|
||||
import axios from 'axios';
|
||||
import { MarketType } from "../type";
|
||||
import { PriceGetter } from './MarketTypePrice';
|
||||
|
||||
export const evepraisalAxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_EVEPRAISAL_URL,
|
||||
@@ -11,21 +9,3 @@ export const evepraisalAxiosInstance = axios.create({
|
||||
},
|
||||
})
|
||||
logResource(evepraisalAxiosInstance)
|
||||
|
||||
const batchSize = 100;
|
||||
|
||||
export const getEvepraisalPrices: PriceGetter = async types => {
|
||||
const batches = [];
|
||||
|
||||
for (let i = 0; i < types.length; i += batchSize) {
|
||||
batches.push(evepraisalAxiosInstance.post(`/appraisal.json?market=jita&persist=no&raw_textarea=${types.slice(i, i + batchSize).map(t => t.name).join("%0A")}`));
|
||||
}
|
||||
return (await Promise.all(batches))
|
||||
.flatMap(b => b.data.appraisal.items)
|
||||
.map((item: any) => ({
|
||||
type: types.find(t => t.name === item.typeName) as MarketType,
|
||||
buy: item.prices.buy.max,
|
||||
sell: item.prices.sell.min,
|
||||
orderCount: item.prices.all.order_count
|
||||
}));
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { logResource } from '@/service';
|
||||
import axios from 'axios';
|
||||
import { MarketType } from "../type";
|
||||
import { PriceGetter } from './MarketTypePrice';
|
||||
|
||||
export const fuzzworkAxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_FUZZWORK_URL,
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
})
|
||||
logResource(fuzzworkAxiosInstance)
|
||||
|
||||
const batchSize = 100;
|
||||
|
||||
export const getfuzzworkPrices: PriceGetter = async types => {
|
||||
const batches = [];
|
||||
|
||||
for (let i = 0; i < types.length; i += batchSize) {
|
||||
batches.push(fuzzworkAxiosInstance.post(`/aggregates/?station=60003760&types=${types.slice(i, i + batchSize).map(t => t.id).join(",")}`));
|
||||
}
|
||||
return (await Promise.all(batches))
|
||||
.flatMap(b => Object.entries(b.data))
|
||||
.map(entry => {
|
||||
const id = doParseInt(entry[0]);
|
||||
const prices = entry[1] as any;
|
||||
|
||||
return {
|
||||
type: types.find(t => t.id === id) as MarketType,
|
||||
buy: doParseFloat(prices?.buy?.max),
|
||||
sell: doParseFloat(prices?.sell?.min),
|
||||
orderCount: doParseInt(prices?.buy?.order_count) + doParseInt(prices?.sell?.order_count)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const doParseInt = (s?: string) => s ? parseInt(s) : 0;
|
||||
const doParseFloat = (s?: string) => s ? parseFloat(s) : 0;
|
||||
@@ -0,0 +1,20 @@
|
||||
import {marketApi} from '@/mammon/mammonService';
|
||||
import {MarketTypePrice, PriceGetter} from './MarketTypePrice';
|
||||
|
||||
export const getMammonPrices: PriceGetter = async types => {
|
||||
if (types.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const typesById = new Map(types.map(t => [t.id, t]));
|
||||
const response = await marketApi.currentPrices(types.map(t => t.id));
|
||||
|
||||
return response.data.reduce<MarketTypePrice[]>((prices, p) => {
|
||||
const type = typesById.get(p.marketTypeId);
|
||||
|
||||
if (type) {
|
||||
prices.push({ type, buy: p.buy, sell: p.sell, orderCount: p.orderCount });
|
||||
}
|
||||
return prices;
|
||||
}, []);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { esiAxiosInstance } from "@/service";
|
||||
import { RegionalMarketCache } from '../RegionalMarketCache';
|
||||
import { jitaId } from "../market";
|
||||
|
||||
|
||||
export type EsiMarketOrderHistory = {
|
||||
average: number;
|
||||
date: string;
|
||||
highest: number;
|
||||
lowest: number;
|
||||
order_count: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
// TODO use pinia store
|
||||
const historyCache: RegionalMarketCache<EsiMarketOrderHistory[]> = new RegionalMarketCache(() => {
|
||||
const date = new Date();
|
||||
|
||||
if (date.getUTCHours() >= 11) {
|
||||
date.setUTCDate(date.getUTCDate() + 1);
|
||||
}
|
||||
date.setUTCHours(11, 0, 0, 0);
|
||||
return date;
|
||||
});
|
||||
|
||||
export const getHistory = async (typeId: number, regionId?: number): Promise<EsiMarketOrderHistory[]> => {
|
||||
const rId = regionId ?? jitaId;
|
||||
|
||||
return historyCache.computeIfAbsent(rId, typeId, async () => (await esiAxiosInstance.get(`/markets/${rId}/history/`, { params: { type_id: typeId } })).data);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EsiMarketOrderHistory } from "@/market";
|
||||
import { MarketHistory } from "@/market";
|
||||
|
||||
export type HistoryQuartils = {
|
||||
totalVolume: number,
|
||||
@@ -7,7 +7,7 @@ export type HistoryQuartils = {
|
||||
q3: number,
|
||||
}
|
||||
|
||||
export const getHistoryQuartils = (history: EsiMarketOrderHistory[], days?: number): HistoryQuartils => {
|
||||
export const getHistoryQuartils = (history: MarketHistory[], days?: number): HistoryQuartils => {
|
||||
const now = Date.now();
|
||||
|
||||
const volumes = history
|
||||
@@ -51,7 +51,7 @@ export const getHistoryQuartils = (history: EsiMarketOrderHistory[], days?: numb
|
||||
};
|
||||
}
|
||||
|
||||
const estimateVolume = (history: EsiMarketOrderHistory): number => {
|
||||
const estimateVolume = (history: MarketHistory): number => {
|
||||
if (history.volume === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { MarketHistoryResponse } from "@/generated/mammon";
|
||||
import { marketApi } from "@/mammon";
|
||||
|
||||
export type MarketHistory = MarketHistoryResponse;
|
||||
|
||||
export const getHistory = async (typeId: number): Promise<MarketHistory[]> =>
|
||||
(await marketApi.findHistory(typeId)).data;
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './EsiMarketOrderHistory';
|
||||
export * from './MarketHistory';
|
||||
export * from './HistoryQuartils';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { SliderCheckbox } from '@/components';
|
||||
import { SortableHeader, useSort, VirtualScrollTable } from '@/components/table';
|
||||
import { formatIsk, percentFormater } from '@/formaters';
|
||||
import { getHistoryQuartils, MarketType, MarketTypeLabel, TaxInput, useMarketTaxStore } from "@/market";
|
||||
import { BookmarkSlashIcon, ShoppingCartIcon } from '@heroicons/vue/24/outline';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAcquiredTypesStore } from '../acquisition';
|
||||
import { TrackingResult } from './tracking';
|
||||
import {SliderCheckbox} from '@/components';
|
||||
import {SortableHeader, useSort, VirtualScrollTable} from '@/components/table';
|
||||
import {formatIsk, percentFormater} from "@/formaters";
|
||||
import {MarketType, MarketTypeLabel} from "@/market";
|
||||
import {ShoppingCartIcon} from '@heroicons/vue/24/outline';
|
||||
import {useStorage} from '@vueuse/core';
|
||||
import {computed, ref} from 'vue';
|
||||
import {useAcquiredTypesStore} from '../acquisition';
|
||||
import {ScanResult} from './scan';
|
||||
|
||||
type Result = {
|
||||
type: MarketType;
|
||||
@@ -18,25 +18,28 @@ type Result = {
|
||||
q1: number;
|
||||
median: number;
|
||||
q3: number;
|
||||
totalVolume: number;
|
||||
acquisitions: number;
|
||||
profit: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items?: TrackingResult[];
|
||||
items?: ScanResult[];
|
||||
infoOnly?: boolean;
|
||||
ignoredColums?: string[] | string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'buy', type: MarketType, buy: number, sell: number): void;
|
||||
(e: 'remove', type: MarketType): void;
|
||||
}
|
||||
|
||||
const scoreFormater = new Intl.NumberFormat("en-US", {
|
||||
maximumFractionDigits: 0
|
||||
});
|
||||
const volumeFormater = new Intl.NumberFormat("en-US", {
|
||||
maximumFractionDigits: 0
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
items: () => [],
|
||||
@@ -45,11 +48,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
});
|
||||
defineEmits<Emits>();
|
||||
|
||||
const marketTaxStore = useMarketTaxStore();
|
||||
const acquiredTypesStore = useAcquiredTypesStore();
|
||||
|
||||
const days = useStorage('market-tracking-days', 365);
|
||||
const threshold = useStorage('market-tracking-threshold', 10);
|
||||
const threshold = useStorage('market-scan-threshold', 10);
|
||||
const filter = ref("");
|
||||
const onlyCheap = ref(false);
|
||||
const columnsToIgnore = computed(() => {
|
||||
@@ -63,9 +64,6 @@ const columnsToIgnore = computed(() => {
|
||||
const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() => props.items
|
||||
.filter(r => r.type.name.toLowerCase().includes(filter.value.toLowerCase()))
|
||||
.map(r => {
|
||||
const quartils = getHistoryQuartils(r.history, days.value);
|
||||
const profit = quartils.q1 === 0 || quartils.q3 === 0 ? 0 : marketTaxStore.calculateProfit(quartils.q1, quartils.q3);
|
||||
const score = profit <= 0 ? 0 : Math.sqrt((Math.pow(quartils.totalVolume, 1.1) * Math.pow(quartils.q1, 1.2) * Math.pow(profit, 0.5) * Math.pow(Math.max(1, r.orderCount), -0.7)) / days.value);
|
||||
const acquisitions = columnsToIgnore.value.includes('acquisitions') ? 0 : acquiredTypesStore.acquiredTypes
|
||||
.filter(t => t.type === r.type.id)
|
||||
.reduce((a, b) => a + b.remaining, 0);
|
||||
@@ -76,12 +74,13 @@ const { sortedArray, headerProps, showColumn } = useSort<Result>(computed(() =>
|
||||
name: r.type.name,
|
||||
buy: r.buy,
|
||||
sell: r.sell,
|
||||
q1: quartils.q1,
|
||||
median: quartils.median,
|
||||
q3: quartils.q3,
|
||||
q1: r.q1,
|
||||
median: r.median,
|
||||
q3: r.q3,
|
||||
totalVolume: r.totalVolume,
|
||||
acquisitions,
|
||||
profit,
|
||||
score
|
||||
profit: r.profit,
|
||||
score: r.score
|
||||
};
|
||||
}).filter(r => !onlyCheap.value || (r.buy <= r.q1 && r.profit >= (threshold.value / 100)))), {
|
||||
defaultSortKey: 'score',
|
||||
@@ -105,15 +104,10 @@ const getLineColor = (result: Result) => {
|
||||
<template>
|
||||
<div v-if="!infoOnly" class="flex mb-2 mt-4">
|
||||
<div class="flex justify-self-end ms-auto">
|
||||
<TaxInput />
|
||||
<div class="end">
|
||||
<span>Profit Threshold: </span>
|
||||
<input type="number" min="0" max="1000" step="1" v-model="threshold" />
|
||||
</div>
|
||||
<div class="end">
|
||||
<span>Days: </span>
|
||||
<input type="number" min="1" max="365" step="1" v-model="days" />
|
||||
</div>
|
||||
<div class="end flex">
|
||||
<SliderCheckbox class="me-1" v-model="onlyCheap" /> Show only cheap items
|
||||
</div>
|
||||
@@ -133,6 +127,7 @@ const getLineColor = (result: Result) => {
|
||||
<SortableHeader v-bind="headerProps" sortKey="q1">Q1</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="median">Median</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="q3">Q3</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="totalVolume">Volume</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="profit">Profit</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="score">Score</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="acquisitions">Acquisitions</SortableHeader>
|
||||
@@ -149,12 +144,12 @@ const getLineColor = (result: Result) => {
|
||||
<td v-if="showColumn('q1')" class="text-right">{{ formatIsk(r.data.q1) }}</td>
|
||||
<td v-if="showColumn('median')" class="text-right">{{ formatIsk(r.data.median) }}</td>
|
||||
<td v-if="showColumn('q3')" class="text-right">{{ formatIsk(r.data.q3) }}</td>
|
||||
<td v-if="showColumn('totalVolume')" class="text-right">{{ volumeFormater.format(r.data.totalVolume) }}</td>
|
||||
<td v-if="showColumn('profit')" class="text-right">{{ percentFormater.format(r.data.profit) }}</td>
|
||||
<td v-if="showColumn('score')" class="text-right">{{ scoreFormater.format(r.data.score) }}</td>
|
||||
<td v-if="showColumn('acquisitions')" class="text-right">{{ r.data.acquisitions }}</td>
|
||||
<td v-if="showColumn('buttons')" class="text-right">
|
||||
<button class="btn-icon me-1" title="Add acquisitions" @click="$emit('buy', r.data.type, r.data.buy, r.data.sell)"><ShoppingCartIcon /></button>
|
||||
<button class="btn-icon me-1" title="Untrack" @click="$emit('remove', r.data.type)"><BookmarkSlashIcon /></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -168,8 +163,8 @@ const getLineColor = (result: Result) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
div.end {
|
||||
@apply justify-self-end ms-2;
|
||||
}
|
||||
</style>../history/HistoryQuartils
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './scan';
|
||||
|
||||
export { default as ScanResultTable } from './ScanResultTable.vue';
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MarketType } from "@/market";
|
||||
import { MarketScanResponse } from "@/generated/mammon";
|
||||
|
||||
export type ScanResult = {
|
||||
type: MarketType;
|
||||
buy: number;
|
||||
sell: number;
|
||||
q1: number;
|
||||
median: number;
|
||||
q3: number;
|
||||
totalVolume: number;
|
||||
profit: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export const toScanResult = (res: MarketScanResponse, type: MarketType): ScanResult => ({
|
||||
type,
|
||||
buy: res.buy,
|
||||
sell: res.sell,
|
||||
q1: res.q1,
|
||||
median: res.median,
|
||||
q3: res.q3,
|
||||
totalVolume: res.totalVolume,
|
||||
profit: res.profit,
|
||||
score: res.score,
|
||||
});
|
||||
@@ -1,24 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useMarketTaxStore } from "./tax";
|
||||
import {useMarketTaxStore} from "./tax";
|
||||
|
||||
const { brokerFee, scc } = storeToRefs(useMarketTaxStore());
|
||||
const marketTaxStore = useMarketTaxStore();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="end">
|
||||
<span>Broker Fee: </span>
|
||||
<input type="number" min="1" max="3" step="0.01" v-model="brokerFee" />
|
||||
<input type="number" min="1" max="3" step="0.01" v-model="marketTaxStore.brokerFee" />
|
||||
</div>
|
||||
<div class="end">
|
||||
<span>SCC: </span>
|
||||
<input type="number" min="3.6" max="8" step="0.01" v-model="scc" >
|
||||
<input type="number" min="3.6" max="8" step="0.01" v-model="marketTaxStore.scc" >
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
div.end {
|
||||
@apply justify-self-end ms-2;
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './tracking';
|
||||
|
||||
export { default as TrackingResultTable } from './TrackingResultTable.vue';
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { EsiMarketOrderHistory, getHistory, MarketType, MarketTypePrice } from "@/market";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
export type TrackingResult = {
|
||||
type: MarketType;
|
||||
history: EsiMarketOrderHistory[];
|
||||
buy: number,
|
||||
sell: number,
|
||||
orderCount: number,
|
||||
}
|
||||
|
||||
const endpoint = '/api/types_tracking/';
|
||||
|
||||
export const useMarketTrackingStore = defineStore('marketTracking', () => {
|
||||
const trackedTypes = ref<any[]>([]); // TODO
|
||||
|
||||
const types = computed(() => trackedTypes.value.map(item => item.type) ?? []);
|
||||
const addType = async (type: number) => {
|
||||
const found = trackedTypes.value.find(item => item.type === type);
|
||||
|
||||
if (!found) {
|
||||
log.info(`Tracking type ${type}`);
|
||||
}
|
||||
}
|
||||
const removeType = async (type: number) => {
|
||||
const found = trackedTypes.value.find(item => item.type === type);
|
||||
|
||||
if (!found) {
|
||||
return;
|
||||
}
|
||||
|
||||
trackedTypes.value = trackedTypes.value.filter(t => t.id !== found.id);
|
||||
}
|
||||
|
||||
return { types, addType, removeType };
|
||||
});
|
||||
|
||||
export const createResult = async (id: number, price: MarketTypePrice): Promise<TrackingResult> => ({ history: await getHistory(id), ...price });
|
||||
@@ -1,28 +1,23 @@
|
||||
import {esiAxiosInstance} from '@/service';
|
||||
import {marketApi} from '@/mammon/mammonService';
|
||||
import type {MarketTypeResponse} from '@/generated/mammon';
|
||||
|
||||
export type MarketType = {
|
||||
type_id: number;
|
||||
group_id: number;
|
||||
market_group_id: number;
|
||||
name: string;
|
||||
published: boolean;
|
||||
description: string;
|
||||
base_price: number;
|
||||
icon_id: number;
|
||||
volume: number;
|
||||
portion_size: number;
|
||||
}
|
||||
export type MarketType = MarketTypeResponse;
|
||||
|
||||
const cache = new Map<number, MarketType>(); // TODO move to pinia store
|
||||
|
||||
const fetchType = (id: number): Promise<MarketType> => {
|
||||
if (cache.has(id)) {
|
||||
return Promise.resolve(cache.get(id)!);
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
const fetchTypes = async (ids: number[]): Promise<void> => {
|
||||
const missing = ids.filter(id => !cache.has(id));
|
||||
if (missing.length === 0) {
|
||||
return;
|
||||
}
|
||||
return esiAxiosInstance.get<MarketType>(`/universe/types/${id}/`).then(r => {
|
||||
cache.set(id, r.data);
|
||||
return r.data;
|
||||
});
|
||||
const batches: Promise<MarketType[]>[] = [];
|
||||
for (let i = 0; i < missing.length; i += BATCH_SIZE) {
|
||||
batches.push(marketApi.findTypes(missing.slice(i, i + BATCH_SIZE)).then(r => r.data));
|
||||
}
|
||||
const results = await Promise.all(batches);
|
||||
results.flat().forEach(t => cache.set(t.id, t));
|
||||
};
|
||||
|
||||
export const getMarketType = async (type: string | number): Promise<MarketType> => (await getMarketTypes([type]))[0];
|
||||
@@ -31,28 +26,15 @@ export const getMarketTypes = async (types: (string | number)[]): Promise<Market
|
||||
return [];
|
||||
}
|
||||
const ids = types.filter((t): t is number => typeof t === 'number');
|
||||
return Promise.all(ids.map(fetchType));
|
||||
await fetchTypes(ids);
|
||||
return ids.map(id => cache.get(id)).filter((t): t is MarketType => t !== undefined);
|
||||
}
|
||||
|
||||
const blueprintMarketGroups = [ // TODO add all groups
|
||||
2,
|
||||
2157,
|
||||
2159,
|
||||
2339,
|
||||
2160,
|
||||
211,
|
||||
1016,
|
||||
339,
|
||||
2290,
|
||||
357,
|
||||
1530,
|
||||
359,
|
||||
1531,
|
||||
1532,
|
||||
1533,
|
||||
358
|
||||
]
|
||||
|
||||
export const searchMarketTypes = async (search: string): Promise<MarketType[]> => {
|
||||
return []
|
||||
if (search.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const types = await marketApi.searchTypes(search).then(r => r.data);
|
||||
types.forEach(t => cache.set(t.id, t));
|
||||
return types;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { useVirtualList } from '@vueuse/core';
|
||||
import {vOnClickOutside} from '@vueuse/components';
|
||||
import {useVirtualList} from '@vueuse/core';
|
||||
import log from 'loglevel';
|
||||
import { nextTick, ref, watch, watchEffect } from 'vue';
|
||||
import { MarketType, searchMarketTypes } from './MarketType';
|
||||
import {nextTick, ref, watch, watchEffect} from 'vue';
|
||||
import {MarketType, searchMarketTypes} from './MarketType';
|
||||
import MarketTypeLabel from "./MarketTypeLabel.vue";
|
||||
|
||||
interface Emits {
|
||||
@@ -105,7 +105,7 @@ watchEffect(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
.fake-input {
|
||||
@apply w-96 flex border bg-slate-500 rounded px-1 py-0.5;
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ClipboardButton } from '@/components';
|
||||
import { InformationCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import { routeNames } from '@/routes';
|
||||
import {ClipboardButton} from '@/components';
|
||||
import {InformationCircleIcon} from '@heroicons/vue/24/outline';
|
||||
import {routeNames} from '@/routes';
|
||||
import {computedAsync} from "@vueuse/core";
|
||||
import {getMarketType} from "@/market";
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -10,29 +12,39 @@ interface Props {
|
||||
hideCopy?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
name: "",
|
||||
id: 0,
|
||||
hideCopy: false
|
||||
});
|
||||
|
||||
const computedName = computedAsync<string>(async () => {
|
||||
if (props.name) {
|
||||
return props.name;
|
||||
} else if (props.id) {
|
||||
return await getMarketType(props.id).then(marketType => marketType.name);
|
||||
}
|
||||
return "";
|
||||
}, "");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="id || name" class="flex flex-row">
|
||||
<div v-if="id || computedName" class="flex flex-row">
|
||||
<img v-if="id" :src="`https://images.evetech.net/types/${id}/icon?size=32`" class="inline-block w-5 h-5 me-1 mt-1" alt="" />
|
||||
<template v-if="name">
|
||||
{{ name }}
|
||||
<RouterLink v-if="id" :to="{ name: routeNames.marketTypes, params: { type: id } }" class="button btn-icon ms-1 me-1 mt-1" title="Show item info">
|
||||
<template v-if="computedName">
|
||||
{{ computedName }}
|
||||
<RouterLink v-if="id" :to="{ name: routeNames.marketTypes, params: { type: id } }" class="btn-icon ms-1 me-1 mt-1" title="Show item info">
|
||||
<InformationCircleIcon />
|
||||
</RouterLink>
|
||||
<ClipboardButton v-if="!hideCopy" :value="name" />
|
||||
<ClipboardButton v-if="!hideCopy" :value="computedName" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
button:deep(>svg), .button:deep(>svg) {
|
||||
@reference "@/style.css";
|
||||
|
||||
button:deep(>svg), .btn-icon:deep(>svg) {
|
||||
@apply !w-4 !h-4;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {mammonAddCharacterUrl} from "@/mammon";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {CharacterLabel, useCharactersStore} from "@/characters";
|
||||
import {ArrowPathIcon} from '@heroicons/vue/24/outline';
|
||||
|
||||
const charactersStore = useCharactersStore()
|
||||
const {characters} = storeToRefs(charactersStore);
|
||||
const {reloadActivities} = charactersStore;
|
||||
|
||||
const addCharacter = () => {
|
||||
window.location.replace(mammonAddCharacterUrl);
|
||||
@@ -19,9 +16,9 @@ const addCharacter = () => {
|
||||
<div class="mb-4 border-b-1 flex justify-end">
|
||||
<button class="mb-2" @click="addCharacter">Add chacarcter</button>
|
||||
</div>
|
||||
<div v-for="character in characters" :key="character.characterId" class="flex items-center mb-2">
|
||||
<div v-for="character in charactersStore.characters" :key="character.characterId" class="flex items-center mb-2">
|
||||
<CharacterLabel class="grow" :character="character" />
|
||||
<button class="btn-icon" @click="reloadActivities(character.characterId)"><ArrowPathIcon /></button>
|
||||
<button class="btn-icon" @click="charactersStore.reloadActivities(character.characterId)"><ArrowPathIcon /></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,15 +2,16 @@
|
||||
import {RouterView} from 'vue-router';
|
||||
import {EditLedgerModal, useLedgersStore} from "@/ledger";
|
||||
import {ref} from "vue";
|
||||
import {processingApi} from "@/mammon";
|
||||
import {activityApi, processingApi} from "@/mammon";
|
||||
|
||||
const {refresh} = useLedgersStore();
|
||||
const ledgersStore = useLedgersStore();
|
||||
|
||||
const editLedgerModal = ref<typeof EditLedgerModal>();
|
||||
|
||||
const processActivities = async () => {
|
||||
await activityApi.fetchAllNewActivities();
|
||||
await processingApi.processNewActivities();
|
||||
await refresh();
|
||||
await ledgersStore.refresh();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import {routeNames} from '@/routes';
|
||||
<RouterLink :to="{name: routeNames.marketTypes}" class="tab">
|
||||
<span>Item Info</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/market/tracking" class="tab">
|
||||
<span>Tracking</span>
|
||||
<RouterLink to="/market/scan" class="tab">
|
||||
<span>Scan</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/market/acquisitions" class="tab">
|
||||
<span>Acquisitions</span>
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {ref, watch} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import log from "loglevel";
|
||||
import {findAllTransactionInLeger, Ledger, TransferTypes, useLedgersStore} from "@/ledger";
|
||||
import {findAllTransactionInLeger, useLedgerParam} from "@/ledger";
|
||||
import {computedAsync} from "@vueuse/core";
|
||||
import {TransactionResponse} from "@/generated/mammon";
|
||||
import {formatEveDate} from "@/formaters.ts";
|
||||
import {IskLabel} from "@/market";
|
||||
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
||||
import {TransferList, TransferTypes} from "@/transaction";
|
||||
import {Dropdown} from "@/components";
|
||||
import {CharacterLabel, useCharactersStore} from "@/characters";
|
||||
import {formatEveDate} from "@/formaters.ts";
|
||||
|
||||
const {findById, refresh} = useLedgersStore();
|
||||
|
||||
const ledger = ref<Ledger>();
|
||||
const {ledgerId} = useLedgerParam();
|
||||
const charactersStore = useCharactersStore();
|
||||
|
||||
const transactions = computedAsync<TransactionResponse[]>(async () => {
|
||||
if (ledger.value) {
|
||||
return await findAllTransactionInLeger(ledger.value.ledgerId);
|
||||
if (ledgerId.value) {
|
||||
return await findAllTransactionInLeger(ledgerId.value);
|
||||
}
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const getIskBalance = (transaction: TransactionResponse) => {
|
||||
const ledgerId = ledger.value?.ledgerId;
|
||||
const { sortedArray, headerProps } = useSort(computedAsync(() => Promise.all(transactions.value.map(async transaction => {
|
||||
const character = await charactersStore.findById(transaction.characterId);
|
||||
return {
|
||||
character,
|
||||
characterName: character?.name ?? "",
|
||||
transactionId: transaction.transactionId,
|
||||
description: transaction.description,
|
||||
date: new Date(transaction.datetime),
|
||||
balance: getIskBalance(transaction),
|
||||
transfers: transaction.transfers
|
||||
}
|
||||
})), []), { defaultSortKey: 'date', defaultSortDirection: 'desc' });
|
||||
|
||||
if (!ledgerId) {
|
||||
const getIskBalance = (transaction: TransactionResponse) => {
|
||||
if (!ledgerId.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -31,37 +42,65 @@ const getIskBalance = (transaction: TransactionResponse) => {
|
||||
|
||||
for (const transfer of transaction.transfers) {
|
||||
if (transfer.type === TransferTypes.Isk) {
|
||||
if (transfer.toLedgerId === ledgerId) {
|
||||
if (transfer.toLedgerId === ledgerId.value) {
|
||||
balance += transfer.amount;
|
||||
} else if (transfer.fromLedgerId === ledgerId) {
|
||||
} else if (transfer.fromLedgerId === ledgerId.value) {
|
||||
balance -= transfer.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
watch(useRoute(), async route => {
|
||||
if (route.params.ledgerId) {
|
||||
const id = typeof route.params.ledgerId === 'string' ? route.params.ledgerId : route.params.ledgerId[0];
|
||||
|
||||
await refresh() // FIXME
|
||||
|
||||
ledger.value = findById(id)
|
||||
log.info('Loaded ledger:', ledger.value);
|
||||
} else {
|
||||
ledger.value = undefined;
|
||||
log.info('No ledger to load');
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-end gap-2 mt-2" v-for="transaction in transactions" :key="transaction.transactionId">
|
||||
<span>{{formatEveDate(new Date(transaction.datetime))}}</span>
|
||||
<IskLabel :amount="getIskBalance(transaction)" />
|
||||
<span>{{transaction.description}}</span>
|
||||
</div>
|
||||
<VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
|
||||
<template #default="{ list }">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortableHeader v-bind="headerProps" sortKey="characterName">Character</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="date">Date</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="balance">Isk Change</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="description">Description</SortableHeader>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="t in list" :key="t.data.transactionId">
|
||||
<td>
|
||||
<CharacterLabel v-if="t.data.character" :character="t.data.character" />
|
||||
</td>
|
||||
<td>
|
||||
<Dropdown class="transfer-dropdown">
|
||||
<template #button>
|
||||
{{formatEveDate(t.data.date)}}
|
||||
</template>
|
||||
<TransferList :transfers="t.data.transfers" />
|
||||
</Dropdown>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<IskLabel :amount="t.data.balance" />
|
||||
</td>
|
||||
<td>{{t.data.description}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</VirtualScrollTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/style.css";
|
||||
|
||||
tr:hover>td>.transfer-dropdown :deep(>button) {
|
||||
@apply bg-slate-900;
|
||||
}
|
||||
|
||||
.transfer-dropdown :deep(>button) {
|
||||
@apply bg-slate-800 hover:bg-slate-900 border-none flex items-center w-full;
|
||||
}
|
||||
|
||||
.transfer-dropdown.dropdown-open :deep(>button) {
|
||||
@apply bg-slate-800 rounded-b-none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {EditLedgerModal, LedgerLabel, useLedgersStore} from "@/ledger";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {EditLedgerModal, Ledger, LedgerLabel, useLedgersStore} from "@/ledger";
|
||||
import {nextTick, ref} from "vue";
|
||||
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
|
||||
import {IskLabel} from "@/market";
|
||||
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
||||
|
||||
const {ledgers} = storeToRefs(useLedgersStore());
|
||||
const ledgersStore = useLedgersStore();
|
||||
|
||||
const { sortedArray, headerProps } = useSort<Ledger>(() => ledgersStore.ledgers);
|
||||
|
||||
const editModal = ref<typeof EditLedgerModal>();
|
||||
const editingLedgerId = ref("");
|
||||
@@ -21,13 +23,35 @@ const openEdit = async (ledgerId: string) => {
|
||||
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div v-for="ledger in ledgers" :key="ledger.ledgerId" class="flex items-center mb-2">
|
||||
<LedgerLabel :ledger="ledger" :link="true" />
|
||||
<div class="flex grow">
|
||||
<IskLabel class="ms-2" :amount="ledger.balance" />
|
||||
</div>
|
||||
<button class="btn-icon ms-2" @click="openEdit(ledger.ledgerId)"><PencilSquareIcon /></button>
|
||||
</div>
|
||||
<VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
|
||||
<template #default="{ list }">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortableHeader v-bind="headerProps" sortKey="name">Ledger</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="balance">Isk Balance</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="buttons" unsortable />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="l in list" :key="l.data.ledgerId">
|
||||
<td>
|
||||
<LedgerLabel :ledger="l.data" :link="true" />
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<IskLabel class="ms-2" :amount="l.data.balance" />
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button class="btn-icon ms-2" @click="openEdit(l.data.ledgerId)"><PencilSquareIcon /></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="text-center mt-4">
|
||||
<span>No ledgers found</span>
|
||||
</div>
|
||||
</template>
|
||||
</VirtualScrollTable>
|
||||
</div>
|
||||
<EditLedgerModal ref="editModal" :ledger-id="editingLedgerId" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import {RouterLink, RouterView} from 'vue-router';
|
||||
import {isMain, useLedgerParam} from "@/ledger";
|
||||
import {routeNames} from "@/routes.ts";
|
||||
|
||||
const {ledgerId, ledger} = useLedgerParam();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="ledger" class="mt-4">
|
||||
<div class="flex border-b-2 border-emerald-500">
|
||||
<RouterLink :to="{name: routeNames.viewLedgerBalance}" class="tab">
|
||||
<span>Balance</span>
|
||||
</RouterLink>
|
||||
<RouterLink v-if="isMain(ledger)" :to="{name: routeNames.listLedgerTransactions}" class="tab">
|
||||
<span>Transactions</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import {getLedgerBalance, useLedgerParam} from "@/ledger";
|
||||
import {computedAsync} from "@vueuse/core";
|
||||
import {BalanceResponse} from "@/generated/mammon";
|
||||
import {getMarketType, IskLabel, MarketTypeLabel} from "@/market";
|
||||
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
|
||||
|
||||
const {ledgerId} = useLedgerParam();
|
||||
|
||||
const balance = computedAsync<BalanceResponse>(async () => {
|
||||
if (ledgerId.value) {
|
||||
return await getLedgerBalance(ledgerId.value);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { sortedArray, headerProps } = useSort(computedAsync(async () => {
|
||||
const itemBalances = balance.value?.itemBalances;
|
||||
|
||||
if (!itemBalances) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await Promise.all(itemBalances.map(async i => {
|
||||
const item = await getMarketType(i.typeId);
|
||||
|
||||
return {
|
||||
...i,
|
||||
name: item.name
|
||||
};
|
||||
}));
|
||||
}, []));
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="balance" class="mt-4">
|
||||
<div class="flex justify-end w-full">
|
||||
<IskLabel class="mb-2" :amount="balance.iskBalance" />
|
||||
</div>
|
||||
<VirtualScrollTable :list="sortedArray" :itemHeight="33" bottom="1rem">
|
||||
<template #default="{ list }">
|
||||
<thead>
|
||||
<tr>
|
||||
<SortableHeader v-bind="headerProps" sortKey="name">Item</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="quantity">Balance</SortableHeader>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="i in list" :key="i.data.typeId">
|
||||
<td>
|
||||
<MarketTypeLabel :id="i.data.typeId" :name="i.data.name" />
|
||||
</td>
|
||||
<td class="text-right">{{i.data.quantity}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</VirtualScrollTable>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { MarketTypePrice, getMarketTypes, useApraisalStore } from "@/market";
|
||||
import { AcquiredType, AcquisitionResultTable, BuyModal, SellModal, useAcquiredTypesStore } from '@/market/acquisition';
|
||||
import { ref, watch } from 'vue';
|
||||
import {getMarketTypes, MarketTypePrice, useApraisalStore} from "@/market";
|
||||
import {AcquiredType, AcquisitionResultTable, BuyModal, SellModal, useAcquiredTypesStore} from '@/market/acquisition';
|
||||
import {ref, watch} from 'vue';
|
||||
import {activityApi, processingApi} from "@/mammon";
|
||||
|
||||
const buyModal = ref<typeof BuyModal>();
|
||||
const sellModal = ref<typeof SellModal>();
|
||||
@@ -10,7 +11,11 @@ const apraisalStore = useApraisalStore();
|
||||
const acquiredTypesStore = useAcquiredTypesStore();
|
||||
const items = ref<AcquiredType[]>([]);
|
||||
|
||||
const refresh = async () => await acquiredTypesStore.refresh();
|
||||
const refresh = async () => {
|
||||
await activityApi.fetchAllNewActivities();
|
||||
await processingApi.processNewActivities();
|
||||
await acquiredTypesStore.refresh();
|
||||
}
|
||||
|
||||
watch(() => acquiredTypesStore.acquiredTypes, async itms => {
|
||||
if (itms.length === 0) {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import {getMarketTypes, TaxInput, useMarketTaxStore} from "@/market";
|
||||
import {BuyModal} from '@/market/acquisition';
|
||||
import {ScanResult, ScanResultTable, toScanResult} from '@/market/scan';
|
||||
import {marketApi} from "@/mammon";
|
||||
import {useStorage} from "@vueuse/core";
|
||||
import {ref, watch} from 'vue';
|
||||
|
||||
const buyModal = ref<typeof BuyModal>();
|
||||
|
||||
const marketTaxStore = useMarketTaxStore();
|
||||
const days = useStorage('market-scan-days', 365);
|
||||
const items = ref<ScanResult[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const scan = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await marketApi.scanMarket(
|
||||
days.value,
|
||||
marketTaxStore.brokerFee / 100,
|
||||
marketTaxStore.scc / 100
|
||||
);
|
||||
const types = await getMarketTypes(data.map(r => r.marketTypeId));
|
||||
|
||||
items.value = data.flatMap(r => {
|
||||
const type = types.find(t => t.id === r.marketTypeId);
|
||||
|
||||
return type ? [toScanResult(r, type)] : [];
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch([days, () => marketTaxStore.brokerFee, () => marketTaxStore.scc], scan, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex mb-2 mt-4">
|
||||
<div class="flex justify-self-end ms-auto">
|
||||
<TaxInput />
|
||||
<div class="end">
|
||||
<span>Days: </span>
|
||||
<input type="number" min="1" max="365" step="1" v-model="days" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div v-if="loading" class="text-center mt-4">
|
||||
<span>Scanning market…</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<ScanResultTable :items="items" @buy="(type, buy, sell) => buyModal?.open(type, { 'Buy': buy, 'Sell': sell })" />
|
||||
<BuyModal ref="buyModal" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/style.css";
|
||||
div.end {
|
||||
@apply justify-self-end ms-2;
|
||||
}
|
||||
</style>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Modal, ProgressBar } from "@/components";
|
||||
import { MarketType, MarketTypeInput, MarketTypePrice, getHistory, getMarketTypes, useApraisalStore } from "@/market";
|
||||
import { BuyModal } from '@/market/acquisition';
|
||||
import { TrackingResult, TrackingResultTable, createResult, useMarketTrackingStore } from '@/market/tracking';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
|
||||
const buyModal = ref<typeof BuyModal>();
|
||||
|
||||
const item = ref<MarketType>();
|
||||
|
||||
const apraisalStore = useApraisalStore();
|
||||
const marketTrackingStore = useMarketTrackingStore();
|
||||
const items = ref<TrackingResult[]>([]);
|
||||
const addOrRelaod = async (type: MarketType) => {
|
||||
const typeID = type.id;
|
||||
const [history, price] = await Promise.all([
|
||||
getHistory(typeID),
|
||||
apraisalStore.getPrice(type)
|
||||
]);
|
||||
const itm = {
|
||||
type,
|
||||
history,
|
||||
buy: price.buy,
|
||||
sell: price.sell,
|
||||
orderCount: price.orderCount
|
||||
};
|
||||
|
||||
if (items.value.some(i => i.type.id === typeID)) {
|
||||
items.value = items.value.map(i => i.type.id === typeID ? itm : i);
|
||||
} else {
|
||||
items.value = [ ...items.value, itm];
|
||||
marketTrackingStore.addType(typeID);
|
||||
}
|
||||
}
|
||||
const addItem = async () => {
|
||||
if (!item.value) {
|
||||
// TODO error
|
||||
return;
|
||||
}
|
||||
|
||||
addOrRelaod(item.value);
|
||||
item.value = undefined;
|
||||
}
|
||||
const removeItem = (type: MarketType) => {
|
||||
items.value = items.value.filter(i => i.type.id !== type.id);
|
||||
marketTrackingStore.removeType(type.id);
|
||||
}
|
||||
|
||||
watch(() => marketTrackingStore.types, async t => {
|
||||
const typesToLoad = t.filter(t => !items.value.some(i => i.type.id === t));
|
||||
|
||||
if (typesToLoad.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prices = await apraisalStore.getPrices(await getMarketTypes(typesToLoad));
|
||||
|
||||
typesToLoad.forEach(async i => items.value.push(await createResult(i, prices.find(p => p.type.id === i) as MarketTypePrice)));
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid mb-2 mt-4">
|
||||
<div class="w-auto flex">
|
||||
<span>Item: </span>
|
||||
<MarketTypeInput class="ms-2" v-model="item" @submit="addItem"/>
|
||||
<button class="justify-self-end ms-2" @click="addItem">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="items.length > 0">
|
||||
<hr />
|
||||
<TrackingResultTable :items="items" @buy="(type, buy, sell) => buyModal?.open(type, { 'Buy': buy, 'Sell': sell })" @remove="removeItem" />
|
||||
<BuyModal ref="buyModal" />
|
||||
<Modal :open="items.length > 0 && items.length < marketTrackingStore.types.length">
|
||||
<div class="ms-auto me-auto mb-2 w-96">
|
||||
<ProgressBar :value="items.length" :total="marketTrackingStore.types.length" />
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
</template>
|
||||
@@ -1,14 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import {ClipboardButton} from '@/components';
|
||||
import {getMarketType, MarketType, MarketTypeInput, useApraisalStore} from "@/market";
|
||||
import {getMarketType, MarketType, MarketTypeInput, useApraisalStore, useMarketTaxStore} from "@/market";
|
||||
import {AcquisitionResultTable, BuyModal, useAcquiredTypesStore} from '@/market/acquisition';
|
||||
import {createResult, TrackingResultTable, useMarketTrackingStore} from '@/market/tracking';
|
||||
import {BookmarkIcon, BookmarkSlashIcon, ShoppingCartIcon} from '@heroicons/vue/24/outline';
|
||||
import {ScanResultTable, toScanResult} from '@/market/scan';
|
||||
import {marketApi} from "@/mammon";
|
||||
import {ShoppingCartIcon} from '@heroicons/vue/24/outline';
|
||||
import log from "loglevel";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {routeNames} from "@/routes";
|
||||
import {computedAsync} from "@vueuse/core";
|
||||
import {computedAsync, useStorage} from "@vueuse/core";
|
||||
|
||||
const buyModal = ref<typeof BuyModal>();
|
||||
|
||||
@@ -18,12 +19,25 @@ const item = ref<MarketType>();
|
||||
const inputItem = ref<MarketType>();
|
||||
|
||||
const apraisalStore = useApraisalStore();
|
||||
const marketTaxStore = useMarketTaxStore();
|
||||
const days = useStorage('market-scan-days', 365);
|
||||
const price = computedAsync(() => item.value ? apraisalStore.getPrice(item.value) : undefined);
|
||||
const marketTrackingStore = useMarketTrackingStore();
|
||||
const result = computedAsync(async () => item.value && price.value ? await createResult(item.value?.id, price.value) : undefined);
|
||||
const result = computedAsync(async () => {
|
||||
if (!item.value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { data } = await marketApi.scanMarketType(
|
||||
item.value.id,
|
||||
days.value,
|
||||
marketTaxStore.brokerFee / 100,
|
||||
marketTaxStore.scc / 100
|
||||
);
|
||||
|
||||
return toScanResult(data, item.value);
|
||||
});
|
||||
const acquiredTypesStore = useAcquiredTypesStore();
|
||||
|
||||
const isTracked = computed(() => item.value ? marketTrackingStore.types.includes(item.value.id) : false);
|
||||
const acquisitions = computed(() => {
|
||||
const p = price.value;
|
||||
|
||||
@@ -36,17 +50,6 @@ const acquisitions = computed(() => {
|
||||
sell: p.sell
|
||||
}));
|
||||
});
|
||||
const toogleTracking = () => {
|
||||
if (!item.value) {
|
||||
return;
|
||||
}
|
||||
if (isTracked.value) {
|
||||
marketTrackingStore.removeType(item.value.id);
|
||||
} else {
|
||||
marketTrackingStore.addType(item.value.id);
|
||||
}
|
||||
}
|
||||
|
||||
const view = () => {
|
||||
if (!inputItem.value) {
|
||||
return;
|
||||
@@ -93,10 +96,6 @@ watch(useRoute(), async route => {
|
||||
<div class="ms-auto">
|
||||
<ClipboardButton class="ms-1" :value="item.name" />
|
||||
<button v-if="price" class="btn-icon ms-1" title="Add acquisitions" @click="buyModal?.open(item, { 'Buy': price.buy, 'Sell': price.sell })"><ShoppingCartIcon /></button>
|
||||
<button class="btn-icon ms-1" :title="isTracked ? 'Untrack' : 'Track'" @click="toogleTracking">
|
||||
<BookmarkSlashIcon v-if="isTracked" />
|
||||
<BookmarkIcon v-else />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="item.description" class="text-sm">{{ item.description }}</p>
|
||||
@@ -104,7 +103,7 @@ watch(useRoute(), async route => {
|
||||
</div>
|
||||
<div v-if="result" class="mb-4">
|
||||
<span>Market Info:</span>
|
||||
<TrackingResultTable :items="[result]" infoOnly :ignoredColums="['name', 'acquisitions']" />
|
||||
<ScanResultTable :items="[result]" infoOnly :ignoredColums="['name', 'acquisitions']" />
|
||||
</div>
|
||||
<div v-if="acquisitions && acquisitions.length > 0">
|
||||
<span>Acquisitions:</span>
|
||||
@@ -115,7 +114,7 @@ watch(useRoute(), async route => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
|
||||
img.type-image {
|
||||
width: 64px;
|
||||
|
||||
@@ -4,37 +4,35 @@ import {useRoute} from "vue-router";
|
||||
import {computed, ref, watch, watchEffect} from "vue";
|
||||
import log from "loglevel";
|
||||
import {
|
||||
findCharacterRuleBookByCharacterId,
|
||||
RuleBook,
|
||||
setCharacterRuleBookForCharacter,
|
||||
useCharacterRuleBooksStore,
|
||||
useRuleBooksStore
|
||||
} from "@/rules";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {isMain, Ledger, LedgerSelect, systemLedger, useLedgersStore} from "@/ledger";
|
||||
|
||||
type Bindings = { [key: string]: Ledger; };
|
||||
|
||||
const ruleBookStore = useRuleBooksStore();
|
||||
const {findById: findCharacterRuleBookById} = ruleBookStore;
|
||||
const {ruleBooks} = storeToRefs(ruleBookStore);
|
||||
const {findById: findCharacterById} = useCharactersStore();
|
||||
const {ledgers} = storeToRefs(useLedgersStore());
|
||||
const characterRuleBooksStore = useCharacterRuleBooksStore();
|
||||
const charactersStore = useCharactersStore();
|
||||
const ledgersStore = useLedgersStore();
|
||||
|
||||
const ledgersToUse = computed(() => [systemLedger, ...ledgers.value.filter(isMain)]);
|
||||
const ledgersToUse = computed(() => [systemLedger, ...ledgersStore.ledgers.filter(isMain)]);
|
||||
|
||||
const character = ref<Character>();
|
||||
const ruleBook = ref<RuleBook>();
|
||||
const bindings = ref<Bindings>({});
|
||||
const ledgerRefs = computed<string[]>(() => ruleBook.value?.ledgerRefs ?? [])
|
||||
|
||||
watchEffect(async () => {
|
||||
const characterId = character.value?.characterId;
|
||||
|
||||
if (characterId) {
|
||||
const characterRuleBook = await findCharacterRuleBookByCharacterId(characterId);
|
||||
const characterRuleBook = characterRuleBooksStore.findByCharacterId(characterId);
|
||||
|
||||
ruleBook.value = findCharacterRuleBookById(characterRuleBook.ruleBookId);
|
||||
ruleBook.value = ruleBookStore.findById(characterRuleBook?.ruleBook.ruleBookId ?? '');
|
||||
bindings.value = Object.fromEntries(
|
||||
Object.entries(characterRuleBook.bindings)
|
||||
Object.entries(characterRuleBook?.bindings ?? {})
|
||||
.map(([key, id]) => [key, ledgersToUse.value.find(l => l.ledgerId === id) ?? systemLedger])
|
||||
);
|
||||
}
|
||||
@@ -45,7 +43,7 @@ const save = () => {
|
||||
const ruleBookId = ruleBook.value?.ruleBookId;
|
||||
|
||||
if (characterId && ruleBookId) {
|
||||
setCharacterRuleBookForCharacter(characterId, {
|
||||
characterRuleBooksStore.setForCharacter(characterId, {
|
||||
ruleBookId,
|
||||
bindings: Object.fromEntries(
|
||||
Object.entries(bindings.value)
|
||||
@@ -59,7 +57,7 @@ watch(useRoute(), async route => {
|
||||
if (route.params.characterId) {
|
||||
const id = parseInt(typeof route.params.characterId === 'string' ? route.params.characterId : route.params.characterId[0]);
|
||||
|
||||
character.value = await findCharacterById(id);
|
||||
character.value = await charactersStore.findById(id);
|
||||
log.info('Loaded character:', character.value);
|
||||
} else {
|
||||
character.value = undefined;
|
||||
@@ -79,15 +77,15 @@ watch(useRoute(), async route => {
|
||||
<div class="flex-col border-b-1">
|
||||
Rule Book:
|
||||
<select class="me-2 mb-2 w-50" v-model="ruleBook">
|
||||
<option v-for="rb in ruleBooks" :key="rb.ruleBookId" :value="rb">{{ rb.name }}</option>
|
||||
<option v-for="rb in ruleBookStore.ruleBooks" :key="rb.ruleBookId" :value="rb">{{ rb.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-col border-b-1">
|
||||
Ledger Bindings:
|
||||
<div class="flex flex-wrap items-center mb-2 mt-2">
|
||||
<div class="me-2" v-for="ref in ruleBook.ledgerRefs" :ref="ref">
|
||||
<div class="me-2" v-for="ref in ledgerRefs" :ref="ref">
|
||||
<span class="me-1">{{ref}}:</span>
|
||||
<LedgerSelect :ledgers="ledgersToUse" :modelValue="bindings[ref] ?? systemLedger" @update:modelValue="value => bindings[ref] = value" />
|
||||
<LedgerSelect :ledgers="ledgersToUse" :modelValue="bindings[ref] ?? systemLedger" @update:modelValue="value => { if (value) bindings[ref] = value }" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {ref, watch} from "vue";
|
||||
import {useDebounceFn} from "@vueuse/core";
|
||||
import {useDebounceFn, useEventListener} from "@vueuse/core";
|
||||
import log from "loglevel";
|
||||
import {activityTypes, RuleInput, Rules, useRuleBooksStore} from "@/rules";
|
||||
import {ScriptEditor, useRuleBooksStore} from "@/rules";
|
||||
import {PlusIcon, TrashIcon} from "@heroicons/vue/24/outline";
|
||||
import {routeNames} from "@/routes";
|
||||
import {Dropdown} from "@/components";
|
||||
import {SliderCheckbox} from "@/components";
|
||||
|
||||
const ruleBookId = ref<string>();
|
||||
const name = ref<string>('');
|
||||
const usedForAcquisitions = ref<boolean>(false);
|
||||
const ledgerRefs = ref<string[]>([]);
|
||||
const rules = ref<Rules>({});
|
||||
const script = ref<string>('');
|
||||
|
||||
const {findById, create, update, refresh} = useRuleBooksStore();
|
||||
const ruleBooksStore = useRuleBooksStore();
|
||||
const router = useRouter();
|
||||
|
||||
const save = async () => {
|
||||
if (!ruleBookId.value) {
|
||||
const created = await create({
|
||||
const created = await ruleBooksStore.create({
|
||||
name: name.value,
|
||||
usedForAcquisitions: usedForAcquisitions.value,
|
||||
ledgerRefs: ledgerRefs.value,
|
||||
rules: rules.value
|
||||
script: script.value
|
||||
})
|
||||
await router.push({ name: routeNames.editRuleBook, params: {ruleBookId: created.ruleBookId}})
|
||||
|
||||
} else {
|
||||
await update(ruleBookId.value, {
|
||||
await ruleBooksStore.update(ruleBookId.value, {
|
||||
name: name.value,
|
||||
usedForAcquisitions: usedForAcquisitions.value,
|
||||
ledgerRefs: ledgerRefs.value,
|
||||
rules: rules.value
|
||||
script: script.value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEventListener(window, 'keydown', (event: KeyboardEvent) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
|
||||
event.preventDefault();
|
||||
save();
|
||||
}
|
||||
}, {capture: true});
|
||||
|
||||
const addLedgerRef = () => {
|
||||
ledgerRefs.value = [...ledgerRefs.value, '']
|
||||
}
|
||||
@@ -47,56 +57,57 @@ const removeLedgerRef = (index: number) => {
|
||||
|
||||
watch(useRoute(), async route => {
|
||||
if (route.params.ruleBookId) {
|
||||
const promise = refresh(); // FIXME don't call refresh
|
||||
const promise = ruleBooksStore.refresh(); // FIXME don't call refresh
|
||||
|
||||
const id = typeof route.params.ruleBookId === 'string' ? route.params.ruleBookId : route.params.ruleBookId[0];
|
||||
|
||||
await promise;
|
||||
|
||||
const ruleBook = findById(id);
|
||||
const ruleBook = ruleBooksStore.findById(id);
|
||||
|
||||
ruleBookId.value = id;
|
||||
name.value = ruleBook?.name ?? '';
|
||||
ledgerRefs.value = [...ruleBook?.ledgerRefs];
|
||||
rules.value = {...ruleBook?.rules}; // TODO fully clone rules
|
||||
usedForAcquisitions.value = ruleBook?.usedForAcquisitions ?? false;
|
||||
ledgerRefs.value = [...(ruleBook?.ledgerRefs ?? [])];
|
||||
script.value = ruleBook?.script ?? '';
|
||||
log.info('Loaded rule book:', ruleBook);
|
||||
} else {
|
||||
ruleBookId.value = undefined;
|
||||
name.value = '';
|
||||
usedForAcquisitions.value = false;
|
||||
ledgerRefs.value = [];
|
||||
rules.value = {};
|
||||
script.value = '';
|
||||
log.info('No rule book to load');
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid mb-2 mt-4">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col mb-2 mt-4 h-[calc(100vh-4.5rem)]">
|
||||
<div class="flex flex-col grow min-h-0">
|
||||
<div class="flex grow border-b-1">
|
||||
Name:
|
||||
<input class="mb-2 ms-2" type="text" v-model="name" />
|
||||
<label class="flex items-center ms-2 mb-2">
|
||||
<SliderCheckbox class="me-2" v-model="usedForAcquisitions" />
|
||||
Used for acquisitions
|
||||
</label>
|
||||
</div>
|
||||
<div class="border-b-1">
|
||||
Ledgers References:
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div class="flex flex-wrap items-center mt-2">
|
||||
<div class="flex items-center mb-2" v-for="(ledgerRef, index) in ledgerRefs" :key="index">
|
||||
<input class="me-1" type="text" :value="ledgerRefs[index]" @input="updateLedgerRef(index, ($event.target as HTMLInputElement).value)" />
|
||||
<button class="btn-icon me-2" @click="addLedgerRef"><TrashIcon /></button>
|
||||
<input class="me-1" type="text" :value="ledgerRef" @input="updateLedgerRef(index, ($event.target as HTMLInputElement).value)" />
|
||||
<button class="btn-icon me-2" @click="removeLedgerRef(index)"><TrashIcon /></button>
|
||||
</div>
|
||||
<div class="flex items-center mb-2">
|
||||
<button class="btn-icon" @click="removeLedgerRef(index)"><PlusIcon /></button>
|
||||
<button class="btn-icon" @click="addLedgerRef"><PlusIcon /></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col grow border-b-1" v-for="activityType in activityTypes" :key="activityType.key">
|
||||
<Dropdown :inline="true" :autoClose="false" class="rule-dropdown">
|
||||
<template #button>
|
||||
<span>{{ activityType.name }}</span>
|
||||
</template>
|
||||
<RuleInput :ledgerRefs="ledgerRefs" v-model="rules[activityType.key]" />
|
||||
</Dropdown>
|
||||
|
||||
<div class="flex flex-col grow min-h-0 border-b-1">
|
||||
Script:
|
||||
<ScriptEditor class="mt-2 mb-2" v-model="script" :ledgerRefs="ledgerRefs" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 justify-end flex">
|
||||
@@ -105,16 +116,4 @@ watch(useRoute(), async route => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.rule-dropdown :deep(>button) {
|
||||
@apply bg-slate-800 hover:bg-slate-800 border-none flex items-center w-full;
|
||||
}
|
||||
|
||||
.rule-dropdown.dropdown-open :deep(>button) {
|
||||
@apply bg-slate-800 rounded-b-none;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
@@ -1,18 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import {storeToRefs} from "pinia";
|
||||
import {CharacterLabel, useCharactersStore} from "@/characters";
|
||||
import {Character, CharacterLabel} from "@/characters";
|
||||
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
|
||||
import {routeNames} from "@/routes";
|
||||
import {CharacterRuleBook, useCharacterRuleBooksStore} from "@/rules";
|
||||
import {routeNames} from "@/routes.ts";
|
||||
import {SortableHeader, useSort} from "@/components/table";
|
||||
|
||||
const {characters} = storeToRefs(useCharactersStore());
|
||||
type CharacterRuleBookView = {
|
||||
character: Character;
|
||||
characterName: string;
|
||||
ruleBookName: string;
|
||||
}
|
||||
|
||||
const characterRuleBooksStore = useCharacterRuleBooksStore();
|
||||
|
||||
const { sortedArray, headerProps } = useSort<CharacterRuleBookView>(() => characterRuleBooksStore.characterRuleBooks.map((characterRuleBook: CharacterRuleBook): CharacterRuleBookView => ({
|
||||
character: characterRuleBook.character,
|
||||
characterName: characterRuleBook.character.name,
|
||||
ruleBookName: characterRuleBook.ruleBook.name
|
||||
})))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid mb-2 mt-4">
|
||||
<div v-for="character in characters" :key="character.characterId" class="flex items-center mb-2">
|
||||
<CharacterLabel class="flex grow" :character="character" />
|
||||
<RouterLink class="btn-icon ms-2" :to="{ name: routeNames.editCharacterRulebook, params: { characterId: character.characterId } }"><PencilSquareIcon /></RouterLink>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<SortableHeader v-bind="headerProps" sortKey="characterName">Character</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="ruleBookName">Rule Book</SortableHeader>
|
||||
<SortableHeader v-bind="headerProps" sortKey="buttons" unsortable />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="characterRuleBookView in sortedArray" :key="characterRuleBookView.character.characterId" >
|
||||
<td>
|
||||
<CharacterLabel :character="characterRuleBookView.character" />
|
||||
</td>
|
||||
<td>{{characterRuleBookView.ruleBookName}}</td>
|
||||
<td class="text-right">
|
||||
<RouterLink class="btn-icon" :to="{ name: routeNames.editCharacterRulebook, params: { characterId: characterRuleBookView.character.characterId } }"><PencilSquareIcon /></RouterLink>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,10 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import {storeToRefs} from "pinia";
|
||||
import {PencilSquareIcon, TrashIcon} from "@heroicons/vue/24/outline";
|
||||
import {useRuleBooksStore} from "@/rules";
|
||||
import {DocumentDuplicateIcon, PencilSquareIcon, TrashIcon} from "@heroicons/vue/24/outline";
|
||||
import {confirm} from "@/confirm";
|
||||
import {RuleBook, useRuleBooksStore} from "@/rules";
|
||||
import {routeNames} from "@/routes";
|
||||
|
||||
const {ruleBooks} = storeToRefs(useRuleBooksStore());
|
||||
const ruleBooksStore = useRuleBooksStore();
|
||||
|
||||
const duplicate = async (ruleBook: RuleBook) => {
|
||||
if (await confirm({title: "Duplicate Rule Book", message: `Duplicate ${ruleBook.name}?`, confirmLabel: "Duplicate"})) {
|
||||
await ruleBooksStore.duplicate(ruleBook);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (ruleBook: RuleBook) => {
|
||||
if (await confirm({title: "Delete Rule Book", message: `Delete ${ruleBook.name}?`, confirmLabel: "Delete", danger: true})) {
|
||||
await ruleBooksStore.remove(ruleBook.ruleBookId);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -13,10 +25,11 @@ const {ruleBooks} = storeToRefs(useRuleBooksStore());
|
||||
<div class="flex justify-end border-b-1">
|
||||
<RouterLink class="button mb-2 ms-2" :to="{ name: routeNames.newRuleBook}">New Rule Book</RouterLink>
|
||||
</div>
|
||||
<div v-for="ruleBook in ruleBooks" :key="ruleBook.ruleBookId" class="flex items-center mt-2">
|
||||
<div v-for="ruleBook in ruleBooksStore.ruleBooks" :key="ruleBook.ruleBookId" class="flex items-center mt-2">
|
||||
<span class="flex grow me-2">{{ruleBook.name}}</span>
|
||||
<RouterLink class="btn-icon me-1" :to="{ name: routeNames.editRuleBook, params: { ruleBookId: ruleBook.ruleBookId } }"><PencilSquareIcon /></RouterLink>
|
||||
<button class="btn-icon"><TrashIcon /></button>
|
||||
<button class="btn-icon me-1" @click="duplicate(ruleBook)"><DocumentDuplicateIcon /></button>
|
||||
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="remove(ruleBook)"><TrashIcon /></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,7 +13,7 @@ const modelValue = defineModel({ default: false });
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
input:checked ~ span:last-child {
|
||||
--tw-translate-x: 1.75rem;
|
||||
}
|
||||
|
||||
+11
-5
@@ -3,7 +3,9 @@ import {RouteRecordRaw} from 'vue-router';
|
||||
export const routeNames = {
|
||||
home: 'home',
|
||||
callback: 'callback',
|
||||
listLedgerTransactions: 'list-ledger-tTransactions',
|
||||
viewLedger: 'view-ledger',
|
||||
viewLedgerBalance: 'view-ledger-balance',
|
||||
listLedgerTransactions: 'list-ledger-transactions',
|
||||
listRuleBooks: 'list-rule-books',
|
||||
newRuleBook: 'new-rule-book',
|
||||
editRuleBook: 'edit-rule-book',
|
||||
@@ -18,11 +20,15 @@ export const routes: RouteRecordRaw[] = [
|
||||
|
||||
{path: '/ledgers', component: () => import('@/pages/Ledgers.vue'), children: [
|
||||
{path: '', component: () => import('@/pages/ledger/ListLedgers.vue')},
|
||||
{path: ':ledgerId/transactions', name: routeNames.listLedgerTransactions, component: () => import('@/pages/ledger/ListLedgerTransactions.vue')},
|
||||
{path: ':ledgerId', component: () => import('./pages/ledger/ViewLedger.vue'), children: [
|
||||
{path: '', name: routeNames.viewLedger, redirect: {name: routeNames.viewLedgerBalance}},
|
||||
{path: 'balance', name: routeNames.viewLedgerBalance, component: () => import('@/pages/ledger/ViewLedgerBalance.vue')},
|
||||
{path: 'transactions', name: routeNames.listLedgerTransactions, component: () => import('@/pages/ledger/ListLedgerTransactions.vue')},
|
||||
]},
|
||||
]},
|
||||
|
||||
{path: '/rules', component: () => import('@/pages/Rules.vue'), children: [
|
||||
{path: '', redirect: '/rule-books'},
|
||||
{path: '', redirect: {name: routeNames.listRuleBooks}},
|
||||
{path: '/rule-books', children: [
|
||||
{path: '', name: routeNames.listRuleBooks, component: () => import('@/pages/rules/ListRuleBooks.vue')},
|
||||
{path: 'new', name: routeNames.newRuleBook, component: () => import('@/pages/rules/EditRuleBook.vue')},
|
||||
@@ -35,9 +41,9 @@ export const routes: RouteRecordRaw[] = [
|
||||
]},
|
||||
|
||||
{path: '/market', component: () => import('@/pages/Market.vue'), children: [
|
||||
{path: '', redirect: '/market/types'},
|
||||
{path: '', redirect: {name: routeNames.marketTypes}},
|
||||
{path: 'types/:type?', name: routeNames.marketTypes, component: () => import('@/pages/market/TypeInfo.vue')},
|
||||
{path: 'tracking', component: () => import('@/pages/market/Tracking.vue')},
|
||||
{path: 'scan', component: () => import('@/pages/market/Scan.vue')},
|
||||
{path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')},
|
||||
]},
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {RuleClauseResponse} from "@/generated/mammon";
|
||||
import {computed, watch} from "vue";
|
||||
import {systemLedgerRef} from "@/ledger";
|
||||
import {ratesTypes} from "@/rules/rules.ts";
|
||||
|
||||
interface Props {
|
||||
ledgerRefs: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const rule = defineModel<RuleClauseResponse>({ default: {
|
||||
rate: ratesTypes.None,
|
||||
fromLedgerRef: systemLedgerRef,
|
||||
toLedgerRef: systemLedgerRef,
|
||||
}});
|
||||
|
||||
const ledgerRefsWithSystem = computed<string[]>(() => [systemLedgerRef, ...props.ledgerRefs])
|
||||
|
||||
watch(ledgerRefsWithSystem, (newVal, oldVal) => {
|
||||
if (newVal.length !== oldVal.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rule.value.fromLedgerRef && rule.value.fromLedgerRef !== systemLedgerRef) {
|
||||
rule.value.fromLedgerRef = newVal[oldVal.findIndex(v => v === rule.value.fromLedgerRef)]
|
||||
}
|
||||
if (rule.value.toLedgerRef && rule.value.toLedgerRef !== systemLedgerRef) {
|
||||
rule.value.toLedgerRef = newVal[oldVal.findIndex(v => v === rule.value.toLedgerRef)]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
From:
|
||||
<select class="me-2 grow" v-model="rule.fromLedgerRef" :class="{'system-ledger': rule.fromLedgerRef === systemLedgerRef}">
|
||||
<option v-for="l in ledgerRefsWithSystem" :key="l" :value="l" :class="{'system-ledger': l === systemLedgerRef}">{{ l }}</option>
|
||||
</select>
|
||||
To:
|
||||
<select class="me-2 grow" v-model="rule.toLedgerRef" :class="{'system-ledger': rule.toLedgerRef === systemLedgerRef}">
|
||||
<option v-for="l in ledgerRefsWithSystem" :key="l" :value="l" :class="{'system-ledger': l === systemLedgerRef}">{{ l }}</option>
|
||||
</select>
|
||||
At:
|
||||
<select class="me-2 grow" v-model="rule.rate">
|
||||
<option v-for="rateType in ratesTypes" :key="rateType.key" :value="rateType.key">{{ rateType.name }}</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.system-ledger {
|
||||
@apply text-emerald-400;
|
||||
}
|
||||
</style>
|
||||
@@ -1,76 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {RuleClauseResponse, RuleClauseResponseRateEnum, RuleResponse} from "@/generated/mammon";
|
||||
import RuleClauseInput from "@/rules/RuleClauseInput.vue";
|
||||
import {computed, useTemplateRef} from "vue";
|
||||
import {Bars4Icon, PlusIcon, TrashIcon} from '@heroicons/vue/24/outline';
|
||||
import {useSortable} from "@vueuse/integrations/useSortable";
|
||||
import {systemLedgerRef} from "@/ledger";
|
||||
|
||||
interface Props {
|
||||
ledgerRefs: string[];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const rule = defineModel<RuleResponse>({default: {clauses:[]}});
|
||||
const clauses = computed<RuleClauseResponse[]>({
|
||||
get: () => rule.value && rule.value.clauses ? rule.value.clauses : [],
|
||||
set: value => rule.value = {clauses: value}
|
||||
})
|
||||
|
||||
const addClause = () => {
|
||||
clauses.value = [...clauses.value, {
|
||||
rate: RuleClauseResponseRateEnum.None,
|
||||
fromLedgerRef: systemLedgerRef,
|
||||
toLedgerRef: systemLedgerRef
|
||||
}]
|
||||
}
|
||||
|
||||
const setClause = (index: number, clause?: RuleClauseResponse) => {
|
||||
if (!clause) {
|
||||
return;
|
||||
}
|
||||
clauses.value = clauses.value.with(index, clause)
|
||||
}
|
||||
|
||||
const removeClause = (index: number) => {
|
||||
clauses.value = clauses.value.toSpliced(index, 1)
|
||||
}
|
||||
|
||||
const sortableContainer = useTemplateRef('sortable-container')
|
||||
useSortable(sortableContainer, clauses, { handle: '.sortable-handle'});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col">
|
||||
<div ref="sortable-container" class="flex-col">
|
||||
<div class="flex items-end gap-2 mt-2" v-for="(clause, index) in clauses" :key="index">
|
||||
<span class="sortable-handle flex">
|
||||
<Bars4Icon class="w-6"/>
|
||||
</span>
|
||||
<RuleClauseInput :ledgerRefs="ledgerRefs" :modelValue="clause" @update:modelValue="v => setClause(index, v)" />
|
||||
<button class="btn-icon" @click="removeClause(index)"><TrashIcon /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mb-2 mt-2">
|
||||
<button class="btn-icon" @click="addClause"><PlusIcon /></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
|
||||
.sortable-handle {
|
||||
@apply cursor-grab;
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
@apply cursor-grabbing;
|
||||
}
|
||||
|
||||
.sortable-chosen .sortable-handle {
|
||||
@apply cursor-grabbing;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import * as monaco from 'monaco-editor';
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
||||
import {onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
||||
import {fetchScriptDefinitions} from './rules';
|
||||
|
||||
(self as unknown as { MonacoEnvironment: { getWorker(workerId: string, label: string): Worker } }).MonacoEnvironment = {
|
||||
getWorker(_workerId: string, label: string) {
|
||||
if (label === 'typescript' || label === 'javascript') {
|
||||
return new tsWorker();
|
||||
}
|
||||
return new editorWorker();
|
||||
}
|
||||
};
|
||||
|
||||
let extraLibLoaded = false;
|
||||
|
||||
const loadScriptDefinitions = async () => {
|
||||
if (extraLibLoaded) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const definitions = await fetchScriptDefinitions();
|
||||
monaco.typescript.javascriptDefaults.addExtraLib(definitions, 'ts:rule-runner.d.ts');
|
||||
extraLibLoaded = true;
|
||||
} catch {
|
||||
// type definitions are optional — the editor still works without autocomplete
|
||||
}
|
||||
};
|
||||
|
||||
const props = defineProps<{ ledgerRefs?: string[] }>();
|
||||
|
||||
let ledgersLib: monaco.IDisposable | undefined;
|
||||
|
||||
const updateLedgerRefs = (refs: readonly string[]) => {
|
||||
ledgersLib?.dispose();
|
||||
const members = refs
|
||||
.filter(ref => ref && ref !== 'system')
|
||||
.map(ref => ` readonly ${JSON.stringify(ref)}: Ledger;`)
|
||||
.join('\n');
|
||||
ledgersLib = monaco.typescript.javascriptDefaults.addExtraLib(
|
||||
`declare interface Ledgers {\n${members}\n}\n`,
|
||||
'ts:rule-runner.ledgers.d.ts'
|
||||
);
|
||||
};
|
||||
|
||||
const model = defineModel<string>({default: ''});
|
||||
const container = ref<HTMLElement>();
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | undefined;
|
||||
|
||||
onMounted(async () => {
|
||||
await loadScriptDefinitions();
|
||||
updateLedgerRefs(props.ledgerRefs ?? []);
|
||||
|
||||
if (!container.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor = monaco.editor.create(container.value, {
|
||||
value: model.value,
|
||||
language: 'javascript',
|
||||
theme: 'vs-dark',
|
||||
automaticLayout: true,
|
||||
minimap: {enabled: false},
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 13,
|
||||
tabSize: 2,
|
||||
});
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
const value = editor!.getValue();
|
||||
|
||||
if (value !== model.value) {
|
||||
model.value = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
watch(model, value => {
|
||||
if (editor && value !== editor.getValue()) {
|
||||
editor.setValue(value ?? '');
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.ledgerRefs, refs => updateLedgerRefs(refs ?? []), {deep: true});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor?.dispose();
|
||||
ledgersLib?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container" class="script-editor"></div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.script-editor {
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
min-height: 12rem;
|
||||
}
|
||||
</style>
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
export * from "./rules";
|
||||
|
||||
export {default as RuleInput} from './RuleInput.vue';
|
||||
export {default as ScriptEditor} from './ScriptEditor.vue';
|
||||
+43
-32
@@ -3,36 +3,12 @@ import {
|
||||
CharacterRuleBookResponse,
|
||||
CreateRuleBookRequest,
|
||||
RuleBookResponse,
|
||||
RuleClauseResponseRateEnum,
|
||||
RuleResponse,
|
||||
SetCharacterRuleBookRequest
|
||||
} from "@/generated/mammon";
|
||||
import {defineStore} from "pinia";
|
||||
import {ref, triggerRef} from "vue";
|
||||
|
||||
export const activityTypes = {
|
||||
itemBought: {key: "ITEM_BOUGHT", name: "Item Bought"},
|
||||
itemSold: {key: "ITEM_SOLD", name: "Item Sold"},
|
||||
itemAcquiredManually: {key: "ITEM_ACQUIRED_MANUALLY", name: "Item Acquired Manually"},
|
||||
itemConsumedManually: {key: "ITEM_CONSUME_MANUALLY", name: "Item Consumed Manually"},
|
||||
// bountyEarned: {id: "BOUNTY_EARNED", name: "Bounty Earned"},
|
||||
// itemManufactured: {id: "ITEM_MANUFACTURED", name: "Item Manufactured"}
|
||||
} as const;
|
||||
|
||||
export type Activity = { key: ActivityType, name: string }
|
||||
export type ActivityType = typeof activityTypes[keyof typeof activityTypes]['key'];
|
||||
export type Rules = { [key: ActivityType]: RuleResponse; };
|
||||
export type RuleBook = RuleBookResponse & { rules: Rules }
|
||||
|
||||
export const ratesTypes = {
|
||||
None: {key: "NONE", name: "0 ISK"},
|
||||
Value: {key: "VALUE", name: "Value"},
|
||||
JitaBuy: {key: "JITA_BUY", name: "Jita Buy Order"},
|
||||
JitaSell: {key: "JITA_SELL", name: "Jita Sell Order"},
|
||||
EveEstimate: {key: "EVE_ESTIMATE", name: "Eve Estimate"},
|
||||
} as const;
|
||||
|
||||
export type Rate = { key: RuleClauseResponseRateEnum, name: string }
|
||||
export type RuleBook = RuleBookResponse;
|
||||
|
||||
export const useRuleBooksStore = defineStore('rule-books', () => {
|
||||
const ruleBooks = ref<RuleBook[]>([]);
|
||||
@@ -56,17 +32,52 @@ export const useRuleBooksStore = defineStore('rule-books', () => {
|
||||
const findById = (ruleBookId: string): RuleBook | undefined => ruleBooks.value.find(rb => rb.ruleBookId === ruleBookId);
|
||||
const create = (ruleBook: CreateRuleBookRequest) => ruleBookApi.createRuleBook(ruleBook).then(response => addRuleBook(response.data));
|
||||
const update = (ruleBookId: string, ruleBook: CreateRuleBookRequest) => ruleBookApi.updateRuleBook(ruleBookId, ruleBook).then(response => replaceRuleBook(response.data));
|
||||
const duplicate = (ruleBook: RuleBook) => create({
|
||||
name: `${ruleBook.name} (copy)`,
|
||||
usedForAcquisitions: ruleBook.usedForAcquisitions,
|
||||
ledgerRefs: [...ruleBook.ledgerRefs],
|
||||
script: ruleBook.script,
|
||||
});
|
||||
|
||||
const refresh = () => ruleBookApi.findAllRuleBooks().then(response => ruleBooks.value = response.data as RuleBook[]);
|
||||
const remove = (ruleBookId: string) => ruleBookApi.deleteRuleBook(ruleBookId).then(() => {
|
||||
ruleBooks.value = ruleBooks.value.filter(rb => rb.ruleBookId !== ruleBookId);
|
||||
});
|
||||
|
||||
const refresh = () => ruleBookApi.findAllRuleBooks().then(response => ruleBooks.value = response.data);
|
||||
|
||||
refresh();
|
||||
|
||||
return {ruleBooks, findById, create, update, refresh};
|
||||
return {ruleBooks, findById, create, update, duplicate, remove, refresh};
|
||||
})
|
||||
|
||||
export const findCharacterRuleBookByCharacterId = (characterId: number): Promise<CharacterRuleBookResponse> => characterRuleBookApi.findCharacterRuleBookByCharacterId(characterId)
|
||||
.then(response => response.data)
|
||||
.catch(() => ({characterId, rules: {}}));
|
||||
export type CharacterRuleBook = CharacterRuleBookResponse;
|
||||
|
||||
export const setCharacterRuleBookForCharacter = (characterId: number, ruleBook: SetCharacterRuleBookRequest): Promise<CharacterRuleBookResponse> => characterRuleBookApi.setCharacterRuleBookForCharacter(characterId, ruleBook)
|
||||
.then(response => response.data);
|
||||
export const useCharacterRuleBooksStore = defineStore('character-rule-books', () => {
|
||||
const characterRuleBooks = ref<CharacterRuleBook[]>([]);
|
||||
|
||||
const replaceCharacterRuleBook = (characterRuleBook: CharacterRuleBook) => {
|
||||
const index = characterRuleBooks.value.findIndex(crb => crb.character.characterId === characterRuleBook.character.characterId);
|
||||
|
||||
if (index !== -1) {
|
||||
characterRuleBooks.value[index] = characterRuleBook;
|
||||
} else {
|
||||
characterRuleBooks.value.push(characterRuleBook);
|
||||
}
|
||||
triggerRef(characterRuleBooks);
|
||||
return characterRuleBook;
|
||||
};
|
||||
|
||||
const findByCharacterId = (characterId: number): CharacterRuleBook | undefined => characterRuleBooks.value.find(crb => crb.character.characterId === characterId);
|
||||
|
||||
const setForCharacter = (characterId: number, ruleBook: SetCharacterRuleBookRequest) => characterRuleBookApi.setCharacterRuleBookForCharacter(characterId, ruleBook)
|
||||
.then(response => replaceCharacterRuleBook(response.data));
|
||||
|
||||
const refresh = () => characterRuleBookApi.findAllCharacterRuleBooks().then(response => characterRuleBooks.value = response.data);
|
||||
|
||||
refresh();
|
||||
|
||||
return {characterRuleBooks, findByCharacterId, setForCharacter, refresh};
|
||||
})
|
||||
|
||||
export const fetchScriptDefinitions = (): Promise<string> =>
|
||||
ruleBookApi.getScriptDefinitions({responseType: 'text'}).then(response => response.data);
|
||||
+1
-11
@@ -1,5 +1,4 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import rateLimit from 'axios-rate-limit';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import log from 'loglevel';
|
||||
|
||||
export const logResource = (a: AxiosInstance) => {
|
||||
@@ -14,12 +13,3 @@ export const logResource = (a: AxiosInstance) => {
|
||||
return Promise.reject(e);
|
||||
});
|
||||
}
|
||||
|
||||
export const esiAxiosInstance = rateLimit(axios.create({
|
||||
baseURL: import.meta.env.VITE_ESI_URL,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
}), { maxRPS: 10 })
|
||||
logResource(esiAxiosInstance)
|
||||
|
||||
+2
-10
@@ -4,9 +4,9 @@ import {RouterLink} from 'vue-router';
|
||||
import {routeNames} from '@/routes';
|
||||
|
||||
const links = [
|
||||
{name: "Market", path: "/market"},
|
||||
{name: "Ledger", path: "/ledgers"},
|
||||
{name: "Rules", path: "/rules"},
|
||||
{name: "Market", path: "/market"},
|
||||
{name: "Reprocess", path: "/reprocess"},
|
||||
{name: "Tools", path: "/tools"}
|
||||
];
|
||||
@@ -49,7 +49,7 @@ const logout = async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "tailwindcss";
|
||||
@reference "@/style.css";
|
||||
|
||||
.sidebar-button {
|
||||
@apply flex items-center rounded-md hover:bg-slate-800 cursor-pointer;
|
||||
@@ -63,14 +63,6 @@ const logout = async () => {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.user-dropdown :deep(>div) {
|
||||
@apply w-full;
|
||||
|
||||
> div {
|
||||
@apply w-full bg-slate-800;
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown :deep(>button) {
|
||||
@apply bg-slate-700 hover:bg-slate-800 border-none flex items-center w-full;
|
||||
}
|
||||
|
||||
+7
-4
@@ -2,6 +2,12 @@
|
||||
|
||||
@custom-variant search-cancel (&::-webkit-search-cancel-button);
|
||||
|
||||
@utility btn-icon {
|
||||
@apply p-0 border-none bg-transparent hover:text-slate-400 hover:bg-transparent cursor-pointer;
|
||||
> svg {
|
||||
@apply w-6 h-6;
|
||||
}
|
||||
}
|
||||
@layer base {
|
||||
span, table, input, th, tr, td, button, a.button, div, hr {
|
||||
@apply border-slate-600 text-slate-100 placeholder-slate-400;
|
||||
@@ -72,10 +78,7 @@
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
@apply p-0 border-none bg-transparent hover:text-slate-400 hover:bg-transparent cursor-pointer;
|
||||
> svg {
|
||||
@apply w-6 h-6;
|
||||
}
|
||||
@apply btn-icon;
|
||||
}
|
||||
|
||||
a.tab {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {TransferTypes} from "@/transaction/transaction.ts";
|
||||
import {LedgerLabel, systemLedger, useLedgersStore} from "@/ledger";
|
||||
import {getMarketType, IskLabel, MarketTypeLabel} from "@/market";
|
||||
import {computedAsync} from "@vueuse/core";
|
||||
import {TransferResponse} from "@/generated/mammon";
|
||||
|
||||
type TransferWithValue = TransferResponse & { marketTypeId: number; };
|
||||
|
||||
interface Props {
|
||||
transfers?: TransferResponse[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const ledgersStore = useLedgersStore();
|
||||
|
||||
const sortedArray = computedAsync(async () => {
|
||||
if (!props.transfers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (await Promise.all(props.transfers.map(async (transfer: TransferWithValue, index) => {
|
||||
const fromLedger = ledgersStore.findById(transfer.fromLedgerId) ?? systemLedger
|
||||
const toLedger = ledgersStore.findById(transfer.toLedgerId) ?? systemLedger
|
||||
|
||||
const item = transfer.marketTypeId ? await getMarketType(transfer.marketTypeId) : undefined;
|
||||
|
||||
return {
|
||||
...transfer,
|
||||
order: index,
|
||||
fromLedger,
|
||||
toLedger,
|
||||
itemName: item ? item.name : '',
|
||||
fromLedgerName: fromLedger.name,
|
||||
toLedgerName: toLedger.name
|
||||
}
|
||||
}))).sort((a, b) => a.order - b.order)
|
||||
}, []);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Item</th>
|
||||
<th>Quantity/Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="t in sortedArray">
|
||||
<td>
|
||||
<LedgerLabel :ledger="t.fromLedger" />
|
||||
</td>
|
||||
<td>
|
||||
<LedgerLabel :ledger="t.toLedger" />
|
||||
</td>
|
||||
<template v-if="t.type === TransferTypes.Item">
|
||||
<td>
|
||||
<MarketTypeLabel :id="t.marketTypeId" :name="t.itemName" />
|
||||
</td>
|
||||
<td class="text-right">{{ t.quantity }}</td>
|
||||
</template>
|
||||
<template v-else-if="t.type === TransferTypes.Isk">
|
||||
<td colspan="2" class="text-right">
|
||||
<IskLabel :amount="t.amount" :colored="false" />
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './transaction'
|
||||
|
||||
export {default as TransferList} from './TransferList.vue';
|
||||
@@ -0,0 +1,7 @@
|
||||
import {TransferResponse} from "@/generated/mammon";
|
||||
|
||||
export const TransferTypes = {
|
||||
Isk: 'ISK',
|
||||
Item: 'ITEM',
|
||||
} as const;
|
||||
export type TransferType = TransferResponse['type'];
|
||||
Reference in New Issue
Block a user