38 Commits

Author SHA1 Message Date
Sirttas 9cd0d5fb5e rule book dup/delete 2026-06-09 23:44:16 +02:00
Sirttas 5ac369a643 fix 2026-06-09 21:56:12 +02:00
Sophie-Gaëlle CALLOCH b2c97c1327 rulebook name 2026-06-09 18:00:25 +02:00
Sirttas 4ae044dace win size 2026-06-08 18:54:53 +02:00
Sirttas c444f51423 js editor + fix js runner 2026-06-08 08:27:42 +02:00
Sirttas a201a95756 js editor + fix js runner 2026-06-07 23:18:53 +02:00
Sirttas b32169f433 js editor 2026-06-07 22:37:18 +02:00
Sirttas 023693c4c8 text area 2026-06-07 22:06:53 +02:00
Sirttas 47bd728530 cleanup 2026-06-06 23:52:50 +02:00
Sirttas 7e0ea10d68 corporation transactions + cleanup 2026-06-06 23:44:09 +02:00
Sirttas cd1965acc4 transfer list order 2026-06-06 20:30:58 +02:00
Sirttas 653f7dbeeb acquisition ui 2026-06-06 15:53:54 +02:00
Sirttas 5506125b2e cleanup 2026-06-06 14:56:03 +02:00
Sirttas e6ee697508 acquisition presentation 2026-06-06 14:49:28 +02:00
Sirttas bef14bcdcc character in transaction list 2026-06-05 18:41:59 +02:00
Sophie-Gaëlle CALLOCH 8d0e5ffc1a cleanup 2026-06-05 16:41:08 +02:00
Sophie-Gaëlle CALLOCH 680e8d8b95 system ledger entity 2026-06-05 16:23:57 +02:00
Sirttas 13ba8556a4 rename 2026-06-05 08:15:03 +02:00
Sirttas 46a2538bef no color in transfers 2026-06-04 19:51:04 +02:00
Sirttas 57b9ec17de fix things 2026-06-04 11:24:55 +02:00
Sirttas af3b26a273 fix things 2026-06-04 11:12:26 +02:00
Sophie-Gaëlle CALLOCH 5da9003b14 bounty activities 2026-06-02 17:54:42 +02:00
Sirttas ba548583ba ledger label system green 2026-06-01 23:05:23 +02:00
Sirttas 5c12a8af43 dropdown floating 2026-06-01 22:29:06 +02:00
Sirttas 05210fea4b transfer list 2026-06-01 22:19:26 +02:00
Sirttas f28201e711 tables display 2026-06-01 21:04:39 +02:00
Sirttas c23ec0cb53 ledger balance 2026-06-01 19:22:27 +02:00
Sirttas 42c7e59d63 balance endpoint 2026-06-01 17:39:14 +02:00
Sirttas 192cf7d9cb ledger view page structure 2026-06-01 16:24:35 +02:00
Sirttas 3235cf21ba transaction list display 2026-05-31 23:10:50 +02:00
Sirttas 47ee14319d transaction list api 2026-05-31 21:54:56 +02:00
Sirttas 457d2a5161 cleanup rules 2026-05-31 18:57:10 +02:00
Sirttas 1358aaa705 cleanup routes names 2026-05-31 18:33:35 +02:00
Sirttas d7bae268da character rule book front 2026-05-31 18:20:46 +02:00
Sirttas 9310397320 character rule book front 2026-05-31 18:19:45 +02:00
Sirttas ba81d7b6a8 rule book front 2026-05-31 17:00:49 +02:00
Sirttas 676ff961ed rename rule set to rule 2026-05-28 13:15:15 +02:00
Sirttas b40b58f866 rename rule to rule clause 2026-05-28 11:37:09 +02:00
58 changed files with 3116 additions and 1050 deletions
+562 -194
View File
@@ -6,255 +6,488 @@ servers:
- url: http://localhost:8080
description: Generated server url
paths:
/ledgers/main/{ledgerId}:
/rule-books/{ruleBookId}:
get:
tags:
- rule-book
summary: Find a rule book by its id
operationId: findRuleBookById
parameters:
- name: ruleBookId
in: path
description: Id of the rule book
required: true
schema:
type: string
format: uuid
responses:
"200":
description: The rule book
content:
'*/*':
schema:
$ref: "#/components/schemas/RuleBookResponse"
"404":
description: No rule book with this id
put:
tags:
- ledger-controller
operationId: updateMainLedger
- rule-book
summary: Update a rule book
operationId: updateRuleBook
parameters:
- name: ledgerId
- name: ruleBookId
in: path
description: Id of the rule book
required: true
schema:
type: string
format: uuid
requestBody:
description: New state of the rule book
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateRuleBookRequest"
required: true
responses:
"200":
description: The updated rule book
content:
'*/*':
schema:
$ref: "#/components/schemas/RuleBookResponse"
"400":
description: Invalid request (e.g. blank name)
delete:
tags:
- rule-book
summary: Delete a rule book
operationId: deleteRuleBook
parameters:
- name: ruleBookId
in: path
description: Id of the rule book
required: true
schema:
type: string
format: uuid
responses:
"204":
description: The rule book was deleted
"400":
description: The rule book is associated to a character
/ledgers/main/{ledgerId}:
put:
tags:
- ledger
summary: Update a main ledger
operationId: updateMainLedger
parameters:
- name: ledgerId
in: path
description: Id of the main ledger
required: true
schema:
type: string
format: uuid
requestBody:
description: New state of the main ledger
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateMainLedgerRequest"
required: true
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
description: The updated main ledger
content:
'*/*':
schema:
$ref: "#/components/schemas/MainLedgerResponse"
"400":
description: "The ledger is not a main ledger, or the request is invalid"
"404":
description: No ledger with this id
/ledgers/combined/{ledgerId}:
put:
tags:
- ledger-controller
- ledger
summary: Update a combined ledger
operationId: updateCombinedLedger
parameters:
- name: ledgerId
in: path
description: Id of the combined ledger
required: true
schema:
type: string
format: uuid
requestBody:
description: New state of the combined ledger
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateCombinedLedgerRequest"
required: true
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
description: The updated combined ledger
content:
'*/*':
schema:
$ref: "#/components/schemas/CombinedLedgerResponse"
/process-activities:
post:
tags:
- processing-controller
operationId: processNewActivities
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
/ledgers/main:
post:
tags:
- ledger-controller
operationId: createMainLedger
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMainLedgerRequest"
required: true
responses:
description: "The ledger is not a combined ledger, or the request is invalid"
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
content:
'*/*':
schema:
$ref: "#/components/schemas/MainLedgerResponse"
/ledgers/combined:
post:
tags:
- ledger-controller
operationId: createCombinedLedger
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateCombinedLedgerRequest"
required: true
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
content:
'*/*':
schema:
$ref: "#/components/schemas/CombinedLedgerResponse"
description: No ledger with this id
/characters/{characterId}/rule-book:
get:
tags:
- rule-book-controller
operationId: findByCharacterId
- character-rule-book
summary: Find the rule book assigned to a character
operationId: findCharacterRuleBookByCharacterId
parameters:
- name: characterId
in: path
description: Id of the character
required: true
schema:
type: integer
format: int64
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
description: The rule book assignment of the character
content:
'*/*':
schema:
$ref: "#/components/schemas/RuleBookResponse"
post:
$ref: "#/components/schemas/CharacterRuleBookResponse"
"400":
description: Invalid character id
"404":
description: No rule book assigned to this character
put:
tags:
- rule-book-controller
operationId: setCharacterRuleBook
- character-rule-book
summary: Assign a rule book to a character
operationId: setCharacterRuleBookForCharacter
parameters:
- name: characterId
in: path
description: Id of the character
required: true
schema:
type: integer
format: int64
requestBody:
description: Rule book and ledger bindings to assign
content:
application/json:
schema:
$ref: "#/components/schemas/SetCharacterRuleBookRequest"
required: true
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
description: The rule book assignment of the character
content:
'*/*':
schema:
$ref: "#/components/schemas/RuleBookResponse"
/activity/fetch/{characterId}:
post:
tags:
- activity-controller
operationId: fetchNewActivitiesForCharacter
parameters:
- name: characterId
in: path
required: true
schema:
type: integer
format: int64
responses:
"404":
description: Not Found
$ref: "#/components/schemas/CharacterRuleBookResponse"
"400":
description: Bad Request
"200":
description: OK
/ledgers:
description: "The referenced rule book or a bound ledger does not exist,\
\ or a ledger binding is missing"
/rule-books:
get:
tags:
- ledger-controller
operationId: findAll
- rule-book
summary: Find all rule books
operationId: findAllRuleBooks
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
description: All rule books
content:
'*/*':
schema:
type: array
items:
oneOf:
- $ref: "#/components/schemas/CombinedLedgerResponse"
- $ref: "#/components/schemas/MainLedgerResponse"
$ref: "#/components/schemas/RuleBookResponse"
post:
tags:
- rule-book
summary: Create a rule book
operationId: createRuleBook
requestBody:
description: Rule book to create
content:
application/json:
schema:
$ref: "#/components/schemas/CreateRuleBookRequest"
required: true
responses:
"201":
description: The created rule book
content:
'*/*':
schema:
$ref: "#/components/schemas/RuleBookResponse"
"400":
description: Invalid request (e.g. blank name)
/process-activities:
post:
tags:
- processing
summary: Process new activities for all characters with a usable token
operationId: processNewActivities
responses:
"200":
description: New activities processed
/ledgers/main:
post:
tags:
- ledger
summary: Create a main ledger
operationId: createMainLedger
requestBody:
description: Main ledger to create
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMainLedgerRequest"
required: true
responses:
"201":
description: The created main ledger
content:
'*/*':
schema:
$ref: "#/components/schemas/MainLedgerResponse"
"400":
description: Invalid request (e.g. blank name)
/ledgers/combined:
post:
tags:
- ledger
summary: Create a combined ledger
operationId: createCombinedLedger
requestBody:
description: Combined ledger to create
content:
application/json:
schema:
$ref: "#/components/schemas/CreateCombinedLedgerRequest"
required: true
responses:
"201":
description: The created combined ledger
content:
'*/*':
schema:
$ref: "#/components/schemas/CombinedLedgerResponse"
"400":
description: "Invalid request (e.g. blank name, a member ledger missing\
\ or already contained)"
/activity/fetch:
post:
tags:
- activity
summary: Fetch all new activities for all characters from the EVE API
operationId: fetchAllNewActivities
responses:
"200":
description: New activities fetched and stored
/activity/fetch/{characterId}:
post:
tags:
- activity
summary: Fetch new activities for a character from the EVE API
operationId: fetchNewActivitiesForCharacter
parameters:
- name: characterId
in: path
description: Id of the character
required: true
schema:
type: integer
format: int64
responses:
"200":
description: New activities fetched and stored
"400":
description: No character with this id
/rule-books/script-definitions:
get:
tags:
- rule-book
summary: Download the TypeScript definitions for the rule script runtime
operationId: getScriptDefinitions
responses:
"200":
description: The rule-runner.d.ts type definitions
content:
text/plain:
schema:
type: string
/ledgers:
get:
tags:
- ledger
summary: Find all ledgers
operationId: findAllLedgers
responses:
"200":
description: All ledgers
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/LedgerResponse"
/ledgers/{ledgerId}:
get:
tags:
- ledger-controller
operationId: findById
- ledger
summary: Find a ledger by its id
operationId: findLedgerById
parameters:
- name: ledgerId
in: path
description: Id of the ledger
required: true
schema:
type: string
format: uuid
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
description: The ledger
content:
'*/*':
schema:
oneOf:
- $ref: "#/components/schemas/CombinedLedgerResponse"
- $ref: "#/components/schemas/MainLedgerResponse"
$ref: "#/components/schemas/LedgerResponse"
"400":
description: The ledger cannot be exposed (system ledger)
"404":
description: No ledger with this id
/ledgers/{ledgerId}/transactions:
get:
tags:
- transaction
summary: Find all transactions in a ledger
operationId: finAllTransactionsInLedger
parameters:
- name: ledgerId
in: path
description: Id of the ledger
required: true
schema:
type: string
format: uuid
responses:
"200":
description: All transactions in the ledger
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/TransactionResponse"
"404":
description: No ledger with this id
/ledgers/{ledgerId}/balance:
get:
tags:
- ledger
summary: Find the balance of a ledger
operationId: findBalanceByLedgerId
parameters:
- name: ledgerId
in: path
description: Id of the ledger
required: true
schema:
type: string
format: uuid
responses:
"200":
description: The balance of the ledger
content:
'*/*':
schema:
$ref: "#/components/schemas/BalanceResponse"
"404":
description: No ledger with this id
/characters:
get:
tags:
- character-controller
operationId: getCharacters
- character
summary: Find all characters with a usable token
operationId: findAllCharacters
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
description: All characters with a usable token
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/CharacterResponse"
/acquisitions:
get:
tags:
- acquisition
summary: Find all acquisitions that still have remaining stock
operationId: findAllAcquisitions
responses:
"200":
description: The acquisitions with remaining stock
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/AcquisitionResponse"
components:
schemas:
UpdateRuleBookRequest:
type: object
properties:
name:
type: string
usedForAcquisitions:
type: boolean
ledgerRefs:
type: array
items:
type: string
pattern: "[a-z][a-zA-Z0-9]*"
script:
type: string
required:
- ledgerRefs
- name
- script
- usedForAcquisitions
RuleBookResponse:
type: object
properties:
ruleBookId:
type: string
format: uuid
name:
type: string
usedForAcquisitions:
type: boolean
ledgerRefs:
type: array
items:
type: string
pattern: "[a-z][a-zA-Z0-9]*"
script:
type: string
required:
- ledgerRefs
- name
- ruleBookId
- script
- usedForAcquisitions
UpdateMainLedgerRequest:
type: object
properties:
@@ -274,10 +507,6 @@ components:
type: string
balance:
type: number
type:
type: string
enum:
- MAIN
required:
- balance
- ledgerId
@@ -312,15 +541,62 @@ components:
items:
type: string
format: uuid
type:
type: string
enum:
- COMBINED
required:
- balance
- ledgerId
- memberLedgerIds
- name
SetCharacterRuleBookRequest:
type: object
properties:
ruleBookId:
type: string
format: uuid
bindings:
type: object
additionalProperties:
type: string
format: uuid
required:
- bindings
- ruleBookId
CharacterRuleBookResponse:
type: object
properties:
characterId:
type: integer
format: int64
ruleBookId:
type: string
format: uuid
bindings:
type: object
additionalProperties:
type: string
format: uuid
required:
- bindings
- characterId
- ruleBookId
CreateRuleBookRequest:
type: object
properties:
name:
type: string
usedForAcquisitions:
type: boolean
ledgerRefs:
type: array
items:
type: string
pattern: "[a-z][a-zA-Z0-9]*"
script:
type: string
required:
- ledgerRefs
- name
- script
- usedForAcquisitions
CreateMainLedgerRequest:
type: object
properties:
@@ -341,66 +617,121 @@ components:
required:
- memberLedgerIds
- name
RuleResponse:
type: object
properties:
rate:
type: string
enum:
- NONE
- VALUE
- JITA_BUY
- JITA_SELL
- EVE_ESTIMATE
fromLedgerId:
type: string
format: uuid
toLedgerId:
type: string
format: uuid
required:
- rate
RuleSetResponse:
type: object
properties:
rules:
type: array
items:
$ref: "#/components/schemas/RuleResponse"
required:
- rules
SetCharacterRuleBookRequest:
type: object
properties:
ruleSets:
type: object
additionalProperties:
$ref: "#/components/schemas/RuleSetResponse"
required:
- ruleSets
RuleBookResponse:
type: object
properties:
characterId:
type: integer
format: int64
ruleSets:
type: object
additionalProperties:
$ref: "#/components/schemas/RuleSetResponse"
required:
- characterId
- ruleSets
LedgerResponse:
type: object
discriminator:
propertyName: type
mapping:
MAIN: "#/components/schemas/MainLedgerResponse"
COMBINED: "#/components/schemas/CombinedLedgerResponse"
oneOf:
- $ref: "#/components/schemas/MainLedgerResponse"
- $ref: "#/components/schemas/CombinedLedgerResponse"
properties:
type:
type: string
enum:
- MAIN
- COMBINED
required:
- type
IskTransferResponse:
allOf:
- $ref: "#/components/schemas/TransferResponse"
- type: object
properties:
fromLedgerId:
type: string
format: uuid
toLedgerId:
type: string
format: uuid
amount:
type: number
required:
- amount
- fromLedgerId
- toLedgerId
ItemTransferResponse:
allOf:
- $ref: "#/components/schemas/TransferResponse"
- type: object
properties:
fromLedgerId:
type: string
format: uuid
toLedgerId:
type: string
format: uuid
marketTypeId:
type: integer
format: int64
quantity:
type: integer
format: int64
required:
- fromLedgerId
- marketTypeId
- quantity
- toLedgerId
TransactionResponse:
type: object
properties:
transactionId:
type: string
format: uuid
characterId:
type: integer
format: int64
datetime:
type: string
format: date-time
description:
type: string
transfers:
type: array
items:
$ref: "#/components/schemas/TransferResponse"
required:
- characterId
- datetime
- description
- transactionId
- transfers
TransferResponse:
discriminator:
propertyName: type
mapping:
ISK: "#/components/schemas/IskTransferResponse"
ITEM: "#/components/schemas/ItemTransferResponse"
oneOf:
- $ref: "#/components/schemas/IskTransferResponse"
- $ref: "#/components/schemas/ItemTransferResponse"
properties:
type:
type: string
required:
- type
BalanceResponse:
type: object
properties:
iskBalance:
type: number
itemBalances:
type: array
items:
$ref: "#/components/schemas/ItemBalanceResponse"
required:
- iskBalance
- itemBalances
ItemBalanceResponse:
type: object
properties:
typeId:
type: integer
format: int64
quantity:
type: integer
format: int64
required:
- quantity
- typeId
CharacterResponse:
type: object
properties:
@@ -412,3 +743,40 @@ components:
required:
- characterId
- name
AcquisitionResponse:
type: object
properties:
acquisitionId:
type: string
format: uuid
characterId:
type: integer
format: int64
marketTypeId:
type: integer
format: int64
source:
type: string
enum:
- BOUGHT
- MANUAL
datetime:
type: string
format: date-time
quantity:
type: integer
format: int64
remaining:
type: integer
format: int64
unitCost:
type: number
required:
- acquisitionId
- characterId
- datetime
- marketTypeId
- quantity
- remaining
- source
- unitCost
+272 -244
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -14,11 +14,13 @@
"@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",
+8 -5
View File
@@ -1,12 +1,14 @@
<script setup lang="ts">
import { computed } from 'vue';
import { RouterView, useRoute } from 'vue-router';
import { Sidebar } from './sidebar';
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();
const hideSidebar = computed(() => {
return route.name === 'callback' || route.name === 'about';
return route.name === routeNames.callback || route.name === routeNames.about;
});
</script>
@@ -20,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;
+3 -3
View File
@@ -1,4 +1,4 @@
import {activityControllerApi, characterControllerApi} from "@/mammon";
import {activityApi, characterApi} from "@/mammon";
import {defineStore} from "pinia";
import {ref} from "vue";
import {CharacterResponse} from "@/generated/mammon";
@@ -18,9 +18,9 @@ export const useCharactersStore = defineStore('characters', () => {
return character;
}
const reloadActivities = (characterId: number): Promise<void> => activityControllerApi.fetchNewActivitiesForCharacter(characterId) as Promise<void>;
const reloadActivities = (characterId: number): Promise<void> => activityApi.fetchNewActivitiesForCharacter(characterId) as Promise<void>;
const refresh = () => characterControllerApi.getCharacters().then(response => characters.value = response.data);
const refresh = () => characterApi.findAllCharacters().then(response => characters.value = response.data);
refresh();
+60 -14
View File
@@ -1,22 +1,47 @@
<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 {ChevronDownIcon, ChevronUpIcon} from '@heroicons/vue/24/outline';
import {vOnClickOutside} from '@vueuse/components';
import {useElementBounding, useEventListener} from '@vueuse/core';
import {computed, ref} from 'vue';
interface Props {
inline?: boolean;
autoClose?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
inline: false,
autoClose: true
})
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) {
isOpen.value = false;
}
}
useEventListener('keyup', e => {
if (e.key === 'Escape') {
isOpen.value = false;
doAutoClose();
}
});
</script>
<template>
<div class="dropdown" :class="{'dropdown-open': isOpen, 'dropdown-close': !isOpen}" v-on-click-outside="() => isOpen = false">
<button @click="isOpen = !isOpen">
<slot name="button" />
<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"
@@ -25,6 +50,7 @@ useEventListener('keyup', e => {
<ChevronDownIcon v-if="!isOpen" class="chevron" />
<ChevronUpIcon v-else class="chevron" />
</Transition>
<slot name="button" />
</button>
<Transition
@@ -32,19 +58,39 @@ useEventListener('keyup', e => {
enter-from-class="opacity-0"
leave-from-class="transition-opacity"
leave-to-class="opacity-0">
<div v-if="isOpen" class="relative">
<div class="z-10 divide-y rounded-b-md absolute">
<slot />
</div>
<div v-if="inline && isOpen">
<slot />
</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 ms-1;
@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>
+4 -4
View File
@@ -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;
}
+1 -1
View File
@@ -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;
}
+5 -4
View File
@@ -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>
+2 -2
View File
@@ -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;
}
+25 -23
View File
@@ -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>
+5 -5
View File
@@ -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'),
+45
View File
@@ -0,0 +1,45 @@
<script setup lang="ts">
import {computed} from "vue";
import {storeToRefs} from "pinia";
import {Modal} from "@/components";
import {useConfirmStore} from "./useConfirm";
const confirmStore = useConfirmStore();
const {open, options} = storeToRefs(confirmStore);
const {accept, cancel} = confirmStore;
const modalOpen = computed({
get: () => open.value,
set: value => {
if (!value) {
cancel();
}
},
});
</script>
<template>
<Modal v-model:open="modalOpen">
<div class="bg-slate-800 rounded pb-4 w-96">
<span class="m-2">{{ options.title ?? "Confirm" }}</span>
<hr />
<div class="m-4">{{ options.message }}</div>
<div class="flex justify-end">
<button class="me-2" @click="cancel">{{ options.cancelLabel ?? "Cancel" }}</button>
<button class="confirm me-4" :class="options.danger ? 'danger' : ''" @click="accept">{{ 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>
+3
View File
@@ -0,0 +1,3 @@
export { default as ConfirmModal } from './ConfirmModal.vue';
export { confirm, useConfirmStore } from './useConfirm';
export type { ConfirmOptions } from './useConfirm';
+37
View File
@@ -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);
+1037 -211
View File
File diff suppressed because it is too large Load Diff
@@ -25,7 +25,7 @@ const name = ref("");
const members = ref<Ledger[]>([]);
const selectedLedger = ref<Ledger>();
const availableLedgers = computed(() => ledgers.value
.filter((ledger) => selectedLedger.value === ledger)
.filter(l => l.ledgerId !== props.ledgerId)
.filter(l => !members.value.includes(l)));
@@ -98,7 +98,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 +136,7 @@ defineExpose({ open });
</template>
<style scoped>
@reference "tailwindcss";
@reference "@/style.css";
button.switch {
@apply flex items-center px-4 rounded-md bg-slate-600;
+14 -2
View File
@@ -1,10 +1,13 @@
<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";
interface Props {
ledger: Ledger;
link?: boolean;
}
const props = defineProps<Props>();
@@ -14,6 +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"/>
<span>{{ 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 -1
View File
@@ -26,7 +26,7 @@ const ledgerId = computed({
</template>
<style scoped>
@reference "tailwindcss";
@reference "@/style.css";
.system-ledger {
@apply text-emerald-400;
+1 -1
View File
@@ -2,4 +2,4 @@ export * from './ledger';
export {default as LedgerLabel} from './LedgerLabel.vue';
export {default as LedgerSelect} from './LedgerSelect.vue';
export {default as CreateLedgerModal} from './CreateLedgerModal.vue';
export {default as EditLedgerModal} from './EditLedgerModal.vue';
+32 -15
View File
@@ -1,28 +1,33 @@
import {
BalanceResponse,
CombinedLedgerResponse,
CombinedLedgerResponseTypeEnum,
CreateCombinedLedgerRequest,
CreateMainLedgerRequest,
LedgerResponseTypeEnum,
LedgerResponse,
MainLedgerResponse,
MainLedgerResponseTypeEnum,
TransactionResponse,
UpdateCombinedLedgerRequest,
UpdateMainLedgerRequest
} from "@/generated/mammon";
import {defineStore} from "pinia";
import {ref, triggerRef} from "vue";
import {ledgerControllerApi} from "@/mammon";
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 systemLedgerRef = 'system';
export const systemLedger = {
type: LedgerTypes.Main,
ledgerId: "",
ledgerId: "00000000-0000-0000-0000-000000000001",
name: "Eve Economy",
balance: 0,
_system: true
@@ -57,15 +62,27 @@ export const useLedgersStore = defineStore('ledgers', () => {
const findById = (ledgerId: string): Ledger | undefined => ledgers.value.find(l => l.ledgerId === ledgerId);
const findAllById = (ledgerIds: string[]): Ledger[] => ledgerIds.map(findById).filter((x): x is Ledger => x !== undefined)
const createMain = (ledger: CreateMainLedgerRequest) => ledgerControllerApi.createMainLedger(ledger).then(response => addLedger(response.data as Ledger));
const createCombined = (ledger: CreateCombinedLedgerRequest) => ledgerControllerApi.createCombinedLedger(ledger).then(response => addLedger(response.data as Ledger));
const updateMain = (ledgerId: string, ledger: UpdateMainLedgerRequest) => ledgerControllerApi.updateMainLedger(ledgerId, ledger).then(response => replaceLedger(response.data as Ledger));
const updateCombined = (ledgerId: string, ledger: UpdateCombinedLedgerRequest) => ledgerControllerApi.updateCombinedLedger(ledgerId, ledger).then(response => replaceLedger(response.data as Ledger));
const createMain = (ledger: CreateMainLedgerRequest) => ledgerApi.createMainLedger(ledger).then(response => addLedger(response.data as Ledger));
const createCombined = (ledger: CreateCombinedLedgerRequest) => ledgerApi.createCombinedLedger(ledger).then(response => addLedger(response.data as Ledger));
const updateMain = (ledgerId: string, ledger: UpdateMainLedgerRequest) => ledgerApi.updateMainLedger(ledgerId, ledger).then(response => replaceLedger(response.data as Ledger));
const updateCombined = (ledgerId: string, ledger: UpdateCombinedLedgerRequest) => ledgerApi.updateCombinedLedger(ledgerId, ledger).then(response => replaceLedger(response.data as Ledger));
const refresh = () => ledgerControllerApi.findAll().then(response => ledgers.value = response.data as Ledger[]);
const refresh = () => ledgerApi.findAllLedgers().then(response => ledgers.value = response.data as Ledger[]);
refresh();
return {ledgers, findById, findAllById, createMain, createCombined, updateMain, updateCombined, refresh};
})
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 {findById} = useLedgersStore();
const ledgerId = useRouteParams<string, string>('ledgerId', '', { transform: v => typeof v === 'string' ? v : v[0]});
const ledger = computed(() => findById(ledgerId.value))
return {ledgerId, ledger};
}
+16 -10
View File
@@ -1,11 +1,14 @@
import {logResource} from "@/service";
import axios from "axios";
import {
ActivityControllerApi,
CharacterControllerApi,
LedgerControllerApi,
ProcessingControllerApi,
RuleBookControllerApi
AcquisitionApi,
ActivityApi,
CharacterApi,
CharacterRuleBookApi,
LedgerApi,
ProcessingApi,
RuleBookApi,
TransactionApi
} from "@/generated/mammon";
export const mammonUrl = import.meta.env.VITE_MAMMON_URL;
@@ -20,8 +23,11 @@ const mammonAxiosInstance = axios.create({
})
logResource(mammonAxiosInstance)
export const ledgerControllerApi = new LedgerControllerApi(undefined, mammonUrl, mammonAxiosInstance);
export const characterControllerApi = new CharacterControllerApi(undefined, mammonUrl, mammonAxiosInstance);
export const ruleBookControllerApi = new RuleBookControllerApi(undefined, mammonUrl, mammonAxiosInstance);
export const activityControllerApi = new ActivityControllerApi(undefined, mammonUrl, mammonAxiosInstance);
export const processingControllerApi = new ProcessingControllerApi(undefined, mammonUrl, mammonAxiosInstance);
export const ledgerApi = new LedgerApi(undefined, mammonUrl, mammonAxiosInstance);
export const transactionApi = new TransactionApi(undefined, mammonUrl, mammonAxiosInstance);
export const characterApi = new CharacterApi(undefined, mammonUrl, mammonAxiosInstance);
export const ruleBookApi = new RuleBookApi(undefined, mammonUrl, mammonAxiosInstance);
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);
+25
View File
@@ -0,0 +1,25 @@
<script setup lang="ts">
import {formatIsk} from "@/formaters";
import {computed} from "vue";
interface Props {
amount: number;
colored?: boolean;
}
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="computedClass">{{ formatIsk(amount) }}</span>
</template>
+2 -2
View File
@@ -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;
}
+31 -29
View File
@@ -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 };
});
});
+2
View File
@@ -6,3 +6,5 @@ export * from './type';
export * from './appraisal';
export * from './market';
export { default as IskLabel } from './IskLabel.vue';
+3 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useMarketTaxStore } from "./tax";
import {storeToRefs} from "pinia";
import {useMarketTaxStore} from "./tax";
const { brokerFee, scc } = storeToRefs(useMarketTaxStore());
@@ -18,7 +18,7 @@ const { brokerFee, scc } = storeToRefs(useMarketTaxStore());
</template>
<style scoped>
@reference "tailwindcss";
@reference "@/style.css";
div.end {
@apply justify-self-end ms-2;
}
+9 -10
View File
@@ -1,13 +1,12 @@
<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 {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';
type Result = {
type: MarketType;
@@ -168,7 +167,7 @@ const getLineColor = (result: Result) => {
</template>
<style scoped>
@reference "tailwindcss";
@reference "@/style.css";
div.end {
@apply justify-self-end ms-2;
}
+22 -7
View File
@@ -1,27 +1,42 @@
import {esiAxiosInstance} from '@/service';
export type MarketType = {
id: number;
group_id: number;
marketgroup_id: number;
market_group_id: number;
name: string;
published: boolean;
description: string;
basePrice: number;
base_price: number;
icon_id: number;
volume: number;
portionSize: number;
portion_size: number;
}
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)!);
}
return esiAxiosInstance.get<Omit<MarketType, 'id'> & { type_id: number }>(`/universe/types/${id}/`).then(r => {
const { type_id, ...rest } = r.data;
const marketType: MarketType = { id: type_id, ...rest };
cache.set(id, marketType);
return marketType;
});
};
export const getMarketType = async (type: string | number): Promise<MarketType> => (await getMarketTypes([type]))[0];
export const getMarketTypes = async (types: (string | number)[]): Promise<MarketType[]> => {
if (types.length === 0) {
return [];
} else if (types.length === 1 && typeof types[0] === "number") {
return [];
}
return []
const ids = types.filter((t): t is number => typeof t === 'number');
return Promise.all(ids.map(fetchType));
}
const blueprintMarketGrous = [ // TODO add all groups
const blueprintMarketGroups = [ // TODO add all groups
2,
2157,
2159,
+6 -6
View File
@@ -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 {
@@ -89,7 +89,7 @@ watchEffect(async () => {
<template>
<div @click="() => isOpen = true" v-on-click-outside="() => isOpen = false">
<div class="fake-input">
<img v-if="modelValue?.id" :src="`https://images.evetech.net/types/${modelValue.id}/icon?size=32`" alt="" />
<img v-if="modelValue?.type_id" :src="`https://images.evetech.net/types/${modelValue.type_id}/icon?size=32`" alt="" />
<input type="text" v-model="name" @keyup.enter="submit" @keyup.down="moveDown" @keyup.up="moveUp" />
</div>
<div v-if="suggestions.length > 1" class="z-20 absolute w-96">
@@ -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;
+23 -10
View File
@@ -1,6 +1,9 @@
<script setup lang="ts">
import { ClipboardButton } from '@/components';
import { InformationCircleIcon } from '@heroicons/vue/24/outline';
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 {
@@ -9,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: 'market-types', 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>
+14 -6
View File
@@ -1,19 +1,27 @@
<script setup lang="ts">
import {RouterView} from 'vue-router';
import {CreateLedgerModal} from "@/ledger";
import {EditLedgerModal, useLedgersStore} from "@/ledger";
import {ref} from "vue";
import {processingControllerApi} from "@/mammon";
import {activityApi, processingApi} from "@/mammon";
const createLedgerModal = ref<typeof CreateLedgerModal>();
const {refresh} = useLedgersStore();
const editLedgerModal = ref<typeof EditLedgerModal>();
const processActivities = async () => {
await activityApi.fetchAllNewActivities();
await processingApi.processNewActivities();
await refresh();
}
</script>
<template>
<div class="mt-4">
<div class="mb-4 border-b-1 flex justify-end">
<button class="mb-2 ms-2" @click="processingControllerApi.processNewActivities()">Process Activities</button>
<button class="mb-2 ms-2" @click="createLedgerModal?.open()">New Ledger</button>
<button class="mb-2 ms-2" @click="processActivities">Process Activities</button>
<button class="mb-2 ms-2" @click="editLedgerModal?.open()">New Ledger</button>
</div>
<CreateLedgerModal ref="createLedgerModal" />
<EditLedgerModal ref="editLedgerModal" />
<RouterView />
</div>
</template>
+4 -15
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router';
import {RouterLink, RouterView} from 'vue-router';
import {routeNames} from '@/routes';
</script>
<template>
<div class="mt-4">
<div class="flex border-b-2 border-emerald-500">
<RouterLink :to="{name: 'market-types'}" class="tab">
<RouterLink :to="{name: routeNames.marketTypes}" class="tab">
<span>Item Info</span>
</RouterLink>
<RouterLink to="/market/tracking" class="tab">
@@ -18,15 +18,4 @@ import { RouterLink, RouterView } from 'vue-router';
</div>
<RouterView />
</div>
</template>
<style scoped>
@reference "tailwindcss";
a.tab {
@apply flex items-center px-4 me-2 rounded-t-md bg-slate-600 hover:bg-slate-700;
&.router-link-active {
@apply bg-emerald-500 hover:bg-emerald-700;
}
}
</style>
</template>
+13 -2
View File
@@ -1,7 +1,18 @@
<script setup lang="ts">
import {RouterLink, RouterView} from "vue-router";
import {routeNames} from '@/routes';
</script>
<template>
<RouterView />
<div class="mt-4">
<div class="flex border-b-2 border-emerald-500">
<RouterLink :to="{ name: routeNames.listRuleBooks }" class="tab">
<span>Rule Books</span>
</RouterLink>
<RouterLink to="/characters/rules" class="tab">
<span>Characters Rules</span>
</RouterLink>
</div>
<RouterView />
</div>
</template>
+106
View File
@@ -0,0 +1,106 @@
<script setup lang="ts">
import {findAllTransactionInLeger, useLedgerParam} from "@/ledger";
import {computedAsync} from "@vueuse/core";
import {TransactionResponse} from "@/generated/mammon";
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 {ledgerId} = useLedgerParam();
const {findById} = useCharactersStore();
const transactions = computedAsync<TransactionResponse[]>(async () => {
if (ledgerId.value) {
return await findAllTransactionInLeger(ledgerId.value);
}
return [];
}, []);
const { sortedArray, headerProps } = useSort(computedAsync(() => Promise.all(transactions.value.map(async transaction => {
const character = await 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' });
const getIskBalance = (transaction: TransactionResponse) => {
if (!ledgerId.value) {
return 0;
}
let balance = 0;
for (const transfer of transaction.transfers) {
if (transfer.type === TransferTypes.Isk) {
if (transfer.toLedgerId === ledgerId.value) {
balance += transfer.amount;
} else if (transfer.fromLedgerId === ledgerId.value) {
balance -= transfer.amount;
}
}
}
return balance;
}
</script>
<template>
<div class="mt-4">
<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>
+36 -11
View File
@@ -1,14 +1,17 @@
<script setup lang="ts">
import {CreateLedgerModal, LedgerLabel, useLedgersStore} from "@/ledger";
import {EditLedgerModal, Ledger, LedgerLabel, useLedgersStore} from "@/ledger";
import {storeToRefs} from "pinia";
import {nextTick, ref} from "vue";
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
import {formatIsk} from "@/formaters.ts";
import {IskLabel} from "@/market";
import {SortableHeader, useSort, VirtualScrollTable} from "@/components/table";
const {ledgers} = storeToRefs(useLedgersStore());
const editModal = ref<typeof CreateLedgerModal>();
const { sortedArray, headerProps } = useSort<Ledger>(ledgers);
const editModal = ref<typeof EditLedgerModal>();
const editingLedgerId = ref("");
const openEdit = async (ledgerId: string) => {
@@ -21,13 +24,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" />
<div class="flex grow">
<span class="ms-2">{{ formatIsk(ledger.balance) }}</span>
</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>
<CreateLedgerModal ref="editModal" :ledger-id="editingLedgerId" />
<EditLedgerModal ref="editModal" :ledger-id="editingLedgerId" />
</template>
+22
View File
@@ -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>
+60
View File
@@ -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>
+9 -4
View File
@@ -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) {
+3 -2
View File
@@ -7,6 +7,7 @@ import {BookmarkIcon, BookmarkSlashIcon, ShoppingCartIcon} from '@heroicons/vue/
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";
const buyModal = ref<typeof BuyModal>();
@@ -52,7 +53,7 @@ const view = () => {
}
router.push({
name: 'market-types',
name: routeNames.marketTypes,
params: {
type: inputItem.value.id
}
@@ -114,7 +115,7 @@ watch(useRoute(), async route => {
</template>
<style scoped>
@reference "tailwindcss";
@reference "@/style.css";
img.type-image {
width: 64px;
+96
View File
@@ -0,0 +1,96 @@
<script setup lang="ts">
import {Character, CharacterLabel, useCharactersStore} from "@/characters";
import {useRoute} from "vue-router";
import {computed, ref, watch, watchEffect} from "vue";
import log from "loglevel";
import {
findCharacterRuleBookByCharacterId,
RuleBook,
setCharacterRuleBookForCharacter,
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: findRuleBookById} = ruleBookStore;
const {ruleBooks} = storeToRefs(ruleBookStore);
const {findById: findCharacterById} = useCharactersStore();
const {ledgers} = storeToRefs(useLedgersStore());
const ledgersToUse = computed(() => [systemLedger, ...ledgers.value.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);
ruleBook.value = findRuleBookById(characterRuleBook.ruleBookId);
bindings.value = Object.fromEntries(
Object.entries(characterRuleBook.bindings)
.map(([key, id]) => [key, ledgersToUse.value.find(l => l.ledgerId === id) ?? systemLedger])
);
}
});
const save = () => {
const characterId = character.value?.characterId;
const ruleBookId = ruleBook.value?.ruleBookId;
if (characterId && ruleBookId) {
setCharacterRuleBookForCharacter(characterId, {
ruleBookId,
bindings: Object.fromEntries(
Object.entries(bindings.value)
.map(([key, ledger]) => [key, ledger.ledgerId])
)
})
}
}
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);
log.info('Loaded character:', character.value);
} else {
character.value = undefined;
log.info('No character to load');
}
}, { immediate: true })
</script>
<template>
<div v-if="character" class="grid mb-2 mt-4">
<div class="mb-2 border-b-1 flex">
<CharacterLabel class="flex grow mb-2" :character="character" :size="64" />
<div>
<button @click="save">Save</button>
</div>
</div>
<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>
</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 ledgerRefs" :ref="ref">
<span class="me-1">{{ref}}:</span>
<LedgerSelect :ledgers="ledgersToUse" :modelValue="bindings[ref] ?? systemLedger" @update:modelValue="value => { if (value) bindings[ref] = value }" />
</div>
</div>
</div>
</div>
</template>
+97 -35
View File
@@ -1,57 +1,119 @@
<script setup lang="ts">
import {Character, CharacterLabel, useCharactersStore} from "@/characters";
import {useRoute} from "vue-router";
import {ref, watch, watchEffect} from "vue";
import {useRoute, useRouter} from "vue-router";
import {ref, watch} from "vue";
import {useDebounceFn, useEventListener} from "@vueuse/core";
import log from "loglevel";
import {activityTypes, findByCharacterId, RuleBook, RuleSetInput, setCharacterRuleBook} from "@/rules";
import {ScriptEditor, useRuleBooksStore} from "@/rules";
import {PlusIcon, TrashIcon} from "@heroicons/vue/24/outline";
import {routeNames} from "@/routes";
import {SliderCheckbox} from "@/components";
const {findById: findCharacterById} = useCharactersStore();
const character = ref<Character>();
const ruleBookId = ref<string>();
const name = ref<string>('');
const usedForAcquisitions = ref<boolean>(false);
const ledgerRefs = ref<string[]>([]);
const script = ref<string>('');
const ruleBook = ref<RuleBook>();
const {findById, create, update, refresh} = useRuleBooksStore();
const router = useRouter();
watchEffect(async () => {
const characterId = character.value?.characterId;
const save = async () => {
if (!ruleBookId.value) {
const created = await create({
name: name.value,
usedForAcquisitions: usedForAcquisitions.value,
ledgerRefs: ledgerRefs.value,
script: script.value
})
await router.push({ name: routeNames.editRuleBook, params: {ruleBookId: created.ruleBookId}})
if (characterId) {
ruleBook.value = await findByCharacterId(characterId);
}
});
const save = () => {
const characterId = character.value?.characterId;
if (characterId && ruleBook.value) {
setCharacterRuleBook(characterId, ruleBook.value);
} else {
await update(ruleBookId.value, {
name: name.value,
usedForAcquisitions: usedForAcquisitions.value,
ledgerRefs: ledgerRefs.value,
script: script.value
})
}
}
watch(useRoute(), async route => {
if (route.params.characterId) {
const id = parseInt(typeof route.params.characterId === 'string' ? route.params.characterId : route.params.characterId[0]);
useEventListener(window, 'keydown', (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
event.preventDefault();
save();
}
}, {capture: true});
character.value = await findCharacterById(id);
log.info('Loaded character:', character.value);
const addLedgerRef = () => {
ledgerRefs.value = [...ledgerRefs.value, '']
}
const updateLedgerRef = useDebounceFn((index: number, value: string) => {
ledgerRefs.value[index] = value;
}, 500);
const removeLedgerRef = (index: number) => {
ledgerRefs.value = ledgerRefs.value.toSpliced(index, 1)
}
watch(useRoute(), async route => {
if (route.params.ruleBookId) {
const promise = 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);
ruleBookId.value = id;
name.value = ruleBook?.name ?? '';
usedForAcquisitions.value = ruleBook?.usedForAcquisitions ?? false;
ledgerRefs.value = [...(ruleBook?.ledgerRefs ?? [])];
script.value = ruleBook?.script ?? '';
log.info('Loaded rule book:', ruleBook);
} else {
character.value = undefined;
log.info('No character to load');
ruleBookId.value = undefined;
name.value = '';
usedForAcquisitions.value = false;
ledgerRefs.value = [];
script.value = '';
log.info('No rule book to load');
}
}, { immediate: true })
</script>
<template>
<div v-if="character" class="grid mb-2 mt-4">
<div class="mb-2 border-b-1 flex">
<CharacterLabel class="flex grow mb-2" :character="character" :size="64" />
<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 mt-2">
<div class="flex items-center mb-2" v-for="(ledgerRef, index) in ledgerRefs" :key="index">
<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="addLedgerRef"><PlusIcon /></button>
</div>
</div>
</div>
<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">
<div>
<button @click="save">Save</button>
</div>
</div>
<div v-if="ruleBook" class="flex-col">
<div class="flex-col grow border-b-1" v-for="activityType in activityTypes" :key="activityType.key">
<span>{{ activityType.name }}</span>
<RuleSetInput v-model="ruleBook.ruleSets[activityType.key]" />
</div>
</div>
</div>
</template>
@@ -0,0 +1,57 @@
<script setup lang="ts">
import {storeToRefs} from "pinia";
import {Character, CharacterLabel, useCharactersStore} from "@/characters";
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
import {findCharacterRuleBookByCharacterId, useRuleBooksStore} from "@/rules";
import {computedAsync} from "@vueuse/core";
import {routeNames} from "@/routes.ts";
import {SortableHeader, useSort} from "@/components/table";
type CharacterRuleBookView = {
character: Character;
characterName: string;
characterId: number;
ruleBookName: string;
}
const {characters} = storeToRefs(useCharactersStore());
const {findById: findRuleBookById} = useRuleBooksStore();
const { sortedArray, headerProps } = useSort(computedAsync<CharacterRuleBookView[]>(async () => await Promise.all(characters.value.map(async (character: Character): Promise<CharacterRuleBookView> => {
const characterRuleBook = await findCharacterRuleBookByCharacterId(character.characterId);
const ruleBook = findRuleBookById(characterRuleBook.ruleBookId);
return {
character,
characterName: character.name,
characterId: character.characterId,
ruleBookName: ruleBook?.name ?? ''
}
})), []))
</script>
<template>
<div class="grid mb-2 mt-4">
<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.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.characterId } }"><PencilSquareIcon /></RouterLink>
</td>
</tr>
</tbody>
</table>
</div>
</template>
+26 -6
View File
@@ -1,17 +1,37 @@
<script setup lang="ts">
import {storeToRefs} from "pinia";
import {CharacterLabel, useCharactersStore} from "@/characters";
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
import {DocumentDuplicateIcon, PencilSquareIcon, TrashIcon} from "@heroicons/vue/24/outline";
import {confirm} from "@/confirm";
import {RuleBook, useRuleBooksStore} from "@/rules";
import {routeNames} from "@/routes";
const {characters} = storeToRefs(useCharactersStore());
const ruleBooksStore = useRuleBooksStore();
const {ruleBooks} = storeToRefs(ruleBooksStore);
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>
<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: 'character-rulebook', params: { characterId: character.characterId } }"><PencilSquareIcon /></RouterLink>
<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">
<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 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>
+1 -1
View File
@@ -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;
}
+35 -8
View File
@@ -1,21 +1,48 @@
import {RouteRecordRaw} from 'vue-router';
export const routeNames = {
home: 'home',
callback: 'callback',
viewLedger: 'view-ledger',
viewLedgerBalance: 'view-ledger-balance',
listLedgerTransactions: 'list-ledger-transactions',
listRuleBooks: 'list-rule-books',
newRuleBook: 'new-rule-book',
editRuleBook: 'edit-rule-book',
editCharacterRulebook: 'edit-character-rule-book',
marketTypes: 'market-types',
about: 'about',
} as const;
export const routes: RouteRecordRaw[] = [
{path: '/', name: 'home', component: () => import('@/pages/Index.vue')},
{path: '/callback', name: 'callback', component: () => import('@/pages/Index.vue')},
{path: '/', name: routeNames.home, component: () => import('@/pages/Index.vue')},
{path: '/callback', name: routeNames.callback, component: () => import('@/pages/Index.vue')},
{path: '/ledgers', component: () => import('@/pages/Ledgers.vue'), children: [
{path: '', component: () => import('@/pages/ledger/ListLedgers.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: '', component: () => import('./pages/rules/ListRuleBooks.vue')},
{path: '/characters/:characterId/rule-book', name: 'character-rulebook', component: () => import('@/pages/rules/EditRuleBook.vue')},
{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')},
{path: ':ruleBookId', name: routeNames.editRuleBook, component: () => import('@/pages/rules/EditRuleBook.vue')},
]},
{path: '/characters/rules', children: [
{path: '', component: () => import('@/pages/rules/ListCharacterRuleBooks.vue')},
{path: '/characters/:characterId/rules', name: routeNames.editCharacterRulebook, component: () => import('@/pages/rules/EditCharacterRuleBook.vue')},
]}
]},
{path: '/market', component: () => import('@/pages/Market.vue'), children: [
{path: '', redirect: '/market/types'},
{path: 'types/:type?', name: 'market-types', component: () => import('@/pages/market/TypeInfo.vue')},
{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: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue')},
]},
@@ -25,5 +52,5 @@ export const routes: RouteRecordRaw[] = [
{path: '/tools', component: () => import('@/pages/Tools.vue')},
{path: '/characters', component: () => import('@/pages/Characters.vue')},
{path: '/about', name: 'about', component: () => import('@/pages/About.vue')},
];
{path: '/about', name: routeNames.about, component: () => import('@/pages/About.vue')},
] as const;
-37
View File
@@ -1,37 +0,0 @@
<script setup lang="ts">
import {RuleResponse} from "@/generated/mammon";
import {computed} from "vue";
import {isMain, Ledger, LedgerSelect, systemLedger, useLedgersStore} from "@/ledger";
import {ratesTypes} from "@/rules/rules.ts";
const rule = defineModel<RuleResponse>();
const ledgersStore = useLedgersStore();
const {findById} = ledgersStore;
const ledgers = computed<Ledger[]>(() => [systemLedger, ...ledgersStore.ledgers.filter(isMain)]);
const ledgerComputed = (key: 'fromLedgerId' | 'toLedgerId') => computed<Ledger>({
get: () => rule.value && rule.value[key] ? findById(rule.value[key]) ?? systemLedger : systemLedger,
set: value => {
if (value) {
rule.value = {...rule.value, [key]: value.ledgerId.length ? value.ledgerId : undefined}
}
}
})
const fromLedger = ledgerComputed('fromLedgerId')
const toLedger = ledgerComputed('toLedgerId')
</script>
<template>
From:
<LedgerSelect class="me-2 grow" v-model="fromLedger" :ledgers="ledgers" />
To:
<LedgerSelect class="me-2 grow" v-model="toLedger" :ledgers="ledgers" />
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>
-65
View File
@@ -1,65 +0,0 @@
<script setup lang="ts">
import {RuleResponse, RuleResponseRateEnum, RuleSetResponse} from "@/generated/mammon";
import RuleInput from "@/rules/RuleInput.vue";
import {computed, useTemplateRef} from "vue";
import {Bars4Icon, PlusIcon, TrashIcon} from '@heroicons/vue/24/outline';
import {useSortable} from "@vueuse/integrations/useSortable";
const ruleSet = defineModel<RuleSetResponse>();
const rules = computed<RuleResponse[]>({
get: () => ruleSet.value && ruleSet.value.rules ? ruleSet.value.rules : [],
set: value => ruleSet.value = {rules: value}
})
const addRule = () => {
rules.value = [...rules.value, {rate: RuleResponseRateEnum.None}]
}
const setRule = (index: number, rule?: RuleResponse) => {
if (!rule) {
return;
}
rules.value = rules.value.with(index, rule)
}
const removeRule = (index: number) => {
rules.value = rules.value.toSpliced(index, 1)
}
const sortableContainer = useTemplateRef('sortable-container')
useSortable(sortableContainer, rules, { 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="(rule, index) in rules" :key="index">
<span class="sortable-handle flex">
<Bars4Icon class="w-6"/>
</span>
<RuleInput :modelValue="rule" @update:modelValue="v => setRule(index, v)" />
<button class="btn-icon" @click="removeRule(index)"><TrashIcon /></button>
</div>
</div>
<div class="flex justify-end mb-2 mt-2">
<button class="btn-icon" @click="addRule"><PlusIcon /></button>
</div>
</div>
</template>
<style scoped>
.sortable-handle {
@apply cursor-grab;
}
.sortable-chosen {
@apply cursor-grabbing;
}
.sortable-chosen .sortable-handle {
@apply cursor-grabbing;
}
</style>
+104
View File
@@ -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
View File
@@ -1,3 +1,3 @@
export * from "./rules";
export {default as RuleSetInput} from './RuleSetInput.vue';
export {default as ScriptEditor} from './ScriptEditor.vue';
+54 -23
View File
@@ -1,30 +1,61 @@
import {ruleBookControllerApi} from "@/mammon";
import {RuleBookResponse, RuleResponseRateEnum, RuleSetResponse} from "@/generated/mammon";
import {characterRuleBookApi, ruleBookApi} from "@/mammon";
import {
CharacterRuleBookResponse,
CreateRuleBookRequest,
RuleBookResponse,
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"},
// bountyEarned: {id: "BOUNTY_EARNED", name: "Bounty Earned"},
// itemManufactured: {id: "ITEM_MANUFACTURED", name: "Item Manufactured"}
} as const;
export type RuleBook = RuleBookResponse;
export type Activity = { key: ActivityType, name: string }
export type ActivityType = typeof activityTypes[keyof typeof activityTypes]['key'];
export type RuleBook = RuleBookResponse & { ruleSets: { [key: ActivityType]: RuleSetResponse; } }
export const useRuleBooksStore = defineStore('rule-books', () => {
const ruleBooks = ref<RuleBook[]>([]);
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;
const addRuleBook = (ruleBook: RuleBook) => {
ruleBooks.value.push(ruleBook);
triggerRef(ruleBooks);
return ruleBook;
};
export type Rate = { key: RuleResponseRateEnum, name: string }
const replaceRuleBook = (ruleBook: RuleBook) => {
const index = ruleBooks.value.findIndex(rb => rb.ruleBookId === ruleBook.ruleBookId);
export const findByCharacterId = (characterId: number): Promise<RuleBook> => ruleBookControllerApi.findByCharacterId(characterId)
if (index !== -1) {
ruleBooks.value[index] = ruleBook;
}
triggerRef(ruleBooks);
return ruleBook;
};
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 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, duplicate, remove, refresh};
})
export const findCharacterRuleBookByCharacterId = (characterId: number): Promise<CharacterRuleBookResponse> => characterRuleBookApi.findCharacterRuleBookByCharacterId(characterId)
.then(response => response.data)
.catch(() => ({characterId, ruleSets: {}}));
.catch(() => ({characterId, ruleBookId: '', bindings: {}}));
export const setCharacterRuleBook = (characterId: number, ruleBook: RuleBook): Promise<RuleBook> => ruleBookControllerApi.setCharacterRuleBook(characterId, ruleBook)
.then(response => response.data);
export const setCharacterRuleBookForCharacter = (characterId: number, ruleBook: SetCharacterRuleBookRequest): Promise<CharacterRuleBookResponse> => characterRuleBookApi.setCharacterRuleBookForCharacter(characterId, ruleBook)
.then(response => response.data);
export const fetchScriptDefinitions = (): Promise<string> =>
ruleBookApi.getScriptDefinitions({responseType: 'text'}).then(response => response.data);
+4 -11
View File
@@ -1,11 +1,12 @@
<script setup lang="ts">
import {Dropdown} from '@/components';
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"}
];
@@ -28,7 +29,7 @@ const logout = async () => {
<RouterLink class="sidebar-button py-0.5 px-2" to="/characters">Characters</RouterLink>
</li>
<li>
<RouterLink class="sidebar-button py-0.5 px-2" :to="{name: 'about'}">About EVE Online</RouterLink>
<RouterLink class="sidebar-button py-0.5 px-2" :to="{name: routeNames.about}">About EVE Online</RouterLink>
</li>
<li>
<a class="sidebar-button py-0.5 px-2 text-amber-700" @click="logout">Logout</a>
@@ -48,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;
@@ -62,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;
}
+14 -3
View File
@@ -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,9 +78,14 @@
}
.btn-icon {
@apply p-0 border-none bg-transparent hover:text-slate-400 hover:bg-transparent;
> svg {
@apply w-6 h-6;
@apply btn-icon;
}
a.tab {
@apply flex items-center px-4 me-2 rounded-t-md bg-slate-600 hover:bg-slate-700;
&.router-link-active {
@apply bg-emerald-500 hover:bg-emerald-700;
}
}
}
+78
View File
@@ -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 {findById} = useLedgersStore();
const sortedArray = computedAsync(async () => {
if (!props.transfers) {
return [];
}
return (await Promise.all(props.transfers.map(async (transfer: TransferWithValue, index) => {
const fromLedger = findById(transfer.fromLedgerId) ?? systemLedger
const toLedger = 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>
+3
View File
@@ -0,0 +1,3 @@
export * from './transaction'
export {default as TransferList} from './TransferList.vue';
+7
View File
@@ -0,0 +1,7 @@
import {TransferResponse} from "@/generated/mammon";
export const TransferTypes = {
Isk: 'ISK',
Item: 'ITEM',
} as const;
export type TransferType = TransferResponse['type'];