# 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 ``` ## `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 ``` ## `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 ``` ## `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 ``` ## `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 ``` ## 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.