rule book ui

This commit is contained in:
Sirttas
2026-05-24 13:39:35 +02:00
parent a1dbe41b6c
commit 4b39d491d2
15 changed files with 524 additions and 17 deletions
+365
View File
@@ -0,0 +1,365 @@
openapi: 3.1.0
info:
title: OpenAPI definition
version: v0
servers:
- url: http://localhost:8080
description: Generated server url
paths:
/ledgers/main/{ledgerId}:
put:
tags:
- ledger-controller
operationId: updateMainLedger
parameters:
- name: ledgerId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateMainLedgerRequest"
required: true
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
content:
'*/*':
schema:
$ref: "#/components/schemas/MainLedgerResponse"
/ledgers/combined/{ledgerId}:
put:
tags:
- ledger-controller
operationId: updateCombinedLedger
parameters:
- name: ledgerId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateCombinedLedgerRequest"
required: true
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
content:
'*/*':
schema:
$ref: "#/components/schemas/CombinedLedgerResponse"
/ledgers/main:
post:
tags:
- ledger-controller
operationId: createMainLedger
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateMainLedgerRequest"
required: true
responses:
"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"
/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
"400":
description: Bad Request
"200":
description: OK
/ledgers:
get:
tags:
- ledger-controller
operationId: findAll
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
content:
'*/*':
schema:
type: array
items:
oneOf:
- $ref: "#/components/schemas/CombinedLedgerResponse"
- $ref: "#/components/schemas/MainLedgerResponse"
/ledgers/{ledgerId}:
get:
tags:
- ledger-controller
operationId: findById
parameters:
- name: ledgerId
in: path
required: true
schema:
type: string
format: uuid
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
content:
'*/*':
schema:
oneOf:
- $ref: "#/components/schemas/CombinedLedgerResponse"
- $ref: "#/components/schemas/MainLedgerResponse"
/characters:
get:
tags:
- character-controller
operationId: getCharacters
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/CharacterResponse"
/characters/{characterId}/rule-book:
get:
tags:
- rule-book-controller
operationId: findByCharacterId
parameters:
- name: characterId
in: path
required: true
schema:
type: integer
format: int64
responses:
"404":
description: Not Found
"400":
description: Bad Request
"200":
description: OK
content:
'*/*':
schema:
$ref: "#/components/schemas/RuleBookResponse"
components:
schemas:
UpdateMainLedgerRequest:
type: object
properties:
name:
type: string
required:
- name
MainLedgerResponse:
allOf:
- $ref: "#/components/schemas/LedgerResponse"
- type: object
properties:
ledgerId:
type: string
format: uuid
name:
type: string
balance:
type: number
type:
type: string
enum:
- MAIN
required:
- balance
- ledgerId
- name
UpdateCombinedLedgerRequest:
type: object
properties:
name:
type: string
memberLedgerIds:
type: array
items:
type: string
format: uuid
required:
- memberLedgerIds
- name
CombinedLedgerResponse:
allOf:
- $ref: "#/components/schemas/LedgerResponse"
- type: object
properties:
ledgerId:
type: string
format: uuid
name:
type: string
balance:
type: number
memberLedgerIds:
type: array
items:
type: string
format: uuid
type:
type: string
enum:
- COMBINED
required:
- balance
- ledgerId
- memberLedgerIds
- name
CreateMainLedgerRequest:
type: object
properties:
name:
type: string
required:
- name
CreateCombinedLedgerRequest:
type: object
properties:
name:
type: string
memberLedgerIds:
type: array
items:
type: string
format: uuid
required:
- memberLedgerIds
- name
LedgerResponse:
type: object
discriminator:
propertyName: type
properties:
type:
type: string
enum:
- MAIN
- COMBINED
CharacterResponse:
type: object
properties:
characterId:
type: integer
format: int64
name:
type: string
required:
- characterId
- name
RuleBookResponse:
type: object
properties:
characterId:
type: integer
format: int64
ruleSets:
type: object
additionalProperties:
$ref: "#/components/schemas/RuleSetResponse"
required:
- characterId
- ruleSets
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
+9 -1
View File
@@ -8,7 +8,15 @@ export type Character = CharacterResponse
export const useCharactersStore = defineStore('characters', () => { export const useCharactersStore = defineStore('characters', () => {
const characters = ref<Character[]>([]); const characters = ref<Character[]>([]);
const findById = (characterId: number): Character | undefined => characters.value.find(c => c.characterId === characterId); const findById = async (characterId: number): Promise<Character | undefined> => {
let character = characters.value.find(c => c.characterId === characterId);
if (!character) {
await refresh(); // TODO call api instead of refresh
character = characters.value.find(c => c.characterId === characterId);
}
return character;
}
const refresh = () => characterControllerApi.getCharacters().then(response => characters.value = response.data); const refresh = () => characterControllerApi.getCharacters().then(response => characters.value = response.data);
-1
View File
@@ -11,7 +11,6 @@ export const percentFormater = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 0 maximumFractionDigits: 0
}); });
const timeFormat = new Intl.NumberFormat("en-US", { const timeFormat = new Intl.NumberFormat("en-US", {
minimumFractionDigits: 0, minimumFractionDigits: 0,
maximumFractionDigits: 0, maximumFractionDigits: 0,
+3 -3
View File
@@ -83,8 +83,8 @@ export interface RuleBookResponse {
} }
export interface RuleResponse { export interface RuleResponse {
'rate': RuleResponseRateEnum; 'rate': RuleResponseRateEnum;
'fromLedgerId': string; 'fromLedgerId'?: string;
'toLedgerId': string; 'toLedgerId'?: string;
} }
export const RuleResponseRateEnum = { export const RuleResponseRateEnum = {
@@ -736,7 +736,7 @@ export const RuleBookControllerApiAxiosParamCreator = function (configuration?:
findByCharacterId: async (characterId: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { findByCharacterId: async (characterId: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'characterId' is not null or undefined // verify required parameter 'characterId' is not null or undefined
assertParamExists('findByCharacterId', 'characterId', characterId) assertParamExists('findByCharacterId', 'characterId', characterId)
const localVarPath = `/rule-books/{characterId}` const localVarPath = `/characters/{characterId}/rule-book`
.replace('{characterId}', encodeURIComponent(String(characterId))); .replace('{characterId}', encodeURIComponent(String(characterId)));
// use dummy base URL string because the URL constructor only accepts absolute URLs. // use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+2 -3
View File
@@ -6,6 +6,7 @@ import {isCombined, Ledger, LedgerType, LedgerTypes, useLedgersStore} from "./le
import {Modal} from "@/components"; import {Modal} from "@/components";
import LedgerLabel from "./LedgerLabel.vue"; import LedgerLabel from "./LedgerLabel.vue";
import {PlusIcon, TrashIcon} from '@heroicons/vue/24/outline'; import {PlusIcon, TrashIcon} from '@heroicons/vue/24/outline';
import LedgerSelect from "@/ledger/LedgerSelect.vue";
interface Props { interface Props {
ledgerId?: string; ledgerId?: string;
@@ -119,9 +120,7 @@ defineExpose({ open });
</div> </div>
</div> </div>
<div v-if="availableLedgers.length" class="flex"> <div v-if="availableLedgers.length" class="flex">
<select v-model="selectedLedger" class="grow"> <LedgerSelect v-model="selectedLedger" class="grow" :ledgers="availableLedgers" />
<option v-for="ledger in availableLedgers" :key="ledger.ledgerId" :value="ledger">{{ ledger.name }}</option>
</select>
<div class="flex justify-end me-4 ms-2"> <div class="flex justify-end me-4 ms-2">
<button class="btn-icon" @click="addMember"><PlusIcon /></button> <button class="btn-icon" @click="addMember"><PlusIcon /></button>
</div> </div>
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
import {Ledger, useLedgersStore} from "@/ledger/ledger.ts";
import {storeToRefs} from "pinia";
import {computed} from "vue";
interface Props {
ledgers?: Ledger[];
}
const props = defineProps<Props>()
const ledger = defineModel<Ledger>();
const {ledgers: allLedgers} = storeToRefs(useLedgersStore());
const ledgersToUse = computed(() => props.ledgers || allLedgers);
</script>
<template>
<select v-model="ledger">
<option v-for="ledger in ledgersToUse" :key="ledger.ledgerId" :value="ledger">{{ ledger.name }}</option>
</select>
</template>
+1
View File
@@ -1,4 +1,5 @@
export * from './ledger'; export * from './ledger';
export {default as LedgerLabel} from './LedgerLabel.vue'; export {default as LedgerLabel} from './LedgerLabel.vue';
export {default as LedgerSelect} from './LedgerSelect.vue';
export {default as CreateLedgerModal} from './CreateLedgerModal.vue'; export {default as CreateLedgerModal} from './CreateLedgerModal.vue';
+2 -1
View File
@@ -1,6 +1,6 @@
import {logResource} from "@/service"; import {logResource} from "@/service";
import axios from "axios"; import axios from "axios";
import {CharacterControllerApi, LedgerControllerApi} from "@/generated/mammon"; import {CharacterControllerApi, LedgerControllerApi, RuleBookControllerApi} from "@/generated/mammon";
export const mammonUrl = import.meta.env.VITE_MAMMON_URL; export const mammonUrl = import.meta.env.VITE_MAMMON_URL;
export const mammonAddCharacterUrl = mammonUrl + "oauth2/authorization/esi" export const mammonAddCharacterUrl = mammonUrl + "oauth2/authorization/esi"
@@ -16,3 +16,4 @@ logResource(mammonAxiosInstance)
export const ledgerControllerApi = new LedgerControllerApi(undefined, mammonUrl, mammonAxiosInstance); export const ledgerControllerApi = new LedgerControllerApi(undefined, mammonUrl, mammonAxiosInstance);
export const characterControllerApi = new CharacterControllerApi(undefined, mammonUrl, mammonAxiosInstance); export const characterControllerApi = new CharacterControllerApi(undefined, mammonUrl, mammonAxiosInstance);
export const ruleBookControllerApi = new RuleBookControllerApi(undefined, mammonUrl, mammonAxiosInstance);
+22 -6
View File
@@ -1,18 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import {Character, CharacterLabel, useCharactersStore} from "@/characters"; import {Character, CharacterLabel, useCharactersStore} from "@/characters";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {ref, watch} from "vue"; import {ref, watch, watchEffect} from "vue";
import log from "loglevel"; import log from "loglevel";
import {activityTypes, findByCharacterId, RuleBook, RuleSetInput} from "@/rules";
const {findById} = useCharactersStore(); const {findById: findCharacterById} = useCharactersStore();
const character = ref<Character>(); const character = ref<Character>();
const ruleBook = ref<RuleBook>();
watchEffect(async () => {
const characterId = character.value?.characterId;
if (characterId) {
ruleBook.value = await findByCharacterId(characterId);
}
});
watch(useRoute(), async route => { watch(useRoute(), async route => {
if (route.params.characterId) { if (route.params.characterId) {
const id = parseInt(typeof route.params.characterId === 'string' ? route.params.characterId : route.params.characterId[0]); const id = parseInt(typeof route.params.characterId === 'string' ? route.params.characterId : route.params.characterId[0]);
character.value = findById(id); character.value = await findCharacterById(id);
log.info('Loaded character:', character.value); log.info('Loaded character:', character.value);
} else { } else {
character.value = undefined; character.value = undefined;
@@ -23,8 +33,14 @@ watch(useRoute(), async route => {
<template> <template>
<div v-if="character" class="grid mb-2 mt-4"> <div v-if="character" class="grid mb-2 mt-4">
<div class="mb-4 border-b-1 flex"> <div class="mb-2 border-b-1 flex">
<CharacterLabel class="flex grow mb-2" :character="character" size="128" /> <CharacterLabel class="flex grow mb-2" :character="character" :size="64" />
</div>
<div v-if="ruleBook" class="flex-col">
<div class="flex-col grow border-b-1" v-for="activityType in activityTypes" :key="activityType">
<span>{{ activityType }}</span>
<RuleSetInput v-model="ruleBook.ruleSets[activityType]" />
</div>
</div> </div>
</div> </div>
</template> </template>
+1 -1
View File
@@ -10,7 +10,7 @@ export const routes: RouteRecordRaw[] = [
{path: '/rules', component: () => import('@/pages/Rules.vue'), children: [ {path: '/rules', component: () => import('@/pages/Rules.vue'), children: [
{path: '', component: () => import('./pages/rules/ListRuleBooks.vue')}, {path: '', component: () => import('./pages/rules/ListRuleBooks.vue')},
{path: ':characterId', name: 'character-rulebook', component: () => import('@/pages/rules/EditRuleBook.vue')}, {path: '/characters/:characterId/rule-book', name: 'character-rulebook', component: () => import('@/pages/rules/EditRuleBook.vue')},
]}, ]},
{path: '/market', component: () => import('@/pages/Market.vue'), children: [ {path: '/market', component: () => import('@/pages/Market.vue'), children: [
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
import {RuleResponse, RuleResponseRateEnum} from "@/generated/mammon";
import {computed} from "vue";
import {isMain, Ledger, LedgerSelect, useLedgersStore} from "@/ledger";
const rule = defineModel<RuleResponse>();
const ledgersStore = useLedgersStore();
const {findById} = ledgersStore;
const ledgers = computed(() => ledgersStore.ledgers.filter(isMain));
const ledgerComputed = (key: 'fromLedgerId' | 'toLedgerId') => computed<Ledger | undefined>({
get: () => rule.value && rule.value[key] ? findById(rule.value[key]) : undefined,
set: value => {
if (value) {
rule.value = {...rule.value, [key]: value.ledgerId}
}
}
})
const fromLedger = ledgerComputed('fromLedgerId')
const toLedger = ledgerComputed('toLedgerId')
</script>
<template>
<select class="me-2 grow" v-model="rule.rate">
<option v-for="rateType in RuleResponseRateEnum" :key="rateType" :value="rateType">{{ rateType }}</option>
</select>
<LedgerSelect class="me-2 grow" v-model="fromLedger" :ledgers="ledgers" />
<LedgerSelect class="me-2 grow" v-model="toLedger" :ledgers="ledgers" />
</template>
<style scoped>
</style>
+40
View File
@@ -0,0 +1,40 @@
<script setup lang="ts">
import {RuleResponse, RuleResponseRateEnum, RuleSetResponse} from "@/generated/mammon";
import RuleInput from "@/rules/RuleInput.vue";
import {computed} from "vue";
import {PlusIcon, TrashIcon} from '@heroicons/vue/24/outline';
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)
}
</script>
<template>
<div class="flex-col">
<div class="flex items-end gap-2 mt-2" v-for="(rule, index) in rules" :key="index">
<RuleInput :modelValue="rule" @update:modelValue="v => setRule(index, v)" />
<button class="btn-icon" @click="removeRule(index)"><TrashIcon /></button>
</div>
<div class="flex justify-end mb-2 mt-2">
<button class="btn-icon" @click="addRule"><PlusIcon /></button>
</div>
</div>
</template>
+3
View File
@@ -0,0 +1,3 @@
export * from "./rules";
export {default as RuleSetInput} from './RuleSetInput.vue';
+16
View File
@@ -0,0 +1,16 @@
import {ruleBookControllerApi} from "@/mammon";
import {RuleBookResponse, RuleSetResponse} from "@/generated/mammon";
export const activityTypes = {
itemBought: "ITEM_BOUGHT",
itemSold: "ITEM_SOLD",
bountyEarned: "BOUNTY_EARNED",
itemManufactured: "ITEM_MANUFACTURED"
} as const;
export type ActivityType = typeof activityTypes[keyof typeof activityTypes];
export type RuleBook = RuleBookResponse & { ruleSets: { [key: ActivityType]: RuleSetResponse; } }
export const findByCharacterId = (characterId: number): Promise<RuleBook> => ruleBookControllerApi.findByCharacterId(characterId)
.then(response => response.data)
.catch(() => ({characterId, ruleSets: {}}));
+1 -1
View File
@@ -1 +1 @@
export const copyToClipboard = (s: string) => navigator.clipboard.writeText(s); export const copyToClipboard = (s: string) => navigator.clipboard.writeText(s);