Compare commits

..

6 Commits

Author SHA1 Message Date
seb
596d0dc6f1 u 2026-10-18 18:40:25 +02:00
seb
c15fa0a53e u 2026-10-18 18:38:38 +02:00
seb
9cb93bddeb u 2026-10-18 17:47:41 +02:00
seb
b42b63a0af u 2026-10-18 17:47:37 +02:00
seb
cf85ec102d u 2026-10-18 16:10:18 +02:00
seb
5644678b7d u 2026-10-18 15:35:58 +02:00
18 changed files with 181 additions and 41 deletions

View File

@@ -35,6 +35,7 @@ Reference guide for the backend LLM agent when emitting route navigation actions
| **Order Detail** | `/auftrag/bestellung/:orderId` | Order document ID: `/auftrag/bestellung/${orderId}` | `/auftrag/bestellung/2005` | | **Order Detail** | `/auftrag/bestellung/:orderId` | Order document ID: `/auftrag/bestellung/${orderId}` | `/auftrag/bestellung/2005` |
| **Shipments (Ausliefern)** | `/auftrag/ausliefern` | Static (shipment manifests & parcels) | `/auftrag/ausliefern` | | **Shipments (Ausliefern)** | `/auftrag/ausliefern` | Static (shipment manifests & parcels) | `/auftrag/ausliefern` |
| **Payments (Zahlungen)** | `/auftrag/zahlungen` | Static (order payments ledger) | `/auftrag/zahlungen` | | **Payments (Zahlungen)** | `/auftrag/zahlungen` | Static (order payments ledger) | `/auftrag/zahlungen` |
| **Carriers (Versandarten)** | `/auftrag/versandarten` | Static (Versanddienstleister & Versandarten) | `/auftrag/versandarten` |
| **Customer List** | `/kunde` | Static | `/kunde` | | **Customer List** | `/kunde` | Static | `/kunde` |
| **New Customer** | `/kunde/neu` | Static form | `/kunde/neu` | | **New Customer** | `/kunde/neu` | Static form | `/kunde/neu` |

View File

@@ -1,8 +1,9 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="de" class="notranslate" translate="no">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="google" content="notranslate" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>QuixPOS * Management Interface</title> <title>QuixPOS * Management Interface</title>

View File

