dodalem dokumentacje opisujaca dzialanie store dodalem services do komunikacji z directus dodalem opis dzialania services dodalem mocki danych przygotowania do refaktoryzacji
1072 lines
21 KiB
Markdown
1072 lines
21 KiB
Markdown
# Frontend Stores
|
|
|
|
Data: 2026-04-29
|
|
|
|
## Cel dokumentu
|
|
|
|
Ten dokument opisuje pięć store Pinia dodanych do frontendu:
|
|
|
|
- `productsStore`
|
|
- `productTimelineStore`
|
|
- `productSpecificationStore`
|
|
- `dictionariesStore`
|
|
- `uiStore`
|
|
|
|
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:
|
|
|
|
```js
|
|
{
|
|
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 po `id`.
|
|
|
|
Przykład:
|
|
|
|
```js
|
|
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ść.
|
|
|
|
```js
|
|
const isLoading = ref(false)
|
|
```
|
|
|
|
Wewnątrz store odczyt i zapis robi się przez `.value`:
|
|
|
|
```js
|
|
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:
|
|
|
|
```js
|
|
const items = computed(() => ids.value.map((id) => byId.value[id]).filter(Boolean))
|
|
```
|
|
|
|
To znaczy:
|
|
|
|
- `ids` trzyma kolejność produktów,
|
|
- `byId` trzyma produkty,
|
|
- `items` zwraca gotową tablicę produktów do wyświetlenia.
|
|
|
|
### `async` i `await`
|
|
|
|
Funkcje pobierające dane z backendu są asynchroniczne.
|
|
|
|
```js
|
|
await productsStore.fetchFirstPage()
|
|
```
|
|
|
|
`await` oznacza: poczekaj, aż backend odpowie.
|
|
|
|
## `productsStore`
|
|
|
|
Plik:
|
|
|
|
```txt
|
|
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
|
|
|
|
```js
|
|
{
|
|
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:
|
|
|
|
```http
|
|
GET /mayo-api/products?limit=30&offset=0&search=regius
|
|
```
|
|
|
|
Odpowiedź:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```js
|
|
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.
|
|
|
|
```js
|
|
await productsStore.fetchNextPage()
|
|
```
|
|
|
|
Pobiera kolejną porcję produktów. Używaj tego przy lazy loading, gdy użytkownik zbliża się do końca listy.
|
|
|
|
```js
|
|
await productsStore.applyFilters({ search: 'regius' })
|
|
```
|
|
|
|
Ustawia filtry i pobiera listę od początku.
|
|
|
|
```js
|
|
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
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```txt
|
|
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
|
|
|
|
```js
|
|
{
|
|
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:
|
|
|
|
```http
|
|
GET /mayo-api/products/123/timeline
|
|
```
|
|
|
|
Odpowiedź:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```js
|
|
await timelineStore.fetchTimeline(productId)
|
|
```
|
|
|
|
Pobiera pełny timeline produktu, jeśli nie został wcześniej pobrany.
|
|
|
|
```js
|
|
await timelineStore.fetchTimeline(productId, { force: true })
|
|
```
|
|
|
|
Pobiera timeline ponownie, nawet jeśli jest już w pamięci.
|
|
|
|
```js
|
|
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
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```txt
|
|
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
|
|
|
|
```js
|
|
{
|
|
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:
|
|
|
|
```http
|
|
GET /mayo-api/products/123/specification
|
|
```
|
|
|
|
Odpowiedź:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```js
|
|
await specificationStore.fetchSpecification(productId)
|
|
```
|
|
|
|
Pobiera specyfikację, jeśli nie ma jej jeszcze w pamięci.
|
|
|
|
```js
|
|
await specificationStore.fetchSpecification(productId, { force: true })
|
|
```
|
|
|
|
Pobiera specyfikację ponownie.
|
|
|
|
```js
|
|
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
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```txt
|
|
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
|
|
|
|
```js
|
|
{
|
|
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:
|
|
|
|
```http
|
|
GET /mayo-api/dictionaries
|
|
```
|
|
|
|
Odpowiedź:
|
|
|
|
```json
|
|
{
|
|
"models": [],
|
|
"clients": [],
|
|
"finishes": [],
|
|
"productionLists": [],
|
|
"operations": [],
|
|
"colors": []
|
|
}
|
|
```
|
|
|
|
### Najważniejsze funkcje
|
|
|
|
```js
|
|
await dictionariesStore.fetchDictionaries()
|
|
```
|
|
|
|
Pobiera słowniki, jeśli nie są jeszcze załadowane.
|
|
|
|
```js
|
|
await dictionariesStore.fetchDictionaries({ force: true })
|
|
```
|
|
|
|
Wymusza ponowne pobranie słowników.
|
|
|
|
### Przykład użycia w filtrach
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```txt
|
|
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
|
|
|
|
```js
|
|
{
|
|
isDrawerOpen: true,
|
|
activePanel: 'productSpecification',
|
|
activeProductId: 123,
|
|
drawerPayload: {
|
|
productId: 123,
|
|
mode: 'view',
|
|
},
|
|
drawerInstanceKey: 4,
|
|
}
|
|
```
|
|
|
|
### Najważniejsze funkcje
|
|
|
|
```js
|
|
uiStore.openDrawer(UI_PANELS.PRODUCT_SPECIFICATION, {
|
|
productId: 123,
|
|
mode: 'view',
|
|
})
|
|
```
|
|
|
|
Otwiera drawer i ustawia aktywny panel.
|
|
|
|
```js
|
|
uiStore.closeDrawer()
|
|
```
|
|
|
|
Zamyka drawer.
|
|
|
|
```js
|
|
uiStore.replaceDrawer(UI_PANELS.PRODUCT_TIMELINE, {
|
|
productId: 123,
|
|
})
|
|
```
|
|
|
|
Zamienia aktualny panel na inny.
|
|
|
|
### Przykład użycia
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```txt
|
|
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:
|
|
|
|
```txt
|
|
frontend/src/services/apiMode.js
|
|
```
|
|
|
|
Aktualna zasada:
|
|
|
|
```js
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```txt
|
|
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:
|
|
|
|
```js
|
|
productsStore.fetchFirstPage()
|
|
```
|
|
|
|
Backend zwraca pierwszą porcję `ProductListItem`.
|
|
|
|
Frontend zapisuje je w:
|
|
|
|
```js
|
|
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:
|
|
|
|
```js
|
|
productsStore.fetchNextPage()
|
|
```
|
|
|
|
Backend zwraca kolejne produkty. Store dopisuje je do istniejącej listy.
|
|
|
|
### 3. Użytkownik zmienia filtr
|
|
|
|
Komponent wywołuje:
|
|
|
|
```js
|
|
productsStore.applyFilters({
|
|
search: 'regius',
|
|
})
|
|
```
|
|
|
|
Store:
|
|
|
|
1. zapisuje nowe filtry,
|
|
2. resetuje offset,
|
|
3. pobiera pierwszą stronę listy od nowa.
|
|
|
|
### 4. Użytkownik otwiera specyfikację produktu
|
|
|
|
Komponent wywołuje:
|
|
|
|
```js
|
|
uiStore.openDrawer(UI_PANELS.PRODUCT_SPECIFICATION, {
|
|
productId: 123,
|
|
})
|
|
```
|
|
|
|
Panel specyfikacji widzi `productId` i wywołuje:
|
|
|
|
```js
|
|
specificationStore.fetchSpecification(123)
|
|
```
|
|
|
|
Specyfikacja trafia do:
|
|
|
|
```js
|
|
productSpecificationStore.byProductId[123]
|
|
```
|
|
|
|
### 5. Użytkownik otwiera pełny timeline
|
|
|
|
Komponent lub panel wywołuje:
|
|
|
|
```js
|
|
timelineStore.fetchTimeline(123)
|
|
```
|
|
|
|
Pełna historia trafia do:
|
|
|
|
```js
|
|
productTimelineStore.byProductId[123]
|
|
```
|
|
|
|
### 6. Użytkownik dodaje event do timeline
|
|
|
|
Komponent wywołuje:
|
|
|
|
```js
|
|
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:
|
|
|
|
1. dopisuje event do `productTimelineStore.byProductId[123].events`,
|
|
2. jeśli backend zwrócił `timelinePreview`, aktualizuje `productsStore.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:
|
|
|
|
```js
|
|
timelinePreview
|
|
```
|
|
|
|
Na panelu szczegółów potrzebna jest pełna historia:
|
|
|
|
```js
|
|
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ł:
|
|
|
|
```txt
|
|
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 `ref` wewną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`:
|
|
|
|
```js
|
|
const noteText = ref('')
|
|
```
|
|
|
|
Nie ma potrzeby robić osobnego store tylko dla tekstu wpisywanego w jedno pole formularza.
|
|
|
|
## Najważniejsze reguły dla tego projektu
|
|
|
|
1. Lista produktów używa `productsStore`.
|
|
2. Lista produktów przechowuje tylko lekkie dane i `timelinePreview`.
|
|
3. Pełny timeline jest w `productTimelineStore`.
|
|
4. Pełna specyfikacja jest w `productSpecificationStore`.
|
|
5. Słowniki są w `dictionariesStore`.
|
|
6. Drawer i aktywny panel są w `uiStore`.
|
|
7. Backend powinien zwracać dane już przygotowane pod ekran, a nie surową strukturę tabel z bazy.
|
|
8. `productId` powinien oznaczać wewnętrzne ID produktu z Directusa.
|
|
9. `orderId` powinien oznaczać numer starego systemu, np. `0143/2025/1`.
|
|
10. Nie pobieraj pełnej specyfikacji i pełnego timeline dla każdego produktu na liście.
|