feat(#7): Rework rule-book UI for one-rule-book-per-user model

This commit is contained in:
Sirttas
2026-07-02 19:28:37 +02:00
parent e66fe9b40e
commit 052a0a5d03
10 changed files with 539 additions and 1288 deletions
+166 -310
View File
@@ -6,44 +6,28 @@ servers:
- url: http://localhost:8080 - url: http://localhost:8080
description: Generated server url description: Generated server url
paths: paths:
/rule-books/{ruleBookId}: /rule-book:
get: get:
tags: tags:
- rule-book - rule-book
summary: Find a rule book by its id summary: Find the caller's rule book
operationId: findRuleBookById operationId: findRuleBook
parameters:
- name: ruleBookId
in: path
description: Id of the rule book
required: true
schema:
type: string
format: uuid
responses: responses:
"200": "200":
description: The rule book description: The caller's rule book
content: content:
'*/*': '*/*':
schema: schema:
$ref: "#/components/schemas/RuleBookResponse" $ref: "#/components/schemas/RuleBookResponse"
"404": "404":
description: No rule book with this id description: The caller has no rule book
put: put:
tags: tags:
- rule-book - rule-book
summary: Update a rule book summary: Update the caller's rule book
operationId: updateRuleBook operationId: updateRuleBook
parameters:
- name: ruleBookId
in: path
description: Id of the rule book
required: true
schema:
type: string
format: uuid
requestBody: requestBody:
description: New state of the rule book description: New state of the caller's rule book
content: content:
application/json: application/json:
schema: schema:
@@ -59,29 +43,8 @@ paths:
"400": "400":
description: |- description: |-
Returned when: Returned when:
- the request is invalid (e.g. blank name) - a binding references a ledger the caller does not own
- the rule book is the default rule book, which cannot be modified - a binding references a ledger that is not a main ledger
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: |-
Returned when:
- the rule book is associated to a character
- the rule book is the default rule book, which cannot be deleted
/ledgers/main/{ledgerId}: /ledgers/main/{ledgerId}:
put: put:
tags: tags:
@@ -152,108 +115,6 @@ paths:
- the request is invalid - the request is invalid
"404": "404":
description: No ledger with this id description: No ledger with this id
/characters/{characterId}/rule-book:
get:
tags:
- 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:
"200":
description: The rule book assignment of the character
content:
'*/*':
schema:
$ref: "#/components/schemas/CharacterRuleBookResponse"
"400":
description: Invalid character id
"404":
description: No rule book assigned to this character
put:
tags:
- 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:
"200":
description: The rule book assignment of the character
content:
'*/*':
schema:
$ref: "#/components/schemas/CharacterRuleBookResponse"
"400":
description: |-
Returned when:
- the referenced rule book does not exist
- a bound ledger does not exist
- a bound ledger is not a main or system ledger
- a required ledger binding is missing
/rule-books:
get:
tags:
- rule-book
summary: Find all rule books
operationId: findAllRuleBooks
responses:
"200":
description: All rule books
content:
'*/*':
schema:
type: array
items:
$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
headers:
Location:
description: URL of the created rule book
style: simple
schema:
type: string
format: uri
content:
'*/*':
schema:
$ref: "#/components/schemas/RuleBookResponse"
"400":
description: Invalid request (e.g. blank name)
/reprocessing: /reprocessing:
post: post:
tags: tags:
@@ -456,7 +317,8 @@ paths:
post: post:
tags: tags:
- activity - activity
summary: Fetch all new activities for all characters from the EVE API summary: Fetch new activities for the authenticated user's characters from the
EVE API
operationId: fetchAllNewActivities operationId: fetchAllNewActivities
responses: responses:
"200": "200":
@@ -484,15 +346,15 @@ paths:
post: post:
tags: tags:
- activity - activity
summary: Process new activities for all characters with a usable token summary: Process new activities for the authenticated user's characters
operationId: processNewActivities operationId: processNewActivities
responses: responses:
"200": "200":
description: New activities processed description: New activities processed
/rule-books/script-definitions: /rule-scripts/definitions:
get: get:
tags: tags:
- rule-book - rule-script
summary: Download the TypeScript definitions for the rule script runtime summary: Download the TypeScript definitions for the rule script runtime
operationId: getScriptDefinitions operationId: getScriptDefinitions
responses: responses:
@@ -829,15 +691,31 @@ paths:
- the types parameter is missing - the types parameter is missing
- a types value is not a numeric id - a types value is not a numeric id
- the days parameter is not greater than 0 - the days parameter is not greater than 0
/market-orders:
get:
tags:
- market-order
summary: List the open market orders across all characters owned by the authenticated
user
operationId: findAllMarketOrders
responses:
"200":
description: The authenticated user's open market orders
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/MarketOrderResponse"
/ledgers: /ledgers:
get: get:
tags: tags:
- ledger - ledger
summary: Find all ledgers summary: Find all ledgers owned by the authenticated user
operationId: findAllLedgers operationId: findAllLedgers
responses: responses:
"200": "200":
description: All ledgers description: The authenticated user's ledgers
content: content:
'*/*': '*/*':
schema: schema:
@@ -921,32 +799,17 @@ paths:
get: get:
tags: tags:
- character - character
summary: Find all characters with a usable token summary: Find the characters owned by the authenticated user
operationId: findAllCharacters operationId: findAllCharacters
responses: responses:
"200": "200":
description: All characters with a usable token description: The authenticated user's characters
content: content:
'*/*': '*/*':
schema: schema:
type: array type: array
items: items:
$ref: "#/components/schemas/CharacterResponse" $ref: "#/components/schemas/CharacterResponse"
/characters/rule-books:
get:
tags:
- character-rule-book
summary: Find the rule books of all characters that have a token
operationId: findAllCharacterRuleBooks
responses:
"200":
description: Rule book assignments of all characters with a token
content:
'*/*':
schema:
type: array
items:
$ref: "#/components/schemas/CharacterRuleBookResponse"
/acquisitions: /acquisitions:
get: get:
tags: tags:
@@ -982,69 +845,58 @@ components:
schemas: schemas:
UpdateRuleBookRequest: UpdateRuleBookRequest:
type: object type: object
description: Request to update an existing rule book; replaces its mutable fields. description: Request to update the caller's rule book; replaces its mutable
fields.
properties: properties:
name:
type: string
description: Rule book name.
example: Trading rules
usedForAcquisitions: usedForAcquisitions:
type: boolean type: boolean
description: Whether this rule book is used to derive acquisitions. description: Whether this rule book is used to derive acquisitions.
example: true example: true
ledgerRefs: bindings:
type: array type: object
description: Symbolic ledger references the script writes to; each must additionalProperties:
be bound to a ledger per character.
items:
type: string type: string
example: sales format: uuid
pattern: "[a-z][a-zA-Z0-9]*" description: "Ledger references the script writes to, each bound to one\
\ of the user's ledgers."
script: script:
type: string type: string
description: The classification script source. description: The classification script source.
example: transfer.to('sales') example: transfer.to('sales')
required: required:
- ledgerRefs - bindings
- name
- script - script
- usedForAcquisitions - usedForAcquisitions
RuleBookResponse: RuleBookResponse:
type: object type: object
description: "A rule book: a named script that classifies transactions, together\ description: "The caller's rule book: a script that classifies transactions,\
\ with the ledger references it expects." \ together with its ledger bindings."
properties: properties:
ruleBookId: userId:
type: string type: string
format: uuid format: uuid
description: Unique rule book identifier. description: Owning user identifier.
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6 example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
name:
type: string
description: Rule book name.
example: Trading rules
usedForAcquisitions: usedForAcquisitions:
type: boolean type: boolean
description: Whether this rule book is used to derive acquisitions. description: Whether this rule book is used to derive acquisitions.
example: true example: true
ledgerRefs: bindings:
type: array type: object
description: Symbolic ledger references the script writes to; each must additionalProperties:
be bound to a ledger per character.
items:
type: string type: string
example: sales format: uuid
pattern: "[a-z][a-zA-Z0-9]*" description: "Ledger references the script writes to, each bound to one\
\ of the user's ledgers."
script: script:
type: string type: string
description: The classification script source. description: The classification script source.
example: transfer.to('sales') example: transfer.to('sales')
required: required:
- ledgerRefs - bindings
- name
- ruleBookId
- script - script
- usedForAcquisitions - usedForAcquisitions
- userId
UpdateMainLedgerRequest: UpdateMainLedgerRequest:
type: object type: object
description: Request to update an existing main ledger. description: Request to update an existing main ledger.
@@ -1127,113 +979,6 @@ components:
- ledgerId - ledgerId
- memberLedgerIds - memberLedgerIds
- name - name
SetCharacterRuleBookRequest:
type: object
description: "Request to assign a rule book to a character, binding each of\
\ its ledger references to a concrete ledger."
properties:
ruleBookId:
type: string
format: uuid
description: Id of the rule book to assign.
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
bindings:
type: object
additionalProperties:
type: string
format: uuid
description: Bindings from each ledger reference name to the ledger id it
resolves to for this character.
example:
sales: 5c1e6f2a-1234-4562-b3fc-2c963f66afa6
required:
- bindings
- ruleBookId
CharacterResponse:
type: object
description: An EVE Online character.
properties:
characterId:
type: integer
format: int64
description: EVE character id.
example: 2112625428
name:
type: string
description: Character name.
example: CCP Falcon
required:
- characterId
- name
CharacterRuleBookResponse:
type: object
description: "A rule book assigned to a character, with the ledger bindings\
\ that resolve its references."
properties:
character:
$ref: "#/components/schemas/CharacterResponse"
description: The character the rule book is assigned to.
ruleBook:
$ref: "#/components/schemas/RuleBookSummaryResponse"
description: Summary of the assigned rule book.
bindings:
type: object
additionalProperties:
type: string
format: uuid
description: Bindings from each ledger reference name to the ledger id it
resolves to for this character.
example:
sales: 5c1e6f2a-1234-4562-b3fc-2c963f66afa6
required:
- bindings
- character
- ruleBook
RuleBookSummaryResponse:
type: object
description: "Lightweight reference to a rule book: its id and name only."
properties:
ruleBookId:
type: string
format: uuid
description: Unique rule book identifier.
example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
name:
type: string
description: Rule book name.
example: Trading rules
required:
- name
- ruleBookId
CreateRuleBookRequest:
type: object
description: Request to create a new rule book.
properties:
name:
type: string
description: Rule book name.
example: Trading rules
usedForAcquisitions:
type: boolean
description: Whether this rule book is used to derive acquisitions.
example: true
ledgerRefs:
type: array
description: Symbolic ledger references the script writes to; each must
be bound to a ledger per character.
items:
type: string
example: sales
pattern: "[a-z][a-zA-Z0-9]*"
script:
type: string
description: The classification script source.
example: transfer.to('sales')
required:
- ledgerRefs
- name
- script
- usedForAcquisitions
MarketTypeStackRequest: MarketTypeStackRequest:
type: object type: object
description: A quantity of a single market type to reprocess. description: A quantity of a single market type to reprocess.
@@ -1373,6 +1118,22 @@ components:
required: required:
- memberLedgerIds - memberLedgerIds
- name - name
CharacterResponse:
type: object
description: An EVE Online character.
properties:
characterId:
type: integer
format: int64
description: EVE character id.
example: 2112625428
name:
type: string
description: Character name.
example: CCP Falcon
required:
- characterId
- name
MeResponse: MeResponse:
type: object type: object
description: The authenticated user and the characters they own. description: The authenticated user and the characters they own.
@@ -1627,6 +1388,101 @@ components:
- marketTypeId - marketTypeId
- orderCount - orderCount
- sell - sell
MarketOrderResponse:
type: object
description: An open market order placed by one of the authenticated user's
characters.
properties:
orderId:
type: integer
format: int64
description: EVE market order id.
example: 5266893063
characterId:
type: integer
format: int64
description: EVE character id that owns the order.
example: 2112625428
characterName:
type: string
description: Name of the character that owns the order.
example: CCP Falcon
marketTypeId:
type: integer
format: int64
description: EVE market type (item) id being traded.
example: 34
locationId:
type: integer
format: int64
description: Location id (station or structure) the order is placed at.
example: 60003760
regionId:
type: integer
format: int64
description: Region id the order resides in.
example: 10000002
direction:
type: string
description: Trade direction of the order.
enum:
- BUY
- SELL
example: SELL
price:
type: number
description: "Price per unit, in ISK."
example: 5.42
volumeTotal:
type: integer
format: int64
description: Total volume the order was created with.
example: 1000000
volumeRemain:
type: integer
format: int64
description: Volume still remaining on the order.
example: 750000
minVolume:
type: integer
format: int32
description: Minimum volume that must be bought or sold in a single transaction.
example: 1
range:
type: string
description: "Order range. For buy orders the matching range, for sell orders\
\ always 'region'."
example: region
duration:
type: integer
format: int32
description: "Duration of the order, in days."
example: 90
issued:
type: string
format: date-time
description: When the order was issued.
example: 2026-06-22T05:12:25Z
escrow:
type: number
description: ISK held in escrow for a buy order; null for sell orders.
example: 4065000.0
required:
- characterId
- characterName
- direction
- duration
- escrow
- issued
- locationId
- marketTypeId
- minVolume
- orderId
- price
- range
- regionId
- volumeRemain
- volumeTotal
LedgerResponse: LedgerResponse:
description: "A ledger, either a standalone main ledger or a combined ledger\ description: "A ledger, either a standalone main ledger or a combined ledger\
\ aggregating others." \ aggregating others."
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -6,12 +6,12 @@ import {
ActivityApi, ActivityApi,
AuthApi, AuthApi,
CharacterApi, CharacterApi,
CharacterRuleBookApi,
LedgerApi, LedgerApi,
MarketApi, MarketApi,
MeResponse, MeResponse,
ReprocessingApi, ReprocessingApi,
RuleBookApi, RuleBookApi,
RuleScriptApi,
TransactionApi TransactionApi
} from "@/generated/mammon"; } from "@/generated/mammon";
@@ -101,7 +101,7 @@ export const ledgerApi = new LedgerApi(undefined, mammonUrl, mammonAxiosInstance
export const transactionApi = new TransactionApi(undefined, mammonUrl, mammonAxiosInstance); export const transactionApi = new TransactionApi(undefined, mammonUrl, mammonAxiosInstance);
export const characterApi = new CharacterApi(undefined, mammonUrl, mammonAxiosInstance); export const characterApi = new CharacterApi(undefined, mammonUrl, mammonAxiosInstance);
export const ruleBookApi = new RuleBookApi(undefined, mammonUrl, mammonAxiosInstance); export const ruleBookApi = new RuleBookApi(undefined, mammonUrl, mammonAxiosInstance);
export const characterRuleBookApi = new CharacterRuleBookApi(undefined, mammonUrl, mammonAxiosInstance); export const ruleScriptApi = new RuleScriptApi(undefined, mammonUrl, mammonAxiosInstance);
export const activityApi = new ActivityApi(undefined, mammonUrl, mammonAxiosInstance); export const activityApi = new ActivityApi(undefined, mammonUrl, mammonAxiosInstance);
export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance); export const acquisitionApi = new AcquisitionApi(undefined, mammonUrl, mammonAxiosInstance);
export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance); export const marketApi = new MarketApi(undefined, mammonUrl, mammonAxiosInstance);
-18
View File
@@ -1,18 +0,0 @@
<script setup lang="ts">
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: routeNames.listRuleBooks }" class="tab">
<span>Rule Books</span>
</RouterLink>
<RouterLink to="/characters/rules" class="tab">
<span>Characters Rules</span>
</RouterLink>
</div>
<RouterView />
</div>
</template>
-93
View File
@@ -1,93 +0,0 @@
<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 {
RuleBook,
useCharacterRuleBooksStore,
useRuleBooksStore
} from "@/rules";
import {isMain, Ledger, LedgerSelect, systemLedger, useLedgersStore} from "@/ledger";
type Bindings = { [key: string]: Ledger; };
const ruleBookStore = useRuleBooksStore();
const characterRuleBooksStore = useCharacterRuleBooksStore();
const charactersStore = useCharactersStore();
const ledgersStore = useLedgersStore();
const ledgersToUse = computed(() => [systemLedger, ...ledgersStore.ledgers.filter(isMain)]);
const character = ref<Character>();
const ruleBook = ref<RuleBook>();
const bindings = ref<Bindings>({});
const ledgerRefs = computed<string[]>(() => ruleBook.value?.ledgerRefs ?? [])
watchEffect(async () => {
const characterId = character.value?.characterId;
if (characterId) {
const characterRuleBook = characterRuleBooksStore.findByCharacterId(characterId);
ruleBook.value = ruleBookStore.findById(characterRuleBook?.ruleBook.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) {
characterRuleBooksStore.setForCharacter(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 charactersStore.findById(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 ruleBookStore.ruleBooks" :key="rb.ruleBookId" :value="rb">{{ rb.name }}</option>
</select>
</div>
<div class="flex-col border-b-1">
Ledger Bindings:
<div class="flex flex-wrap items-center mb-2 mt-2">
<div class="me-2" v-for="ref in 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>
+49 -75
View File
@@ -1,41 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import {useRoute, useRouter} from "vue-router"; import {computed, ref, watchEffect} from "vue";
import {ref, watch} from "vue"; import {useEventListener} from "@vueuse/core";
import {useDebounceFn, useEventListener} from "@vueuse/core";
import log from "loglevel"; import log from "loglevel";
import {ScriptEditor, useRuleBooksStore} from "@/rules"; import {ScriptEditor, useRuleBookStore} from "@/rules";
import {PlusIcon, TrashIcon} from "@heroicons/vue/24/outline"; import {PlusIcon, TrashIcon} from "@heroicons/vue/24/outline";
import {routeNames} from "@/routes";
import {SliderCheckbox} from "@/components"; import {SliderCheckbox} from "@/components";
import {isMain, Ledger, LedgerSelect, systemLedger, useLedgersStore} from "@/ledger";
type Binding = { ref: string; ledger: Ledger };
const ruleBookStore = useRuleBookStore();
const ledgersStore = useLedgersStore();
const ledgersToUse = computed(() => [systemLedger, ...ledgersStore.ledgers.filter(isMain)]);
const ruleBookId = ref<string>();
const name = ref<string>('');
const usedForAcquisitions = ref<boolean>(false); const usedForAcquisitions = ref<boolean>(false);
const ledgerRefs = ref<string[]>([]); const bindings = ref<Binding[]>([]);
const script = ref<string>(''); const script = ref<string>('');
const ruleBooksStore = useRuleBooksStore(); const ledgerRefs = computed<string[]>(() => bindings.value.map(b => b.ref));
const router = useRouter();
const save = async () => { watchEffect(() => {
if (!ruleBookId.value) { const ruleBook = ruleBookStore.ruleBook;
const created = await ruleBooksStore.create({
name: name.value,
usedForAcquisitions: usedForAcquisitions.value,
ledgerRefs: ledgerRefs.value,
script: script.value
})
await router.push({ name: routeNames.editRuleBook, params: {ruleBookId: created.ruleBookId}})
} else { usedForAcquisitions.value = ruleBook?.usedForAcquisitions ?? false;
await ruleBooksStore.update(ruleBookId.value, { script.value = ruleBook?.script ?? '';
name: name.value, bindings.value = Object.entries(ruleBook?.bindings ?? {})
usedForAcquisitions: usedForAcquisitions.value, .map(([ref, id]) => ({ref, ledger: ledgersToUse.value.find(l => l.ledgerId === id) ?? systemLedger}));
ledgerRefs: ledgerRefs.value, log.info('Loaded rule book:', ruleBook);
script: script.value });
})
} const addBinding = () => {
} bindings.value = [...bindings.value, {ref: '', ledger: systemLedger}];
};
const removeBinding = (index: number) => {
bindings.value = bindings.value.toSpliced(index, 1);
};
const save = () => ruleBookStore.update({
usedForAcquisitions: usedForAcquisitions.value,
bindings: Object.fromEntries(
bindings.value
.filter(b => b.ref)
.map(b => [b.ref, b.ledger.ledgerId])
),
script: script.value
});
useEventListener(window, 'keydown', (event: KeyboardEvent) => { useEventListener(window, 'keydown', (event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
@@ -43,65 +54,28 @@ useEventListener(window, 'keydown', (event: KeyboardEvent) => {
save(); save();
} }
}, {capture: true}); }, {capture: true});
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 = ruleBooksStore.refresh(); // FIXME don't call refresh
const id = typeof route.params.ruleBookId === 'string' ? route.params.ruleBookId : route.params.ruleBookId[0];
await promise;
const ruleBook = ruleBooksStore.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 {
ruleBookId.value = undefined;
name.value = '';
usedForAcquisitions.value = false;
ledgerRefs.value = [];
script.value = '';
log.info('No rule book to load');
}
}, { immediate: true })
</script> </script>
<template> <template>
<div class="flex flex-col mb-2 mt-4 h-[calc(100vh-4.5rem)]"> <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 flex-col grow min-h-0">
<div class="flex grow border-b-1"> <div class="flex grow border-b-1">
Name: <label class="flex items-center mb-2">
<input class="mb-2 ms-2" type="text" v-model="name" /> <SliderCheckbox class="me-2" v-model="usedForAcquisitions" />
<label class="flex items-center ms-2 mb-2">
<SliderCheckbox class="me-2" v-model="usedForAcquisitions" />
Used for acquisitions Used for acquisitions
</label> </label>
</div> </div>
<div class="border-b-1"> <div class="border-b-1">
Ledgers References: Ledger Bindings:
<div class="flex flex-wrap items-center mt-2"> <div class="flex flex-wrap items-center mt-2">
<div class="flex items-center mb-2" v-for="(ledgerRef, index) in ledgerRefs" :key="index"> <div class="flex items-center mb-2 me-2" v-for="(binding, index) in bindings" :key="index">
<input class="me-1" type="text" :value="ledgerRef" @input="updateLedgerRef(index, ($event.target as HTMLInputElement).value)" /> <input class="me-1" type="text" v-model="binding.ref" />
<button class="btn-icon me-2" @click="removeLedgerRef(index)"><TrashIcon /></button> <span class="me-1">:</span>
<LedgerSelect class="me-1" :ledgers="ledgersToUse" v-model="binding.ledger" />
<button class="btn-icon text-amber-700 hover:text-amber-600" @click="removeBinding(index)"><TrashIcon /></button>
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<button class="btn-icon" @click="addLedgerRef"><PlusIcon /></button> <button class="btn-icon" @click="addBinding"><PlusIcon /></button>
</div> </div>
</div> </div>
</div> </div>
@@ -1,46 +0,0 @@
<script setup lang="ts">
import {Character, CharacterLabel} from "@/characters";
import {PencilSquareIcon} from "@heroicons/vue/24/outline";
import {CharacterRuleBook, useCharacterRuleBooksStore} from "@/rules";
import {routeNames} from "@/routes.ts";
import {SortableHeader, useSort} from "@/components/table";
type CharacterRuleBookView = {
character: Character;
characterName: string;
ruleBookName: string;
}
const characterRuleBooksStore = useCharacterRuleBooksStore();
const { sortedArray, headerProps } = useSort<CharacterRuleBookView>(() => characterRuleBooksStore.characterRuleBooks.map((characterRuleBook: CharacterRuleBook): CharacterRuleBookView => ({
character: characterRuleBook.character,
characterName: characterRuleBook.character.name,
ruleBookName: characterRuleBook.ruleBook.name
})))
</script>
<template>
<div class="grid mb-2 mt-4">
<table>
<thead>
<tr>
<SortableHeader v-bind="headerProps" sortKey="characterName">Character</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="ruleBookName">Rule Book</SortableHeader>
<SortableHeader v-bind="headerProps" sortKey="buttons" unsortable />
</tr>
</thead>
<tbody>
<tr v-for="characterRuleBookView in sortedArray" :key="characterRuleBookView.character.characterId" >
<td>
<CharacterLabel :character="characterRuleBookView.character" />
</td>
<td>{{characterRuleBookView.ruleBookName}}</td>
<td class="text-right">
<RouterLink class="btn-icon" :to="{ name: routeNames.editCharacterRulebook, params: { characterId: characterRuleBookView.character.characterId } }"><PencilSquareIcon /></RouterLink>
</td>
</tr>
</tbody>
</table>
</div>
</template>
-35
View File
@@ -1,35 +0,0 @@
<script setup lang="ts">
import {DocumentDuplicateIcon, PencilSquareIcon, TrashIcon} from "@heroicons/vue/24/outline";
import {confirm} from "@/confirm";
import {RuleBook, useRuleBooksStore} from "@/rules";
import {routeNames} from "@/routes";
const ruleBooksStore = useRuleBooksStore();
const duplicate = async (ruleBook: RuleBook) => {
if (await confirm({title: "Duplicate Rule Book", message: `Duplicate ${ruleBook.name}?`, confirmLabel: "Duplicate"})) {
await ruleBooksStore.duplicate(ruleBook);
}
};
const remove = async (ruleBook: RuleBook) => {
if (await confirm({title: "Delete Rule Book", message: `Delete ${ruleBook.name}?`, confirmLabel: "Delete", danger: true})) {
await ruleBooksStore.remove(ruleBook.ruleBookId);
}
};
</script>
<template>
<div class="grid mb-2 mt-4">
<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 ruleBooksStore.ruleBooks" :key="ruleBook.ruleBookId" class="flex items-center mt-2">
<span class="flex grow me-2">{{ruleBook.name}}</span>
<RouterLink class="btn-icon me-1" :to="{ name: routeNames.editRuleBook, params: { ruleBookId: ruleBook.ruleBookId } }"><PencilSquareIcon /></RouterLink>
<button class="btn-icon 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 -15
View File
@@ -12,10 +12,7 @@ export const routeNames = {
viewLedger: 'view-ledger', viewLedger: 'view-ledger',
viewLedgerBalance: 'view-ledger-balance', viewLedgerBalance: 'view-ledger-balance',
listLedgerTransactions: 'list-ledger-transactions', listLedgerTransactions: 'list-ledger-transactions',
listRuleBooks: 'list-rule-books',
newRuleBook: 'new-rule-book',
editRuleBook: 'edit-rule-book', editRuleBook: 'edit-rule-book',
editCharacterRulebook: 'edit-character-rule-book',
marketTypes: 'market-types', marketTypes: 'market-types',
about: 'about', about: 'about',
} as const; } as const;
@@ -33,18 +30,7 @@ export const routes: RouteRecordRaw[] = [
]}, ]},
]}, ]},
{path: '/rules', component: () => import('@/pages/Rules.vue'), children: [ {path: '/rules', name: routeNames.editRuleBook, 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: '/market', component: () => import('@/pages/Market.vue'), children: [
{path: '', redirect: {name: routeNames.marketTypes}}, {path: '', redirect: {name: routeNames.marketTypes}},
+19 -71
View File
@@ -1,83 +1,31 @@
import {characterRuleBookApi, ruleBookApi} from "@/mammon"; import {ruleBookApi, ruleScriptApi} from "@/mammon";
import { import {RuleBookResponse, UpdateRuleBookRequest} from "@/generated/mammon";
CharacterRuleBookResponse, import {isAxiosError} from "axios";
CreateRuleBookRequest,
RuleBookResponse,
SetCharacterRuleBookRequest
} from "@/generated/mammon";
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {ref, triggerRef} from "vue"; import {ref} from "vue";
export type RuleBook = RuleBookResponse; export type RuleBook = RuleBookResponse;
export const useRuleBooksStore = defineStore('rule-books', () => { export const useRuleBookStore = defineStore('rule-book', () => {
const ruleBooks = ref<RuleBook[]>([]); const ruleBook = ref<RuleBook | null>(null);
const addRuleBook = (ruleBook: RuleBook) => { const refresh = () => ruleBookApi.findRuleBook()
ruleBooks.value.push(ruleBook); .then(response => ruleBook.value = response.data)
triggerRef(ruleBooks); .catch(error => {
return ruleBook; if (isAxiosError(error) && error.response?.status === 404) {
}; ruleBook.value = null;
return;
}
throw error;
});
const replaceRuleBook = (ruleBook: RuleBook) => { const update = (request: UpdateRuleBookRequest) => ruleBookApi.updateRuleBook(request)
const index = ruleBooks.value.findIndex(rb => rb.ruleBookId === ruleBook.ruleBookId); .then(response => ruleBook.value = response.data);
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(); refresh();
return {ruleBooks, findById, create, update, duplicate, remove, refresh}; return {ruleBook, refresh, update};
})
export type CharacterRuleBook = CharacterRuleBookResponse;
export const useCharacterRuleBooksStore = defineStore('character-rule-books', () => {
const characterRuleBooks = ref<CharacterRuleBook[]>([]);
const replaceCharacterRuleBook = (characterRuleBook: CharacterRuleBook) => {
const index = characterRuleBooks.value.findIndex(crb => crb.character.characterId === characterRuleBook.character.characterId);
if (index !== -1) {
characterRuleBooks.value[index] = characterRuleBook;
} else {
characterRuleBooks.value.push(characterRuleBook);
}
triggerRef(characterRuleBooks);
return characterRuleBook;
};
const findByCharacterId = (characterId: number): CharacterRuleBook | undefined => characterRuleBooks.value.find(crb => crb.character.characterId === characterId);
const setForCharacter = (characterId: number, ruleBook: SetCharacterRuleBookRequest) => characterRuleBookApi.setCharacterRuleBookForCharacter(characterId, ruleBook)
.then(response => replaceCharacterRuleBook(response.data));
const refresh = () => characterRuleBookApi.findAllCharacterRuleBooks().then(response => characterRuleBooks.value = response.data);
refresh();
return {characterRuleBooks, findByCharacterId, setForCharacter, refresh};
}) })
export const fetchScriptDefinitions = (): Promise<string> => export const fetchScriptDefinitions = (): Promise<string> =>
ruleBookApi.getScriptDefinitions({responseType: 'text'}).then(response => response.data); ruleScriptApi.getScriptDefinitions({responseType: 'text'}).then(response => response.data);