modified: docker-compose.yml
modified: package-lock.json modified: package.json modified: src/screens/ImportExportScreen.tsx modified: src/screens/ManageProductsScreen.tsx modified: src/services/importExportService.test.ts modified: src/services/importExportService.ts
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
version: '3.9'
|
|
||||||
services:
|
services:
|
||||||
stockfill:
|
stockfill:
|
||||||
build: .
|
build: .
|
||||||
|
|||||||
Generated
+11
@@ -29,6 +29,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/papaparse": "^5.5.0",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||||
@@ -2386,6 +2387,16 @@
|
|||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/papaparse": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-GVs5iMQmUr54BAZYYkByv8zPofFxmyxUpISPb2oh8sayR3+1zbxasrOvoKiHJ/nnoq/uULuPsu1Lze1EkagVFg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/papaparse": "^5.5.0",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
"@typescript-eslint/eslint-plugin": "^8.47.0",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
Divider,
|
Divider,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Grid,
|
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
@@ -107,8 +106,14 @@ export const ImportExportScreen = () => {
|
|||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
Import / Export Data
|
Import / Export Data
|
||||||
</Typography>
|
</Typography>
|
||||||
<Grid container spacing={2}>
|
<Box
|
||||||
<Grid item xs={12} md={3}>
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
gap: 2,
|
||||||
|
gridTemplateColumns: { xs: '1fr', md: 'repeat(12, 1fr)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ gridColumn: { xs: '1 / -1', md: 'span 3' } }}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
@@ -123,8 +128,9 @@ export const ImportExportScreen = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Box>
|
||||||
<Grid item xs={12} md={5}>
|
|
||||||
|
<Box sx={{ gridColumn: { xs: '1 / -1', md: 'span 5' } }}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
@@ -146,8 +152,9 @@ export const ImportExportScreen = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Box>
|
||||||
<Grid item xs={12} md={4}>
|
|
||||||
|
<Box sx={{ gridColumn: { xs: '1 / -1', md: 'span 4' } }}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
@@ -176,8 +183,8 @@ export const ImportExportScreen = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Box>
|
||||||
</Grid>
|
</Box>
|
||||||
<Box mt={3}>
|
<Box mt={3}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ export const ManageProductsScreen = () => {
|
|||||||
onClose={() => setFeedback(null)}
|
onClose={() => setFeedback(null)}
|
||||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||||
>
|
>
|
||||||
{feedback ? <Alert severity={feedback.severity}>{feedback.text}</Alert> : null}
|
{feedback ? <Alert severity={feedback.severity}>{feedback.text}</Alert> : undefined}
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
<Button component={RouterLink} to="/categories" variant="outlined" sx={{ alignSelf: 'flex-start' }}>
|
<Button component={RouterLink} to="/categories" variant="outlined" sx={{ alignSelf: 'flex-start' }}>
|
||||||
Edit Categories
|
Edit Categories
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const createMockDb = (data?: {
|
|||||||
const callback = args[args.length - 1];
|
const callback = args[args.length - 1];
|
||||||
return callback();
|
return callback();
|
||||||
},
|
},
|
||||||
} as unknown as StockFillDB;
|
} as unknown as any;
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
@@ -52,9 +52,9 @@ const stubDownloads = () => {
|
|||||||
const anchor = { href: '', download: '', click: vi.fn() } as unknown as HTMLAnchorElement;
|
const anchor = { href: '', download: '', click: vi.fn() } as unknown as HTMLAnchorElement;
|
||||||
const createObjectURL = vi.fn(() => 'blob:url');
|
const createObjectURL = vi.fn(() => 'blob:url');
|
||||||
const revokeObjectURL = vi.fn();
|
const revokeObjectURL = vi.fn();
|
||||||
// @ts-expect-error jsdom stub
|
// @ts-ignore jsdom stub
|
||||||
vi.stubGlobal('document', { createElement: () => anchor });
|
vi.stubGlobal('document', { createElement: () => anchor });
|
||||||
// @ts-expect-error jsdom stub
|
// @ts-ignore jsdom stub
|
||||||
vi.stubGlobal('URL', { createObjectURL, revokeObjectURL });
|
vi.stubGlobal('URL', { createObjectURL, revokeObjectURL });
|
||||||
return { anchor, createObjectURL, revokeObjectURL };
|
return { anchor, createObjectURL, revokeObjectURL };
|
||||||
};
|
};
|
||||||
@@ -118,7 +118,7 @@ describe('import/export service', () => {
|
|||||||
|
|
||||||
expect(productCsv).toContain('Clothing');
|
expect(productCsv).toContain('Clothing');
|
||||||
expect(pickItemsCsv).toContain('Shirt');
|
expect(pickItemsCsv).toContain('Shirt');
|
||||||
expect(db.importExportLogs.items).toHaveLength(1);
|
expect((db.importExportLogs as any).items).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('imports products while deduplicating by name and auto-creating categories', async () => {
|
it('imports products while deduplicating by name and auto-creating categories', async () => {
|
||||||
@@ -155,8 +155,9 @@ describe('import/export service', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.log.summary.inserted).toBeGreaterThanOrEqual(1);
|
expect(result.log.summary.inserted).toBeGreaterThanOrEqual(1);
|
||||||
expect(db.products.items.find((p) => p.name === 'Existing')).toBeTruthy();
|
expect((db.products as any).items.find((p: any) => p.name === 'Existing')).toBeTruthy();
|
||||||
expect(db.products.items.find((p) => p.name === 'New Shirt')).toBeTruthy();
|
expect((db.products as any).items.find((p: any) => p.name === 'New Shirt')).toBeTruthy();
|
||||||
expect(db.categories.items.find((c) => c.name === 'New Category')).toBeTruthy();
|
expect((db.categories as any).items.find((c: any) => c.name === 'New Category')).toBeTruthy();
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,8 +52,9 @@ const parseCsv = async (content: string) => {
|
|||||||
const result = Papa.parse<Record<string, string>>(content, {
|
const result = Papa.parse<Record<string, string>>(content, {
|
||||||
header: true,
|
header: true,
|
||||||
skipEmptyLines: true,
|
skipEmptyLines: true,
|
||||||
transformHeader: (header) => header.trim().toLowerCase(),
|
transformHeader: (header: string) => header.trim().toLowerCase(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
if (result.errors.length > 0) {
|
||||||
throw new Error(result.errors[0].message);
|
throw new Error(result.errors[0].message);
|
||||||
@@ -330,11 +331,7 @@ export const importFiles = async (
|
|||||||
try {
|
try {
|
||||||
await db.transaction(
|
await db.transaction(
|
||||||
'rw',
|
'rw',
|
||||||
db.areas,
|
[db.areas, db.categories, db.products, db.pickLists, db.pickItems],
|
||||||
db.categories,
|
|
||||||
db.products,
|
|
||||||
db.pickLists,
|
|
||||||
db.pickItems,
|
|
||||||
async () => {
|
async () => {
|
||||||
const parseTimestamp = (value?: string) =>
|
const parseTimestamp = (value?: string) =>
|
||||||
value && value.trim() !== '' ? Number(value) || now : now;
|
value && value.trim() !== '' ? Number(value) || now : now;
|
||||||
|
|||||||
Reference in New Issue
Block a user