@
feat(temper_rejects): "Maschinen" filter label + brighter entry-card meta - Rename left filter heading Pressen → Maschinen (de.ts) - Bump entry-card meta info one step in size and brightness (article, WAGEN/ETIKETT labels+values, count labels, "getempert vor …") - Ship rebuilt dist bundle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
parent
83d86dfe90
commit
10aac9ac5f
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@ -9,8 +9,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Ausschuss-Station</title>
|
<title>Ausschuss-Station</title>
|
||||||
<script type="module" crossorigin src="/module/temper_rejects/assets/index-D0flZxiB.js"></script>
|
<script type="module" crossorigin src="/module/temper_rejects/assets/index-DceU8_gJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/module/temper_rejects/assets/index-mkbUZDoP.css">
|
<link rel="stylesheet" crossorigin href="/module/temper_rejects/assets/index-Oxhdj_dg.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@ -1,136 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import Dialog from 'primevue/dialog'
|
|
||||||
import Select from 'primevue/select'
|
|
||||||
import InputNumber from 'primevue/inputnumber'
|
|
||||||
import Button from 'primevue/button'
|
|
||||||
import type { RejectEntry, Reason, RejectRequest } from '@/types/station'
|
|
||||||
|
|
||||||
const props = defineProps<{ entry: RejectEntry | null; reasons: Reason[]; submitting: boolean }>()
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void; (e: 'submit', req: RejectRequest): void }>()
|
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
|
||||||
|
|
||||||
const count = ref<number | null>(null)
|
|
||||||
const reasonId = ref<number | null>(null)
|
|
||||||
const touched = ref(false)
|
|
||||||
|
|
||||||
const visible = computed({
|
|
||||||
get: () => props.entry !== null,
|
|
||||||
set: (v: boolean) => {
|
|
||||||
if (!v) emit('close')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Reset the form whenever a new entry opens the dialog.
|
|
||||||
watch(
|
|
||||||
() => props.entry,
|
|
||||||
(e) => {
|
|
||||||
if (e) {
|
|
||||||
count.value = null
|
|
||||||
reasonId.value = null
|
|
||||||
touched.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const reasonOptions = computed(() =>
|
|
||||||
props.reasons.map((r) => ({ id: r.id, label: locale.value === 'en' ? r.reason_en : r.reason_de })),
|
|
||||||
)
|
|
||||||
|
|
||||||
const valid = computed(() => (count.value ?? 0) > 0 && reasonId.value !== null)
|
|
||||||
|
|
||||||
function onBook() {
|
|
||||||
touched.value = true
|
|
||||||
if (!valid.value || !props.entry) return
|
|
||||||
emit('submit', {
|
|
||||||
press: props.entry.press,
|
|
||||||
orderNo: props.entry.orderNo,
|
|
||||||
ttId: props.entry.ttId,
|
|
||||||
count: count.value as number,
|
|
||||||
reason: reasonId.value as number,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Dialog
|
|
||||||
v-model:visible="visible"
|
|
||||||
modal
|
|
||||||
:closable="!submitting"
|
|
||||||
:draggable="false"
|
|
||||||
:style="{ width: '30rem', maxWidth: '94vw' }"
|
|
||||||
:header="t('dialog.title')"
|
|
||||||
>
|
|
||||||
<div v-if="entry" class="body">
|
|
||||||
<p class="for">{{ t('dialog.for', { cart: entry.cartId, order: entry.orderNo }) }}</p>
|
|
||||||
|
|
||||||
<label class="fld">
|
|
||||||
<span class="lab">{{ t('dialog.count') }} <em>({{ t('dialog.pieces') }})</em></span>
|
|
||||||
<InputNumber
|
|
||||||
v-model="count"
|
|
||||||
:min="1"
|
|
||||||
:max="entry.partsGood"
|
|
||||||
show-buttons
|
|
||||||
button-layout="horizontal"
|
|
||||||
:input-style="{ width: '4rem', textAlign: 'center' }"
|
|
||||||
incrementButtonIcon="pi pi-plus"
|
|
||||||
decrementButtonIcon="pi pi-minus"
|
|
||||||
/>
|
|
||||||
<span v-if="touched && (count ?? 0) <= 0" class="err">{{ t('dialog.needCount') }}</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="fld">
|
|
||||||
<span class="lab">{{ t('dialog.reason') }}</span>
|
|
||||||
<Select
|
|
||||||
v-model="reasonId"
|
|
||||||
:options="reasonOptions"
|
|
||||||
option-label="label"
|
|
||||||
option-value="id"
|
|
||||||
:placeholder="t('dialog.chooseReason')"
|
|
||||||
/>
|
|
||||||
<span v-if="touched && reasonId === null" class="err">{{ t('dialog.needReason') }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<Button :label="t('dialog.cancel')" text :disabled="submitting" @click="emit('close')" />
|
|
||||||
<Button :label="t('dialog.confirm')" severity="danger" :loading="submitting" @click="onBook" />
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
padding-top: 4px;
|
|
||||||
}
|
|
||||||
.for {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--mono);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--ink-dim);
|
|
||||||
}
|
|
||||||
.fld {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.lab {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
.lab em {
|
|
||||||
color: var(--ink-faint);
|
|
||||||
font-style: normal;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
}
|
|
||||||
.err {
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: var(--ausschuss);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -5,7 +5,8 @@ import TemperBelt from '@/components/TemperBelt.vue'
|
|||||||
import type { RejectEntry } from '@/types/station'
|
import type { RejectEntry } from '@/types/station'
|
||||||
|
|
||||||
const props = defineProps<{ entry: RejectEntry }>()
|
const props = defineProps<{ entry: RejectEntry }>()
|
||||||
const emit = defineEmits<{ (e: 'book', entry: RejectEntry): void }>()
|
// Forward the raw click event; the page anchors the booking popover to this button.
|
||||||
|
const emit = defineEmits<{ (e: 'book', ev: MouseEvent): void }>()
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ const relTime = computed(() => {
|
|||||||
|
|
||||||
<div class="belt-wrap">
|
<div class="belt-wrap">
|
||||||
<TemperBelt :flow="entry.flow" />
|
<TemperBelt :flow="entry.flow" />
|
||||||
<button type="button" class="book" @click="emit('book', entry)">{{ t('entry.book') }}</button>
|
<button type="button" class="book" @click="emit('book', $event)">{{ t('entry.book') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
@ -65,7 +66,7 @@ const relTime = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 9px 12px;
|
padding: 4px 12px;
|
||||||
background: var(--panel-3);
|
background: var(--panel-3);
|
||||||
border-bottom: 1px solid var(--line);
|
border-bottom: 1px solid var(--line);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -73,25 +74,31 @@ const relTime = computed(() => {
|
|||||||
.press {
|
.press {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
font-size: 0.9rem;
|
/* Machine number is the row's headline: no box, near the full header height.
|
||||||
|
line-height < 1 crops the font's own leading so the tall number doesn't
|
||||||
|
inflate the row (press values are caps+digits, e.g. P5 — no descenders to clip). */
|
||||||
|
font-size: 2.1rem;
|
||||||
|
line-height: 0.85;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background: var(--ground);
|
|
||||||
border: 1px solid var(--line-2);
|
|
||||||
border-radius: var(--r);
|
|
||||||
padding: 3px 9px;
|
|
||||||
}
|
}
|
||||||
.ord {
|
.ord {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
|
/* The box moves off the press onto the order info sitting right next to it.
|
||||||
|
Text sizes below stay put — boxed, not shrunk. */
|
||||||
|
background: var(--ground);
|
||||||
|
border: 1px solid var(--line-2);
|
||||||
|
border-radius: var(--r);
|
||||||
|
padding: 3px 10px;
|
||||||
}
|
}
|
||||||
.on {
|
.on {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
.art {
|
.art {
|
||||||
font-size: 0.66rem;
|
font-size: 0.74rem;
|
||||||
color: var(--ink-dim);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
.spacer {
|
.spacer {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@ -99,17 +106,17 @@ const relTime = computed(() => {
|
|||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 5px;
|
gap: 9px;
|
||||||
}
|
}
|
||||||
.mk {
|
.mk {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 0.55rem;
|
font-size: 0.62rem;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--ink-faint);
|
color: var(--ink-dim);
|
||||||
}
|
}
|
||||||
.mv {
|
.mv {
|
||||||
font-size: 0.8rem;
|
font-size: 0.88rem;
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
.counts {
|
.counts {
|
||||||
@ -125,9 +132,10 @@ const relTime = computed(() => {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.ck {
|
.ck {
|
||||||
font-size: 0.6rem;
|
font-size: 0.66rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--ink-faint);
|
color: var(--ink-dim);
|
||||||
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
.dot {
|
.dot {
|
||||||
color: var(--ink-faint);
|
color: var(--ink-faint);
|
||||||
@ -135,8 +143,8 @@ const relTime = computed(() => {
|
|||||||
}
|
}
|
||||||
.ago {
|
.ago {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 0.62rem;
|
font-size: 0.7rem;
|
||||||
color: var(--ink-faint);
|
color: var(--ink-dim);
|
||||||
}
|
}
|
||||||
.belt-wrap {
|
.belt-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -154,8 +162,8 @@ const relTime = computed(() => {
|
|||||||
background: var(--ausschuss);
|
background: var(--ausschuss);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--r);
|
border-radius: var(--r);
|
||||||
padding: 12px 18px;
|
padding: 8px 18px;
|
||||||
min-height: var(--size-touch-target, 44px);
|
min-height: 36px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
195
src/components/RejectPopover.vue
Normal file
195
src/components/RejectPopover.vue
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<!--
|
||||||
|
RejectPopover — the booking form as a Popover anchored to the card's "Ausschuss
|
||||||
|
buchen" button (a tooltip-style panel with an arrow), NOT a centered modal.
|
||||||
|
|
||||||
|
Single instance owned by RejectStationPage: the page sets `entry` then calls
|
||||||
|
toggle(event) with the click that came from the card, so the panel points at the
|
||||||
|
clicked button. The reason is preselected to "Temperprozess nach Ofen" and its
|
||||||
|
Select is filterable — typing e.g. "Ofen" narrows to the two Temperprozess reasons.
|
||||||
|
-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import Popover from 'primevue/popover'
|
||||||
|
import Select from 'primevue/select'
|
||||||
|
import InputNumber from 'primevue/inputnumber'
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import type { RejectEntry, Reason, RejectRequest } from '@/types/station'
|
||||||
|
|
||||||
|
const props = defineProps<{ entry: RejectEntry | null; reasons: Reason[]; submitting: boolean }>()
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void; (e: 'submit', req: RejectRequest): void }>()
|
||||||
|
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
|
// The reason preselected each time the popover opens. Matched by German TEXT, not id
|
||||||
|
// (ids differ per deployment); degrades to "no selection" where this reason is absent
|
||||||
|
// (e.g. a deployment without it, or the generic dev sample set).
|
||||||
|
const DEFAULT_REASON_DE = 'Temperprozess nach Ofen'
|
||||||
|
|
||||||
|
const op = ref<InstanceType<typeof Popover> | null>(null)
|
||||||
|
const count = ref<number | null>(null)
|
||||||
|
const reasonId = ref<number | null>(null)
|
||||||
|
const touched = ref(false)
|
||||||
|
|
||||||
|
const reasonOptions = computed(() =>
|
||||||
|
props.reasons.map((r) => ({ id: r.id, label: locale.value === 'en' ? r.reason_en : r.reason_de })),
|
||||||
|
)
|
||||||
|
|
||||||
|
function defaultReasonId(): number | null {
|
||||||
|
const want = DEFAULT_REASON_DE.trim().toLowerCase()
|
||||||
|
const hit = props.reasons.find((r) => (r.reason_de ?? '').trim().toLowerCase() === want)
|
||||||
|
return hit ? hit.id : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset + preselect whenever a new entry opens the popover. `entry` always goes
|
||||||
|
// null → entry on open (the page nulls it on close), so this fires every time.
|
||||||
|
watch(
|
||||||
|
() => props.entry,
|
||||||
|
(e) => {
|
||||||
|
if (e) {
|
||||||
|
count.value = null
|
||||||
|
reasonId.value = defaultReasonId()
|
||||||
|
touched.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const valid = computed(() => (count.value ?? 0) > 0 && reasonId.value !== null)
|
||||||
|
|
||||||
|
function onBook() {
|
||||||
|
touched.value = true
|
||||||
|
if (!valid.value || !props.entry) return
|
||||||
|
emit('submit', {
|
||||||
|
press: props.entry.press,
|
||||||
|
orderNo: props.entry.orderNo,
|
||||||
|
ttId: props.entry.ttId,
|
||||||
|
count: count.value as number,
|
||||||
|
reason: reasonId.value as number,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imperative handle for the page: toggle anchored to the click event, or hide.
|
||||||
|
function toggle(ev: Event) {
|
||||||
|
op.value?.toggle(ev)
|
||||||
|
}
|
||||||
|
function hide() {
|
||||||
|
op.value?.hide()
|
||||||
|
}
|
||||||
|
defineExpose({ toggle, hide })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Popover ref="op" :dismissable="!submitting" @hide="emit('close')">
|
||||||
|
<div v-if="entry" class="body">
|
||||||
|
<div class="head">
|
||||||
|
<span class="title">{{ t('dialog.title') }}</span>
|
||||||
|
<span class="for">{{ t('dialog.for', { cart: entry.cartId, order: entry.orderNo }) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="fld">
|
||||||
|
<span class="lab">{{ t('dialog.count') }} <em>({{ t('dialog.pieces') }})</em></span>
|
||||||
|
<InputNumber
|
||||||
|
v-model="count"
|
||||||
|
:min="1"
|
||||||
|
:max="entry.partsGood"
|
||||||
|
show-buttons
|
||||||
|
button-layout="horizontal"
|
||||||
|
:input-style="{ width: '4rem', textAlign: 'center' }"
|
||||||
|
incrementButtonIcon="pi pi-plus"
|
||||||
|
decrementButtonIcon="pi pi-minus"
|
||||||
|
/>
|
||||||
|
<span v-if="touched && (count ?? 0) <= 0" class="err">{{ t('dialog.needCount') }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="fld">
|
||||||
|
<span class="lab">{{ t('dialog.reason') }}</span>
|
||||||
|
<Select
|
||||||
|
v-model="reasonId"
|
||||||
|
:options="reasonOptions"
|
||||||
|
option-label="label"
|
||||||
|
option-value="id"
|
||||||
|
filter
|
||||||
|
auto-filter-focus
|
||||||
|
reset-filter-on-hide
|
||||||
|
:filter-placeholder="t('dialog.filterReason')"
|
||||||
|
:placeholder="t('dialog.chooseReason')"
|
||||||
|
/>
|
||||||
|
<span v-if="touched && reasonId === null" class="err">{{ t('dialog.needReason') }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<Button :label="t('dialog.cancel')" text :disabled="submitting" @click="hide" />
|
||||||
|
<Button
|
||||||
|
class="confirm"
|
||||||
|
:label="t('dialog.confirm')"
|
||||||
|
severity="danger"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="onBook"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
width: min(22rem, 86vw);
|
||||||
|
padding: 4px 2px;
|
||||||
|
}
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.for {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
}
|
||||||
|
.fld {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.lab {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.lab em {
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
.err {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--ausschuss);
|
||||||
|
}
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
/* Match the card's "Ausschuss buchen" button — burgundy, not PrimeVue's theme red. */
|
||||||
|
.confirm:deep(.p-button),
|
||||||
|
.confirm.p-button {
|
||||||
|
background: var(--ausschuss);
|
||||||
|
border-color: var(--ausschuss);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.confirm:deep(.p-button:not(:disabled):hover),
|
||||||
|
.confirm.p-button:not(:disabled):hover {
|
||||||
|
background: var(--ausschuss);
|
||||||
|
border-color: var(--ausschuss);
|
||||||
|
filter: brightness(1.12);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -19,6 +19,14 @@ const stations = computed(() => [
|
|||||||
{ key: 'ofen', label: t('belt.ofen'), value: props.flow.ofen, color: 'var(--oven)' },
|
{ key: 'ofen', label: t('belt.ofen'), value: props.flow.ofen, color: 'var(--oven)' },
|
||||||
{ key: 'fertig', label: t('belt.fertig'), value: props.flow.fertig, color: 'var(--done)' },
|
{ key: 'fertig', label: t('belt.fertig'), value: props.flow.fertig, color: 'var(--done)' },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// The oven→done link tracks the oven's own count only: once it's empty there's
|
||||||
|
// nothing left to hand off, regardless of how many carts already sit at fertig.
|
||||||
|
function isConvOn(i: number): boolean {
|
||||||
|
const cur = stations.value[i]
|
||||||
|
if (cur.key === 'ofen') return cur.value > 0
|
||||||
|
return cur.value > 0 || stations.value[i + 1].value > 0
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -28,7 +36,7 @@ const stations = computed(() => [
|
|||||||
<span class="badge num" :style="{ color: s.color, borderColor: s.color }">{{ s.value }}</span>
|
<span class="badge num" :style="{ color: s.color, borderColor: s.color }">{{ s.value }}</span>
|
||||||
<span class="lbl">{{ s.label }}</span>
|
<span class="lbl">{{ s.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="i < stations.length - 1" class="conv" :class="{ on: s.value > 0 || stations[i + 1].value > 0 }"></span>
|
<span v-if="i < stations.length - 1" class="conv" :class="{ on: isConvOn(i) }"></span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -7,13 +7,18 @@ const WINDOW_MINUTES: Record<WindowCode, number> = { '12h': 720, '1T': 1440, '7T
|
|||||||
|
|
||||||
const PRESSES = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9']
|
const PRESSES = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7', 'P8', 'P9']
|
||||||
|
|
||||||
|
// Includes the two "Temperprozess …" reasons so the dev/demo build exercises the
|
||||||
|
// popover's default-preselect ("Temperprozess nach Ofen") and the reason filter
|
||||||
|
// (typing "Ofen" narrows to both). Live reasons come from settings.reject_reasons.
|
||||||
export const SAMPLE_REASONS: Reason[] = [
|
export const SAMPLE_REASONS: Reason[] = [
|
||||||
{ id: 1, reason_de: 'Maßabweichung', reason_en: 'Dimensional deviation' },
|
{ id: 1, reason_de: 'Temperprozess vor Ofen', reason_en: 'Temper process before oven' },
|
||||||
{ id: 2, reason_de: 'Lunker', reason_en: 'Voids' },
|
{ id: 2, reason_de: 'Temperprozess nach Ofen', reason_en: 'Temper process after oven' },
|
||||||
{ id: 3, reason_de: 'Bindenaht', reason_en: 'Weld line' },
|
{ id: 3, reason_de: 'Maßabweichung', reason_en: 'Dimensional deviation' },
|
||||||
{ id: 4, reason_de: 'Verbrennung', reason_en: 'Burn mark' },
|
{ id: 4, reason_de: 'Lunker', reason_en: 'Voids' },
|
||||||
{ id: 5, reason_de: 'Verzug', reason_en: 'Warping' },
|
{ id: 5, reason_de: 'Bindenaht', reason_en: 'Weld line' },
|
||||||
{ id: 6, reason_de: 'Sonstiges', reason_en: 'Other' },
|
{ id: 6, reason_de: 'Verbrennung', reason_en: 'Burn mark' },
|
||||||
|
{ id: 7, reason_de: 'Verzug', reason_en: 'Warping' },
|
||||||
|
{ id: 8, reason_de: 'Sonstiges', reason_en: 'Other' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const flow = (notYetSent: number, wagen: number, unterwegs: number, ofen: number, fertig: number): FlowDistribution => ({
|
const flow = (notYetSent: number, wagen: number, unterwegs: number, ofen: number, fertig: number): FlowDistribution => ({
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default {
|
|||||||
demo: 'Demo-Daten',
|
demo: 'Demo-Daten',
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
title: 'Pressen',
|
title: 'Maschinen',
|
||||||
all: 'Alle',
|
all: 'Alle',
|
||||||
none: 'Keine',
|
none: 'Keine',
|
||||||
empty: 'keine Wagen',
|
empty: 'keine Wagen',
|
||||||
@ -44,6 +44,7 @@ export default {
|
|||||||
pieces: 'Stück',
|
pieces: 'Stück',
|
||||||
reason: 'Grund',
|
reason: 'Grund',
|
||||||
chooseReason: 'Grund wählen …',
|
chooseReason: 'Grund wählen …',
|
||||||
|
filterReason: 'Grund suchen …',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
confirm: 'Buchen',
|
confirm: 'Buchen',
|
||||||
needCount: 'Menge eingeben',
|
needCount: 'Menge eingeben',
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export default {
|
|||||||
pieces: 'pcs',
|
pieces: 'pcs',
|
||||||
reason: 'Reason',
|
reason: 'Reason',
|
||||||
chooseReason: 'Choose a reason …',
|
chooseReason: 'Choose a reason …',
|
||||||
|
filterReason: 'Search reason …',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
confirm: 'Book',
|
confirm: 'Book',
|
||||||
needCount: 'Enter a quantity',
|
needCount: 'Enter a quantity',
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
--ink: #e8edf2;
|
--ink: #e8edf2;
|
||||||
--ink-dim: #97a3b1;
|
--ink-dim: #97a3b1;
|
||||||
--ink-faint: #5a6573;
|
--ink-faint: #7f8b9a;
|
||||||
|
|
||||||
--press: #a78bfa; /* violet — Nicht gesendet */
|
--press: #a78bfa; /* violet — Nicht gesendet */
|
||||||
--way: #22d3ee; /* cyan — Wagen */
|
--way: #22d3ee; /* cyan — Wagen */
|
||||||
@ -36,7 +36,7 @@
|
|||||||
--oven: #f97316; /* orange — Ofen */
|
--oven: #f97316; /* orange — Ofen */
|
||||||
--hot: #ef4444; /* red — Ofen (peak) */
|
--hot: #ef4444; /* red — Ofen (peak) */
|
||||||
--done: #34d399; /* emerald — Fertig */
|
--done: #34d399; /* emerald — Fertig */
|
||||||
--ausschuss: #c4162a; /* dark-red — Ausschuss */
|
--ausschuss: #983048; /* burgundy — Ausschuss */
|
||||||
|
|
||||||
--o1: #5794f2;
|
--o1: #5794f2;
|
||||||
--o2: #e8825a;
|
--o2: #e8825a;
|
||||||
@ -59,7 +59,7 @@ html:not(.dark) {
|
|||||||
|
|
||||||
--ink: #0f172a;
|
--ink: #0f172a;
|
||||||
--ink-dim: #475569;
|
--ink-dim: #475569;
|
||||||
--ink-faint: #64748b;
|
--ink-faint: #4b586b;
|
||||||
|
|
||||||
--press: #6d28d9;
|
--press: #6d28d9;
|
||||||
--way: #0e7490;
|
--way: #0e7490;
|
||||||
@ -67,7 +67,7 @@ html:not(.dark) {
|
|||||||
--oven: #c2410c;
|
--oven: #c2410c;
|
||||||
--hot: #dc2626;
|
--hot: #dc2626;
|
||||||
--done: #15803d;
|
--done: #15803d;
|
||||||
--ausschuss: #a11223;
|
--ausschuss: #86243c;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
RejectStationPage — the central Ausschuss-Station terminal (design LOCKED):
|
RejectStationPage — the central Ausschuss-Station terminal (design LOCKED):
|
||||||
left = press filter checkboxes (default all) + time-window picker in the header;
|
left = press filter checkboxes (default all) + time-window picker in the header;
|
||||||
right = tempered carts (one row per ttId, newest first), each with the style-C
|
right = tempered carts (one row per ttId, newest first), each with the style-C
|
||||||
5-station temper belt and an "Ausschuss buchen" action → booking dialog.
|
5-station temper belt and an "Ausschuss buchen" action → booking popover
|
||||||
|
(a tooltip-style panel anchored to the button, not a centered modal).
|
||||||
|
|
||||||
Login-free by design. Data from the store (polled); no-backend falls back to
|
Login-free by design. Data from the store (polled); no-backend falls back to
|
||||||
bundled sample data so `npm run dev` renders + the booking flow is demoable.
|
bundled sample data so `npm run dev` renders + the booking flow is demoable.
|
||||||
@ -14,7 +15,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import StationHeader from '@/components/StationHeader.vue'
|
import StationHeader from '@/components/StationHeader.vue'
|
||||||
import PressFilter from '@/components/PressFilter.vue'
|
import PressFilter from '@/components/PressFilter.vue'
|
||||||
import RejectEntryCard from '@/components/RejectEntryCard.vue'
|
import RejectEntryCard from '@/components/RejectEntryCard.vue'
|
||||||
import RejectDialog from '@/components/RejectDialog.vue'
|
import RejectPopover from '@/components/RejectPopover.vue'
|
||||||
import { useTemperRejectsStore } from '@/stores/temperRejects'
|
import { useTemperRejectsStore } from '@/stores/temperRejects'
|
||||||
import type { RejectEntry, RejectRequest } from '@/types/station'
|
import type { RejectEntry, RejectRequest } from '@/types/station'
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ const { t } = useI18n()
|
|||||||
const store = useTemperRejectsStore()
|
const store = useTemperRejectsStore()
|
||||||
const { window, reasons, allPresses, checkedPresses, pressCounts, filteredEntries, usingSample } = storeToRefs(store)
|
const { window, reasons, allPresses, checkedPresses, pressCounts, filteredEntries, usingSample } = storeToRefs(store)
|
||||||
|
|
||||||
|
const pop = ref<InstanceType<typeof RejectPopover> | null>(null)
|
||||||
const bookingEntry = ref<RejectEntry | null>(null)
|
const bookingEntry = ref<RejectEntry | null>(null)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const toast = ref<string | null>(null)
|
const toast = ref<string | null>(null)
|
||||||
@ -35,15 +37,33 @@ function flashToast(msg: string, ok = true) {
|
|||||||
toastTimer = setTimeout(() => (toast.value = null), 3200)
|
toastTimer = setTimeout(() => (toast.value = null), 3200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open the booking popover anchored to the card's button that was clicked. `ev` is
|
||||||
|
// the native click event forwarded by the card; `toggle` reads its target so the
|
||||||
|
// panel points at that button.
|
||||||
|
//
|
||||||
|
// If a popover is already open, this tap only dismisses it (PrimeVue's toggle hides
|
||||||
|
// on the same trigger, and an outside-click closes it too). We deliberately do NOT
|
||||||
|
// arm the new entry in that case — otherwise its data would flash into the closing
|
||||||
|
// panel; a second tap opens the next card. A fresh tap anchors a new panel and, via
|
||||||
|
// the RejectPopover watcher, resets the form + preselects "Temperprozess nach Ofen".
|
||||||
|
function onBook(ev: Event, entry: RejectEntry) {
|
||||||
|
if (bookingEntry.value) {
|
||||||
|
pop.value?.hide()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bookingEntry.value = entry
|
||||||
|
pop.value?.toggle(ev)
|
||||||
|
}
|
||||||
|
|
||||||
async function onSubmit(req: RejectRequest) {
|
async function onSubmit(req: RejectRequest) {
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
const res = await store.submitReject(req)
|
const res = await store.submitReject(req)
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
bookingEntry.value = null
|
pop.value?.hide() // → @hide → bookingEntry = null
|
||||||
flashToast(res.message || t('dialog.success'), true)
|
flashToast(res.message || t('dialog.success'), true)
|
||||||
} else {
|
} else {
|
||||||
// keep the dialog open so the operator can retry a booking that did NOT go through
|
// keep the popover open so the operator can retry a booking that did NOT go through
|
||||||
flashToast(res.message || t('dialog.failed'), false)
|
flashToast(res.message || t('dialog.failed'), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,12 +96,12 @@ onUnmounted(() => {
|
|||||||
<div v-if="checkedPresses.length === 0" class="state">{{ t('empty.noPress') }}</div>
|
<div v-if="checkedPresses.length === 0" class="state">{{ t('empty.noPress') }}</div>
|
||||||
<div v-else-if="filteredEntries.length === 0" class="state">{{ t('empty.noEntries') }}</div>
|
<div v-else-if="filteredEntries.length === 0" class="state">{{ t('empty.noEntries') }}</div>
|
||||||
<div v-else class="list">
|
<div v-else class="list">
|
||||||
<RejectEntryCard v-for="e in filteredEntries" :key="e.ttId" :entry="e" @book="bookingEntry = $event" />
|
<RejectEntryCard v-for="e in filteredEntries" :key="e.ttId" :entry="e" @book="onBook($event, e)" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RejectDialog :entry="bookingEntry" :reasons="reasons" :submitting="submitting" @close="bookingEntry = null" @submit="onSubmit" />
|
<RejectPopover ref="pop" :entry="bookingEntry" :reasons="reasons" :submitting="submitting" @close="bookingEntry = null" @submit="onSubmit" />
|
||||||
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="toast" class="toast" :class="{ err: !toastOk }">{{ toast }}</div>
|
<div v-if="toast" class="toast" :class="{ err: !toastOk }">{{ toast }}</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user