refaktoryzacja paneli i widokow

This commit is contained in:
2026-04-30 16:37:04 +02:00
parent 6e3d722e69
commit e03257f6fb
8 changed files with 709 additions and 141 deletions

View File

@@ -3,60 +3,45 @@
<div class="mark"></div>
<div class="card-head">
<div class="order-info">
<span class="order-id">{{ order.orderId }}</span>
<h3 class="model">{{ order.model }}</h3>
<p class="client">{{ order.client }}</p>
<span class="order-id">{{ product.orderId }}</span>
<h3 class="model">{{ product.model }}</h3>
<p class="client">{{ product.client }}</p>
<div class="order-lists">
<span
v-for="assignedOrder in order.assignedOrders"
:key="assignedOrder"
v-for="productionList in product.productionLists"
:key="productionList"
class="order-list"
>
{{ assignedOrder }}
{{ productionList }}
</span>
</div>
<span class="finish">{{ order.finish }}</span>
</div>
</div>
<div class="flow">
<div class="flow-head">
<span class="label">Deska</span>
<div class="line"></div>
</div>
<div class="steps no-scrollbar">
<template v-for="(step, index) in order.steps.body" :key="`${step}-${index}`">
<div class="step">{{ step }}</div>
<span class="arrow">&#x2192;</span>
</template>
<button class="step add">
<q-icon class="add-icon" name="add" />
</button>
</div>
</div>
<div class="flow" v-if="order.steps.neck">
<div class="flow-head">
<span class="label">Gryf</span>
<div class="line"></div>
</div>
<div class="steps no-scrollbar">
<template v-for="(step, index) in order.steps.neck" :key="`${step}-${index}`">
<div class="step">{{ step }}</div>
<span class="arrow">&#x2192;</span>
</template>
<button class="step add">
<q-icon class="add-icon" name="add" />
</button>
<span class="finish">{{ product.finish }}</span>
</div>
</div>
<production-preview
label="Deska"
:steps="product.timelinePreview.body"
@add="emit('addProductionEvent', { product, partType: 'BODY' })"
/>
<production-preview
v-if="product.timelinePreview.neck.length"
label="Gryf"
:steps="product.timelinePreview.neck"
@add="emit('addProductionEvent', { product, partType: 'NECK' })"
/>
</div>
</template>
<script setup>
import ProductionPreview from 'src/components/ProductionPreview.vue'
defineProps({
order: {
product: {
type: Object,
required: true,
},
})
const emit = defineEmits(['addProductionEvent'])
</script>
<style lang="scss" scoped>
.my-order-card {
@@ -155,62 +140,5 @@ defineProps({
border-radius: var(--my-radius-sm);
}
}
.flow {
margin-top: 1rem;
.flow-head {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
.label {
color: var(--my-on-surface-variant);
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.line {
flex: 1;
height: 1px;
background: color-mix(in srgb, var(--my-outline-variant) 30%, transparent);
}
}
.steps {
display: flex;
align-items: center;
gap: 0.5rem;
overflow-x: auto;
.arrow {
color: var(--my-outline-variant);
font-size: 0.75rem;
}
.step {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--my-primary);
background: color-mix(in srgb, var(--my-primary-dim) 20%, transparent);
border: 1px solid color-mix(in srgb, var(--my-primary) 30%, transparent);
border-radius: var(--my-radius-sm);
&.add {
background: var(--my-surface-container-lowest);
border-style: dashed;
border-color: var(--my-outline-variant);
color: var(--my-on-surface-variant);
font-size: 0.875rem;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,91 @@
<template>
<div class="flow">
<div class="flow-head">
<span class="label">{{ label }}</span>
<div class="line"></div>
</div>
<div class="steps no-scrollbar">
<template v-for="step in steps" :key="step.id">
<div class="step">{{ step.code }}</div>
<span class="arrow">&#x2192;</span>
</template>
<button class="step add" type="button" @click="emit('add')">
<q-icon class="add-icon" name="add" />
</button>
</div>
</div>
</template>
<script setup>
defineProps({
label: {
type: String,
required: true,
},
steps: {
type: Array,
default: () => [],
},
})
const emit = defineEmits(['add'])
</script>
<style lang="scss" scoped>
.flow {
margin-top: 1rem;
.flow-head {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
.label {
color: var(--my-on-surface-variant);
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.line {
flex: 1;
height: 1px;
background: color-mix(in srgb, var(--my-outline-variant) 30%, transparent);
}
}
.steps {
display: flex;
align-items: center;
gap: 0.5rem;
overflow-x: auto;
.arrow {
color: var(--my-outline-variant);
font-size: 0.75rem;
}
.step {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
font-size: 0.75rem;
font-weight: 700;
color: var(--my-primary);
background: color-mix(in srgb, var(--my-primary-dim) 20%, transparent);
border: 1px solid color-mix(in srgb, var(--my-primary) 30%, transparent);
border-radius: var(--my-radius-sm);
&.add {
background: var(--my-surface-container-lowest);
border-style: dashed;
border-color: var(--my-outline-variant);
color: var(--my-on-surface-variant);
font-size: 0.875rem;
}
}
}
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<div class="right-drawer-host">
<component
:is="activePanelComponent"
v-if="activePanelComponent"
:key="uiStore.drawerInstanceKey"
v-bind="uiStore.drawerPayload"
@cancel="closeDrawer"
@close="closeDrawer"
@saved="closeDrawer"
/>
<div v-else class="right-drawer-host__empty">
Brak aktywnego panelu.
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useUiStore } from 'src/stores/uiStore'
import { rightDrawerPanels } from './panels'
const uiStore = useUiStore()
const activePanelComponent = computed(() => {
if (!uiStore.activePanel) {
return null
}
return rightDrawerPanels[uiStore.activePanel] ?? null
})
function closeDrawer() {
uiStore.closeDrawer()
}
</script>
<style lang="scss" scoped>
.right-drawer-host {
min-height: 100%;
}
.right-drawer-host__empty {
padding: 1.25rem;
color: var(--my-on-surface-variant);
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<section class="drawer-panel">
<header class="drawer-panel__header">
<div>
<div class="drawer-panel__eyebrow">Search</div>
<h2 class="drawer-panel__title">Advanced Search</h2>
</div>
<q-btn
flat
round
dense
icon="close"
aria-label="Close advanced search panel"
@click="emit('close')"
/>
</header>
<div class="drawer-panel__body">
<p class="drawer-panel__description">
Ten panel bedzie trzymal robocza konfiguracje filtrow i zapisze ja dopiero po
zatwierdzeniu przez uzytkownika.
</p>
<div class="drawer-panel__placeholder">
Tu trafia kontrolki filtrow wybieranych bez wpisywania tekstu.
</div>
</div>
<footer class="drawer-panel__footer">
<q-btn flat label="Cancel" @click="emit('cancel')" />
<q-btn color="primary" label="Apply" @click="applyFilters" />
</footer>
</section>
</template>
<script setup>
const emit = defineEmits(['cancel', 'close', 'apply'])
function applyFilters() {
emit('apply')
}
</script>
<style lang="scss" scoped>
.drawer-panel {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100%;
background: var(--my-surface);
}
.drawer-panel__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.25rem 1.25rem 1rem;
border-bottom: 1px solid color-mix(in srgb, var(--my-outline-variant) 60%, transparent);
}
.drawer-panel__eyebrow {
margin-bottom: 0.35rem;
color: var(--my-on-surface-variant);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.drawer-panel__title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
}
.drawer-panel__body {
display: grid;
align-content: start;
gap: 1rem;
padding: 1.25rem;
}
.drawer-panel__description {
margin: 0;
color: var(--my-on-surface-variant);
line-height: 1.5;
}
.drawer-panel__placeholder {
padding: 1rem;
background: var(--my-surface-container-low);
border: 1px dashed var(--my-outline-variant);
border-radius: var(--my-radius-md);
color: var(--my-on-surface-variant);
}
.drawer-panel__footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.25rem 1.25rem;
border-top: 1px solid color-mix(in srgb, var(--my-outline-variant) 60%, transparent);
}
</style>

View File

@@ -0,0 +1,178 @@
<template>
<section class="drawer-panel">
<header class="header">
<div>
<h3 class="header-title">Edit Instrument</h3>
<p class="header-info">0029/2025/12</p>
</div>
<q-btn
flat
round
dense
icon="close"
aria-label="Close product specification panel"
@click="emit('close')"
/>
</header>
<section class="spec">
<div class="spec-header">
<q-icon name="label" class="spec-icon" />
<h4 class="spec-title">tadada</h4>
</div>
<div class="spec-details">
<div class="spec-record">
<span class="spec-label">Radius:</span>
<span class="spec-value">16"</span>
</div>
<div class="spec-record">
<span class="spec-label">Drewno Szyjka:</span>
<span class="spec-value">5ply Wenge/Klon with Carbon Rods</span>
</div>
</div>
<div class="spec-header">
<q-icon name="label" class="spec-icon" />
<h4 class="spec-title">tadada</h4>
</div>
<div class="spec-details">
<div class="spec-record">
<span class="spec-label">Radius:</span>
<span class="spec-value">16"</span>
</div>
<div class="spec-record">
<span class="spec-label">Drewno Szyjka:</span>
<span class="spec-value">5ply Wenge/Klon with Carbon Rods</span>
</div>
</div>
</section>
<footer class="drawer-panel__footer">
<q-btn flat label="Cancel" @click="emit('cancel')" />
<q-btn color="primary" label="Save" @click="saveSpecification" />
</footer>
</section>
</template>
<script setup>
const props = defineProps({
productId: {
type: [Number, String],
default: null,
},
mode: {
type: String,
default: 'view',
},
})
const emit = defineEmits(['cancel', 'close', 'saved'])
function saveSpecification() {
emit('saved', {
productId: props.productId,
})
}
</script>
<style lang="scss" scoped>
.drawer-panel {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100%;
background: var(--my-surface-container);
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
// border-bottom: 1px solid color-mix(in srgb, var(--my-outline-variant) 60%, transparent);
.header-title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
line-height: normal;
}
.header-info {
margin: 0;
color: var(--my-primary);
font-size: 0.7rem;
font-weight: 700;
// letter-spacing: 0.08em;
// letter-spacing: -0.05em;
text-transform: uppercase;
}
}
.spec {
padding: 1rem;
.spec-header {
display: flex;
justify-content: start;
align-items: center;
gap: 10px;
text-transform: uppercase;
margin-bottom: 16px;
.spec-icon {
color: var(--my-primary);
}
.spec-title {
color: var(--my-on-surface-variant);
margin: 0;
font-size: 0.75rem;
font-weight: 900;
line-height: 1rem;
letter-spacing: 0.1rem;
}
}
.spec-details {
.spec-record {
padding: 0.5rem;
margin-bottom: 0.5rem;
background-color: var(--my-surface-container-high);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
.spec-label {
color: var(--my-on-surface-variant);
}
.spec-value {
margin-left: auto;
}
}
}
}
.body {
display: grid;
align-content: start;
gap: 1rem;
padding: 1.25rem;
}
.description {
margin: 0;
color: var(--my-on-surface-variant);
line-height: 1.5;
}
.placeholder {
display: grid;
gap: 0.5rem;
padding: 1rem;
background: var(--my-surface-container-low);
border: 1px dashed var(--my-outline-variant);
border-radius: var(--my-radius-md);
color: var(--my-on-surface-variant);
}
.footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.25rem 1.25rem;
border-top: 1px solid color-mix(in srgb, var(--my-outline-variant) 60%, transparent);
}
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<section class="drawer-panel">
<header class="drawer-panel__header">
<div>
<div class="drawer-panel__eyebrow">Production</div>
<h2 class="drawer-panel__title">Production Statuses</h2>
</div>
<q-btn
flat
round
dense
icon="close"
aria-label="Close production statuses panel"
@click="emit('close')"
/>
</header>
<div class="drawer-panel__body">
<p class="drawer-panel__description">
Ten panel dostanie identyfikator kontekstu produkcyjnego i utworzy lokalny draft do
przegladania oraz edycji statusow.
</p>
<div class="drawer-panel__placeholder">
<div><strong>orderId:</strong> {{ orderId ?? 'brak' }}</div>
<div><strong>productId:</strong> {{ productId ?? 'brak' }}</div>
</div>
</div>
<footer class="drawer-panel__footer">
<q-btn flat label="Cancel" @click="emit('cancel')" />
<q-btn color="primary" label="Save" @click="saveStatuses" />
</footer>
</section>
</template>
<script setup>
const props = defineProps({
orderId: {
type: [Number, String],
default: null,
},
productId: {
type: [Number, String],
default: null,
},
})
const emit = defineEmits(['cancel', 'close', 'saved'])
function saveStatuses() {
emit('saved', {
orderId: props.orderId,
productId: props.productId,
})
}
</script>
<style lang="scss" scoped>
.drawer-panel {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100%;
background: var(--my-surface);
}
.drawer-panel__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1.25rem 1.25rem 1rem;
border-bottom: 1px solid color-mix(in srgb, var(--my-outline-variant) 60%, transparent);
}
.drawer-panel__eyebrow {
margin-bottom: 0.35rem;
color: var(--my-on-surface-variant);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.drawer-panel__title {
margin: 0;
font-size: 1.1rem;
font-weight: 700;
}
.drawer-panel__body {
display: grid;
align-content: start;
gap: 1rem;
padding: 1.25rem;
}
.drawer-panel__description {
margin: 0;
color: var(--my-on-surface-variant);
line-height: 1.5;
}
.drawer-panel__placeholder {
display: grid;
gap: 0.5rem;
padding: 1rem;
background: var(--my-surface-container-low);
border: 1px dashed var(--my-outline-variant);
border-radius: var(--my-radius-md);
color: var(--my-on-surface-variant);
}
.drawer-panel__footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.25rem 1.25rem;
border-top: 1px solid color-mix(in srgb, var(--my-outline-variant) 60%, transparent);
}
</style>

View File

@@ -21,6 +21,16 @@
</q-list>
</q-drawer>
<q-drawer
v-model="rightDrawerOpen"
side="right"
overlay
:width="$q.screen.width"
class="right-drawer"
>
<RightDrawerHost />
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
@@ -30,7 +40,9 @@
<script setup>
import { ref, computed } from 'vue'
import EssentialLink from 'components/EssentialLink.vue'
import RightDrawerHost from 'src/components/right-drawer/RightDrawerHost.vue'
import { useQuasar } from 'quasar'
import { useUiStore } from 'src/stores/uiStore'
const linksList = [
{
@@ -78,12 +90,22 @@ const linksList = [
]
const $q = useQuasar()
const leftDrawerOpen = ref(false)
const uiStore = useUiStore()
const theme = computed({
get: () => $q.dark.isActive,
set: (val) => $q.dark.set(val),
})
const rightDrawerOpen = computed({
get: () => uiStore.isDrawerOpen,
set: (value) => {
if (!value) {
uiStore.closeDrawer()
}
},
})
function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value
}

View File

@@ -1,28 +1,57 @@
<template>
<q-page class="content">
<section class="drawer-test-actions">
<q-btn
color="primary"
label="Open Advanced Search"
unelevated
@click="openAdvancedSearch"
/>
<q-btn
color="secondary"
label="Open Product Specification"
unelevated
@click="openProductSpecification"
/>
<q-btn
color="accent"
label="Open Production Statuses"
unelevated
@click="openProductionStatuses"
/>
</section>
<section class="filters">
<div class="search-field">
<input class="input" placeholder="Search orders or models..." type="text" />
<div class="icon-wrap">
<input
v-model="searchQuery"
class="input"
placeholder="Search orders or models..."
type="text"
@keyup.enter="applySearch"
/>
<div class="icon-wrap" @click="applySearch">
<span class="material-symbols-outlined" data-icon="tune">tune</span>
</div>
</div>
<div class="month-tabs no-scrollbar">
<button class="tab active">All</button>
<button class="tab">Jan</button>
<button class="tab">Feb</button>
<button class="tab">Mar</button>
<button class="tab">Apr</button>
<button class="tab">May</button>
<button class="tab">Jun</button>
<button
v-for="month in monthFilters"
:key="month.value"
class="tab"
:class="{ active: activeMonth === month.value }"
@click="selectMonth(month.value)"
>
{{ month.label }}
</button>
</div>
</section>
<div class="stats">
<div class="stat-card primary">
<span class="label">In Progress</span>
<span class="value">14 Units</span>
<span class="value">{{ loadedProductsCount }} Units</span>
</div>
<div class="stat-card tertiary">
<span class="label">Avg Lead Time</span>
@@ -31,49 +60,87 @@
</div>
<div class="orders">
<order-card :order="order[0]" />
<order-card :order="order[1]" />
<order-card :order="order[2]" />
<order-card
v-for="product in productsStore.items"
:key="product.id"
:product="product"
@add-production-event="openProductionStatuses"
/>
</div>
</q-page>
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import OrderCard from 'src/components/OrderCard.vue'
const order = [
{
orderId: '0112/2025/12',
model: 'Duvell 6',
client: 'Sleek Elite',
assignedOrders: ['PAZ-25'],
finish: 'GLOSS',
steps: {
body: ['B', 'IZ', 'AK', 'LD', 'LD', 'LD', 'UV'],
neck: ['B', 'IZ', 'AK'],
},
},
{
orderId: '0029/2024/1',
model: 'Legend 6',
client: 'Heindeburs Indonesia',
assignedOrders: ['STY-25'],
finish: 'MAT',
steps: {
body: ['B', 'M'],
},
},
{
orderId: '0001/2025/20',
model: 'Regius 6 Core',
client: 'Mayo Stock',
assignedOrders: ['KWI-25', 'SUMMIT'],
finish: 'MIX',
steps: {
body: ['B', 'IZ', 'AK', 'LD'],
},
},
import { UI_PANELS, useUiStore } from 'src/stores/uiStore'
import { useProductsStore } from 'src/stores/productsStore'
const productsStore = useProductsStore()
const uiStore = useUiStore()
const searchQuery = ref(productsStore.filters.search)
const activeMonth = ref('all')
const monthFilters = [
{ label: 'All', value: 'all' },
{ label: 'Jan', value: 1 },
{ label: 'Feb', value: 2 },
{ label: 'Mar', value: 3 },
{ label: 'Apr', value: 4 },
{ label: 'May', value: 5 },
{ label: 'Jun', value: 6 },
]
//
const loadedProductsCount = computed(() => productsStore.count)
onMounted(() => {
productsStore.fetchFirstPage()
})
watch(searchQuery, (value) => {
productsStore.setFilters({ search: value })
})
function openAdvancedSearch() {
uiStore.openDrawer(UI_PANELS.ADVANCED_SEARCH, {
source: 'index-page',
})
}
function openProductSpecification() {
const product = productsStore.items[0]
if (!product) {
return
}
uiStore.openDrawer(UI_PANELS.PRODUCT_SPECIFICATION, {
productId: product.id,
mode: 'edit',
})
}
function openProductionStatuses({ product, partType } = {}) {
const selectedProduct = product ?? productsStore.items[0]
if (!selectedProduct) {
return
}
uiStore.openDrawer(UI_PANELS.PRODUCTION_STATUSES, {
orderId: selectedProduct.orderId,
productId: selectedProduct.id,
partType,
})
}
function applySearch() {
productsStore.applyFilters({ search: searchQuery.value })
}
function selectMonth(month) {
activeMonth.value = month
}
</script>
<style lang="scss" scoped>
@@ -88,6 +155,13 @@ const order = [
padding: 1rem;
}
.drawer-test-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.filters {
display: grid;
gap: 1rem;