@@ -1,7 +1,7 @@
import { http } from './http.js'; import { http } from './http.js';
/** /**
* Company-scoped carriers / Lieferarten. * Company-scoped carriers / Versandarten.
* Shape: Carrier = { id, name, sort, hasPackages, isReverse } * Shape: Carrier = { id, name, sort, hasPackages, isReverse }
*/ */
export const carrier = { export const carrier = {

View File

@@ -23,7 +23,7 @@ import AuftragBestellungNeuPage from './routes/auftrag/bestellung/neu';
import AuftragBestellungEditPage from './routes/auftrag/bestellung/_orderId'; import AuftragBestellungEditPage from './routes/auftrag/bestellung/_orderId';
import AuftragAusliefernPage from './routes/auftrag/ausliefern'; import AuftragAusliefernPage from './routes/auftrag/ausliefern';
import AuftragZahlungenPage from './routes/auftrag/zahlungen'; import AuftragZahlungenPage from './routes/auftrag/zahlungen';
import AuftragLieferartenPage from './routes/auftrag/lieferarten'; import AuftragVersandartenPage from './routes/auftrag/versandarten';
import KundeListePage from './routes/kunde'; import KundeListePage from './routes/kunde';
import KundeNeuPage from './routes/kunde/neu'; import KundeNeuPage from './routes/kunde/neu';
@@ -107,7 +107,8 @@ const routes = [
{ path: 'bestellung/:orderId', element: <AuftragBestellungEditPage /> }, { path: 'bestellung/:orderId', element: <AuftragBestellungEditPage /> },
{ path: 'ausliefern', element: <AuftragAusliefernPage /> }, { path: 'ausliefern', element: <AuftragAusliefernPage /> },
{ path: 'zahlungen', element: <AuftragZahlungenPage /> }, { path: 'zahlungen', element: <AuftragZahlungenPage /> },
{ path: 'lieferarten', element: <AuftragLieferartenPage /> }, { path: 'versandarten', element: <AuftragVersandartenPage /> },
{ path: 'lieferarten', element: <Navigate to="/auftrag/versandarten" replace /> },
{ path: ':quoteId', element: <AuftragAngebotEditPage /> }, { path: ':quoteId', element: <AuftragAngebotEditPage /> },
], ],

View File

@@ -1,6 +1,6 @@
/** /**
* @file carrierAutomat.js * @file carrierAutomat.js
* @description Manages cached shipping carriers / Lieferarten (`GET /api/carrier`) for the active company. * @description Manages cached shipping carriers / Versandarten (`GET /api/carrier`) for the active company.
* *
* Upstream Triggers: * Upstream Triggers:
* - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers * - `authAutomat`: Subscribes to auth state changes (`syncWithAuth`) — triggers

View File

@@ -14,6 +14,7 @@
import { Automat } from 'react-automat'; import { Automat } from 'react-automat';
import { article as articleApi, supplier as supplierApi } from '../api'; import { article as articleApi, supplier as supplierApi } from '../api';
import { todayInputValue } from '../components/manifest/manifestConfig';
/** /**
* @typedef {Object} DeliveryLine * @typedef {Object} DeliveryLine
@@ -155,7 +156,7 @@ export function buildSupplierArticlesMap(articles = [], existingMap = {}) {
export const initialDelivery = { export const initialDelivery = {
id: null, id: null,
deliveryId: null, deliveryId: null,
date: null, date: todayInputValue(),
deliveryEntity: null, deliveryEntity: null,
partyId: null, partyId: null,
externalNumber: null, externalNumber: null,
@@ -226,12 +227,14 @@ export function createDeliveryEditorAutomat(options = {}) {
: delivery.notes ? [delivery.notes] : []; : delivery.notes ? [delivery.notes] : [];
const rawAttachments = delivery.attachments ?? delivery.files ?? []; const rawAttachments = delivery.attachments ?? delivery.files ?? [];
const attachments = Array.isArray(rawAttachments) ? rawAttachments : []; const attachments = Array.isArray(rawAttachments) ? rawAttachments : [];
const date = delivery.date !== undefined ? delivery.date : (id != null ? null : (automat.state.date || todayInputValue()));
automat.setState({ automat.setState({
...initialDelivery, ...initialDelivery,
...delivery, ...delivery,
id, id,
deliveryId: id, deliveryId: id,
date,
partyId, partyId,
deliveryEntity: partyId, deliveryEntity: partyId,
externalNumber: extNum, externalNumber: extNum,
@@ -670,6 +673,7 @@ export function createDeliveryEditorAutomat(options = {}) {
resetDelivery() { resetDelivery() {
automat.setState({ automat.setState({
...initialDelivery, ...initialDelivery,
date: todayInputValue(),
}); });
}, },
@@ -677,6 +681,7 @@ export function createDeliveryEditorAutomat(options = {}) {
resetDeliveryEditor() { resetDeliveryEditor() {
automat.setState({ automat.setState({
...initialState, ...initialState,
date: todayInputValue(),
}); });
}, },
@@ -718,7 +723,7 @@ export function createDeliveryEditorAutomat(options = {}) {
basedata.invoiceNumber ?? basedata.invoiceNumber ??
basedata.invoice_number ?? basedata.invoice_number ??
null; null;
const date = basedata.date ?? null; const date = basedata.date ?? automat.state.date ?? todayInputValue();
const type = basedata.manifestType ?? basedata.type ?? 'rechnung'; const type = basedata.manifestType ?? basedata.type ?? 'rechnung';
let matchedPartyId = partyId != null && partyId !== '' ? partyId : null; let matchedPartyId = partyId != null && partyId !== '' ? partyId : null;

View File

@@ -26,7 +26,7 @@ export function createInitialManifestDraftState(kind) {
kind, kind,
mode: 'create', mode: 'create',
id: null, id: null,
date: kind === 'quote' ? todayInputValue() : '', date: todayInputValue(),
partyId: null, partyId: null,
number: '', number: '',
externalNumber: '', externalNumber: '',

View File

@@ -17,6 +17,7 @@ import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined
import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import GoogleIcon from '@mui/icons-material/Google';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import AppDialog from './AppDialog'; import AppDialog from './AppDialog';
import { thumb, article as articleApi } from '../api'; import { thumb, article as articleApi } from '../api';
@@ -29,6 +30,7 @@ import {
articleWithPhotoOrder, articleWithPhotoOrder,
articleWithUploadedPhoto, articleWithUploadedPhoto,
articleWithoutPhoto, articleWithoutPhoto,
buildGoogleSearchQuery,
formatPhotoDimensions, formatPhotoDimensions,
formatPhotoSizeKb, formatPhotoSizeKb,
sortedPhotos, sortedPhotos,
@@ -182,6 +184,22 @@ function resolveTitle(props, single) {
} }
return single ? 'Bild' : 'Bilder'; return single ? 'Bild' : 'Bilder';
} }
function resolveGoogleImagesQuery(props) {
if (props.article) {
return buildGoogleSearchQuery(props.article);
}
if (props.brand?.name) {
return String(props.brand.name).trim();
}
if (props.category?.name) {
return String(props.category.name).trim();
}
if (props.attributeValue) {
return String(props.attributeValue.value || props.attributeValue.name || '').trim();
}
return '';
}
// #endregion // #endregion
/** /**
@@ -400,6 +418,13 @@ export default class PicturesDialog extends PureComponent {
handleSearchClose = () => { handleSearchClose = () => {
this.setState({ searchOpen: false }); this.setState({ searchOpen: false });
}; };
handleOpenGoogleImages = () => {
const query = resolveGoogleImagesQuery(this.props);
if (!query) return;
const url = `https://www.google.com/search?tbm=isch&q=${encodeURIComponent(query)}`;
window.open(url, '_blank', 'noopener,noreferrer');
};
// #endregion // #endregion
// #region render helpers // #region render helpers
@@ -627,6 +652,7 @@ export default class PicturesDialog extends PureComponent {
const pics = resolvePictures(this.props, single); const pics = resolvePictures(this.props, single);
const title = resolveTitle(this.props, single); const title = resolveTitle(this.props, single);
const hasPic = Boolean(pics[0]?.fullHash || pics[0]?.thumbHash); const hasPic = Boolean(pics[0]?.fullHash || pics[0]?.thumbHash);
const googleQuery = resolveGoogleImagesQuery(this.props);
return ( return (
<> <>
@@ -781,6 +807,16 @@ export default class PicturesDialog extends PureComponent {
> >
Bild suchen Bild suchen
</Button> </Button>
<Button
size="small"
variant="outlined"
startIcon={<GoogleIcon />}
disabled={busy || disabled || !googleQuery}
onClick={this.handleOpenGoogleImages}
title={googleQuery ? `Google Bilder-Suche: ${googleQuery}` : 'Google'}
>
Google
</Button>
</Box> </Box>
<Button <Button
size="small" size="small"

View File

@@ -244,7 +244,6 @@ class SearchResultRow extends PureComponent {
</Box> </Box>
</> </>
)} )}
{article.available != null && ` · Verfügbar ${formatStock(article.available)}`}
{primaryEan ? ` · EAN ${primaryEan}` : ''} {primaryEan ? ` · EAN ${primaryEan}` : ''}
</Typography> </Typography>
} }

View File

@@ -5,14 +5,24 @@ import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip'; import Tooltip from '@mui/material/Tooltip';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import CallMergeIcon from '@mui/icons-material/CallMerge'; import CallMergeIcon from '@mui/icons-material/CallMerge';
import GoogleIcon from '@mui/icons-material/Google';
import { enqueueSnackbar } from 'notistack'; import { enqueueSnackbar } from 'notistack';
import { article as articleApi } from '../../../api'; import { article as articleApi } from '../../../api';
import { artikelAutomat, getArticle } from '../../../automats'; import { artikelAutomat, getArticle } from '../../../automats';
import { notifyError } from '../../../utils/notify'; import { notifyError } from '../../../utils/notify';
import {
articleEans,
getArticleEan,
getArticleBrand,
getArticleName,
buildGoogleSearchQuery,
} from '../../../utils/articleDetail';
import ArticleSearch from '../ArticleSearch'; import ArticleSearch from '../ArticleSearch';
import ArticleUnifyDialog from '../ArticleUnifyDialog'; import ArticleUnifyDialog from '../ArticleUnifyDialog';
export { getArticleEan, getArticleBrand, getArticleName, buildGoogleSearchQuery };
export function isArticleFather(article) { export function isArticleFather(article) {
if (!article) return false; if (!article) return false;
return Boolean( return Boolean(
@@ -48,6 +58,15 @@ export default class ArticleSettingsTab extends Component {
this.handleDeleteArticle = this.handleDeleteArticle.bind(this); this.handleDeleteArticle = this.handleDeleteArticle.bind(this);
this.handleOpenSearch = this.handleOpenSearch.bind(this); this.handleOpenSearch = this.handleOpenSearch.bind(this);
this.handleSelectTargetArticle = this.handleSelectTargetArticle.bind(this); this.handleSelectTargetArticle = this.handleSelectTargetArticle.bind(this);
this.handleOpenGoogle = this.handleOpenGoogle.bind(this);
}
handleOpenGoogle() {
const { article: articleData } = this.props;
const query = buildGoogleSearchQuery(articleData);
if (!query) return;
const url = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
window.open(url, '_blank', 'noopener,noreferrer');
} }
async handleDeleteArticle() { async handleDeleteArticle() {
@@ -128,6 +147,7 @@ export default class ArticleSettingsTab extends Component {
: isStueckliste : isStueckliste
? 'Ein Stücklisten-Artikel kann nicht vereint werden.' ? 'Ein Stücklisten-Artikel kann nicht vereint werden.'
: null; : null;
const googleQuery = buildGoogleSearchQuery(articleData);
return ( return (
<Box> <Box>
@@ -169,6 +189,18 @@ export default class ArticleSettingsTab extends Component {
Artikel vereinen Artikel vereinen
</Button> </Button>
)} )}
<Button
type="button"
color="primary"
variant="outlined"
startIcon={<GoogleIcon />}
disabled={busy || !googleQuery}
onClick={this.handleOpenGoogle}
title={googleQuery ? `Google-Suche: ${googleQuery}` : 'Google'}
>
Google
</Button>
</Stack> </Stack>
<ArticleSearch <ArticleSearch

View File

@@ -56,7 +56,7 @@ class CarrierItem extends PureComponent {
async updateCarrier(item, patch) { async updateCarrier(item, patch) {
try { try {
await carrierAutomat.actions.update(item.id, patch); await carrierAutomat.actions.update(item.id, patch);
enqueueSnackbar('Lieferart gespeichert.', { variant: 'success' }); enqueueSnackbar('Versandart gespeichert.', { variant: 'success' });
} catch (err) { } catch (err) {
this.setState({ draft: item.name ?? '' }); this.setState({ draft: item.name ?? '' });
notifyError(err); notifyError(err);
@@ -93,11 +93,11 @@ class CarrierItem extends PureComponent {
handleDelete = () => { handleDelete = () => {
const item = this.props.carrier ?? this.props.item; const item = this.props.carrier ?? this.props.item;
if (!item) return; if (!item) return;
if (!window.confirm(`Lieferart „${item.name}“ wirklich löschen?`)) return; if (!window.confirm(`Versandart „${item.name}“ wirklich löschen?`)) return;
void (async () => { void (async () => {
try { try {
await carrierAutomat.actions.remove(item.id); await carrierAutomat.actions.remove(item.id);
enqueueSnackbar('Lieferart gelöscht.', { variant: 'success' }); enqueueSnackbar('Versandart gelöscht.', { variant: 'success' });
} catch (err) { } catch (err) {
notifyError(err); notifyError(err);
} }

View File

@@ -62,11 +62,10 @@ class ManifestForm extends PureComponent {
const initialDate = const initialDate =
initialDoc?.date || initialDoc?.date ||
(props.mode === 'create' && props.kind === 'quote' ? todayInputValue() : ''); (props.mode === 'create' ? todayInputValue() : '');
if ( if (
props.mode === 'create' && props.mode === 'create' &&
props.kind === 'quote' &&
this.draftAutomat && this.draftAutomat &&
!this.draftAutomat.state.date !this.draftAutomat.state.date
) { ) {

View File

@@ -118,14 +118,14 @@ class AddPackageDialog extends PureComponent {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}>
<TextField <TextField
select select
label="Lieferart (Carrier)" label="Versandart (Carrier)"
size="small" size="small"
value={carrierId} value={carrierId}
onChange={(e) => this.setState({ carrierId: e.target.value })} onChange={(e) => this.setState({ carrierId: e.target.value })}
error={carriers.length === 0} error={carriers.length === 0}
helperText={ helperText={
carriers.length === 0 carriers.length === 0
? 'Keine Lieferarten eingerichtet (unter Auftrag → Lieferarten anlegen)' ? 'Keine Versandarten eingerichtet (unter Auftrag → Versandarten anlegen)'
: undefined : undefined
} }
> >

View File

@@ -229,11 +229,11 @@ export const SUB_NAV = {
help: 'Zahlungseingänge erfassen und Auftragszahlungen überwachen', help: 'Zahlungseingänge erfassen und Auftragszahlungen überwachen',
}, },
{ {
segment: 'lieferarten', segment: 'versandarten',
label: 'Lieferarten', label: 'Versandarten',
title: 'Lieferarten', title: 'Versandarten',
icon: LocalShippingOutlinedIcon, icon: LocalShippingOutlinedIcon,
help: 'Versanddienstleister und Lieferarten (Carriers) verwalten', help: 'Versanddienstleister und Versandarten (Carriers) verwalten',
}, },
], ],
'/kunde': [ '/kunde': [

View File

@@ -202,8 +202,11 @@ class ResultRow extends Component {
height: '20px', height: '20px',
}} }}
> >
{(row.brand?.name || row.brandName) && ( {(row.brand?.name || row.brandName) ? (
<> <Box
component="span"
sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}
>
<BrandLabel <BrandLabel
brand={row.brand || { name: row.brandName, thumbHash: row.brandThumbHash }} brand={row.brand || { name: row.brandName, thumbHash: row.brandThumbHash }}
name={row.brand?.name || row.brandName} name={row.brand?.name || row.brandName}
@@ -215,13 +218,18 @@ class ResultRow extends Component {
noWrap: true, noWrap: true,
}} }}
/> />
{' · '} <span>·</span>
</> </Box>
)} ) : null}
{row.sku} <Box component="span" sx={{ flexShrink: 0 }}>
{' · '} {row.sku}
</Box>
<Box component="span" sx={{ flexShrink: 0 }}>
·
</Box>
<Box <Box
component="span" component="span"
sx={{ flexShrink: 0 }}
title={ title={
row.warehouses?.length > 0 row.warehouses?.length > 0
? row.warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ') ? row.warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
@@ -230,11 +238,14 @@ class ResultRow extends Component {
> >
Bestand {formatStock(row.stock)} Bestand {formatStock(row.stock)}
</Box> </Box>
{' · '} {row.categories ? (
Verfügbar {formatStock(row.available)} <Box
{row.categories component="span"
? ` · ${formatCategoryList(row.categories, categoryFacets ?? {})}` sx={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
: ''} >
{` · ${formatCategoryList(row.categories, categoryFacets ?? {})}`}
</Box>
) : null}
</Typography> </Typography>
} }
slotProps={{ primary: { fontWeight: 500, noWrap: true } }} slotProps={{ primary: { fontWeight: 500, noWrap: true } }}
@@ -1028,7 +1039,7 @@ class ArtikelSearchPage extends Component {
<PanelTile minHeight={0} sx={{ pb: 0 }}> <PanelTile minHeight={0} sx={{ pb: 0 }}>
<FillSpace <FillSpace
ref={this.fillSpaceRef} ref={this.fillSpaceRef}
aboveRef={[this.filterHeaderRef]} aboveRef={this.filterHeaderRef}
items={rows} items={rows}
renderItem={ResultRow} renderItem={ResultRow}
reserveBottom={32} reserveBottom={32}

View File

@@ -16,7 +16,7 @@ import carrierAutomat from '../../automats/carrierAutomat';
import { CarrierItem } from '../../components/carrier'; import { CarrierItem } from '../../components/carrier';
import { GREEN_PANEL } from '../../colors'; import { GREEN_PANEL } from '../../colors';
class LieferartenSearchField extends PureComponent { class VersandartenSearchField extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
const initial = carrierAutomat.state.filter || ''; const initial = carrierAutomat.state.filter || '';
@@ -53,7 +53,7 @@ class LieferartenSearchField extends PureComponent {
hasPackages: true, hasPackages: true,
isReverse: false, isReverse: false,
}); });
enqueueSnackbar(`Lieferart „${row.name || name}“ angelegt.`, { variant: 'success' }); enqueueSnackbar(`Versandart „${row.name || name}“ angelegt.`, { variant: 'success' });
this.setState({ input: '', submitting: false }); this.setState({ input: '', submitting: false });
carrierAutomat.actions.filter(''); carrierAutomat.actions.filter('');
} catch (err) { } catch (err) {
@@ -89,7 +89,7 @@ class LieferartenSearchField extends PureComponent {
size="small" size="small"
search search
clear clear
placeholder="Lieferart suchen oder neu anlegen..." placeholder="Versandart suchen oder neu anlegen..."
value={input} value={input}
disabled={submitting} disabled={submitting}
onChange={(next) => this.onInputChange(next)} onChange={(next) => this.onInputChange(next)}
@@ -114,7 +114,7 @@ class LieferartenSearchField extends PureComponent {
} }
} }
class AuftragLieferartenPage extends PureComponent { class AuftragVersandartenPage extends PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
const initialCarriers = const initialCarriers =
@@ -240,7 +240,7 @@ class AuftragLieferartenPage extends PureComponent {
return ( return (
<> <>
<LieferartenSearchField inputRef={this.searchRef} /> <VersandartenSearchField inputRef={this.searchRef} />
<PanelTileGrid> <PanelTileGrid>
<PanelTile wide={2} minHeight={0} color={GREEN_PANEL}> <PanelTile wide={2} minHeight={0} color={GREEN_PANEL}>
<List dense disablePadding ref={this.listRef} sx={{ width: '100%' }}> <List dense disablePadding ref={this.listRef} sx={{ width: '100%' }}>
@@ -248,7 +248,7 @@ class AuftragLieferartenPage extends PureComponent {
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}> <Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
{trimmedFilter {trimmedFilter
? `Keine Treffer für „${trimmedFilter}“.` ? `Keine Treffer für „${trimmedFilter}“.`
: 'Noch keine Lieferarten angelegt.'} : 'Noch keine Versandarten angelegt.'}
</Typography> </Typography>
) : ( ) : (
carriers.map((c) => ( carriers.map((c) => (
@@ -266,5 +266,5 @@ class AuftragLieferartenPage extends PureComponent {
} }
} }
export { CarrierItem, LieferartenSearchField }; export { CarrierItem, VersandartenSearchField };
export default withNavigate(AuftragLieferartenPage); export default withNavigate(AuftragVersandartenPage);

View File

@@ -28,6 +28,7 @@ import {
buildManifestBody, buildManifestBody,
restoreAttachmentsFromApi, restoreAttachmentsFromApi,
dateInputValue, dateInputValue,
todayInputValue,
emptyLine, emptyLine,
lineFromApi, lineFromApi,
manifestConfig, manifestConfig,
@@ -107,8 +108,13 @@ class LieferungNeuPage extends PureComponent {
super(props); super(props);
this.cfg = manifestConfig('delivery'); this.cfg = manifestConfig('delivery');
this.automat = getDeliveryEditorAutomat(props.docId); this.automat = getDeliveryEditorAutomat(props.docId);
const initialDate = this.automat.state.date || (props.docId == null ? todayInputValue() : null);
if (props.docId == null && !this.automat.state.date) {
this.automat.actions.setDate(initialDate);
}
this.state = { this.state = {
...this.automat.state, ...this.automat.state,
date: initialDate,
status: props.docId != null ? 'loading' : 'loadingParties', status: props.docId != null ? 'loading' : 'loadingParties',
error: null, error: null,
partiesLoading: true, partiesLoading: true,
@@ -199,6 +205,9 @@ class LieferungNeuPage extends PureComponent {
if (this.props.docId != null) { if (this.props.docId != null) {
void this.loadEdit(); void this.loadEdit();
} else { } else {
if (!this.automat.state.date) {
this.automat.actions.setDate(todayInputValue());
}
void this.loadPartiesOnly(); void this.loadPartiesOnly();
if (this.props.initialDelivery) { if (this.props.initialDelivery) {
this.automat.actions.setDelivery(this.props.initialDelivery); this.automat.actions.setDelivery(this.props.initialDelivery);

View File

@@ -6,6 +6,7 @@ import {
netFromGross, netFromGross,
parsePriceInput, parsePriceInput,
} from './price'; } from './price';
import brandAutomat from '../automats/brandAutomat';
export function sortedPhotos(article) { export function sortedPhotos(article) {
if (!Array.isArray(article?.photos)) return []; if (!Array.isArray(article?.photos)) return [];
@@ -150,6 +151,51 @@ export function articleEans(article) {
return article.eans.filter(Boolean); return article.eans.filter(Boolean);
} }
export function getArticleEan(article) {
if (!article) return '';
const eans = articleEans(article);
if (eans.length > 0) {
const first = eans[0];
return String(typeof first === 'object' && first ? (first.ean ?? '') : first ?? '').trim();
}
if (article.ean) {
return String(typeof article.ean === 'object' && article.ean ? (article.ean.ean ?? '') : article.ean).trim();
}
return '';
}
export function getArticleBrand(article) {
if (!article) return '';
if (typeof article.brand === 'object' && article.brand?.name) {
return String(article.brand.name).trim();
}
if (typeof article.brand === 'string' && article.brand.trim()) {
return article.brand.trim();
}
if (article.brandName) {
return String(article.brandName).trim();
}
if (article.brandId != null) {
const brands = brandAutomat?.state?.brands || [];
const b = brands.find((x) => x.id === article.brandId);
if (b?.name) return String(b.name).trim();
}
return '';
}
export function getArticleName(article) {
if (!article) return '';
return String(article.name ?? article.sku ?? '').trim();
}
export function buildGoogleSearchQuery(article) {
if (!article) return '';
const ean = getArticleEan(article);
const brand = getArticleBrand(article);
const name = getArticleName(article);
return [ean, brand, name].filter(Boolean).join(' ');
}
export function formatPhotoSizeKb(fullSize) { export function formatPhotoSizeKb(fullSize) {
return `${Math.round(fullSize / 1024)} KB`; return `${Math.round(fullSize / 1024)} KB`;
} }