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:
2025-11-25 09:15:30 +10:00
parent 2ec46c733b
commit fc55073ca6
7 changed files with 45 additions and 29 deletions
-1
View File
@@ -1,4 +1,3 @@
version: '3.9'
services: services:
stockfill: stockfill:
build: . build: .
+11
View File
@@ -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",
+1
View File
@@ -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",
+16 -9
View File
@@ -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>
+1 -1
View File
@@ -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
+8 -7
View File
@@ -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();
}); });
}); });
+3 -6
View File
@@ -52,9 +52,10 @@ 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;