u
This commit is contained in:
@@ -84,6 +84,19 @@ export const delivery = {
|
||||
},
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* DELETE /api/delivery/:id/file/:fileId
|
||||
* Deletes delivery file attachment.
|
||||
* @param {number|string} id Delivery ID
|
||||
* @param {number|string} fileId File ID
|
||||
* → 204 void
|
||||
*/
|
||||
// #region DELETE /api/delivery/:id/file/:fileId — deleteFile
|
||||
deleteFile(id, fileId) {
|
||||
return http(`/api/delivery/${id}/file/${fileId}`, { method: 'DELETE' });
|
||||
},
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* POST /api/delivery/parse — multipart/form-data, field "file"
|
||||
* Accepts PDF, TXT or CSV document (max 50 MB)
|
||||
|
||||
@@ -62,4 +62,17 @@ export const order = {
|
||||
return http(`/api/order/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* DELETE /api/order/:id/file/:fileId
|
||||
* Deletes order file attachment.
|
||||
* @param {number|string} id Order ID
|
||||
* @param {number|string} fileId File ID
|
||||
* → 204 void
|
||||
*/
|
||||
// #region DELETE /api/order/:id/file/:fileId — deleteFile
|
||||
deleteFile(id, fileId) {
|
||||
return http(`/api/order/${id}/file/${fileId}`, { method: 'DELETE' });
|
||||
},
|
||||
// #endregion
|
||||
};
|
||||
|
||||
@@ -363,7 +363,7 @@ export function getManifestDirtyDetails(current, baseline) {
|
||||
const count = dirtyLineKeys.length;
|
||||
summary.push(count > 0 ? `${count} Artikelzeile${count === 1 ? '' : 'n'}` : 'Artikelzeilen');
|
||||
}
|
||||
if (fields.attachments) summary.push('Dateien');
|
||||
if (fields.attachments && current.kind !== 'quote') summary.push('Dateien');
|
||||
|
||||
const isDirty = summary.length > 0;
|
||||
|
||||
@@ -455,6 +455,16 @@ export function createManifestEditAutomat({ kind, name }) {
|
||||
});
|
||||
},
|
||||
|
||||
setAttachments(attachments) {
|
||||
const list = Array.isArray(attachments) ? attachments : [];
|
||||
const base = automat.state.baseline;
|
||||
const nextBaseline = base ? { ...base, attachments: [...list] } : null;
|
||||
automat.setState({
|
||||
attachments: list,
|
||||
baseline: nextBaseline,
|
||||
});
|
||||
},
|
||||
|
||||
restoreBaseline() {
|
||||
if (!automat.state.baseline) return;
|
||||
const base = automat.state.baseline;
|
||||
|
||||
@@ -66,19 +66,37 @@ class AppSidebar extends PureComponent {
|
||||
<Box
|
||||
component="nav"
|
||||
sx={(theme) => ({
|
||||
width: SIDEBAR_WIDTH,
|
||||
width: { xs: '100%', lg: SIDEBAR_WIDTH },
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexDirection: { xs: 'row', lg: 'column' },
|
||||
alignItems: { xs: 'center', lg: 'stretch' },
|
||||
backgroundColor: theme.nav.backgroundColor || NAV_SURFACE,
|
||||
background: theme.nav.background,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: theme.zIndex.appBar || 1100,
|
||||
alignSelf: 'flex-start',
|
||||
height: 'var(--df-vh, 100vh)',
|
||||
height: { xs: 52, sm: 56, lg: 'var(--df-vh, 100vh)' },
|
||||
px: { xs: 1, sm: 1.5, lg: 0 },
|
||||
boxSizing: 'border-box',
|
||||
boxShadow: { xs: '0 2px 8px rgba(0,0,0,0.18)', lg: 'none' },
|
||||
})}
|
||||
>
|
||||
<List disablePadding sx={{ flexGrow: 1 }}>
|
||||
<List
|
||||
disablePadding
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'row', lg: 'column' },
|
||||
alignItems: 'center',
|
||||
height: { xs: '100%', lg: 'auto' },
|
||||
overflowX: { xs: 'auto', lg: 'visible' },
|
||||
scrollbarWidth: 'none',
|
||||
'&::-webkit-scrollbar': { display: 'none' },
|
||||
gap: { xs: 0.5, sm: 1, lg: 0 },
|
||||
}}
|
||||
>
|
||||
{NAV_ITEMS.map(({ path, label, help, icon: Icon }) => {
|
||||
const active = path === activeNav;
|
||||
|
||||
@@ -110,12 +128,21 @@ class AppSidebar extends PureComponent {
|
||||
}
|
||||
}}
|
||||
sx={(theme) => ({
|
||||
borderRadius: 0,
|
||||
px: 1.5,
|
||||
py: 1.25,
|
||||
borderLeft: '3px solid transparent',
|
||||
borderRadius: { xs: 1, lg: 0 },
|
||||
width: { xs: 'auto', lg: '100%' },
|
||||
height: { xs: '100%', lg: 'auto' },
|
||||
px: { xs: 1.25, sm: 1.5 },
|
||||
py: { xs: 0.75, sm: 1, lg: 1.25 },
|
||||
borderLeft: { xs: 'none', lg: '3px solid transparent' },
|
||||
borderBottom: { xs: '3px solid transparent', lg: 'none' },
|
||||
color: theme.nav.item,
|
||||
'& .MuiListItemIcon-root': { color: 'inherit' },
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
'& .MuiListItemIcon-root': {
|
||||
color: 'inherit',
|
||||
minWidth: { xs: 'auto', lg: 36 },
|
||||
mr: { xs: 0.75, lg: 0 },
|
||||
},
|
||||
'&:hover': {
|
||||
bgcolor: theme.nav.itemHover,
|
||||
color: theme.nav.itemHoverText,
|
||||
@@ -124,7 +151,8 @@ class AppSidebar extends PureComponent {
|
||||
bgcolor: theme.nav.itemActive,
|
||||
color: theme.nav.itemActiveText,
|
||||
fontWeight: 600,
|
||||
borderLeftColor: theme.nav.itemActiveAccent,
|
||||
borderLeftColor: { xs: 'transparent', lg: theme.nav.itemActiveAccent },
|
||||
borderBottomColor: { xs: theme.nav.itemActiveAccent, lg: 'transparent' },
|
||||
'&:hover': {
|
||||
bgcolor: theme.nav.itemActive,
|
||||
color: theme.nav.itemActiveText,
|
||||
@@ -132,18 +160,28 @@ class AppSidebar extends PureComponent {
|
||||
},
|
||||
})}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
<ListItemIcon>
|
||||
<HelpBadge title={label} help={help} placement="right">
|
||||
<Icon />
|
||||
</HelpBadge>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={label} />
|
||||
<ListItemText
|
||||
primary={label}
|
||||
primaryTypographyProps={{
|
||||
fontSize: { xs: '0.8rem', sm: '0.875rem' },
|
||||
fontWeight: active ? 600 : 500,
|
||||
noWrap: true,
|
||||
}}
|
||||
sx={{ m: 0 }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
|
||||
<UserMenu />
|
||||
<Box sx={{ ml: { xs: 'auto', lg: 0 }, flexShrink: 0, display: 'flex', alignItems: 'center' }}>
|
||||
<UserMenu />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -187,10 +225,11 @@ class AppSubNav extends PureComponent {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: SUB_NAV_WIDTH,
|
||||
width: { xs: 0, lg: SUB_NAV_WIDTH },
|
||||
display: { xs: 'none', lg: 'block' },
|
||||
flexShrink: 0,
|
||||
ml: 3,
|
||||
mr: 2,
|
||||
ml: { xs: 0, lg: 3 },
|
||||
mr: { xs: 0, lg: 2 },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -201,19 +240,23 @@ class AppSubNav extends PureComponent {
|
||||
component="nav"
|
||||
aria-label="Unternavigation"
|
||||
sx={{
|
||||
width: SUB_NAV_WIDTH,
|
||||
width: { xs: 60, sm: 72, md: SUB_NAV_WIDTH },
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
top: { xs: 52, sm: 56, lg: 0 },
|
||||
alignSelf: 'flex-start',
|
||||
height: 'var(--df-vh, 100vh)',
|
||||
py: 4,
|
||||
ml: 3,
|
||||
mr: 2,
|
||||
height: {
|
||||
xs: 'calc(var(--df-vh, 100vh) - 52px)',
|
||||
sm: 'calc(var(--df-vh, 100vh) - 56px)',
|
||||
lg: 'var(--df-vh, 100vh)',
|
||||
},
|
||||
py: { xs: 2, lg: 4 },
|
||||
ml: { xs: 1, sm: 2, lg: 3 },
|
||||
mr: { xs: 1, sm: 1.5, lg: 2 },
|
||||
}}
|
||||
>
|
||||
{subNav.map(({ segment, label, title, help, icon: Icon }) => {
|
||||
@@ -247,7 +290,7 @@ class AppSubNav extends PureComponent {
|
||||
navigate(segment ? `${activeNav}/${segment}` : activeNav);
|
||||
}}
|
||||
sx={(theme) => ({
|
||||
width: SUB_NAV_WIDTH,
|
||||
width: { xs: 60, sm: 72, md: SUB_NAV_WIDTH },
|
||||
py: 1,
|
||||
px: 0.5,
|
||||
borderRadius: 2,
|
||||
@@ -271,14 +314,14 @@ class AppSubNav extends PureComponent {
|
||||
})}
|
||||
>
|
||||
<HelpBadge title={title || label} help={help} placement="right">
|
||||
<Icon sx={{ fontSize: 32 }} />
|
||||
<Icon sx={{ fontSize: { xs: 24, sm: 28, md: 32 } }} />
|
||||
</HelpBadge>
|
||||
{label ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
noWrap
|
||||
sx={{
|
||||
fontSize: '0.68rem',
|
||||
fontSize: { xs: '0.62rem', sm: '0.68rem' },
|
||||
fontWeight: 500,
|
||||
lineHeight: 1.2,
|
||||
textAlign: 'center',
|
||||
@@ -303,26 +346,43 @@ export default function AppShell() {
|
||||
const activeSub = activeNav ? resolveActiveSub(pathname, activeNav) : null;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', minHeight: 'var(--df-vh, 100vh)', bgcolor: 'background.default' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: { xs: 'column', lg: 'row' },
|
||||
minHeight: 'var(--df-vh, 100vh)',
|
||||
bgcolor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<AppSidebar activeNav={activeNav} navigate={navigate} />
|
||||
<AppSubNav
|
||||
activeNav={activeNav}
|
||||
activeSub={activeSub}
|
||||
navigate={navigate}
|
||||
/>
|
||||
<Box
|
||||
component="main"
|
||||
sx={(theme) => ({
|
||||
flexGrow: 1,
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
py: 4,
|
||||
pr: 3,
|
||||
bgcolor: theme.nav.mainBackground,
|
||||
flexDirection: 'row',
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
})}
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
<AppSubNav
|
||||
activeNav={activeNav}
|
||||
activeSub={activeSub}
|
||||
navigate={navigate}
|
||||
/>
|
||||
<Box
|
||||
component="main"
|
||||
sx={(theme) => ({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
py: { xs: 2, md: 3, lg: 4 },
|
||||
pr: { xs: 1.5, sm: 2, lg: 3 },
|
||||
pl: { xs: 1.5, sm: 2, lg: 0 },
|
||||
bgcolor: theme.nav.mainBackground,
|
||||
minWidth: 0,
|
||||
})}
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
<ChatFab />
|
||||
<ChatDrawer />
|
||||
|
||||
@@ -110,9 +110,21 @@ class UserMenu extends PureComponent {
|
||||
return `data:${mime};base64,${pic}`;
|
||||
})();
|
||||
|
||||
const isTop = Boolean(
|
||||
anchorEl && typeof anchorEl.getBoundingClientRect === 'function' && anchorEl.getBoundingClientRect().top < 200
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ px: 1.5, py: 1.5, alignSelf: 'flex-start' }}>
|
||||
<Box
|
||||
sx={{
|
||||
px: { xs: 0.5, sm: 1, lg: 1.5 },
|
||||
py: { xs: 0.5, lg: 1.5 },
|
||||
alignSelf: { xs: 'center', lg: 'flex-start' },
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={this.handleOpenMenu}
|
||||
onContextMenu={this.handleContextMenu}
|
||||
@@ -138,8 +150,14 @@ class UserMenu extends PureComponent {
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={this.handleCloseMenu}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
||||
anchorOrigin={{
|
||||
vertical: isTop ? 'bottom' : 'top',
|
||||
horizontal: isTop ? 'right' : 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: isTop ? 'top' : 'bottom',
|
||||
horizontal: isTop ? 'right' : 'left',
|
||||
}}
|
||||
>
|
||||
{pwaUpdateAvailable && (
|
||||
<MenuItem onClick={this.handleOpenPwaUpdate} sx={{ bgcolor: 'action.hover' }}>
|
||||
|
||||
@@ -73,7 +73,7 @@ export default class ChatFab extends PureComponent {
|
||||
sx={(theme) => ({
|
||||
position: 'fixed',
|
||||
bottom: { xs: 20, sm: 28 },
|
||||
left: { xs: 20, sm: 164 },
|
||||
left: { xs: 20, lg: 164 },
|
||||
zIndex: theme.zIndex.speedDial || 1050,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -220,15 +220,13 @@ class ManifestForm extends PureComponent {
|
||||
prevState.date !== this.state.date ||
|
||||
prevState.partyId !== this.state.partyId ||
|
||||
prevState.externalNumber !== this.state.externalNumber ||
|
||||
prevState.lines !== this.state.lines ||
|
||||
prevState.attachments !== this.state.attachments
|
||||
prevState.lines !== this.state.lines
|
||||
) {
|
||||
this.editAutomat.actions.update({
|
||||
date: this.state.date,
|
||||
partyId: this.state.partyId,
|
||||
externalNumber: this.state.externalNumber,
|
||||
lines: this.state.lines,
|
||||
attachments: this.state.attachments,
|
||||
});
|
||||
const { isDirty, dirtyFields, dirtyLineKeys, dirtyLineFields, dirtySummary } = this.editAutomat.state;
|
||||
if (
|
||||
@@ -809,18 +807,13 @@ class ManifestForm extends PureComponent {
|
||||
this.setState({ pdfDialogOpen: false, pdfUploading: false });
|
||||
}
|
||||
|
||||
async handleCreatePdf() {
|
||||
async handleCreatePdf(e) {
|
||||
e?.preventDefault?.();
|
||||
e?.stopPropagation?.();
|
||||
const { docId } = this.props;
|
||||
if (this.state.pdfCreating || this.state.saving) return;
|
||||
if (docId == null) return;
|
||||
|
||||
if (this.state.isDirty) {
|
||||
await this.handleSave();
|
||||
if (this.state.isDirty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ pdfCreating: true });
|
||||
try {
|
||||
const res = await quoteApi.createPdf(docId);
|
||||
@@ -834,20 +827,11 @@ class ManifestForm extends PureComponent {
|
||||
);
|
||||
this.setState({ attachments });
|
||||
if (this.editAutomat) {
|
||||
this.editAutomat.actions.updateDraft({ attachments });
|
||||
this.editAutomat.actions.setAttachments?.(attachments);
|
||||
}
|
||||
}
|
||||
|
||||
enqueueSnackbar('Angebot-PDF erfolgreich erstellt.', { variant: 'success' });
|
||||
|
||||
const fileUrl =
|
||||
res.fileUrl ||
|
||||
(res.attachment?.fileId
|
||||
? `/api/quote/${docId}/file/${res.attachment.fileId}`
|
||||
: null);
|
||||
if (fileUrl) {
|
||||
window.open(fileUrl, '_blank');
|
||||
}
|
||||
} catch (err) {
|
||||
if (!this._cancelled) {
|
||||
notifyError(err);
|
||||
@@ -861,23 +845,23 @@ class ManifestForm extends PureComponent {
|
||||
|
||||
async handleAttachmentDelete(attachment) {
|
||||
if (!attachment) return;
|
||||
const targetId = attachment.id ?? attachment.fileId;
|
||||
const fileId = attachment.fileId ?? attachment.id;
|
||||
const { docId } = this.props;
|
||||
|
||||
const nextAttachments = (this.state.attachments || []).filter(
|
||||
(a) => (a.id ?? a.fileId) !== targetId && a !== attachment,
|
||||
(a) => (a.fileId ?? a.id) !== fileId && a !== attachment,
|
||||
);
|
||||
this.setState({ attachments: nextAttachments });
|
||||
|
||||
if (this.editAutomat) {
|
||||
this.editAutomat.actions.updateDraft({ attachments: nextAttachments });
|
||||
this.editAutomat.actions.setAttachments?.(nextAttachments);
|
||||
}
|
||||
|
||||
if (docId != null && targetId != null && attachment.isPersisted) {
|
||||
if (docId != null && fileId != null && attachment.isPersisted) {
|
||||
try {
|
||||
const api = apiFor(this.cfg);
|
||||
if (typeof api.deleteFile === 'function') {
|
||||
await api.deleteFile(docId, targetId);
|
||||
await api.deleteFile(docId, fileId);
|
||||
enqueueSnackbar('Anhang gelöscht.', { variant: 'success' });
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -525,6 +525,7 @@ class ManifestHeader extends PureComponent {
|
||||
)}
|
||||
{kind === 'quote' && onPdfCreate && docId != null && Boolean(number) && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
startIcon={pdfCreating ? <CircularProgress size={16} color="inherit" /> : <PictureAsPdfIcon />}
|
||||
disabled={saving || pdfCreating}
|
||||
|
||||
Reference in New Issue
Block a user