dodalem dokumentacje opisujaca dzialanie store dodalem services do komunikacji z directus dodalem opis dzialania services dodalem mocki danych przygotowania do refaktoryzacji
21 KiB
Frontend Stores
Data: 2026-04-29
Cel dokumentu
Ten dokument opisuje pięć store Pinia dodanych do frontendu:
productsStoreproductTimelineStoreproductSpecificationStoredictionariesStoreuiStore
Store to miejsce, w którym frontend przechowuje stan aplikacji. Stanem są na przykład:
- produkty pobrane z backendu,
- aktualne filtry listy,
- informacja czy trwa ładowanie,
- pełna specyfikacja otwartego produktu,
- pełny timeline otwartego produktu,
- informacja jaki panel drawera jest aktualnie otwarty.
W tym projekcie store są napisane jako Pinia setup stores. To oznacza, że ich składnia jest podobna do Composition API w Vue: używają ref, computed i zwykłych funkcji.
Najważniejsza zasada
Frontend nie powinien trzymać jednego ogromnego obiektu produktu, który zawiera wszystko.
Zamiast tego aplikacja używa kilku reprezentacji produktu:
ProductListItem- lekki obiekt do głównej listy produktów.ProductTimeline- pełna historia produkcji dla jednego produktu.ProductSpecification- pełna specyfikacja dla jednego produktu.Dictionaries- słowniki do filtrów i formularzy.UiState- stan interfejsu, np. otwarty drawer.
To jest celowe. Lista produktów musi działać szybko, szczególnie przy virtual scroll i lazy loading. Dlatego lista dostaje tylko dane potrzebne do pokazania kart produktów. Cięższe dane, takie jak pełna specyfikacja albo pełna historia, są pobierane dopiero wtedy, gdy użytkownik ich potrzebuje.
Normalizacja danych
W wielu store dane są przechowywane w strukturze:
{
ids: [123, 124],
byId: {
123: { id: 123, model: 'Regius Core 6' },
124: { id: 124, model: 'Duvell Elite 7' },
},
}
To nazywa się normalizacją danych.
Zamiast trzymać jedną dużą tablicę i szukać produktu pętlą, store trzyma:
ids- kolejność elementów na liście,byId- szybki dostęp do elementu poid.
Przykład:
const product = productsStore.byId[123]
To jest wygodne, gdy trzeba zaktualizować jeden produkt bez przebudowywania całej listy.
Ważne pojęcia JavaScript
ref
ref tworzy reaktywną wartość.
const isLoading = ref(false)
Wewnątrz store odczyt i zapis robi się przez .value:
isLoading.value = true
W komponencie Vue, po użyciu store, zwykle nie trzeba pisać .value w template.
computed
computed tworzy wartość wyliczaną z innych wartości.
W productsStore jest:
const items = computed(() => ids.value.map((id) => byId.value[id]).filter(Boolean))
To znaczy:
idstrzyma kolejność produktów,byIdtrzyma produkty,itemszwraca gotową tablicę produktów do wyświetlenia.
async i await
Funkcje pobierające dane z backendu są asynchroniczne.
await productsStore.fetchFirstPage()
await oznacza: poczekaj, aż backend odpowie.
productsStore
Plik:
frontend/src/stores/productsStore.js
Odpowiedzialność
Ten store obsługuje główną listę produktów.
Przechowuje:
- lekkie dane produktów,
- kolejność produktów na liście,
- filtry,
- informacje do lazy loading,
- informację czy trwa pobieranie danych,
- błąd pobierania, jeśli wystąpił.
Ten store powinien dostać z backendu ProductListItem, czyli dane przygotowane specjalnie pod listę.
Czego nie powinien przechowywać
Nie powinien przechowywać:
- pełnej specyfikacji produktu,
- pełnego timeline,
- wszystkich zdjęć,
- pełnych danych technicznych, jeśli nie są pokazywane na liście.
Jeśli na karcie produktu ma być widoczny timeline, ten store powinien trzymać tylko timelinePreview, czyli skrót historii.
Spodziewana struktura stanu
{
ids: [123, 124, 125],
byId: {
123: {
id: 123,
orderId: '0143/2025/1',
model: 'Regius Core 6',
client: 'HIENDGUITAR.COM / INDONESIA',
finish: 'S+M',
productionLists: ['CZE-00'],
timelinePreview: {
body: [
{
id: 901,
code: 'B',
label: 'Bejca',
date: '2026-04-20',
status: 'done',
},
],
neck: [
{
id: 902,
code: 'IZ',
label: 'Izolant',
date: '2026-04-21',
status: 'done',
},
],
},
},
},
filters: {
search: '',
modelId: null,
clientId: null,
finish: null,
productionListId: null,
year: null,
},
limit: 30,
offset: 30,
total: 184,
hasMore: true,
isLoading: false,
error: null,
}
Spodziewana odpowiedź endpointu
Endpoint:
GET /mayo-api/products?limit=30&offset=0&search=regius
Odpowiedź:
{
"items": [
{
"id": 123,
"orderId": "0143/2025/1",
"model": "Regius Core 6",
"client": "HIENDGUITAR.COM / INDONESIA",
"finish": "S+M",
"productionLists": ["CZE-00"],
"timelinePreview": {
"body": [
{
"id": 901,
"code": "B",
"label": "Bejca",
"date": "2026-04-20",
"status": "done"
}
],
"neck": []
}
}
],
"pageInfo": {
"limit": 30,
"offset": 0,
"hasMore": true,
"total": 184
}
}
Najważniejsze funkcje
await productsStore.fetchFirstPage()
Pobiera pierwszą stronę produktów. Używaj tego:
- przy wejściu na ekran listy,
- po zmianie filtrów,
- po ręcznym odświeżeniu listy.
await productsStore.fetchNextPage()
Pobiera kolejną porcję produktów. Używaj tego przy lazy loading, gdy użytkownik zbliża się do końca listy.
await productsStore.applyFilters({ search: 'regius' })
Ustawia filtry i pobiera listę od początku.
productsStore.applyTimelinePreviewUpdate(productId, timelinePreview)
Aktualizuje skrócony timeline na karcie produktu. Ta funkcja jest użyteczna po dodaniu nowego eventu do pełnego timeline.
Przykład użycia w komponencie
<script setup>
import { onMounted } from 'vue'
import { useProductsStore } from 'src/stores/productsStore'
const productsStore = useProductsStore()
onMounted(() => {
productsStore.fetchFirstPage()
})
async function loadMore() {
await productsStore.fetchNextPage()
}
</script>
<template>
<div v-if="productsStore.isLoading">Ladowanie...</div>
<article v-for="product in productsStore.items" :key="product.id">
<h3>{{ product.model }}</h3>
<p>{{ product.orderId }}</p>
</article>
<button v-if="productsStore.hasMore" @click="loadMore">
Zaladuj wiecej
</button>
</template>
productTimelineStore
Plik:
frontend/src/stores/productTimelineStore.js
Odpowiedzialność
Ten store przechowuje pełny timeline produktów.
productsStore trzyma tylko timelinePreview, bo lista potrzebuje skrótu. productTimelineStore trzyma pełną historię, czyli wszystkie operacje, notatki i inne zdarzenia.
Spodziewana struktura stanu
{
byProductId: {
123: {
productId: 123,
events: [
{
id: 901,
productId: 123,
partId: 10,
partType: 'BODY',
type: 'operation',
operationId: 7,
operationCode: 'B',
operationName: 'Bejca',
date: '2026-04-20',
note: null,
photosCount: 0,
},
{
id: 944,
productId: 123,
partId: 10,
partType: 'BODY',
type: 'note',
operationId: null,
operationCode: null,
operationName: null,
date: '2026-04-23',
note: 'Problem z kolorem po pierwszej warstwie.',
photosCount: 2,
},
],
loadedAt: 1777460000000,
isLoading: false,
error: null,
},
},
}
Spodziewana odpowiedź endpointu
Endpoint:
GET /mayo-api/products/123/timeline
Odpowiedź:
{
"productId": 123,
"events": [
{
"id": 901,
"productId": 123,
"partId": 10,
"partType": "BODY",
"type": "operation",
"operationId": 7,
"operationCode": "B",
"operationName": "Bejca",
"date": "2026-04-20",
"note": null,
"photosCount": 0
}
],
"timelinePreview": {
"body": [
{
"id": 901,
"code": "B",
"label": "Bejca",
"date": "2026-04-20",
"status": "done"
}
],
"neck": []
}
}
Najważniejsze funkcje
await timelineStore.fetchTimeline(productId)
Pobiera pełny timeline produktu, jeśli nie został wcześniej pobrany.
await timelineStore.fetchTimeline(productId, { force: true })
Pobiera timeline ponownie, nawet jeśli jest już w pamięci.
await timelineStore.addEvent(productId, payload)
Dodaje nowe zdarzenie do timeline. Po dodaniu eventu store może też zaktualizować timelinePreview w productsStore, jeśli backend zwróci nowy preview.
Przykład użycia
<script setup>
import { computed, onMounted } from 'vue'
import { useProductTimelineStore } from 'src/stores/productTimelineStore'
const props = defineProps({
productId: {
type: Number,
required: true,
},
})
const timelineStore = useProductTimelineStore()
const events = computed(() => timelineStore.getEvents(props.productId))
onMounted(() => {
timelineStore.fetchTimeline(props.productId)
})
async function addOperation() {
await timelineStore.addEvent(props.productId, {
type: 'operation',
partId: 10,
operationId: 7,
date: '2026-04-29',
note: null,
})
}
</script>
productSpecificationStore
Plik:
frontend/src/stores/productSpecificationStore.js
Odpowiedzialność
Ten store przechowuje pełną specyfikację produktu.
Specyfikacja nie jest pobierana razem z główną listą. Powinna być pobrana dopiero wtedy, gdy użytkownik otworzy panel specyfikacji albo ekran szczegółów.
Spodziewana struktura stanu
{
byProductId: {
123: {
productId: 123,
orderId: '0143/2025/1',
sourceUrl: 'http://10.8.0.6/mayo2/index.php?...',
lastFetchedAt: '2026-04-22T10:30:00Z',
sections: [
{
key: 'szyjka',
label: 'Szyjka',
fields: [
{
key: 'radius',
label: 'Radius',
values: ['GITARA SETIUS/REGIUS/CUSTOM/ 16'],
},
{
key: 'drewno-szyjka',
label: 'Drewno szyjka',
values: [
'Klon amerykanski-Mahon-Wenge-Amazakoe (11 czesci)',
'Regius Core/ Profil laczenia szyjki z korpusem / wyzlobienie schodkowe',
],
},
],
},
],
diff: [
{
path: 'kolor.kolor-top',
type: 'changed',
initial: 'Black',
current: 'Cherry Burst',
},
],
loadedAt: 1777460000000,
isLoading: false,
isRefreshing: false,
error: null,
},
},
}
Spodziewana odpowiedź endpointu
Endpoint:
GET /mayo-api/products/123/specification
Odpowiedź:
{
"productId": 123,
"orderId": "0143/2025/1",
"sourceUrl": "http://10.8.0.6/mayo2/index.php?...",
"lastFetchedAt": "2026-04-22T10:30:00Z",
"specification": {
"sections": [
{
"key": "szyjka",
"label": "Szyjka",
"fields": [
{
"key": "radius",
"label": "Radius",
"values": ["GITARA SETIUS/REGIUS/CUSTOM/ 16"]
}
]
}
]
},
"diff": []
}
Najważniejsze funkcje
await specificationStore.fetchSpecification(productId)
Pobiera specyfikację, jeśli nie ma jej jeszcze w pamięci.
await specificationStore.fetchSpecification(productId, { force: true })
Pobiera specyfikację ponownie.
await specificationStore.refreshSpecification(productId)
Wysyła żądanie do backendu, żeby backend pobrał aktualną specyfikację ze starego systemu i zapisał ją w Directus.
Przykład użycia
<script setup>
import { computed, onMounted } from 'vue'
import { useProductSpecificationStore } from 'src/stores/productSpecificationStore'
const props = defineProps({
productId: {
type: Number,
required: true,
},
})
const specificationStore = useProductSpecificationStore()
const specification = computed(() =>
specificationStore.getSpecification(props.productId),
)
onMounted(() => {
specificationStore.fetchSpecification(props.productId)
})
</script>
<template>
<div v-if="specification?.isLoading">Ladowanie specyfikacji...</div>
<section v-for="section in specification?.sections" :key="section.key">
<h3>{{ section.label }}</h3>
<div v-for="field in section.fields" :key="field.key">
<strong>{{ field.label }}</strong>
<p v-for="value in field.values" :key="value">{{ value }}</p>
</div>
</section>
</template>
dictionariesStore
Plik:
frontend/src/stores/dictionariesStore.js
Odpowiedzialność
Ten store przechowuje słowniki.
Słownik to lista wartości używana w filtrach, formularzach albo wyborach. Przykłady:
- modele produktów,
- klienci,
- typy wykończenia,
- listy produkcyjne,
- operacje produkcyjne,
- kolory.
Takich danych nie trzeba pobierać przy każdym wejściu na ekran. Zwykle pobiera się je raz i trzyma w pamięci.
Spodziewana struktura stanu
{
models: [
{
id: 1,
name: 'Regius Core 6',
},
],
clients: [
{
id: 4,
name: 'HIENDGUITAR.COM / INDONESIA',
country: 'INDONESIA',
},
],
finishes: [
{
value: 'GLOSS',
label: 'Gloss',
},
{
value: 'SATIN',
label: 'Satin',
},
],
productionLists: [
{
id: 8,
name: 'CZE-00',
},
],
operations: [
{
id: 7,
code: 'B',
name: 'Bejca',
},
],
colors: [
{
id: 12,
name: 'Trans Natural Satine',
},
],
loadedAt: 1777460000000,
isLoading: false,
error: null,
}
Spodziewana odpowiedź endpointu
Endpoint:
GET /mayo-api/dictionaries
Odpowiedź:
{
"models": [],
"clients": [],
"finishes": [],
"productionLists": [],
"operations": [],
"colors": []
}
Najważniejsze funkcje
await dictionariesStore.fetchDictionaries()
Pobiera słowniki, jeśli nie są jeszcze załadowane.
await dictionariesStore.fetchDictionaries({ force: true })
Wymusza ponowne pobranie słowników.
Przykład użycia w filtrach
<script setup>
import { onMounted } from 'vue'
import { useDictionariesStore } from 'src/stores/dictionariesStore'
import { useProductsStore } from 'src/stores/productsStore'
const dictionariesStore = useDictionariesStore()
const productsStore = useProductsStore()
onMounted(() => {
dictionariesStore.fetchDictionaries()
})
function selectModel(modelId) {
productsStore.applyFilters({ modelId })
}
</script>
uiStore
Plik:
frontend/src/stores/uiStore.js
Odpowiedzialność
Ten store przechowuje stan interfejsu użytkownika.
Nie przechowuje danych biznesowych. To znaczy, że nie powinien trzymać specyfikacji, timeline ani produktów.
Przechowuje tylko informacje typu:
- czy drawer jest otwarty,
- jaki panel jest otwarty,
- dla jakiego produktu otwarty jest panel,
- dodatkowe parametry panelu.
Spodziewana struktura stanu
{
isDrawerOpen: true,
activePanel: 'productSpecification',
activeProductId: 123,
drawerPayload: {
productId: 123,
mode: 'view',
},
drawerInstanceKey: 4,
}
Najważniejsze funkcje
uiStore.openDrawer(UI_PANELS.PRODUCT_SPECIFICATION, {
productId: 123,
mode: 'view',
})
Otwiera drawer i ustawia aktywny panel.
uiStore.closeDrawer()
Zamyka drawer.
uiStore.replaceDrawer(UI_PANELS.PRODUCT_TIMELINE, {
productId: 123,
})
Zamienia aktualny panel na inny.
Przykład użycia
<script setup>
import { UI_PANELS, useUiStore } from 'src/stores/uiStore'
const uiStore = useUiStore()
function openSpecification(productId) {
uiStore.openDrawer(UI_PANELS.PRODUCT_SPECIFICATION, {
productId,
})
}
</script>
<template>
<button @click="openSpecification(123)">
Otworz specyfikacje
</button>
</template>
Typowy przepływ aplikacji
Mock API
Warstwa services obsługuje teraz dwa tryby:
- mock API,
- prawdziwe API przez Axios.
Mock API jest domyślnie włączone. To znaczy, że store mogą pobierać dane bez działającego backendu.
Pliki z przykładowymi danymi są tutaj:
frontend/src/mocks/products.json
frontend/src/mocks/timelines.json
frontend/src/mocks/specifications.json
frontend/src/mocks/dictionaries.json
Przełącznik znajduje się w:
frontend/src/services/apiMode.js
Aktualna zasada:
export const USE_MOCK_API = import.meta.env.VITE_USE_MOCK_API !== 'false'
To oznacza:
- jeśli nie ustawisz nic, aplikacja używa mocków,
- jeśli ustawisz
VITE_USE_MOCK_API=false, aplikacja używa prawdziwego backendu.
Przykład uruchomienia z prawdziwym API:
VITE_USE_MOCK_API=false npm run dev
Komponenty i store nie powinny wiedzieć, czy dane pochodzą z mocków czy z backendu. Dla nich API wygląda tak samo:
komponent -> store -> service -> mock JSON albo Axios
Dlatego można spokojnie pracować nad wyglądem strony na danych przykładowych, a później przełączyć się na Directus bez przepisywania komponentów.
1. Użytkownik wchodzi na główną stronę
Komponent strony wywołuje:
productsStore.fetchFirstPage()
Backend zwraca pierwszą porcję ProductListItem.
Frontend zapisuje je w:
productsStore.ids
productsStore.byId
Na ekranie widać listę produktów oraz timelinePreview.
2. Użytkownik przewija listę
Gdy użytkownik zbliża się do końca listy, komponent wywołuje:
productsStore.fetchNextPage()
Backend zwraca kolejne produkty. Store dopisuje je do istniejącej listy.
3. Użytkownik zmienia filtr
Komponent wywołuje:
productsStore.applyFilters({
search: 'regius',
})
Store:
- zapisuje nowe filtry,
- resetuje offset,
- pobiera pierwszą stronę listy od nowa.
4. Użytkownik otwiera specyfikację produktu
Komponent wywołuje:
uiStore.openDrawer(UI_PANELS.PRODUCT_SPECIFICATION, {
productId: 123,
})
Panel specyfikacji widzi productId i wywołuje:
specificationStore.fetchSpecification(123)
Specyfikacja trafia do:
productSpecificationStore.byProductId[123]
5. Użytkownik otwiera pełny timeline
Komponent lub panel wywołuje:
timelineStore.fetchTimeline(123)
Pełna historia trafia do:
productTimelineStore.byProductId[123]
6. Użytkownik dodaje event do timeline
Komponent wywołuje:
timelineStore.addEvent(123, {
type: 'operation',
partId: 10,
operationId: 7,
date: '2026-04-29',
note: null,
})
Backend zapisuje event i zwraca:
- utworzony event,
- opcjonalnie nowy
timelinePreview.
Store:
- dopisuje event do
productTimelineStore.byProductId[123].events, - jeśli backend zwrócił
timelinePreview, aktualizujeproductsStore.byId[123].timelinePreview.
Dlaczego timeline jest w dwóch miejscach
To może wyglądać jak duplikacja, ale ma konkretny sens.
Na głównej liście potrzebny jest tylko skrót:
timelinePreview
Na panelu szczegółów potrzebna jest pełna historia:
events
Gdyby lista trzymała pełny timeline każdego produktu, frontend pobierałby dużo danych, których użytkownik często w ogóle nie zobaczy.
Lepszy podział:
productsStore
ProductListItem
timelinePreview
productTimelineStore
ProductTimeline
events
Kiedy dodawać nowy store
Nie dodawaj nowego store dla każdego nowego endpointu.
Nowy store ma sens wtedy, gdy pojawia się nowy obszar stanu aplikacji.
Przykłady dobrych powodów:
- osobny moduł aplikacji,
- osobny cache danych,
- dane używane w wielu komponentach,
- dane z własnym ładowaniem i błędami,
- stan, który powinien przeżyć zmianę komponentu.
Przykłady złych powodów:
- powstał jeden nowy endpoint,
- jeden komponent ma jedną lokalną zmienną,
- coś da się spokojnie trzymać w
refwewnątrz komponentu.
Kiedy używać store, a kiedy lokalnego ref
Użyj store, gdy dane:
- są potrzebne w kilku komponentach,
- mają zostać w pamięci po zamknięciu panelu,
- są pobierane z backendu i cache ma znaczenie,
- opisują ważny stan aplikacji.
Użyj lokalnego ref w komponencie, gdy dane:
- są potrzebne tylko w tym jednym komponencie,
- dotyczą chwilowego formularza,
- nie muszą być znane poza komponentem.
Przykład lokalnego ref:
const noteText = ref('')
Nie ma potrzeby robić osobnego store tylko dla tekstu wpisywanego w jedno pole formularza.
Najważniejsze reguły dla tego projektu
- Lista produktów używa
productsStore. - Lista produktów przechowuje tylko lekkie dane i
timelinePreview. - Pełny timeline jest w
productTimelineStore. - Pełna specyfikacja jest w
productSpecificationStore. - Słowniki są w
dictionariesStore. - Drawer i aktywny panel są w
uiStore. - Backend powinien zwracać dane już przygotowane pod ekran, a nie surową strukturę tabel z bazy.
productIdpowinien oznaczać wewnętrzne ID produktu z Directusa.orderIdpowinien oznaczać numer starego systemu, np.0143/2025/1.- Nie pobieraj pełnej specyfikacji i pełnego timeline dla każdego produktu na liście.