dodalem pinia store.

dodalem dokumentacje opisujaca dzialanie store
dodalem services do komunikacji z directus
dodalem opis dzialania services
dodalem mocki danych
przygotowania do refaktoryzacji
This commit is contained in:
2026-04-30 16:35:53 +02:00
parent 5725c024dc
commit 6e3d722e69
19 changed files with 3286 additions and 29 deletions

View File

@@ -11,14 +11,10 @@ export default defineConfig((/* ctx */) => {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [
'axios'
],
boot: ['axios'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
css: [
'app.scss'
],
css: ['app.scss'],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
@@ -38,7 +34,7 @@ export default defineConfig((/* ctx */) => {
build: {
target: {
browser: 'baseline-widely-available',
node: 'node22'
node: 'node22',
},
vueRouterMode: 'hash', // available values: 'hash', 'history'
@@ -59,21 +55,27 @@ export default defineConfig((/* ctx */) => {
// extendViteConf (viteConf) {},
// viteVuePluginOptions: {},
vitePlugins: [
['vite-plugin-checker', {
eslint: {
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
useFlatConfig: true
}
}, { server: false }]
]
[
'vite-plugin-checker',
{
eslint: {
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
useFlatConfig: true,
},
},
{ server: false },
],
],
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
devServer: {
// https: true,
open: true // opens browser window automatically
open: false, // opens browser window automatically
host: '0.0.0.0',
port: 9000,
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
@@ -91,7 +93,7 @@ export default defineConfig((/* ctx */) => {
// directives: [],
// Quasar plugins
plugins: []
plugins: [],
},
// animations: 'all', // --- includes all animations
@@ -114,10 +116,10 @@ export default defineConfig((/* ctx */) => {
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
ssr: {
prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
// (gets superseded if process.env.PORT is specified at runtime)
middlewares: [
'render' // keep this as last one
'render', // keep this as last one
],
// extendPackageJson (json) {},
@@ -128,7 +130,7 @@ export default defineConfig((/* ctx */) => {
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
pwa: false
pwa: false,
// pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
// pwaExtendGenerateSWOptions (cfg) {},
@@ -137,7 +139,7 @@ export default defineConfig((/* ctx */) => {
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
pwa: {
workboxMode: 'GenerateSW' // 'GenerateSW' or 'InjectManifest'
workboxMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
// swFilename: 'sw.js',
// manifestFilename: 'manifest.json',
// extendManifestJson (json) {},
@@ -155,7 +157,7 @@ export default defineConfig((/* ctx */) => {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
hideSplashscreen: true,
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
@@ -166,7 +168,7 @@ export default defineConfig((/* ctx */) => {
// extendPackageJson (json) {},
// Electron preload scripts (if any) from /src-electron, WITHOUT file extension
preloadScripts: [ 'electron-preload' ],
preloadScripts: ['electron-preload'],
// specify the debugging port to use for the Electron app when running in development mode
inspectPort: 5858,
@@ -175,13 +177,11 @@ export default defineConfig((/* ctx */) => {
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
@@ -189,8 +189,8 @@ export default defineConfig((/* ctx */) => {
builder: {
// https://www.electron.build/configuration
appId: 'package.json'
}
appId: 'package.json',
},
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
@@ -206,7 +206,7 @@ export default defineConfig((/* ctx */) => {
*
* @example [ 'my-script.ts', 'sub-folder/my-other-script.js' ]
*/
extraScripts: []
}
extraScripts: [],
},
}
})

View File

@@ -0,0 +1,10 @@
import AdvancedSearchPanel from './AdvancedSearchPanel.vue'
import ProductSpecificationPanel from './ProductSpecificationPanel.vue'
import ProductionStatusesPanel from './ProductionStatusesPanel.vue'
import { UI_PANELS } from 'src/stores/uiStore'
export const rightDrawerPanels = {
[UI_PANELS.ADVANCED_SEARCH]: AdvancedSearchPanel,
[UI_PANELS.PRODUCT_SPECIFICATION]: ProductSpecificationPanel,
[UI_PANELS.PRODUCTION_STATUSES]: ProductionStatusesPanel,
}

View File

@@ -0,0 +1,109 @@
{
"models": [
{
"id": 1,
"name": "Regius Core 6"
},
{
"id": 2,
"name": "Duvell Elite 7 B26,5"
},
{
"id": 3,
"name": "Legend 6"
}
],
"clients": [
{
"id": 1,
"name": "HIENDGUITAR.COM / INDONESIA",
"country": "INDONESIA"
},
{
"id": 2,
"name": "USA | Sebastopol | USA",
"country": "USA"
},
{
"id": 3,
"name": "Mayo Stock",
"country": "POLAND"
}
],
"finishes": [
{
"value": "GLOSS",
"label": "Gloss"
},
{
"value": "SATIN",
"label": "Satin"
},
{
"value": "MAT",
"label": "Mat"
},
{
"value": "S+M",
"label": "Satin + Mat"
},
{
"value": "G+M",
"label": "Gloss + Mat"
}
],
"productionLists": [
{
"id": 1,
"name": "CZE-00"
},
{
"id": 2,
"name": "LIS-25"
},
{
"id": 3,
"name": "STY-25"
},
{
"id": 4,
"name": "SUMMIT"
}
],
"operations": [
{
"id": 1,
"code": "B",
"name": "Bejca"
},
{
"id": 2,
"code": "IZ",
"name": "Izolant"
},
{
"id": 3,
"code": "AK",
"name": "Akryl"
},
{
"id": 4,
"code": "M",
"name": "Monolith"
}
],
"colors": [
{
"id": 1,
"name": "Trans Natural Satine"
},
{
"id": 2,
"name": "Trans Natural Matt"
},
{
"id": 3,
"name": "Fluo Orange"
}
]
}

View File

@@ -0,0 +1,117 @@
{
"items": [
{
"id": 101,
"orderId": "0143/2025/1",
"orderNumber": "0143",
"orderYear": 2025,
"orderIndex": 1,
"model": "Regius Core 6",
"client": "HIENDGUITAR.COM / INDONESIA",
"finish": "S+M",
"productionLists": ["CZE-00"],
"timelinePreview": {
"body": [
{
"id": 9001,
"code": "B",
"label": "Bejca",
"date": "2026-04-20",
"status": "done"
},
{
"id": 9002,
"code": "IZ",
"label": "Izolant",
"date": "2026-04-22",
"status": "done"
},
{
"id": 9003,
"code": "AK",
"label": "Akryl",
"date": "2026-04-24",
"status": "done"
}
],
"neck": [
{
"id": 9004,
"code": "B",
"label": "Bejca",
"date": "2026-04-21",
"status": "done"
},
{
"id": 9005,
"code": "IZ",
"label": "Izolant",
"date": "2026-04-23",
"status": "done"
}
]
}
},
{
"id": 102,
"orderId": "0367/2025/1",
"orderNumber": "0367",
"orderYear": 2025,
"orderIndex": 1,
"model": "Duvell Elite 7 B26,5",
"client": "USA | Sebastopol | USA",
"finish": "G+M",
"productionLists": ["LIS-25"],
"timelinePreview": {
"body": [
{
"id": 9011,
"code": "B",
"label": "Bejca",
"date": "2026-04-18",
"status": "done"
},
{
"id": 9012,
"code": "M",
"label": "Monolith",
"date": "2026-04-25",
"status": "in_progress"
}
],
"neck": [
{
"id": 9013,
"code": "IZ",
"label": "Izolant",
"date": "2026-04-19",
"status": "done"
}
]
}
},
{
"id": 103,
"orderId": "0029/2024/12",
"orderNumber": "0029",
"orderYear": 2024,
"orderIndex": 12,
"model": "Legend 6",
"client": "Mayo Stock",
"finish": "MAT",
"productionLists": ["STY-25", "SUMMIT"],
"timelinePreview": {
"body": [
{
"id": 9021,
"code": "B",
"label": "Bejca",
"date": "2026-04-14",
"status": "done"
}
],
"neck": []
}
}
]
}

View File

@@ -0,0 +1,93 @@
{
"101": {
"productId": 101,
"orderId": "0143/2025/1",
"sourceUrl": "http://10.8.0.6/mayo2/index.php?&modul=14&id_zamowienia=8055&id_zestawu=35994",
"lastFetchedAt": "2026-04-22T10:30:00Z",
"specification": {
"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"
]
}
]
},
{
"key": "kolor",
"label": "Kolor",
"fields": [
{
"key": "kolor-top",
"label": "Kolor top",
"values": ["T-NAT-S/ Trans Natural Satine"]
},
{
"key": "kolor-korpus",
"label": "Kolor korpus",
"values": ["T-NAT-M/ Trans Natural Matt"]
}
]
}
]
},
"diff": []
},
"102": {
"productId": 102,
"orderId": "0367/2025/1",
"sourceUrl": "http://10.8.0.6/mayo2/index.php?id_zestawu=37692&id_zamowienia=8286&modul=14&pozycja=",
"lastFetchedAt": "2026-04-22T10:35:00Z",
"specification": {
"sections": [
{
"key": "konstrukcja",
"label": "Konstrukcja",
"fields": [
{
"key": "wersja",
"label": "Wersja",
"values": ["BARYTON 26.5"]
},
{
"key": "konstrukcja",
"label": "Konstrukcja",
"values": ["bolt-on (gryf przykrecany)"]
}
]
},
{
"key": "elektronika",
"label": "Elektronika",
"fields": [
{
"key": "przetworniki-gitara",
"label": "Przetworniki gitara",
"values": ["SEYMOUR DUNCAN / Pegasus 7 / Bridge Humbucker"]
}
]
}
]
},
"diff": [
{
"path": "kolor.kolor-top",
"type": "changed",
"initial": "Black",
"current": "Fluo Orange"
}
]
}
}

View File

@@ -0,0 +1,111 @@
{
"101": {
"productId": 101,
"events": [
{
"id": 9001,
"productId": 101,
"partId": 501,
"partType": "BODY",
"type": "operation",
"operationId": 1,
"operationCode": "B",
"operationName": "Bejca",
"date": "2026-04-20",
"note": null,
"photosCount": 0
},
{
"id": 9002,
"productId": 101,
"partId": 501,
"partType": "BODY",
"type": "operation",
"operationId": 2,
"operationCode": "IZ",
"operationName": "Izolant",
"date": "2026-04-22",
"note": null,
"photosCount": 1
},
{
"id": 9006,
"productId": 101,
"partId": 501,
"partType": "BODY",
"type": "note",
"operationId": null,
"operationCode": null,
"operationName": null,
"date": "2026-04-23",
"note": "Do sprawdzenia rownomiernosc koloru na krawedzi topu.",
"photosCount": 2
}
],
"timelinePreview": {
"body": [
{
"id": 9001,
"code": "B",
"label": "Bejca",
"date": "2026-04-20",
"status": "done"
},
{
"id": 9002,
"code": "IZ",
"label": "Izolant",
"date": "2026-04-22",
"status": "done"
}
],
"neck": [
{
"id": 9004,
"code": "B",
"label": "Bejca",
"date": "2026-04-21",
"status": "done"
}
]
}
},
"102": {
"productId": 102,
"events": [
{
"id": 9011,
"productId": 102,
"partId": 511,
"partType": "BODY",
"type": "operation",
"operationId": 1,
"operationCode": "B",
"operationName": "Bejca",
"date": "2026-04-18",
"note": null,
"photosCount": 0
}
],
"timelinePreview": {
"body": [
{
"id": 9011,
"code": "B",
"label": "Bejca",
"date": "2026-04-18",
"status": "done"
}
],
"neck": []
}
},
"103": {
"productId": 103,
"events": [],
"timelinePreview": {
"body": [],
"neck": []
}
}
}

View File

@@ -0,0 +1,7 @@
export const USE_MOCK_API = import.meta.env.VITE_USE_MOCK_API !== 'false'
export function waitForMockApi() {
return new Promise((resolve) => {
globalThis.setTimeout(resolve, 250)
})
}

View File

@@ -0,0 +1,26 @@
import { api } from 'src/boot/axios'
import mockDictionaries from 'src/mocks/dictionaries.json'
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
export async function fetchDictionaries() {
if (USE_MOCK_API) {
await waitForMockApi()
return normalizeDictionaries(mockDictionaries)
}
const response = await api.get('/mayo-api/dictionaries')
return normalizeDictionaries(response.data)
}
function normalizeDictionaries(data) {
return {
models: data.models ?? [],
clients: data.clients ?? [],
finishes: data.finishes ?? [],
productionLists: data.productionLists ?? [],
operations: data.operations ?? [],
colors: data.colors ?? [],
}
}

View File

@@ -0,0 +1,56 @@
import { api } from 'src/boot/axios'
import mockSpecifications from 'src/mocks/specifications.json'
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
function normalizeSpecificationResponse(data) {
return {
productId: data.productId,
orderId: data.orderId ?? null,
sourceUrl: data.sourceUrl ?? null,
lastFetchedAt: data.lastFetchedAt ?? null,
sections: data.specification?.sections ?? data.sections ?? [],
diff: data.diff ?? [],
}
}
export async function fetchProductSpecification(productId) {
if (USE_MOCK_API) {
await waitForMockApi()
return normalizeSpecificationResponse(getMockSpecification(productId))
}
const response = await api.get(`/mayo-api/products/${productId}/specification`)
return normalizeSpecificationResponse(response.data)
}
export async function refreshProductSpecification(productId) {
if (USE_MOCK_API) {
await waitForMockApi()
return normalizeSpecificationResponse({
...getMockSpecification(productId),
lastFetchedAt: new Date().toISOString(),
})
}
const response = await api.post(`/mayo-api/products/${productId}/specification/refresh`)
return normalizeSpecificationResponse(response.data)
}
function getMockSpecification(productId) {
return (
mockSpecifications[productId] ?? {
productId,
orderId: null,
sourceUrl: null,
lastFetchedAt: null,
specification: {
sections: [],
},
diff: [],
}
)
}

View File

@@ -0,0 +1,104 @@
import { api } from 'src/boot/axios'
import mockTimelines from 'src/mocks/timelines.json'
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
const timelinesByProductId = structuredClone(mockTimelines)
export async function fetchProductTimeline(productId) {
if (USE_MOCK_API) {
await waitForMockApi()
return timelinesByProductId[productId] ?? {
productId,
events: [],
timelinePreview: {
body: [],
neck: [],
},
}
}
const response = await api.get(`/mayo-api/products/${productId}/timeline`)
return response.data
}
export async function createProductTimelineEvent(productId, payload) {
if (USE_MOCK_API) {
await waitForMockApi()
const timeline = ensureMockTimeline(productId)
const event = createMockEvent(productId, payload)
timeline.events.push(event)
timeline.timelinePreview = createTimelinePreview(timeline.events)
return {
event,
timelinePreview: timeline.timelinePreview,
}
}
const response = await api.post(`/mayo-api/products/${productId}/timeline/events`, payload)
return {
event: response.data.event ?? response.data,
timelinePreview: response.data.timelinePreview,
}
}
function ensureMockTimeline(productId) {
if (!timelinesByProductId[productId]) {
timelinesByProductId[productId] = {
productId,
events: [],
timelinePreview: {
body: [],
neck: [],
},
}
}
return timelinesByProductId[productId]
}
function createMockEvent(productId, payload) {
const now = Date.now()
return {
id: now,
productId,
partId: payload.partId ?? null,
partType: normalizePartType(payload.partType),
type: payload.type ?? 'operation',
operationId: payload.operationId ?? null,
operationCode: payload.operationCode ?? payload.code ?? 'NEW',
operationName: payload.operationName ?? payload.label ?? 'Nowa operacja',
date: payload.date ?? new Date().toISOString().slice(0, 10),
note: payload.note ?? null,
photosCount: payload.photosCount ?? 0,
}
}
function createTimelinePreview(events) {
return {
body: createPartPreview(events, 'BODY'),
neck: createPartPreview(events, 'NECK'),
}
}
function createPartPreview(events, partType) {
return events
.filter((event) => event.type === 'operation' && event.partType === partType)
.map((event) => ({
id: event.id,
code: event.operationCode,
label: event.operationName,
date: event.date,
status: 'done',
}))
}
function normalizePartType(partType) {
return String(partType ?? 'BODY').toUpperCase()
}

View File

@@ -0,0 +1,64 @@
import { api } from 'src/boot/axios'
import mockProducts from 'src/mocks/products.json'
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
const mockItems = [...mockProducts.items]
export async function fetchProducts(params) {
if (USE_MOCK_API) {
await waitForMockApi()
const filteredItems = filterMockProducts(mockItems, params)
const offset = Number(params?.offset ?? 0)
const limit = Number(params?.limit ?? filteredItems.length)
const items = filteredItems.slice(offset, offset + limit)
return {
items,
pageInfo: {
limit,
offset,
hasMore: offset + limit < filteredItems.length,
total: filteredItems.length,
},
}
}
const response = await api.get('/mayo-api/products', { params })
return {
items: response.data.items ?? response.data,
pageInfo: response.data.pageInfo ?? {},
}
}
function filterMockProducts(items, params = {}) {
return items.filter((product) => {
const search = params.search?.toLowerCase()
const productionList = params.productionList
if (search && !matchesSearch(product, search)) {
return false
}
if (params.finish && product.finish !== params.finish) {
return false
}
if (params.year && product.orderYear !== Number(params.year)) {
return false
}
if (productionList && !product.productionLists.includes(productionList)) {
return false
}
return true
})
}
function matchesSearch(product, search) {
return [product.orderId, product.model, product.client, product.finish]
.filter(Boolean)
.some((value) => value.toLowerCase().includes(search))
}

View File

@@ -0,0 +1,73 @@
import { computed, ref } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { fetchDictionaries as fetchDictionariesFromApi } from 'src/services/dictionariesApi'
export const useDictionariesStore = defineStore('dictionaries', () => {
const models = ref([])
const clients = ref([])
const finishes = ref([])
const productionLists = ref([])
const operations = ref([])
const colors = ref([])
const loadedAt = ref(null)
const isLoading = ref(false)
const error = ref(null)
const isLoaded = computed(() => loadedAt.value !== null)
async function fetchDictionaries({ force = false } = {}) {
if (isLoading.value || (isLoaded.value && !force)) {
return
}
isLoading.value = true
error.value = null
try {
const data = await fetchDictionariesFromApi()
models.value = data.models
clients.value = data.clients
finishes.value = data.finishes
productionLists.value = data.productionLists
operations.value = data.operations
colors.value = data.colors
loadedAt.value = Date.now()
} catch (err) {
error.value = err
throw err
} finally {
isLoading.value = false
}
}
function clear() {
models.value = []
clients.value = []
finishes.value = []
productionLists.value = []
operations.value = []
colors.value = []
loadedAt.value = null
error.value = null
}
return {
models,
clients,
finishes,
productionLists,
operations,
colors,
loadedAt,
isLoading,
error,
isLoaded,
fetchDictionaries,
clear,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useDictionariesStore, import.meta.hot))
}

View File

@@ -0,0 +1,141 @@
import { computed, ref } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia'
import {
fetchProductSpecification,
refreshProductSpecification,
} from 'src/services/productSpecificationApi'
function createEmptySpecification(productId) {
return {
productId,
orderId: null,
sourceUrl: null,
lastFetchedAt: null,
sections: [],
diff: [],
loadedAt: null,
isLoading: false,
isRefreshing: false,
error: null,
}
}
export const useProductSpecificationStore = defineStore('productSpecification', () => {
const byProductId = ref({})
const loadedProductIds = computed(() => Object.keys(byProductId.value).map(Number))
function ensureSpecification(productId) {
if (!byProductId.value[productId]) {
byProductId.value = {
...byProductId.value,
[productId]: createEmptySpecification(productId),
}
}
return byProductId.value[productId]
}
function setSpecification(productId, patch) {
const current = ensureSpecification(productId)
byProductId.value = {
...byProductId.value,
[productId]: {
...current,
...patch,
},
}
}
function getSpecification(productId) {
return byProductId.value[productId] ?? null
}
async function fetchSpecification(productId, { force = false } = {}) {
const current = ensureSpecification(productId)
if (current.isLoading || (current.loadedAt && !force)) {
return current
}
setSpecification(productId, {
isLoading: true,
error: null,
})
try {
const specification = await fetchProductSpecification(productId)
setSpecification(productId, {
productId,
orderId: specification.orderId,
sourceUrl: specification.sourceUrl,
lastFetchedAt: specification.lastFetchedAt,
sections: specification.sections,
diff: specification.diff,
loadedAt: Date.now(),
isLoading: false,
})
return byProductId.value[productId]
} catch (err) {
setSpecification(productId, {
isLoading: false,
error: err,
})
throw err
}
}
async function refreshSpecification(productId) {
ensureSpecification(productId)
setSpecification(productId, {
isRefreshing: true,
error: null,
})
try {
const specification = await refreshProductSpecification(productId)
setSpecification(productId, {
productId,
orderId: specification.orderId,
sourceUrl: specification.sourceUrl,
lastFetchedAt: specification.lastFetchedAt,
sections: specification.sections,
diff: specification.diff,
loadedAt: Date.now(),
isRefreshing: false,
})
return byProductId.value[productId]
} catch (err) {
setSpecification(productId, {
isRefreshing: false,
error: err,
})
throw err
}
}
function clearProduct(productId) {
const next = { ...byProductId.value }
delete next[productId]
byProductId.value = next
}
return {
byProductId,
loadedProductIds,
ensureSpecification,
getSpecification,
fetchSpecification,
refreshSpecification,
clearProduct,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useProductSpecificationStore, import.meta.hot))
}

View File

@@ -0,0 +1,126 @@
import { computed, ref } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia'
import {
createProductTimelineEvent,
fetchProductTimeline,
} from 'src/services/productTimelineApi'
import { useProductsStore } from 'src/stores/productsStore'
function createEmptyTimeline(productId) {
return {
productId,
events: [],
loadedAt: null,
isLoading: false,
error: null,
}
}
export const useProductTimelineStore = defineStore('productTimeline', () => {
const byProductId = ref({})
const loadedProductIds = computed(() => Object.keys(byProductId.value).map(Number))
function ensureTimeline(productId) {
if (!byProductId.value[productId]) {
byProductId.value = {
...byProductId.value,
[productId]: createEmptyTimeline(productId),
}
}
return byProductId.value[productId]
}
function setTimeline(productId, patch) {
const current = ensureTimeline(productId)
byProductId.value = {
...byProductId.value,
[productId]: {
...current,
...patch,
},
}
}
function getEvents(productId) {
return byProductId.value[productId]?.events ?? []
}
async function fetchTimeline(productId, { force = false } = {}) {
const current = ensureTimeline(productId)
if (current.isLoading || (current.loadedAt && !force)) {
return current
}
setTimeline(productId, {
isLoading: true,
error: null,
})
try {
const timeline = await fetchProductTimeline(productId)
setTimeline(productId, {
events: timeline.events ?? [],
timelinePreview: timeline.timelinePreview,
loadedAt: Date.now(),
isLoading: false,
})
if (timeline.timelinePreview) {
useProductsStore().applyTimelinePreviewUpdate(productId, timeline.timelinePreview)
}
return byProductId.value[productId]
} catch (err) {
setTimeline(productId, {
isLoading: false,
error: err,
})
throw err
}
}
async function addEvent(productId, payload) {
const { event: createdEvent, timelinePreview } = await createProductTimelineEvent(
productId,
payload,
)
const current = ensureTimeline(productId)
setTimeline(productId, {
events: [...current.events, createdEvent],
loadedAt: Date.now(),
})
if (timelinePreview) {
useProductsStore().applyTimelinePreviewUpdate(productId, timelinePreview)
}
return createdEvent
}
function clearProduct(productId) {
const next = { ...byProductId.value }
delete next[productId]
byProductId.value = next
}
return {
byProductId,
loadedProductIds,
ensureTimeline,
getEvents,
fetchTimeline,
addEvent,
clearProduct,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useProductTimelineStore, import.meta.hot))
}

View File

@@ -0,0 +1,156 @@
import { computed, ref } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { fetchProducts } from 'src/services/productsApi'
const DEFAULT_LIMIT = 30
function createDefaultFilters() {
return {
search: '',
modelId: null,
clientId: null,
finish: null,
productionListId: null,
year: null,
}
}
export const useProductsStore = defineStore('products', () => {
const ids = ref([])
const byId = ref({})
const filters = ref(createDefaultFilters())
const limit = ref(DEFAULT_LIMIT)
const offset = ref(0)
const total = ref(null)
const hasMore = ref(true)
const isLoading = ref(false)
const error = ref(null)
const items = computed(() => ids.value.map((id) => byId.value[id]).filter(Boolean))
const count = computed(() => ids.value.length)
function buildListParams() {
return {
limit: limit.value,
offset: offset.value,
search: filters.value.search || undefined,
model: filters.value.modelId || undefined,
client: filters.value.clientId || undefined,
finish: filters.value.finish || undefined,
productionList: filters.value.productionListId || undefined,
year: filters.value.year || undefined,
}
}
function setProducts(products, { append = false } = {}) {
const nextIds = append ? [...ids.value] : []
const nextById = append ? { ...byId.value } : {}
for (const product of products) {
nextById[product.id] = product
if (!nextIds.includes(product.id)) {
nextIds.push(product.id)
}
}
ids.value = nextIds
byId.value = nextById
}
async function fetchFirstPage() {
offset.value = 0
hasMore.value = true
return fetchNextPage({ append: false })
}
async function fetchNextPage({ append = true } = {}) {
if (isLoading.value || !hasMore.value) {
return
}
isLoading.value = true
error.value = null
try {
const { items: products, pageInfo } = await fetchProducts(buildListParams())
setProducts(products, { append })
offset.value = append ? offset.value + products.length : products.length
total.value = pageInfo.total ?? total.value
hasMore.value = pageInfo.hasMore ?? products.length === limit.value
} catch (err) {
error.value = err
throw err
} finally {
isLoading.value = false
}
}
function setFilters(nextFilters) {
filters.value = {
...filters.value,
...nextFilters,
}
}
async function applyFilters(nextFilters) {
setFilters(nextFilters)
await fetchFirstPage()
}
function updateProduct(productId, patch) {
const current = byId.value[productId]
if (!current) {
return
}
byId.value = {
...byId.value,
[productId]: {
...current,
...patch,
},
}
}
function applyTimelinePreviewUpdate(productId, timelinePreview) {
updateProduct(productId, { timelinePreview })
}
function clear() {
ids.value = []
byId.value = {}
offset.value = 0
total.value = null
hasMore.value = true
error.value = null
}
return {
ids,
byId,
filters,
limit,
offset,
total,
hasMore,
isLoading,
error,
items,
count,
fetchFirstPage,
fetchNextPage,
setFilters,
applyFilters,
updateProduct,
applyTimelinePreviewUpdate,
clear,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useProductsStore, import.meta.hot))
}

View File

@@ -0,0 +1,54 @@
import { computed, ref } from 'vue'
import { acceptHMRUpdate, defineStore } from 'pinia'
export const UI_PANELS = {
ADVANCED_SEARCH: 'advancedSearch',
PRODUCT_SPECIFICATION: 'productSpecification',
PRODUCTION_STATUSES: 'productionStatuses',
PRODUCT_TIMELINE: 'productTimeline',
}
export const useUiStore = defineStore('ui', () => {
const isDrawerOpen = ref(false)
const activePanel = ref(null)
const activeProductId = ref(null)
const drawerPayload = ref({})
const drawerInstanceKey = ref(0)
const hasActivePanel = computed(() => activePanel.value !== null)
function openDrawer(panel, payload = {}) {
isDrawerOpen.value = true
activePanel.value = panel
activeProductId.value = payload.productId ?? null
drawerPayload.value = payload
drawerInstanceKey.value += 1
}
function replaceDrawer(panel, payload = {}) {
openDrawer(panel, payload)
}
function closeDrawer() {
isDrawerOpen.value = false
activePanel.value = null
activeProductId.value = null
drawerPayload.value = {}
}
return {
isDrawerOpen,
activePanel,
activeProductId,
drawerPayload,
drawerInstanceKey,
hasActivePanel,
openDrawer,
replaceDrawer,
closeDrawer,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUiStore, import.meta.hot))
}

502
notes/api_services.md Normal file
View File

@@ -0,0 +1,502 @@
# API Services I Mocki
Data: 2026-04-29
## Cel dokumentu
Ten dokument opisuje warstwe API we frontendzie.
Warstwa API znajduje sie w:
```txt
frontend/src/services/
```
Jej zadaniem jest odseparowanie store od szczegolow komunikacji HTTP.
Store nie powinien wiedziec:
- jaki dokladnie jest URL endpointu,
- czy komunikacja idzie przez Axios,
- czy dane pochodza z prawdziwego backendu,
- czy dane pochodza z mockow JSON.
Store powinien wywolac funkcje domenowa, np.:
```js
fetchProducts(params)
```
a service powinien zajac sie reszta.
## Aktualne pliki services
```txt
frontend/src/services/apiMode.js
frontend/src/services/productsApi.js
frontend/src/services/productTimelineApi.js
frontend/src/services/productSpecificationApi.js
frontend/src/services/dictionariesApi.js
```
## Przeplyw danych
Docelowy przeplyw wyglada tak:
```txt
komponent Vue
-> store Pinia
-> service API
-> mock JSON albo Axios
-> backend Directus
```
Przyklad:
```txt
IndexPage.vue
-> productsStore.fetchFirstPage()
-> fetchProducts(params)
-> products.json albo GET /mayo-api/products
```
## Dlaczego services sa osobna warstwa
Bez services store musialby robic cos takiego:
```js
api.get('/mayo-api/products', { params })
```
Wtedy store zna:
- Axios,
- endpoint,
- strukture odpowiedzi HTTP.
To miesza odpowiedzialnosci.
Po wydzieleniu services store robi:
```js
const { items, pageInfo } = await fetchProducts(params)
```
To jest lepsze, bo:
- store skupia sie na stanie aplikacji,
- services skupiaja sie na komunikacji,
- endpoint mozna zmienic w jednym miejscu,
- latwiej dodac mocki,
- latwiej testowac logike store.
## `apiMode.js`
Plik:
```txt
frontend/src/services/apiMode.js
```
Zawiera przelacznik miedzy mock API i prawdziwym API:
```js
export const USE_MOCK_API = import.meta.env.VITE_USE_MOCK_API !== 'false'
```
To oznacza:
- domyslnie mocki sa wlaczone,
- jesli ustawisz `VITE_USE_MOCK_API=false`, aplikacja uzyje prawdziwego backendu.
Uruchomienie z prawdziwym backendem:
```bash
VITE_USE_MOCK_API=false npm run dev
```
Uruchomienie z mockami:
```bash
npm run dev
```
Mocki maja sztuczne opoznienie:
```js
waitForMockApi()
```
Dzieki temu podczas pracy nad UI widac stany ladowania podobne do prawdziwego API.
## Pliki mockow
Mocki sa zwyklymi plikami JSON:
```txt
frontend/src/mocks/products.json
frontend/src/mocks/timelines.json
frontend/src/mocks/specifications.json
frontend/src/mocks/dictionaries.json
```
Sa uzywane tylko przez warstwe `services`.
Komponenty i store nie importuja mockow bezposrednio.
To jest wazne, bo komponent nie powinien wiedziec, skad przyszly dane. Dla komponentu dane z mocka i z backendu powinny wygladac identycznie.
## `productsApi.js`
Plik:
```txt
frontend/src/services/productsApi.js
```
Eksportuje:
```js
fetchProducts(params)
```
Odpowiedzialnosc:
- pobranie listy produktow,
- obsluga filtrow,
- obsluga `limit` i `offset`,
- zwrocenie danych w formacie oczekiwanym przez `productsStore`.
Zwraca:
```js
{
items: [],
pageInfo: {
limit: 30,
offset: 0,
hasMore: true,
total: 184,
},
}
```
W trybie mock:
- czyta `frontend/src/mocks/products.json`,
- filtruje po `search`, `finish`, `year`, `productionList`,
- ucina wynik wedlug `limit` i `offset`,
- zwraca `items` i `pageInfo`.
W trybie prawdziwego API:
```http
GET /mayo-api/products
```
z parametrami:
```js
{
limit,
offset,
search,
model,
client,
finish,
productionList,
year,
}
```
## `productTimelineApi.js`
Plik:
```txt
frontend/src/services/productTimelineApi.js
```
Eksportuje:
```js
fetchProductTimeline(productId)
createProductTimelineEvent(productId, payload)
```
### `fetchProductTimeline`
Pobiera pelny timeline produktu.
W trybie mock:
- czyta `frontend/src/mocks/timelines.json`,
- zwraca timeline dla `productId`,
- jesli brak danych, zwraca pusty timeline.
W trybie prawdziwego API:
```http
GET /mayo-api/products/:id/timeline
```
### `createProductTimelineEvent`
Dodaje event do timeline.
W trybie mock:
- tworzy sztuczny event,
- dodaje go do pamieci mockow,
- przelicza `timelinePreview`,
- zwraca nowy event i nowy preview.
W trybie prawdziwego API:
```http
POST /mayo-api/products/:id/timeline/events
```
Zwracany format:
```js
{
event: {},
timelinePreview: {
body: [],
neck: [],
},
}
```
To pozwala store od razu zaktualizowac:
- pelny timeline,
- preview na glownej karcie produktu.
## `productSpecificationApi.js`
Plik:
```txt
frontend/src/services/productSpecificationApi.js
```
Eksportuje:
```js
fetchProductSpecification(productId)
refreshProductSpecification(productId)
```
### `fetchProductSpecification`
Pobiera specyfikacje produktu.
W trybie mock:
- czyta `frontend/src/mocks/specifications.json`.
W trybie prawdziwego API:
```http
GET /mayo-api/products/:id/specification
```
### `refreshProductSpecification`
Odswieza specyfikacje produktu.
W trybie mock:
- zwraca dane z mocka,
- aktualizuje `lastFetchedAt` na aktualny czas.
W trybie prawdziwego API:
```http
POST /mayo-api/products/:id/specification/refresh
```
### Normalizacja odpowiedzi
Ten plik zawiera funkcje:
```js
normalizeSpecificationResponse(data)
```
Jej zadaniem jest zamiana odpowiedzi backendu na prosty format dla store:
```js
{
productId,
orderId,
sourceUrl,
lastFetchedAt,
sections,
diff,
}
```
Service akceptuje dwa ksztalty:
```js
data.specification.sections
```
albo:
```js
data.sections
```
Dzieki temu store nie musi znac szczegolow odpowiedzi HTTP.
## `dictionariesApi.js`
Plik:
```txt
frontend/src/services/dictionariesApi.js
```
Eksportuje:
```js
fetchDictionaries()
```
Odpowiedzialnosc:
- pobranie slownikow do filtrow i formularzy,
- normalizacja pustych pol do pustych tablic.
W trybie mock:
- czyta `frontend/src/mocks/dictionaries.json`.
W trybie prawdziwego API:
```http
GET /mayo-api/dictionaries
```
Zwraca:
```js
{
models: [],
clients: [],
finishes: [],
productionLists: [],
operations: [],
colors: [],
}
```
## Jak dodac nowy endpoint
Przyklad: chcesz dodac API do zdjec produktu.
Nie dodawaj Axiosa bezposrednio w komponencie ani w store.
Zrob nowy service:
```txt
frontend/src/services/productPhotosApi.js
```
W nim:
```js
import { api } from 'src/boot/axios'
import { USE_MOCK_API, waitForMockApi } from 'src/services/apiMode'
export async function fetchProductPhotos(productId) {
if (USE_MOCK_API) {
await waitForMockApi()
return []
}
const response = await api.get(`/mayo-api/products/${productId}/photos`)
return response.data
}
```
Dopiero potem store importuje:
```js
import { fetchProductPhotos } from 'src/services/productPhotosApi'
```
## Zasady pracy z mockami
Mocki powinny miec taki sam ksztalt jak prawdziwe API.
To jest najwazniejsza zasada.
Jesli prawdziwy endpoint ma zwrocic:
```js
{
items: [],
pageInfo: {}
}
```
to mock tez powinien zwrocic:
```js
{
items: [],
pageInfo: {}
}
```
Nie nalezy robic osobnych struktur danych tylko dla widoku, jesli nie beda zgodne z przyszlym API.
## Co zostalo juz zrobione
Utworzono:
```txt
frontend/src/services/apiMode.js
frontend/src/services/productsApi.js
frontend/src/services/productTimelineApi.js
frontend/src/services/productSpecificationApi.js
frontend/src/services/dictionariesApi.js
```
Utworzono mocki:
```txt
frontend/src/mocks/products.json
frontend/src/mocks/timelines.json
frontend/src/mocks/specifications.json
frontend/src/mocks/dictionaries.json
```
Zaktualizowano store tak, aby uzywaly services zamiast Axiosa bezposrednio.
Zaktualizowano widok glownej strony tak, aby pobieral dane ze store zamiast trzymac lokalne mocki w komponencie.
## Najwazniejsza regula
Komponenty nie powinny importowac:
```js
api
```
ani:
```js
src/mocks/*.json
```
Komponent powinien uzywac store albo dostawac dane przez props.
Store powinien uzywac services.
Services powinny uzywac Axiosa albo mockow.

437
notes/arch_struct.md Normal file
View File

@@ -0,0 +1,437 @@
# Architektura Danych I Endpointow
Data: 2026-04-29
## Cel dokumentu
Ten dokument podsumowuje ustalenia dotyczace architektury danych pomiedzy:
- frontendem Quasar/Vue,
- Directusem jako glownym backendem dla frontendu,
- FastAPI jako integracja ze starym systemem Mayo.
Najwazniejszy problem, ktory rozwiazywalismy:
> Ile danych frontend powinien pobierac i trzymac w pamieci oraz jaki ksztalt powinny miec endpointy Directusa, zeby aplikacja byla szybka, czytelna i latwa do dalszej rozbudowy.
## Glowne zalozenie
Frontend nie powinien pobierac pelnych danych kazdego produktu na glowna liste.
Glowna lista produktow powinna dostawac lekki obiekt przygotowany specjalnie pod widok listy. Pelna specyfikacja i pelny timeline powinny byc pobierane dopiero wtedy, gdy uzytkownik ich potrzebuje.
To jest dobre rozwiazanie, bo:
- lista produktow bedzie szybciej sie ladowac,
- virtual scroll i lazy loading beda prostsze,
- frontend nie bedzie trzymal w pamieci danych, ktorych uzytkownik moze nigdy nie otworzyc,
- backend moze przygotowac dane dokladnie pod ekran,
- komponenty Vue nie musza znac struktury tabel w Directusie.
## Trzy rozne reprezentacje produktu
Ten sam produkt moze miec kilka reprezentacji w aplikacji. To nie jest blad ani niepotrzebna duplikacja. To jest normalny podzial danych pod konkretne widoki.
### `ProductListItem`
Lekki obiekt do glownej listy produktow.
Zawiera tylko dane potrzebne do pokazania karty produktu, filtrowania i szybkiej pracy listy:
```js
{
id: 101,
orderId: '0143/2025/1',
orderNumber: '0143',
orderYear: 2025,
orderIndex: 1,
model: 'Regius Core 6',
client: 'HIENDGUITAR.COM / INDONESIA',
finish: 'S+M',
productionLists: ['CZE-00'],
timelinePreview: {
body: [
{
id: 9001,
code: 'B',
label: 'Bejca',
date: '2026-04-20',
status: 'done',
},
],
neck: [],
},
}
```
To jest format, z ktorego korzysta karta produktu na glownej stronie.
### `ProductTimeline`
Pelny timeline jednego produktu.
Jest pobierany osobno, np. do prawego drawera albo widoku szczegolowego:
```js
{
productId: 101,
events: [
{
id: 9001,
productId: 101,
partId: 501,
partType: 'BODY',
type: 'operation',
operationId: 1,
operationCode: 'B',
operationName: 'Bejca',
date: '2026-04-20',
note: null,
photosCount: 0,
},
{
id: 9006,
productId: 101,
partId: 501,
partType: 'BODY',
type: 'note',
operationId: null,
operationCode: null,
operationName: null,
date: '2026-04-23',
note: 'Do sprawdzenia rownomiernosc koloru.',
photosCount: 2,
},
],
timelinePreview: {
body: [],
neck: [],
},
}
```
Pelny timeline zawiera operacje, notatki i pozniej moze zawierac informacje o zdjeciach.
### `ProductSpecification`
Pelna specyfikacja produktu ze starego systemu Mayo.
Nie powinna byc czescia `ProductListItem`. Jest pobierana dopiero po otwarciu panelu specyfikacji:
```js
{
productId: 101,
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'],
},
],
},
],
diff: [],
}
```
## Dlaczego timeline jest w dwoch miejscach
Na glownej liscie produktow potrzebny jest tylko skrot timeline:
```js
timelinePreview
```
W prawym drawerze albo widoku szczegolowym potrzebna jest pelna historia:
```js
events
```
Dlatego mamy dwa poziomy danych:
```txt
ProductListItem.timelinePreview
szybki skrot na karte produktu
ProductTimeline.events
pelny timeline do szczegolow
```
To jest dobre rozwiazanie, bo na liscie moze byc duzo produktow. Gdyby kazdy produkt mial pelny timeline, frontend pobieralby i przechowywal duzo danych, ktore czesto nie beda uzyte.
## Czy produkt na liscie powinien miec pelna specyfikacje
Nie.
Produkt na liscie nie powinien miec pelnej specyfikacji. Powinien miec tylko informacje potrzebne do pokazania karty:
- numer zamowienia,
- model,
- klient,
- finish,
- listy produkcyjne,
- skrot timeline.
Pelna specyfikacja powinna byc pobierana przez osobny endpoint po kliknieciu produktu albo otwarciu drawera.
## Rola backendu Directus
Directus powinien byc glownym API dla frontendu.
Frontend nie powinien skladac produktu z wielu tabel Directusa. Backend powinien przygotowac gotowy ksztalt odpowiedzi pod konkretny ekran.
To oznacza, ze zamiast zmuszac frontend do pobierania:
```txt
products
orders
clients
models
parts
events
lists
```
lepiej utworzyc custom endpoint:
```http
GET /mayo-api/products
```
ktory zwroci gotowe `ProductListItem`.
To jest dobre rozwiazanie, bo:
- frontend jest prostszy,
- mniej logiki laczenia danych jest w Vue,
- backend moze zoptymalizowac zapytania SQL,
- jeden endpoint odpowiada jednemu widokowi aplikacji,
- latwiej utrzymac stabilny kontrakt API.
## Rola FastAPI
FastAPI nie powinien byc glownym API dla frontendu.
Jego rola to integracja ze starym systemem Mayo:
- pobranie specyfikacji po numerze zamowienia,
- parsowanie strony starego systemu,
- zwrocenie danych Directusowi albo procesowi importu.
Docelowy przeplyw:
```txt
frontend -> Directus -> FastAPI -> stary system Mayo
```
Frontend powinien jak najczesciej rozmawiac tylko z Directusem.
## Planowane endpointy
### Lista produktow
```http
GET /mayo-api/products
```
Parametry:
```http
?limit=30&offset=0&search=regius&finish=GLOSS&year=2025&productionList=CZE-00
```
Cel:
- pobieranie danych do glownej listy,
- obsluga virtual scroll i lazy loading,
- filtrowanie,
- zwrocenie `timelinePreview`.
Przykladowa odpowiedz:
```json
{
"items": [],
"pageInfo": {
"limit": 30,
"offset": 0,
"hasMore": true,
"total": 184
}
}
```
### Pelny timeline produktu
```http
GET /mayo-api/products/:id/timeline
```
Cel:
- pobranie pelnej historii produkcji jednego produktu,
- uzycie w prawym drawerze albo widoku szczegolowym.
### Dodanie eventu do timeline
```http
POST /mayo-api/products/:id/timeline/events
```
Cel:
- dodanie operacji, notatki albo innego wpisu produkcyjnego.
Backend powinien zwrocic:
- utworzony event,
- opcjonalnie nowy `timelinePreview`.
Dzieki temu frontend moze od razu:
- dopisac event do pelnego timeline,
- zaktualizowac skrot na karcie produktu.
### Pelna specyfikacja produktu
```http
GET /mayo-api/products/:id/specification
```
Cel:
- pobranie pelnej specyfikacji jednego produktu,
- uzycie w panelu specyfikacji.
### Odswiezenie specyfikacji
```http
POST /mayo-api/products/:id/specification/refresh
```
Cel:
- Directus prosi FastAPI o pobranie aktualnych danych ze starego systemu,
- backend liczy hash/diff,
- zapisuje nowa wersje tylko jesli dane sie zmienily,
- zwraca aktualny stan specyfikacji.
### Slowniki
```http
GET /mayo-api/dictionaries
```
Cel:
- pobranie danych do filtrow i formularzy.
Przyklady:
- modele,
- klienci,
- finisze,
- listy produkcyjne,
- operacje,
- kolory.
## Virtual scroll i lazy loading
Glowna lista powinna dzialac na porcjach danych.
Przyklad:
```txt
limit = 30
offset = 0
```
Nastepne pobranie:
```txt
limit = 30
offset = 30
```
Frontend nie powinien pobierac calej bazy tylko dlatego, ze lista nie ma paginacji widocznej dla uzytkownika.
Lista moze wygladac jak jedna ciagla lista, ale technicznie powinna pobierac dane porcjami.
## Nazewnictwo `productId` i `orderId`
Wazne rozroznienie:
```js
productId
```
To wewnetrzne ID produktu w Directusie.
```js
orderId
```
To numer starego systemu Mayo, np.:
```txt
0143/2025/1
```
Nie nalezy uzywac `orderId` jako `productId`.
## Podzial odpowiedzialnosci
Docelowy przeplyw:
```txt
komponent Vue
-> store Pinia
-> service API
-> Axios albo mock JSON
-> Directus
```
Komponent:
- renderuje UI,
- dostaje dane przez props,
- emituje zdarzenia.
Store:
- trzyma stan,
- trzyma loading/error,
- zarzadza cache,
- decyduje kiedy pobrac dane.
Service API:
- zna endpointy,
- zna Axios,
- normalizuje odpowiedzi backendu.
Backend:
- laczy dane z tabel,
- przygotowuje odpowiedz pod frontend,
- pilnuje spojnosci danych.
## Dlaczego to jest dobre rozwiazanie
Ten podzial jest dobry, bo utrzymuje osobne odpowiedzialnosci:
- komponenty nie wiedza nic o Directusie,
- store nie musza znac szczegolow Axiosa,
- services nie przechowuja stanu,
- backend moze zmieniac strukture bazy bez przepisywania calego frontendu,
- mock API pozwala pracowac nad wygladem bez gotowego backendu.
To jest praktyczny kompromis dla MVP: nie jest przesadnie skomplikowany, ale od razu porzadkuje najwazniejsze granice aplikacji.

1071
notes/stores.md Normal file

File diff suppressed because it is too large Load Diff