u
This commit is contained in:
@@ -4,6 +4,130 @@ description: >-
|
|||||||
Reference documentation for backend service.
|
Reference documentation for backend service.
|
||||||
Use this skill whenever working with backend APIs.
|
Use this skill whenever working with backend APIs.
|
||||||
---
|
---
|
||||||
|
# we should cache in client (at least) : company,language,tax,unit,unittext,customergroup,warehouse,account,brand,category,categorytext,attributename,attributevalue,propertyname,companysetting,printer,labeltemplate,posterminal,posterminalcategory
|
||||||
|
|
||||||
|
todos:
|
||||||
|
|
||||||
|
1. Replace 13 List GETs with 1 /bootstrap Endpoint
|
||||||
|
Because these reference tables are cached in memory on login/company-switch and invalidated via GET /events SSE, all individual list endpoints can be merged into a single GET /bootstrap (or folded into GET /auth/me / PUT /auth/selected-company):
|
||||||
|
|
||||||
|
GET /account
|
||||||
|
|
||||||
|
GET /attributename (already returns nested values: T28[] in T16)
|
||||||
|
|
||||||
|
GET /brand (unless you have thousands of brands requiring server pagination)
|
||||||
|
|
||||||
|
GET /carrier
|
||||||
|
|
||||||
|
GET /category/tree
|
||||||
|
|
||||||
|
GET /company & GET /company/setting
|
||||||
|
|
||||||
|
GET /customergroup
|
||||||
|
|
||||||
|
GET /labeltemplate (metadata list without the heavy content.objects array)
|
||||||
|
|
||||||
|
GET /posterminal
|
||||||
|
|
||||||
|
GET /printer
|
||||||
|
|
||||||
|
GET /propertyname
|
||||||
|
|
||||||
|
GET /supplier
|
||||||
|
|
||||||
|
GET /unit
|
||||||
|
|
||||||
|
GET /warehouse
|
||||||
|
|
||||||
|
|
||||||
|
Drop 6 Redundant Single-Item & Child GET Endpoints
|
||||||
|
Once the parent lists above are in the client cache, looking up a single item by ID or key over the network is dead weight:
|
||||||
|
|
||||||
|
Endpoint to Remove,Why It's Redundant with Client Cache
|
||||||
|
GET /attributename/{attributeId}/value,"GET /attributename returns T16[], which already embeds values: T28[]."
|
||||||
|
GET /carrier/{id},Already in cached carriers: T31[].
|
||||||
|
GET /company/setting/{key},Already in cached settings map.
|
||||||
|
GET /printer/{id},Already in cached printers: T22[].
|
||||||
|
GET /warehouse/{id},Already in cached warehouses list.
|
||||||
|
GET /order/{id}/paymentGET /order/{id}/shipmentGET /order/{id}/shipping,"GET /order/{id} returns T1, which already embeds payments: T30[], shipments: T3[], and shippings: T19[]."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Consolidate Micro-Mutation Endpoints (~20 Routes Saved)
|
||||||
|
Several resources expose separate endpoints for single-column updates or duplicate HTTP verbs. Folding these into their parent PATCH or PUT routes cuts massive routing boilerplate:
|
||||||
|
|
||||||
|
Article single-field setters (Save 4 routes):
|
||||||
|
PUT /article/{sku}/brand, PUT /article/{sku}/track_stock, PUT /article/{sku}/weight, and POST /article/{sku}/parent all return T2/T32. Fold brandId, track_stock, weight, isParent, and attributeNameId directly into PATCH /article/{sku}.
|
||||||
|
|
||||||
|
Article sub-resource getters (Save 4 routes):
|
||||||
|
If you embed attributeValueIds: int[], properties: { propertyId: int, value: str }[], articlelist, and optionslist directly inside T2 (GET /article/{sku}), you can drop GET /article/{sku}/attributevalue, GET /article/{sku}/property, GET /article/{sku}/articlelist, and GET /article/{sku}/optionslist.
|
||||||
|
|
||||||
|
POS Terminal category toggles (Save 4 routes):
|
||||||
|
DELETE /posterminal/{id}/all, PUT /posterminal/{id}/all, DELETE /posterminal/{id}/category/{categoryId}, and PUT /posterminal/{id}/category/{categoryId} can all be replaced by passing categoryIds?: int[] to PATCH /posterminal/{id}.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Duplicate PUT + PATCH & Export verbs (Save 4 routes):
|
||||||
|
|
||||||
|
labeltemplate/{id}, printer/{id}, and warehouse/{id} each define both PATCH and PUT with identical request bodies and 200 responses. Drop the PUT routes.
|
||||||
|
|
||||||
|
company/export defines both GET and POST for the exact same SSE stream.
|
||||||
|
|
||||||
|
Ledger & Supplier query duplicates (Save 4 routes):
|
||||||
|
|
||||||
|
GET /ledger/manifestarticle/{id} and GET /ledger/sku/{sku} are 100% covered by GET /ledger?manifestarticle_id=...&sku=....
|
||||||
|
|
||||||
|
GET /supplier/{id}/articles?skus=... and POST /supplier/{id}/articles do the exact same thing.
|
||||||
|
|
||||||
|
DELETE /article/{sku}/property/{propertyId} and DELETE /category/{id}/property/{propertyId} are redundant when you already use bulk PUT .../property to replace the property array.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Unify Document File Attachments (Save 8 Routes)
|
||||||
|
delivery, order, quote, and shipment all repeat identical attachment routes because they are all backed by manifest and manifestattachment in the database:
|
||||||
|
|
||||||
|
GET /{delivery|order|quote|shipment}/{id}/file/{fileId} (4 routes)
|
||||||
|
|
||||||
|
DELETE /{delivery|order|quote|shipment}/{id}/file/{fileId} (4 routes)
|
||||||
|
|
||||||
|
POST /{delivery|order|quote|shipment}/{id}/pdf (4 routes)
|
||||||
|
|
||||||
|
Since file.id is a primary key (or scoped via manifestattachment), you can replace all 8 file routes with GET /file/{fileId} and DELETE /file/{fileId} (saving 6 routes).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. Slim Down API Payloads (Stop Joining Cached Data on the Server)
|
||||||
|
Even for endpoints you keep, the client cache lets you remove server-side SQL JOINs and strip redundant strings from your response types:
|
||||||
|
|
||||||
|
T2 (GET /article/{sku}) & GET /category/{id}/articles:
|
||||||
|
|
||||||
|
Currently returns full unit: T27 ({ si, basesi, basesifactor, symbol, name }) and brand: T26 ({ id, name, fullHash, thumbW, thumbH, thumbHash }).
|
||||||
|
|
||||||
|
Reduction: Return only unitId: int (or unitSi: str) and brandId: int|null. The client resolves the unit symbols, conversion factors, brand name, and brand logo hash instantly from the cache.
|
||||||
|
|
||||||
|
GET /article/{sku}/attributevalue & GET /category/{id}/attributevalue:
|
||||||
|
|
||||||
|
Currently joins and returns attributeId, attributeName, value, sort, exclusive.
|
||||||
|
|
||||||
|
Reduction: Return only attributeValueIds: int[]. The client already has the entire attributename -> attributevalue tree cached.
|
||||||
|
|
||||||
|
GET /article/{sku}/property & GET /category/{id}/property:
|
||||||
|
|
||||||
|
Currently joins propertyname to return name: str and exclusive: bool on every property row.
|
||||||
|
|
||||||
|
Reduction: Return { propertyId: int, value: str, sort: int }[] and resolve name and exclusive from the cached propertyname table.
|
||||||
|
|
||||||
|
T11 (articlesupplier) & T19 (shipping):
|
||||||
|
|
||||||
|
Strip supplierName?: str from T11 (resolve via entityId in cached supplier list) and strip carrier?: str from T19 (resolve via carrierId in cached carrier list).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import WarehouseIcon from '@mui/icons-material/Warehouse';
|
|||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { formatDate } from './manifestConfig';
|
import { formatDate, resolveLineSkus } from './manifestConfig';
|
||||||
import {
|
import {
|
||||||
fetchDeliveryLedgerSummary,
|
fetchDeliveryLedgerSummary,
|
||||||
bookDeliveryLines,
|
bookDeliveryLines,
|
||||||
@@ -45,66 +45,6 @@ import {
|
|||||||
SURFACE_RAISED,
|
SURFACE_RAISED,
|
||||||
} from '../../colors';
|
} from '../../colors';
|
||||||
|
|
||||||
function resolveLineSkus(line, delivery, supplierArticlesMap = {}) {
|
|
||||||
const rawSku = line?.sku ? String(line.sku).trim() : '';
|
|
||||||
const supplierId = delivery?.supplierId ?? delivery?.partyId;
|
|
||||||
|
|
||||||
// 1. Direct line properties if available
|
|
||||||
let supplierSku =
|
|
||||||
line?.supplierSku ||
|
|
||||||
line?.supplier_sku ||
|
|
||||||
line?.supplierArticle?.sku ||
|
|
||||||
'';
|
|
||||||
let ownSku =
|
|
||||||
line?.articleSku ||
|
|
||||||
line?.article_sku ||
|
|
||||||
line?.article?.sku ||
|
|
||||||
line?.supplierArticle?.article?.sku ||
|
|
||||||
'';
|
|
||||||
|
|
||||||
// 2. Check supplierArticlesMap
|
|
||||||
const sa = rawSku
|
|
||||||
? (supplierArticlesMap[rawSku] || supplierArticlesMap[rawSku.toLowerCase()])
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (sa) {
|
|
||||||
if (!supplierSku && sa.sku) supplierSku = sa.sku;
|
|
||||||
if (!ownSku && sa.article?.sku) ownSku = sa.article.sku;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check loaded article details
|
|
||||||
const cachedArticle =
|
|
||||||
line?.article ||
|
|
||||||
(ownSku ? getArticle(ownSku) : null) ||
|
|
||||||
(rawSku ? getArticle(rawSku) : null);
|
|
||||||
|
|
||||||
if (cachedArticle) {
|
|
||||||
if (!ownSku && cachedArticle.sku) {
|
|
||||||
ownSku = cachedArticle.sku;
|
|
||||||
}
|
|
||||||
const sup =
|
|
||||||
(supplierId && cachedArticle.suppliers?.find((s) => s.entityId === supplierId)) ||
|
|
||||||
cachedArticle.suppliers?.[0];
|
|
||||||
if (!supplierSku && sup?.sku) {
|
|
||||||
supplierSku = sup.sku;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Fallback heuristics if one is still missing
|
|
||||||
if (!supplierSku && !ownSku) {
|
|
||||||
ownSku = rawSku;
|
|
||||||
} else if (!supplierSku && rawSku && rawSku !== ownSku) {
|
|
||||||
supplierSku = rawSku;
|
|
||||||
} else if (!ownSku && rawSku && rawSku !== supplierSku) {
|
|
||||||
ownSku = rawSku;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
supplierSku: supplierSku || '–',
|
|
||||||
ownSku: ownSku || '–',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class BookDeliveryDialog extends PureComponent {
|
export default class BookDeliveryDialog extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|||||||
@@ -707,3 +707,67 @@ export function manifestConfig(cfgOrKind) {
|
|||||||
}
|
}
|
||||||
throw new Error(`Unknown manifest kind: ${kind}`);
|
throw new Error(`Unknown manifest kind: ${kind}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves supplier SKU and own SKU for a manifest line.
|
||||||
|
*/
|
||||||
|
export function resolveLineSkus(line, delivery, supplierArticlesMap = {}) {
|
||||||
|
const rawSku = line?.sku ? String(line.sku).trim() : '';
|
||||||
|
const supplierId = delivery?.supplierId ?? delivery?.partyId;
|
||||||
|
|
||||||
|
// 1. Direct line properties if available
|
||||||
|
let supplierSku =
|
||||||
|
line?.supplierSku ||
|
||||||
|
line?.supplier_sku ||
|
||||||
|
line?.supplierArticle?.sku ||
|
||||||
|
'';
|
||||||
|
let ownSku =
|
||||||
|
line?.articleSku ||
|
||||||
|
line?.article_sku ||
|
||||||
|
line?.article?.sku ||
|
||||||
|
line?.supplierArticle?.article?.sku ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
// 2. Check supplierArticlesMap
|
||||||
|
const sa = rawSku
|
||||||
|
? (supplierArticlesMap[rawSku] || supplierArticlesMap[rawSku.toLowerCase()])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (sa) {
|
||||||
|
if (!supplierSku && sa.sku) supplierSku = sa.sku;
|
||||||
|
if (!ownSku && sa.article?.sku) ownSku = sa.article.sku;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check loaded article details
|
||||||
|
const cachedArticle =
|
||||||
|
line?.article ||
|
||||||
|
(ownSku ? getArticle(ownSku) : null) ||
|
||||||
|
(rawSku ? getArticle(rawSku) : null);
|
||||||
|
|
||||||
|
if (cachedArticle) {
|
||||||
|
if (!ownSku && cachedArticle.sku) {
|
||||||
|
ownSku = cachedArticle.sku;
|
||||||
|
}
|
||||||
|
const sup =
|
||||||
|
(supplierId && cachedArticle.suppliers?.find((s) => s.entityId === supplierId)) ||
|
||||||
|
cachedArticle.suppliers?.[0];
|
||||||
|
if (!supplierSku && sup?.sku) {
|
||||||
|
supplierSku = sup.sku;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback heuristics if one is still missing
|
||||||
|
if (!supplierSku && !ownSku) {
|
||||||
|
ownSku = rawSku;
|
||||||
|
} else if (!supplierSku && rawSku && rawSku !== ownSku) {
|
||||||
|
supplierSku = rawSku;
|
||||||
|
} else if (!ownSku && rawSku && rawSku !== supplierSku) {
|
||||||
|
ownSku = rawSku;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
supplierSku: supplierSku || '–',
|
||||||
|
ownSku: ownSku || '–',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ import {
|
|||||||
ArticleSearchDialog,
|
ArticleSearchDialog,
|
||||||
BookDeliveryDialog,
|
BookDeliveryDialog,
|
||||||
} from '../../components/manifest';
|
} from '../../components/manifest';
|
||||||
import { loadParties, apiFor } from '../../components/manifest/manifestConfig';
|
import { loadParties, apiFor, resolveLineSkus } from '../../components/manifest/manifestConfig';
|
||||||
|
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||||
import { article as articleApi, delivery as deliveryApi } from '../../api';
|
import { article as articleApi, delivery as deliveryApi } from '../../api';
|
||||||
import { getCategoryState } from '../../automats/categoryAutomat';
|
import { getCategoryState } from '../../automats/categoryAutomat';
|
||||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
@@ -79,6 +80,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
ledgerSummary: null,
|
ledgerSummary: null,
|
||||||
bookDeliveryDialogOpen: false,
|
bookDeliveryDialogOpen: false,
|
||||||
bookDeliveryPrefillLineId: null,
|
bookDeliveryPrefillLineId: null,
|
||||||
|
warehouses: warehouseAutomat.state.warehouses || [],
|
||||||
};
|
};
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
this.handleRetry = this.handleRetry.bind(this);
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
@@ -123,6 +125,10 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this._settingsUnsub = userSettingsAutomat.subscribe(this, (state) => ({
|
this._settingsUnsub = userSettingsAutomat.subscribe(this, (state) => ({
|
||||||
viewerPosition: state.viewerPosition,
|
viewerPosition: state.viewerPosition,
|
||||||
}));
|
}));
|
||||||
|
this._warehouseUnsub = warehouseAutomat.subscribe(this, (state) => ({
|
||||||
|
warehouses: state.warehouses,
|
||||||
|
}));
|
||||||
|
void warehouseAutomat.actions.load().catch(() => {});
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
if (this.props.docId != null) {
|
if (this.props.docId != null) {
|
||||||
setLastDeliveryPath(`/lieferung/${this.props.docId}`);
|
setLastDeliveryPath(`/lieferung/${this.props.docId}`);
|
||||||
@@ -161,6 +167,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
this.unsubscribe?.();
|
this.unsubscribe?.();
|
||||||
this._settingsUnsub?.();
|
this._settingsUnsub?.();
|
||||||
|
this._warehouseUnsub?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPartiesOnly() {
|
async loadPartiesOnly() {
|
||||||
@@ -1265,27 +1272,42 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLedgerPanel() {
|
renderLedgerPanel() {
|
||||||
const { ledgerSummary } = this.state;
|
const { ledgerSummary, warehouses = [] } = this.state;
|
||||||
if (!ledgerSummary) return null;
|
if (!ledgerSummary) return null;
|
||||||
|
|
||||||
const {
|
const { lines } = ledgerSummary;
|
||||||
status,
|
const delivery = this.state.delivery || this.automat?.state?.delivery;
|
||||||
totalPositions,
|
const supplierArticlesMap = this.state.supplierArticlesMap || {};
|
||||||
bookedPositions,
|
|
||||||
totalQuantity,
|
|
||||||
bookedQuantity,
|
|
||||||
lines,
|
|
||||||
} = ledgerSummary;
|
|
||||||
|
|
||||||
const allEntries = [];
|
const allEntries = [];
|
||||||
if (Array.isArray(lines)) {
|
if (Array.isArray(lines)) {
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (Array.isArray(line.ledgerEntries)) {
|
if (Array.isArray(line.ledgerEntries)) {
|
||||||
|
const { supplierSku, ownSku } = resolveLineSkus(line, delivery, supplierArticlesMap);
|
||||||
|
const lineName = line.name || line.article?.name || line.description || '–';
|
||||||
|
const deliveredAmount = line.amount ?? line.count ?? line.targetAmount ?? '–';
|
||||||
|
|
||||||
for (const entry of line.ledgerEntries) {
|
for (const entry of line.ledgerEntries) {
|
||||||
|
const hasWarehouse = entry.warehouse_id != null;
|
||||||
|
const wh = hasWarehouse
|
||||||
|
? (warehouses.find((w) => Number(w.id) === Number(entry.warehouse_id)) ||
|
||||||
|
warehouseAutomat.state.warehouses?.find((w) => Number(w.id) === Number(entry.warehouse_id)))
|
||||||
|
: null;
|
||||||
|
const warehouseName = wh?.name || (hasWarehouse ? `Lager #${entry.warehouse_id}` : '–');
|
||||||
|
|
||||||
|
const rawBookedAmount = entry.warehouse_amount ?? (hasWarehouse ? entry.amount : null);
|
||||||
|
const mengeEingebucht = rawBookedAmount != null
|
||||||
|
? (Number(rawBookedAmount) > 0 ? `+${rawBookedAmount}` : String(rawBookedAmount))
|
||||||
|
: '–';
|
||||||
|
|
||||||
allEntries.push({
|
allEntries.push({
|
||||||
...entry,
|
...entry,
|
||||||
lineSku: line.sku,
|
supplierSku,
|
||||||
lineName: line.name,
|
ownSku,
|
||||||
|
name: lineName,
|
||||||
|
deliveredAmount,
|
||||||
|
warehouseName,
|
||||||
|
mengeEingebucht,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1293,190 +1315,85 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
allEntries.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
|
allEntries.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
|
||||||
|
|
||||||
if (!ledgerSummary.hasTrackStock && allEntries.length === 0) return null;
|
if (allEntries.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelTile
|
<PanelTile
|
||||||
wide={4}
|
wide={4}
|
||||||
title="Lagerstatus (Ledger)"
|
title="Buchungen"
|
||||||
color={SURFACE_BASE}
|
color={SURFACE_BASE}
|
||||||
sx={{
|
sx={{
|
||||||
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },
|
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },
|
||||||
border: '1px solid',
|
|
||||||
borderColor:
|
|
||||||
status === 'booked'
|
|
||||||
? 'success.light'
|
|
||||||
: status === 'partial'
|
|
||||||
? 'warning.light'
|
|
||||||
: 'divider',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack spacing={2}>
|
<TableContainer component={Paper} variant="outlined">
|
||||||
<Box
|
<Table size="small">
|
||||||
sx={{
|
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
||||||
display: 'flex',
|
<TableRow>
|
||||||
flexWrap: 'wrap',
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Zeitpunkt</TableCell>
|
||||||
gap: 3,
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lieferanten SKU</TableCell>
|
||||||
alignItems: 'center',
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Eigene SKU</TableCell>
|
||||||
justifyContent: 'space-between',
|
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Name</TableCell>
|
||||||
}}
|
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge geliefert</TableCell>
|
||||||
>
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lager</TableCell>
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }}>
|
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge eingebucht</TableCell>
|
||||||
<Box>
|
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Referenz</TableCell>
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
</TableRow>
|
||||||
Status
|
</TableHead>
|
||||||
</Typography>
|
<TableBody>
|
||||||
{status === 'booked' && (
|
{allEntries.map((entry) => (
|
||||||
<Chip
|
<TableRow key={entry.id} hover>
|
||||||
size="small"
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
icon={<CheckCircleOutlineOutlinedIcon />}
|
<Typography variant="caption">
|
||||||
label="Vollständig gebucht"
|
{entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '–'}
|
||||||
color="success"
|
</Typography>
|
||||||
variant="outlined"
|
</TableCell>
|
||||||
sx={{ fontWeight: 600, height: 26 }}
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
/>
|
<Typography variant="caption" sx={{ fontFamily: 'monospace' }}>
|
||||||
)}
|
{entry.supplierSku}
|
||||||
{status === 'partial' && (
|
</Typography>
|
||||||
<Chip
|
</TableCell>
|
||||||
size="small"
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
icon={<Inventory2OutlinedIcon />}
|
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
|
||||||
label={`Teilweise gebucht (${bookedPositions}/${totalPositions})`}
|
{entry.ownSku}
|
||||||
color="warning"
|
</Typography>
|
||||||
variant="outlined"
|
</TableCell>
|
||||||
sx={{ fontWeight: 600, height: 26 }}
|
<TableCell sx={{ py: 0.5 }}>
|
||||||
/>
|
<Typography variant="caption">
|
||||||
)}
|
{entry.name}
|
||||||
{status === 'unbooked' && (
|
</Typography>
|
||||||
<Chip
|
</TableCell>
|
||||||
size="small"
|
<TableCell align="right" sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
label={`Offen (${totalPositions} Positionen)`}
|
<Typography variant="caption">
|
||||||
variant="outlined"
|
{entry.deliveredAmount}
|
||||||
sx={{ color: 'text.secondary', borderColor: 'divider', fontWeight: 500, height: 26 }}
|
</Typography>
|
||||||
/>
|
</TableCell>
|
||||||
)}
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
{status === 'empty' && (
|
<Typography variant="caption">
|
||||||
<Chip size="small" label="Keine Artikel" variant="outlined" />
|
{entry.warehouseName}
|
||||||
)}
|
</Typography>
|
||||||
</Box>
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
<Box>
|
align="right"
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
sx={{
|
||||||
Positionen
|
py: 0.5,
|
||||||
</Typography>
|
fontWeight: 700,
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
color: entry.mengeEingebucht !== '–' ? 'success.main' : 'text.disabled',
|
||||||
{bookedPositions} / {totalPositions} gebucht
|
whiteSpace: 'nowrap',
|
||||||
</Typography>
|
}}
|
||||||
</Box>
|
>
|
||||||
|
{entry.mengeEingebucht}
|
||||||
<Box>
|
</TableCell>
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
<TableCell sx={{ py: 0.5 }}>
|
||||||
Menge
|
<Typography variant="caption">
|
||||||
</Typography>
|
{entry.transaction_text || '–'}
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
</Typography>
|
||||||
{bookedQuantity} / {totalQuantity} Stk. gebucht
|
</TableCell>
|
||||||
</Typography>
|
</TableRow>
|
||||||
</Box>
|
))}
|
||||||
|
</TableBody>
|
||||||
{totalQuantity > bookedQuantity && (
|
</Table>
|
||||||
<Box>
|
</TableContainer>
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Noch offen
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'warning.dark' }}>
|
|
||||||
{totalQuantity - bookedQuantity} Stk.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{ledgerSummary.hasTrackStock && (
|
|
||||||
<Button
|
|
||||||
variant={status === 'booked' ? 'outlined' : 'contained'}
|
|
||||||
color={status === 'booked' ? 'success' : status === 'partial' ? 'warning' : 'primary'}
|
|
||||||
startIcon={
|
|
||||||
status === 'booked' ? (
|
|
||||||
<CheckCircleOutlineOutlinedIcon />
|
|
||||||
) : (
|
|
||||||
<Inventory2OutlinedIcon />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={this.handleOpenBookDialog}
|
|
||||||
sx={{ fontWeight: 600, textTransform: 'none' }}
|
|
||||||
>
|
|
||||||
{status === 'booked'
|
|
||||||
? 'Buchungsdetails anzeigen'
|
|
||||||
: status === 'partial'
|
|
||||||
? 'Restliche Mengen einbuchen'
|
|
||||||
: 'Ins Lager buchen'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{allEntries.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', mb: 1, display: 'block' }}>
|
|
||||||
Bisherige Ledger-Buchungen ({allEntries.length}):
|
|
||||||
</Typography>
|
|
||||||
<TableContainer component={Paper} variant="outlined">
|
|
||||||
<Table size="small">
|
|
||||||
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Zeitpunkt</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Typ</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>SKU / Artikel</TableCell>
|
|
||||||
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5 }}>Menge</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Buchungstext</TableCell>
|
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5, fontFamily: 'monospace', fontSize: '0.75rem' }}>Prüfsumme (prev_hash)</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{allEntries.map((entry) => (
|
|
||||||
<TableRow key={entry.id} hover>
|
|
||||||
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
|
||||||
<Typography variant="caption">
|
|
||||||
{entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '–'}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ py: 0.5 }}>
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
label={entry.transaction_type}
|
|
||||||
color="success"
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600 }}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ py: 0.5 }}>
|
|
||||||
<Typography variant="caption" sx={{ fontWeight: 600, fontFamily: 'monospace' }}>
|
|
||||||
{entry.lineSku || entry.sku || '–'}
|
|
||||||
</Typography>
|
|
||||||
{entry.lineName && (
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
|
|
||||||
{entry.lineName}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell align="right" sx={{ py: 0.5, fontWeight: 700, color: 'success.main' }}>
|
|
||||||
+{entry.amount}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ py: 0.5 }}>
|
|
||||||
<Typography variant="caption">
|
|
||||||
{entry.transaction_text || '–'}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ py: 0.5, fontFamily: 'monospace', fontSize: '0.7rem', color: 'text.secondary' }}>
|
|
||||||
<Tooltip title={entry.prev_hash}>
|
|
||||||
<span>{entry.prev_hash ? `${entry.prev_hash.slice(0, 16)}…` : '–'}</span>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,10 +118,41 @@ export async function fetchDeliveryLedgerSummary(deliveryId, { deliveryApi = def
|
|||||||
|
|
||||||
const trackStock = isLineStockTracked(line, article);
|
const trackStock = isLineStockTracked(line, article);
|
||||||
|
|
||||||
const deliveryEntries = ledgerEntries.filter((e) => {
|
const acceptanceEntries = ledgerEntries.filter((e) => {
|
||||||
const t = String(e.transaction_type).toUpperCase();
|
const t = String(e.transaction_type).toUpperCase();
|
||||||
return t === 'ACCEPTANCE' || t === 'RETURN' || t === 'DELIVERY';
|
return t === 'ACCEPTANCE' || t === 'RETURN' || t === 'DELIVERY';
|
||||||
});
|
});
|
||||||
|
const warehouseEntries = ledgerEntries.filter((e) => {
|
||||||
|
const t = String(e.transaction_type).toUpperCase();
|
||||||
|
return t === 'WAREHOUSE';
|
||||||
|
});
|
||||||
|
|
||||||
|
let deliveryEntries;
|
||||||
|
if (acceptanceEntries.length > 0) {
|
||||||
|
deliveryEntries = acceptanceEntries.map((acc, idx) => {
|
||||||
|
const matchingWh =
|
||||||
|
warehouseEntries.find(
|
||||||
|
(w) =>
|
||||||
|
w.warehouse_id != null &&
|
||||||
|
(w.transaction_text === acc.transaction_text ||
|
||||||
|
Math.abs(new Date(w.created_at) - new Date(acc.created_at)) < 15000)
|
||||||
|
) || warehouseEntries[idx] || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
warehouse_id: matchingWh?.warehouse_id ?? acc.warehouse_id ?? null,
|
||||||
|
warehouse_amount: matchingWh?.amount ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (warehouseEntries.length > 0) {
|
||||||
|
deliveryEntries = warehouseEntries.map((w) => ({
|
||||||
|
...w,
|
||||||
|
warehouse_id: w.warehouse_id ?? null,
|
||||||
|
warehouse_amount: w.amount ?? null,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
deliveryEntries = [];
|
||||||
|
}
|
||||||
const bookedAmount = deliveryEntries.reduce((sum, e) => sum + Number(e.amount || 0), 0);
|
const bookedAmount = deliveryEntries.reduce((sum, e) => sum + Number(e.amount || 0), 0);
|
||||||
const targetAmount = Number(line.amount || 0);
|
const targetAmount = Number(line.amount || 0);
|
||||||
const remainingAmount = Math.max(0, targetAmount - bookedAmount);
|
const remainingAmount = Math.max(0, targetAmount - bookedAmount);
|
||||||
|
|||||||
Reference in New Issue
Block a user