u
This commit is contained in:
@@ -497,6 +497,22 @@ export class ApiClient {
|
||||
'DELETE',
|
||||
`/api/article/${enc(sku)}/children/${enc(childSku)}${deleteArticle ? '?deleteArticle=true' : ''}`
|
||||
),
|
||||
/** GET /api/article/{sku}/articlelist */
|
||||
getArticlelist: (sku) =>
|
||||
this.#json('GET', `/api/article/${enc(sku)}/articlelist`),
|
||||
/** PUT /api/article/{sku}/articlelist */
|
||||
setArticlelist: (sku, items) =>
|
||||
this.#json('PUT', `/api/article/${enc(sku)}/articlelist`, {
|
||||
body: Array.isArray(items) ? { items } : items,
|
||||
}),
|
||||
/** GET /api/article/{sku}/optionslist */
|
||||
getOptionslist: (sku) =>
|
||||
this.#json('GET', `/api/article/${enc(sku)}/optionslist`),
|
||||
/** PUT /api/article/{sku}/optionslist */
|
||||
setOptionslist: (sku, options) =>
|
||||
this.#json('PUT', `/api/article/${enc(sku)}/optionslist`, {
|
||||
body: Array.isArray(options) ? { options } : options,
|
||||
}),
|
||||
};
|
||||
|
||||
this.posterminal = {
|
||||
|
||||
@@ -519,6 +519,42 @@ export const article = {
|
||||
{ method: 'DELETE' }
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/article/:sku/articlelist → { items: [...] }
|
||||
*/
|
||||
getArticlelist(sku) {
|
||||
return http(`/api/article/${encodeURIComponent(sku)}/articlelist`);
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/article/:sku/articlelist
|
||||
* Body: { items: [{ articleId, sku, count, sort }] }
|
||||
*/
|
||||
setArticlelist(sku, items) {
|
||||
return http(`/api/article/${encodeURIComponent(sku)}/articlelist`, {
|
||||
method: 'PUT',
|
||||
body: Array.isArray(items) ? { items } : items,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* GET /api/article/:sku/optionslist → { options: [...] }
|
||||
*/
|
||||
getOptionslist(sku) {
|
||||
return http(`/api/article/${encodeURIComponent(sku)}/optionslist`);
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT /api/article/:sku/optionslist
|
||||
* Body: { options: [{ articleId, sku, count, sort, mandated }] }
|
||||
*/
|
||||
setOptionslist(sku, options) {
|
||||
return http(`/api/article/${encodeURIComponent(sku)}/optionslist`, {
|
||||
method: 'PUT',
|
||||
body: Array.isArray(options) ? { options } : options,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -611,7 +647,7 @@ export const category = {
|
||||
|
||||
/**
|
||||
* GET /api/category/:id/articles → { articles: [...] }
|
||||
* Article rows: sku, name, net, taxRate, fullHash, thumbW, thumbH, thumbHash, …
|
||||
* Article rows: sku, name, net, taxRate, fullHash, thumbW, thumbH, thumbHash, childCount, …
|
||||
*/
|
||||
getArticles(categoryId) {
|
||||
return http(`/api/category/${categoryId}/articles`);
|
||||
|
||||
BIN
src/assets/quixpos-logo.png
Normal file
BIN
src/assets/quixpos-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -1,6 +1,8 @@
|
||||
import { PureComponent } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import GoogleIcon from '@mui/icons-material/Google';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { notifyErrorMessage } from '../utils/notify';
|
||||
@@ -15,6 +17,8 @@ import {
|
||||
requestGoogleIdToken,
|
||||
} from '../auth/googleSignIn';
|
||||
|
||||
import quixposLogo from '../assets/quixpos-logo.png';
|
||||
|
||||
class LoginPage extends PureComponent {
|
||||
static contextType = StorageManagerContext;
|
||||
|
||||
@@ -93,17 +97,62 @@ class LoginPage extends PureComponent {
|
||||
justifyContent: 'center',
|
||||
minHeight: '100vh',
|
||||
bgcolor: 'background.default',
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<GoogleIcon />}
|
||||
disabled={busy || loading || !ready}
|
||||
onClick={this.handleLogin}
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 2.5,
|
||||
px: 5,
|
||||
py: 5,
|
||||
maxWidth: 400,
|
||||
width: '100%',
|
||||
borderRadius: 3,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{busy ? 'Signing in…' : 'Sign in with Google'}
|
||||
</Button>
|
||||
<Box
|
||||
component="img"
|
||||
src={quixposLogo}
|
||||
alt="QuixPOS Logo"
|
||||
sx={{
|
||||
width: 96,
|
||||
height: 96,
|
||||
borderRadius: 3,
|
||||
display: 'block',
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h1"
|
||||
sx={{ fontWeight: 700, color: '#1a3533', letterSpacing: '-0.01em' }}
|
||||
>
|
||||
QuixPOS
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{ color: 'text.secondary', fontWeight: 500, mt: 0.5 }}
|
||||
>
|
||||
Management Interface Login
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
fullWidth
|
||||
startIcon={<GoogleIcon />}
|
||||
disabled={busy || loading || !ready}
|
||||
onClick={this.handleLogin}
|
||||
sx={{ mt: 1, py: 1.25, textTransform: 'none', fontWeight: 600 }}
|
||||
>
|
||||
{busy ? 'Anmeldung läuft…' : 'Mit Google anmelden'}
|
||||
</Button>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,8 +79,19 @@ const SETTINGS_TAB = {
|
||||
help: 'Artikeleinstellungen, Einheiten, Steuersatz und Löschen',
|
||||
};
|
||||
|
||||
function visibleTabs() {
|
||||
return [...ALWAYS_TABS, SETTINGS_TAB];
|
||||
function visibleTabs(article) {
|
||||
const isFather = Boolean(article?.isFather);
|
||||
const tabs = ALWAYS_TABS.map((row) => {
|
||||
if (row.id === ARTICLE_TAB.supplier && isFather) {
|
||||
return {
|
||||
...row,
|
||||
disabled: true,
|
||||
help: 'Lieferanten sind für Vaterartikel deaktiviert (werden auf Variantenebene gepflegt).',
|
||||
};
|
||||
}
|
||||
return row;
|
||||
});
|
||||
return [...tabs, SETTINGS_TAB];
|
||||
}
|
||||
|
||||
function readRouteTab() {
|
||||
@@ -264,6 +275,7 @@ class ArticleTileTab extends PureComponent {
|
||||
<Tab
|
||||
key={row.id}
|
||||
value={row.id}
|
||||
disabled={Boolean(row.disabled)}
|
||||
aria-label={row.label}
|
||||
icon={
|
||||
<>
|
||||
@@ -300,6 +312,12 @@ class ArticleTileTab extends PureComponent {
|
||||
color: 'text.primary',
|
||||
borderBottom: 'none',
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.4,
|
||||
bgcolor: '#e2e8f0',
|
||||
cursor: 'not-allowed',
|
||||
pointerEvents: 'auto',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -30,8 +30,12 @@ export function articleSku(article) {
|
||||
return sku || null;
|
||||
}
|
||||
|
||||
export function articleTabIds() {
|
||||
return [...BASE_ARTICLE_TAB_IDS, ARTICLE_TAB.settings];
|
||||
export function articleTabIds(article) {
|
||||
let ids = [...BASE_ARTICLE_TAB_IDS];
|
||||
if (article?.isFather) {
|
||||
ids = ids.filter((id) => id !== ARTICLE_TAB.supplier);
|
||||
}
|
||||
return [...ids, ARTICLE_TAB.settings];
|
||||
}
|
||||
|
||||
export function isArticleTabId(tab) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import Box from '@mui/material/Box';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { EditablePanelTile } from 'dashgui/panels';
|
||||
import { EditablePanelTile, PanelTile } from 'dashgui/panels';
|
||||
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
|
||||
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
|
||||
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||
@@ -485,6 +485,53 @@ export default class ArticleViewPrice extends PureComponent {
|
||||
|
||||
render() {
|
||||
const { article: articleData } = this.props;
|
||||
|
||||
if (articleData?.isFather) {
|
||||
return (
|
||||
<PanelTile
|
||||
wide={1}
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
color="#e2e8f0"
|
||||
contentAlignItems="flex-start"
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.6,
|
||||
fontWeight: 600,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
Preis
|
||||
</Typography>
|
||||
<Box sx={{ my: 0.75 }}>
|
||||
<Typography
|
||||
component="div"
|
||||
sx={{
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
Kein Preis
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block', mt: 0.25 }}
|
||||
>
|
||||
Preise werden auf Variantenebene gepflegt
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</PanelTile>
|
||||
);
|
||||
}
|
||||
|
||||
const { company, tiersDialogOpen, unitEditorOpen } = this.state;
|
||||
const priceValue = priceDraftValue(articleData, company);
|
||||
const basePrice = primaryPrice(articleData);
|
||||
|
||||
@@ -31,8 +31,9 @@ export default class ArticleViewStock extends PureComponent {
|
||||
render() {
|
||||
const { article: articleData } = this.props;
|
||||
const { adjustOpen } = this.state;
|
||||
const isFather = Boolean(articleData?.isFather);
|
||||
const isTrackStock =
|
||||
articleData?.track_stock !== false && articleData?.track_stock !== 0;
|
||||
!isFather && articleData?.track_stock !== false && articleData?.track_stock !== 0;
|
||||
|
||||
const rawWarehouses = Array.isArray(articleData?.warehouses)
|
||||
? articleData.warehouses
|
||||
@@ -60,12 +61,16 @@ export default class ArticleViewStock extends PureComponent {
|
||||
<PanelTile
|
||||
wide={1}
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
color="#bbf7d0"
|
||||
action={{
|
||||
label: 'Bestand anpassen',
|
||||
icon: EditOutlinedIcon,
|
||||
onClick: this.handleOpenAdjust,
|
||||
}}
|
||||
color={isFather ? '#e2e8f0' : '#bbf7d0'}
|
||||
action={
|
||||
isFather
|
||||
? null
|
||||
: {
|
||||
label: 'Bestand anpassen',
|
||||
icon: EditOutlinedIcon,
|
||||
onClick: this.handleOpenAdjust,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -90,7 +95,28 @@ export default class ArticleViewStock extends PureComponent {
|
||||
Bestand
|
||||
</Typography>
|
||||
|
||||
{isTrackStock ? (
|
||||
{isFather ? (
|
||||
<Box sx={{ my: 0.75 }}>
|
||||
<Typography
|
||||
component="div"
|
||||
sx={{
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.2,
|
||||
color: 'text.secondary',
|
||||
}}
|
||||
>
|
||||
Kein Bestand
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ display: 'block', mt: 0.25 }}
|
||||
>
|
||||
Bestand wird auf Variantenebene geführt
|
||||
</Typography>
|
||||
</Box>
|
||||
) : isTrackStock ? (
|
||||
<Tooltip title="Klicken, um Bestand anzupassen" arrow placement="top-start">
|
||||
<Box
|
||||
onClick={this.handleOpenAdjust}
|
||||
@@ -251,11 +277,13 @@ export default class ArticleViewStock extends PureComponent {
|
||||
</Box>
|
||||
</PanelTile>
|
||||
|
||||
<ArticleStockAdjustDialog
|
||||
open={adjustOpen}
|
||||
onClose={this.handleCloseAdjust}
|
||||
article={articleData}
|
||||
/>
|
||||
{!isFather ? (
|
||||
<ArticleStockAdjustDialog
|
||||
open={adjustOpen}
|
||||
onClose={this.handleCloseAdjust}
|
||||
article={articleData}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,6 +197,33 @@ export default class ArticleSupplierTab extends PureComponent {
|
||||
busyId,
|
||||
} = this.state;
|
||||
|
||||
if (this.props.article?.isFather) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
py: 6,
|
||||
px: 2,
|
||||
borderRadius: 2,
|
||||
border: '1px dashed rgba(0, 0, 0, 0.15)',
|
||||
bgcolor: 'rgba(0, 0, 0, 0.01)',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<LocalShippingOutlinedIcon sx={{ fontSize: 48, mb: 1.5, color: 'text.secondary', opacity: 0.4 }} />
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 0.5, color: 'text.secondary' }}>
|
||||
Lieferanten sind für Vaterartikel deaktiviert
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 400 }}>
|
||||
Lieferanten, Einkaufspreise und Lieferanten-Artikelnummern werden für jede Variante separat gepflegt.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
|
||||
|
||||
@@ -53,6 +53,33 @@ export class ArticleTile extends Tile {
|
||||
);
|
||||
}
|
||||
|
||||
renderExtra() {
|
||||
const childCount = Number(this.props.article?.childCount) || 0;
|
||||
if (childCount <= 0) return null;
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
zIndex: 1,
|
||||
px: 0.75,
|
||||
py: 0.25,
|
||||
borderRadius: 999,
|
||||
bgcolor: 'rgba(0, 0, 0, 0.65)',
|
||||
color: 'common.white',
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.2,
|
||||
whiteSpace: 'nowrap',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{childCount}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.hasPhoto()) {
|
||||
return super.render();
|
||||
@@ -87,6 +114,7 @@ export class ArticleTile extends Tile {
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{this.renderExtra()}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
|
||||
Reference in New Issue
Block a user