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:
@@ -11,14 +11,10 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// app boot file (/src/boot)
|
// app boot file (/src/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||||
boot: [
|
boot: ['axios'],
|
||||||
'axios'
|
|
||||||
],
|
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
|
||||||
css: [
|
css: ['app.scss'],
|
||||||
'app.scss'
|
|
||||||
],
|
|
||||||
|
|
||||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||||
extras: [
|
extras: [
|
||||||
@@ -38,7 +34,7 @@ export default defineConfig((/* ctx */) => {
|
|||||||
build: {
|
build: {
|
||||||
target: {
|
target: {
|
||||||
browser: 'baseline-widely-available',
|
browser: 'baseline-widely-available',
|
||||||
node: 'node22'
|
node: 'node22',
|
||||||
},
|
},
|
||||||
|
|
||||||
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
vueRouterMode: 'hash', // available values: 'hash', 'history'
|
||||||
@@ -61,19 +57,25 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// viteVuePluginOptions: {},
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
vitePlugins: [
|
vitePlugins: [
|
||||||
['vite-plugin-checker', {
|
[
|
||||||
eslint: {
|
'vite-plugin-checker',
|
||||||
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
|
{
|
||||||
useFlatConfig: true
|
eslint: {
|
||||||
}
|
lintCommand: 'eslint -c ./eslint.config.js "./src*/**/*.{js,mjs,cjs,vue}"',
|
||||||
}, { server: false }]
|
useFlatConfig: true,
|
||||||
]
|
},
|
||||||
|
},
|
||||||
|
{ server: false },
|
||||||
|
],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver
|
||||||
devServer: {
|
devServer: {
|
||||||
// https: true,
|
// 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
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
||||||
@@ -91,7 +93,7 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// directives: [],
|
// directives: [],
|
||||||
|
|
||||||
// Quasar plugins
|
// Quasar plugins
|
||||||
plugins: []
|
plugins: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
// animations: 'all', // --- includes all animations
|
// animations: 'all', // --- includes all animations
|
||||||
@@ -114,10 +116,10 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
|
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
|
||||||
ssr: {
|
ssr: {
|
||||||
prodPort: 3000, // The default port that the production server should use
|
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: [
|
middlewares: [
|
||||||
'render' // keep this as last one
|
'render', // keep this as last one
|
||||||
],
|
],
|
||||||
|
|
||||||
// extendPackageJson (json) {},
|
// extendPackageJson (json) {},
|
||||||
@@ -128,7 +130,7 @@ export default defineConfig((/* ctx */) => {
|
|||||||
// manualStoreHydration: true,
|
// manualStoreHydration: true,
|
||||||
// manualPostHydrationTrigger: true,
|
// manualPostHydrationTrigger: true,
|
||||||
|
|
||||||
pwa: false
|
pwa: false,
|
||||||
// pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
// pwaOfflineHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||||
|
|
||||||
// pwaExtendGenerateSWOptions (cfg) {},
|
// pwaExtendGenerateSWOptions (cfg) {},
|
||||||
@@ -137,7 +139,7 @@ export default defineConfig((/* ctx */) => {
|
|||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
|
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
|
||||||
pwa: {
|
pwa: {
|
||||||
workboxMode: 'GenerateSW' // 'GenerateSW' or 'InjectManifest'
|
workboxMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest'
|
||||||
// swFilename: 'sw.js',
|
// swFilename: 'sw.js',
|
||||||
// manifestFilename: 'manifest.json',
|
// manifestFilename: 'manifest.json',
|
||||||
// extendManifestJson (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
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
|
||||||
capacitor: {
|
capacitor: {
|
||||||
hideSplashscreen: true
|
hideSplashscreen: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
|
// 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) {},
|
// extendPackageJson (json) {},
|
||||||
|
|
||||||
// Electron preload scripts (if any) from /src-electron, WITHOUT file extension
|
// 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
|
// specify the debugging port to use for the Electron app when running in development mode
|
||||||
inspectPort: 5858,
|
inspectPort: 5858,
|
||||||
@@ -175,13 +177,11 @@ export default defineConfig((/* ctx */) => {
|
|||||||
|
|
||||||
packager: {
|
packager: {
|
||||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||||
|
|
||||||
// OS X / Mac App Store
|
// OS X / Mac App Store
|
||||||
// appBundleId: '',
|
// appBundleId: '',
|
||||||
// appCategoryType: '',
|
// appCategoryType: '',
|
||||||
// osxSign: '',
|
// osxSign: '',
|
||||||
// protocol: 'myapp://path',
|
// protocol: 'myapp://path',
|
||||||
|
|
||||||
// Windows only
|
// Windows only
|
||||||
// win32metadata: { ... }
|
// win32metadata: { ... }
|
||||||
},
|
},
|
||||||
@@ -189,8 +189,8 @@ export default defineConfig((/* ctx */) => {
|
|||||||
builder: {
|
builder: {
|
||||||
// https://www.electron.build/configuration
|
// 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
|
// 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' ]
|
* @example [ 'my-script.ts', 'sub-folder/my-other-script.js' ]
|
||||||
*/
|
*/
|
||||||
extraScripts: []
|
extraScripts: [],
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
10
frontend/src/components/right-drawer/panels/index.js
Normal file
10
frontend/src/components/right-drawer/panels/index.js
Normal 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,
|
||||||
|
}
|
||||||
109
frontend/src/mocks/dictionaries.json
Normal file
109
frontend/src/mocks/dictionaries.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
117
frontend/src/mocks/products.json
Normal file
117
frontend/src/mocks/products.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
93
frontend/src/mocks/specifications.json
Normal file
93
frontend/src/mocks/specifications.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
111
frontend/src/mocks/timelines.json
Normal file
111
frontend/src/mocks/timelines.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/src/services/apiMode.js
Normal file
7
frontend/src/services/apiMode.js
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
26
frontend/src/services/dictionariesApi.js
Normal file
26
frontend/src/services/dictionariesApi.js
Normal 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 ?? [],
|
||||||
|
}
|
||||||
|
}
|
||||||
56
frontend/src/services/productSpecificationApi.js
Normal file
56
frontend/src/services/productSpecificationApi.js
Normal 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: [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
104
frontend/src/services/productTimelineApi.js
Normal file
104
frontend/src/services/productTimelineApi.js
Normal 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()
|
||||||
|
}
|
||||||
64
frontend/src/services/productsApi.js
Normal file
64
frontend/src/services/productsApi.js
Normal 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))
|
||||||
|
}
|
||||||
73
frontend/src/stores/dictionariesStore.js
Normal file
73
frontend/src/stores/dictionariesStore.js
Normal 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))
|
||||||
|
}
|
||||||
141
frontend/src/stores/productSpecificationStore.js
Normal file
141
frontend/src/stores/productSpecificationStore.js
Normal 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))
|
||||||
|
}
|
||||||
126
frontend/src/stores/productTimelineStore.js
Normal file
126
frontend/src/stores/productTimelineStore.js
Normal 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))
|
||||||
|
}
|
||||||
156
frontend/src/stores/productsStore.js
Normal file
156
frontend/src/stores/productsStore.js
Normal 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))
|
||||||
|
}
|
||||||
54
frontend/src/stores/uiStore.js
Normal file
54
frontend/src/stores/uiStore.js
Normal 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
502
notes/api_services.md
Normal 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
437
notes/arch_struct.md
Normal 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
1071
notes/stores.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user