Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08314098a2 | ||
|
|
96bbc6c936 | ||
|
|
c024881caa | ||
|
|
df33a81c67 | ||
|
|
22559112f0 | ||
|
|
71327bddd8 | ||
|
|
2d0d1de32d | ||
|
|
1251517227 |
17266
backend/package-lock.json
generated
Normal file
17266
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,7 @@
|
||||
"chokidar": "^4.0.3",
|
||||
"cors": "2.8.5",
|
||||
"csv-parser": "^3.0.0",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "4.18.2",
|
||||
"formidable": "1.2.2",
|
||||
"helmet": "4.1.1",
|
||||
|
||||
@ -296,7 +296,7 @@ const DataEntitiesData = [
|
||||
|
||||
|
||||
|
||||
"code": "customers",
|
||||
"code": "products",
|
||||
|
||||
|
||||
|
||||
@ -342,7 +342,7 @@ const DataEntitiesData = [
|
||||
|
||||
|
||||
|
||||
"code": "products",
|
||||
"code": "customers",
|
||||
|
||||
|
||||
|
||||
@ -434,7 +434,7 @@ const DataEntitiesData = [
|
||||
|
||||
|
||||
|
||||
"code": "customers",
|
||||
"code": "availability",
|
||||
|
||||
|
||||
|
||||
|
||||
@ -20,4 +20,27 @@ module.exports = class Helpers {
|
||||
static jwtSign(data) {
|
||||
return jwt.sign(data, config.secret_key, {expiresIn: '6h'});
|
||||
};
|
||||
|
||||
static async exportData(res, type, entityName, fields, rows) {
|
||||
if (type === 'csv') {
|
||||
const { parse } = require('json2csv');
|
||||
const csv = parse(rows, { fields });
|
||||
res.header('Content-Type', 'text/csv');
|
||||
res.attachment(`${entityName}.csv`);
|
||||
return res.send(csv);
|
||||
} else if (type === 'xlsx') {
|
||||
const ExcelJS = require('exceljs');
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet(entityName);
|
||||
|
||||
worksheet.columns = fields.map(field => ({ header: field, key: field }));
|
||||
worksheet.addRows(rows);
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', `attachment; filename=${entityName}.xlsx`);
|
||||
|
||||
await workbook.xlsx.write(res);
|
||||
return res.end();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -3,15 +3,14 @@ const express = require('express');
|
||||
|
||||
const Availability_recordsService = require('../services/availability_records');
|
||||
const Availability_recordsDBApi = require('../db/api/availability_records');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Helpers = require('../helpers');
|
||||
const wrapAsync = Helpers.wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const { parse } = require('json2csv');
|
||||
|
||||
|
||||
const {
|
||||
checkCrudPermissions,
|
||||
@ -303,20 +302,16 @@ router.get('/', wrapAsync(async (req, res) => {
|
||||
const payload = await Availability_recordsDBApi.findAll(
|
||||
req.query, globalAccess, { currentUser }
|
||||
);
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id','location_code','product_sku','source_reference',
|
||||
|
||||
'quantity_on_hand','quantity_available',
|
||||
'as_of',
|
||||
];
|
||||
const opts = { fields };
|
||||
if (filetype && (filetype === 'csv' || filetype === 'xlsx')) {
|
||||
let fields = ['id','location_code','product_sku','source_reference', 'quantity_on_hand','quantity_available', 'as_of'];
|
||||
if (req.query.fields) {
|
||||
fields = req.query.fields.split(',');
|
||||
}
|
||||
try {
|
||||
const csv = parse(payload.rows, opts);
|
||||
res.status(200).attachment(csv);
|
||||
res.send(csv)
|
||||
|
||||
await Helpers.exportData(res, filetype, 'availability_records', fields, payload.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Export failed');
|
||||
}
|
||||
} else {
|
||||
res.status(200).send(payload);
|
||||
|
||||
@ -3,15 +3,14 @@ const express = require('express');
|
||||
|
||||
const CustomersService = require('../services/customers');
|
||||
const CustomersDBApi = require('../db/api/customers');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Helpers = require('../helpers');
|
||||
const wrapAsync = Helpers.wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const { parse } = require('json2csv');
|
||||
|
||||
|
||||
const {
|
||||
checkCrudPermissions,
|
||||
@ -307,20 +306,16 @@ router.get('/', wrapAsync(async (req, res) => {
|
||||
const payload = await CustomersDBApi.findAll(
|
||||
req.query, globalAccess, { currentUser }
|
||||
);
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id','customer_code','name','email','phone','billing_address','shipping_address',
|
||||
|
||||
|
||||
|
||||
];
|
||||
const opts = { fields };
|
||||
if (filetype && (filetype === 'csv' || filetype === 'xlsx')) {
|
||||
let fields = ['id','customer_code','name','email','phone','billing_address','shipping_address'];
|
||||
if (req.query.fields) {
|
||||
fields = req.query.fields.split(',');
|
||||
}
|
||||
try {
|
||||
const csv = parse(payload.rows, opts);
|
||||
res.status(200).attachment(csv);
|
||||
res.send(csv)
|
||||
|
||||
await Helpers.exportData(res, filetype, 'customers', fields, payload.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Export failed');
|
||||
}
|
||||
} else {
|
||||
res.status(200).send(payload);
|
||||
|
||||
@ -3,15 +3,14 @@ const express = require('express');
|
||||
|
||||
const ProductsService = require('../services/products');
|
||||
const ProductsDBApi = require('../db/api/products');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Helpers = require('../helpers');
|
||||
const wrapAsync = Helpers.wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const { parse } = require('json2csv');
|
||||
|
||||
|
||||
const {
|
||||
checkCrudPermissions,
|
||||
@ -307,20 +306,16 @@ router.get('/', wrapAsync(async (req, res) => {
|
||||
const payload = await ProductsDBApi.findAll(
|
||||
req.query, globalAccess, { currentUser }
|
||||
);
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id','sku','name','description','uom','currency',
|
||||
|
||||
'price',
|
||||
|
||||
];
|
||||
const opts = { fields };
|
||||
if (filetype && (filetype === 'csv' || filetype === 'xlsx')) {
|
||||
let fields = ['id','sku','name','description','uom','currency', 'price'];
|
||||
if (req.query.fields) {
|
||||
fields = req.query.fields.split(',');
|
||||
}
|
||||
try {
|
||||
const csv = parse(payload.rows, opts);
|
||||
res.status(200).attachment(csv);
|
||||
res.send(csv)
|
||||
|
||||
await Helpers.exportData(res, filetype, 'products', fields, payload.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Export failed');
|
||||
}
|
||||
} else {
|
||||
res.status(200).send(payload);
|
||||
|
||||
@ -3,15 +3,14 @@ const express = require('express');
|
||||
|
||||
const SuppliersService = require('../services/suppliers');
|
||||
const SuppliersDBApi = require('../db/api/suppliers');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const Helpers = require('../helpers');
|
||||
const wrapAsync = Helpers.wrapAsync;
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const { parse } = require('json2csv');
|
||||
|
||||
|
||||
const {
|
||||
checkCrudPermissions,
|
||||
@ -304,20 +303,16 @@ router.get('/', wrapAsync(async (req, res) => {
|
||||
const payload = await SuppliersDBApi.findAll(
|
||||
req.query, globalAccess, { currentUser }
|
||||
);
|
||||
if (filetype && filetype === 'csv') {
|
||||
const fields = ['id','supplier_code','name','email','phone','address',
|
||||
|
||||
|
||||
|
||||
];
|
||||
const opts = { fields };
|
||||
if (filetype && (filetype === 'csv' || filetype === 'xlsx')) {
|
||||
let fields = ['id','supplier_code','name','email','phone','address'];
|
||||
if (req.query.fields) {
|
||||
fields = req.query.fields.split(',');
|
||||
}
|
||||
try {
|
||||
const csv = parse(payload.rows, opts);
|
||||
res.status(200).attachment(csv);
|
||||
res.send(csv)
|
||||
|
||||
await Helpers.exportData(res, filetype, 'suppliers', fields, payload.rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).send('Export failed');
|
||||
}
|
||||
} else {
|
||||
res.status(200).send(payload);
|
||||
|
||||
3238
backend/yarn.lock
3238
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,16 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
label: 'Dashboard',
|
||||
},
|
||||
{
|
||||
href: '/imports',
|
||||
label: 'Import Wizard',
|
||||
icon: icon.mdiFileUploadOutline,
|
||||
},
|
||||
{
|
||||
href: '/export-wizard',
|
||||
label: 'Export Wizard',
|
||||
icon: icon.mdiFileExportOutline,
|
||||
},
|
||||
|
||||
{
|
||||
href: '/users/users-list',
|
||||
|
||||
@ -65,7 +65,31 @@ const CustomersTablesPage = () => {
|
||||
};
|
||||
|
||||
const getCustomersCSV = async () => {
|
||||
const response = await axios({url: '/customers?filetype=csv', method: 'GET',responseType: 'blob'});
|
||||
let filterQuery = '';
|
||||
filterItems.forEach((item) => {
|
||||
const isRangeFilter = filters.find(
|
||||
(filter) =>
|
||||
filter.title === item.fields.selectedField &&
|
||||
(filter.number || filter.date),
|
||||
);
|
||||
|
||||
if (isRangeFilter) {
|
||||
const from = item.fields.filterValueFrom;
|
||||
const to = item.fields.filterValueTo;
|
||||
if (from) {
|
||||
filterQuery += `&${item.fields.selectedField}Range=${from}`;
|
||||
}
|
||||
if (to) {
|
||||
filterQuery += `&${item.fields.selectedField}Range=${to}`;
|
||||
}
|
||||
} else {
|
||||
const value = item.fields.filterValue;
|
||||
if (value) {
|
||||
filterQuery += `&${item.fields.selectedField}=${value}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
const response = await axios({url: `/customers?filetype=csv${filterQuery}`, method: 'GET',responseType: 'blob'});
|
||||
const type = response.headers['content-type']
|
||||
const blob = new Blob([response.data], { type: type })
|
||||
const link = document.createElement('a')
|
||||
|
||||
@ -65,7 +65,31 @@ const CustomersTablesPage = () => {
|
||||
};
|
||||
|
||||
const getCustomersCSV = async () => {
|
||||
const response = await axios({url: '/customers?filetype=csv', method: 'GET',responseType: 'blob'});
|
||||
let filterQuery = '';
|
||||
filterItems.forEach((item) => {
|
||||
const isRangeFilter = filters.find(
|
||||
(filter) =>
|
||||
filter.title === item.fields.selectedField &&
|
||||
(filter.number || filter.date),
|
||||
);
|
||||
|
||||
if (isRangeFilter) {
|
||||
const from = item.fields.filterValueFrom;
|
||||
const to = item.fields.filterValueTo;
|
||||
if (from) {
|
||||
filterQuery += `&${item.fields.selectedField}Range=${from}`;
|
||||
}
|
||||
if (to) {
|
||||
filterQuery += `&${item.fields.selectedField}Range=${to}`;
|
||||
}
|
||||
} else {
|
||||
const value = item.fields.filterValue;
|
||||
if (value) {
|
||||
filterQuery += `&${item.fields.selectedField}=${value}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
const response = await axios({url: `/customers?filetype=csv${filterQuery}`, method: 'GET',responseType: 'blob'});
|
||||
const type = response.headers['content-type']
|
||||
const blob = new Blob([response.data], { type: type })
|
||||
const link = document.createElement('a')
|
||||
|
||||
172
frontend/src/pages/export-wizard.tsx
Normal file
172
frontend/src/pages/export-wizard.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import {
|
||||
mdiFileExportOutline,
|
||||
mdiPackageVariantClosed,
|
||||
mdiAccountGroupOutline,
|
||||
mdiTruckOutline,
|
||||
mdiWarehouse,
|
||||
mdiChevronRight,
|
||||
mdiChevronLeft,
|
||||
mdiDownload,
|
||||
} from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
import CardBox from '../components/CardBox'
|
||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||
import SectionMain from '../components/SectionMain'
|
||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||
import { getPageTitle } from '../config'
|
||||
import BaseButton from '../components/BaseButton'
|
||||
import BaseButtons from '../components/BaseButtons'
|
||||
import FormField from '../components/FormField'
|
||||
import axios from 'axios'
|
||||
import BaseIcon from '../components/BaseIcon'
|
||||
|
||||
const entities = [
|
||||
{ id: 'products', label: 'Products', icon: mdiPackageVariantClosed, fields: ['id', 'tenant', 'sku', 'name', 'description', 'uom', 'currency', 'price'] },
|
||||
{ id: 'customers', label: 'Customers', icon: mdiAccountGroupOutline, fields: ['id', 'tenant', 'customer_code', 'name', 'email', 'phone', 'billing_address', 'shipping_address'] },
|
||||
{ id: 'suppliers', label: 'Suppliers', icon: mdiTruckOutline, fields: ['id', 'tenant', 'supplier_code', 'name', 'email', 'phone', 'address'] },
|
||||
{ id: 'availability_records', label: 'Availability records', icon: mdiWarehouse, fields: ['id', 'tenant', 'location_code', 'product_sku', 'source_reference', 'quantity_on_hand', 'quantity_available', 'as_of'] },
|
||||
]
|
||||
|
||||
const ExportWizard = () => {
|
||||
const [step, setStep] = useState(1)
|
||||
const [selectedEntity, setSelectedEntity] = useState('')
|
||||
const [selectedFields, setSelectedFields] = useState<string[]>([])
|
||||
|
||||
const currentEntity = entities.find((e) => e.id === selectedEntity)
|
||||
|
||||
const handleEntitySelect = (entityId: string) => {
|
||||
setSelectedEntity(entityId)
|
||||
const entity = entities.find((e) => e.id === entityId)
|
||||
if (entity) {
|
||||
setSelectedFields(entity.fields)
|
||||
}
|
||||
setStep(2)
|
||||
}
|
||||
|
||||
const toggleField = (field: string) => {
|
||||
if (selectedFields.includes(field)) {
|
||||
setSelectedFields(selectedFields.filter((f) => f !== field))
|
||||
} else {
|
||||
setSelectedFields([...selectedFields, field])
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/${selectedEntity}`, {
|
||||
params: {
|
||||
filetype: 'xlsx',
|
||||
fields: selectedFields.join(','),
|
||||
},
|
||||
responseType: 'blob',
|
||||
})
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', `${selectedEntity}_export_${new Date().toISOString().split('T')[0]}.xlsx`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
} catch (error) {
|
||||
console.error('Export failed', error)
|
||||
alert('Export failed. Please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Export Wizard')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiFileExportOutline} title="Export Wizard" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<CardBox className="mb-6">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className={`flex flex-col items-center ${step >= 1 ? 'text-blue-600' : 'text-gray-400'}`}>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${step >= 1 ? 'border-blue-600 bg-blue-50' : 'border-gray-300'}`}>1</div>
|
||||
<span className="text-xs mt-1 font-bold">Select Entity</span>
|
||||
</div>
|
||||
<div className={`flex-1 h-1 mx-4 ${step >= 2 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
|
||||
<div className={`flex flex-col items-center ${step >= 2 ? 'text-blue-600' : 'text-gray-400'}`}>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${step >= 2 ? 'border-blue-600 bg-blue-50' : 'border-gray-300'}`}>2</div>
|
||||
<span className="text-xs mt-1 font-bold">Select Fields</span>
|
||||
</div>
|
||||
<div className={`flex-1 h-1 mx-4 ${step >= 3 ? 'bg-blue-600' : 'bg-gray-200'}`}></div>
|
||||
<div className={`flex flex-col items-center ${step >= 3 ? 'text-blue-600' : 'text-gray-400'}`}>
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${step >= 3 ? 'border-blue-600 bg-blue-50' : 'border-gray-300'}`}>3</div>
|
||||
<span className="text-xs mt-1 font-bold">Download</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{step === 1 && (
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">What would you like to export?</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{entities.map((entity) => (
|
||||
<div
|
||||
key={entity.id}
|
||||
onClick={() => handleEntitySelect(entity.id)}
|
||||
className="p-6 border-2 rounded-lg cursor-pointer hover:border-blue-500 hover:bg-blue-50 transition-colors flex flex-col items-center text-center group"
|
||||
>
|
||||
<BaseIcon path={entity.icon} size={48} className="mb-4 text-gray-500 group-hover:text-blue-500" />
|
||||
<span className="font-bold">{entity.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 2 && currentEntity && (
|
||||
<div>
|
||||
<div className="flex items-center mb-4">
|
||||
<BaseIcon path={currentEntity.icon} className="mr-2" />
|
||||
<h2 className="text-xl font-bold">Select fields for {currentEntity.label}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 mb-8">
|
||||
{currentEntity.fields.map((field) => (
|
||||
<label key={field} className="flex items-center space-x-3 p-3 border rounded hover:bg-gray-50 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedFields.includes(field)}
|
||||
onChange={() => toggleField(field)}
|
||||
className="form-checkbox h-5 w-5 text-blue-600"
|
||||
/>
|
||||
<span className="capitalize">{field.replace('_', ' ')}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<BaseButtons>
|
||||
<BaseButton label="Back" icon={mdiChevronLeft} onClick={() => setStep(1)} />
|
||||
<BaseButton label="Next" color="info" icon={mdiChevronRight} onClick={() => setStep(3)} disabled={selectedFields.length === 0} />
|
||||
</BaseButtons>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && currentEntity && (
|
||||
<div className="text-center py-8">
|
||||
<BaseIcon path={mdiDownload} size={64} className="mb-4 text-blue-500 mx-auto" />
|
||||
<h2 className="text-2xl font-bold mb-2">Ready to export!</h2>
|
||||
<p className="text-gray-500 mb-8">
|
||||
You are about to export <strong>{currentEntity.label}</strong> with <strong>{selectedFields.length}</strong> fields.
|
||||
</p>
|
||||
<BaseButtons className="justify-center">
|
||||
<BaseButton label="Back" icon={mdiChevronLeft} onClick={() => setStep(2)} />
|
||||
<BaseButton label="Download XLSX" color="success" icon={mdiDownload} onClick={handleExport} />
|
||||
</BaseButtons>
|
||||
</div>
|
||||
)}
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ExportWizard.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||
}
|
||||
|
||||
export default ExportWizard
|
||||
92
frontend/src/pages/imports/index.tsx
Normal file
92
frontend/src/pages/imports/index.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { mdiFileUploadOutline, mdiPackageVariantClosed, mdiAccountGroupOutline, mdiTruckOutline, mdiWarehouse } from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../../config';
|
||||
import axios from 'axios';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
|
||||
const ImportWizard = () => {
|
||||
const [entities, setEntities] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchEntities = async () => {
|
||||
try {
|
||||
const response = await axios.get('/data_entities');
|
||||
setEntities(response.data.rows || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch entities', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchEntities();
|
||||
}, []);
|
||||
|
||||
const getIcon = (code: string) => {
|
||||
switch (code) {
|
||||
case 'products': return mdiPackageVariantClosed;
|
||||
case 'customers': return mdiAccountGroupOutline;
|
||||
case 'suppliers': return mdiTruckOutline;
|
||||
case 'availability': return mdiWarehouse;
|
||||
default: return mdiFileUploadOutline;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Import Wizard')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiFileUploadOutline} title='Import Wizard' main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
|
||||
{loading ? (
|
||||
<p>Loading entities...</p>
|
||||
) : (
|
||||
entities.map((entity: any) => (
|
||||
<CardBox key={entity.id} className="hover:shadow-lg transition-shadow border-2 border-transparent hover:border-indigo-500">
|
||||
<div className="flex flex-col items-center text-center p-4">
|
||||
<div className="bg-indigo-100 p-4 rounded-full mb-4">
|
||||
<svg viewBox="0 0 24 24" className="w-12 h-12 text-indigo-600">
|
||||
<path fill="currentColor" d={getIcon(entity.code)} />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-2">{entity.name}</h3>
|
||||
<p className="text-gray-500 text-sm mb-4 h-10">{entity.description}</p>
|
||||
<BaseButton
|
||||
color="info"
|
||||
label="Start Import"
|
||||
className="w-full"
|
||||
href={`/imports/upload?entityId=${entity.id}&code=${entity.code}`}
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!loading && entities.length === 0 && (
|
||||
<CardBox>
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 italic">No entities available for import. Please contact admin.</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
)}
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ImportWizard.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default ImportWizard;
|
||||
118
frontend/src/pages/imports/upload.tsx
Normal file
118
frontend/src/pages/imports/upload.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { mdiArrowLeft, mdiFileUploadOutline, mdiCheckCircle, mdiAlertCircle } from '@mdi/js';
|
||||
import Head from 'next/head';
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import { getPageTitle } from '../../config';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import BaseIcon from '../../components/BaseIcon';
|
||||
|
||||
const ImportUpload = () => {
|
||||
const router = useRouter();
|
||||
const { code } = router.query;
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
const selectedFile = e.target.files[0];
|
||||
if (selectedFile.name.endsWith('.csv') || selectedFile.name.endsWith('.xls') || selectedFile.name.endsWith('.xlsx')) {
|
||||
setFile(selectedFile);
|
||||
setError(null);
|
||||
} else {
|
||||
setError('Please select a valid CSV or Excel file.');
|
||||
setFile(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!file) return;
|
||||
setUploading(true);
|
||||
// In a real app, we would upload the file here.
|
||||
// For now, we'll simulate a successful upload and redirect to a mapping page (next task)
|
||||
setTimeout(() => {
|
||||
setUploading(false);
|
||||
alert('File uploaded successfully! Next: Column Mapping (Coming Soon)');
|
||||
router.push('/imports');
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle(`Upload ${code}`)}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiFileUploadOutline} title={`Upload ${code} Data`} main>
|
||||
<BaseButton href="/imports" icon={mdiArrowLeft} label="Back" color="white" />
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<CardBox className="max-w-2xl mx-auto">
|
||||
<div className="space-y-6">
|
||||
<div className="text-center p-8 border-2 border-dashed border-gray-300 rounded-lg bg-gray-50">
|
||||
<BaseIcon path={mdiFileUploadOutline} size={48} className="mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-lg font-medium text-gray-700">Select file to import for {code}</p>
|
||||
<p className="text-sm text-gray-500 mb-6">Supported formats: CSV, XLS, XLSX</p>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="file-upload"
|
||||
className="hidden"
|
||||
accept=".csv, .xls, .xlsx"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
className="cursor-pointer bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
Browse Files
|
||||
</label>
|
||||
|
||||
{file && (
|
||||
<div className="mt-4 flex items-center justify-center text-indigo-600 font-medium">
|
||||
<BaseIcon path={mdiCheckCircle} size={20} className="mr-2" />
|
||||
{file.name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 flex items-center justify-center text-red-600 font-medium">
|
||||
<BaseIcon path={mdiAlertCircle} size={20} className="mr-2" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 p-4 rounded-md">
|
||||
<h4 className="text-blue-800 font-bold mb-1">Import Rules for {code}:</h4>
|
||||
<ul className="text-blue-700 text-sm list-disc list-inside">
|
||||
<li>File must include a header row.</li>
|
||||
<li>Required fields must not be empty.</li>
|
||||
<li>Data will be validated against tenant-specific rules.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<BaseButton
|
||||
color="info"
|
||||
label={uploading ? "Uploading..." : "Process File"}
|
||||
disabled={!file || uploading}
|
||||
className="w-full"
|
||||
onClick={handleUpload}
|
||||
/>
|
||||
</div>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ImportUpload.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default ImportUpload;
|
||||
Loading…
x
Reference in New Issue
Block a user