Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
17266
backend/package-lock.json
generated
17266
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,6 @@
|
|||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"csv-parser": "^3.0.0",
|
"csv-parser": "^3.0.0",
|
||||||
"exceljs": "^4.4.0",
|
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"formidable": "1.2.2",
|
"formidable": "1.2.2",
|
||||||
"helmet": "4.1.1",
|
"helmet": "4.1.1",
|
||||||
|
|||||||
@ -296,7 +296,7 @@ const DataEntitiesData = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
"code": "products",
|
"code": "customers",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -342,7 +342,7 @@ const DataEntitiesData = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
"code": "customers",
|
"code": "products",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -434,7 +434,7 @@ const DataEntitiesData = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
"code": "availability",
|
"code": "customers",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -20,27 +20,4 @@ module.exports = class Helpers {
|
|||||||
static jwtSign(data) {
|
static jwtSign(data) {
|
||||||
return jwt.sign(data, config.secret_key, {expiresIn: '6h'});
|
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,14 +3,15 @@ const express = require('express');
|
|||||||
|
|
||||||
const Availability_recordsService = require('../services/availability_records');
|
const Availability_recordsService = require('../services/availability_records');
|
||||||
const Availability_recordsDBApi = require('../db/api/availability_records');
|
const Availability_recordsDBApi = require('../db/api/availability_records');
|
||||||
const Helpers = require('../helpers');
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
const wrapAsync = Helpers.wrapAsync;
|
|
||||||
|
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const { parse } = require('json2csv');
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
@ -302,16 +303,20 @@ router.get('/', wrapAsync(async (req, res) => {
|
|||||||
const payload = await Availability_recordsDBApi.findAll(
|
const payload = await Availability_recordsDBApi.findAll(
|
||||||
req.query, globalAccess, { currentUser }
|
req.query, globalAccess, { currentUser }
|
||||||
);
|
);
|
||||||
if (filetype && (filetype === 'csv' || filetype === 'xlsx')) {
|
if (filetype && filetype === 'csv') {
|
||||||
let fields = ['id','location_code','product_sku','source_reference', 'quantity_on_hand','quantity_available', 'as_of'];
|
const fields = ['id','location_code','product_sku','source_reference',
|
||||||
if (req.query.fields) {
|
|
||||||
fields = req.query.fields.split(',');
|
'quantity_on_hand','quantity_available',
|
||||||
}
|
'as_of',
|
||||||
|
];
|
||||||
|
const opts = { fields };
|
||||||
try {
|
try {
|
||||||
await Helpers.exportData(res, filetype, 'availability_records', fields, payload.rows);
|
const csv = parse(payload.rows, opts);
|
||||||
|
res.status(200).attachment(csv);
|
||||||
|
res.send(csv)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send('Export failed');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
|
|||||||
@ -3,14 +3,15 @@ const express = require('express');
|
|||||||
|
|
||||||
const CustomersService = require('../services/customers');
|
const CustomersService = require('../services/customers');
|
||||||
const CustomersDBApi = require('../db/api/customers');
|
const CustomersDBApi = require('../db/api/customers');
|
||||||
const Helpers = require('../helpers');
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
const wrapAsync = Helpers.wrapAsync;
|
|
||||||
|
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const { parse } = require('json2csv');
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
@ -306,16 +307,20 @@ router.get('/', wrapAsync(async (req, res) => {
|
|||||||
const payload = await CustomersDBApi.findAll(
|
const payload = await CustomersDBApi.findAll(
|
||||||
req.query, globalAccess, { currentUser }
|
req.query, globalAccess, { currentUser }
|
||||||
);
|
);
|
||||||
if (filetype && (filetype === 'csv' || filetype === 'xlsx')) {
|
if (filetype && filetype === 'csv') {
|
||||||
let fields = ['id','customer_code','name','email','phone','billing_address','shipping_address'];
|
const fields = ['id','customer_code','name','email','phone','billing_address','shipping_address',
|
||||||
if (req.query.fields) {
|
|
||||||
fields = req.query.fields.split(',');
|
|
||||||
}
|
|
||||||
|
];
|
||||||
|
const opts = { fields };
|
||||||
try {
|
try {
|
||||||
await Helpers.exportData(res, filetype, 'customers', fields, payload.rows);
|
const csv = parse(payload.rows, opts);
|
||||||
|
res.status(200).attachment(csv);
|
||||||
|
res.send(csv)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send('Export failed');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
|
|||||||
@ -3,14 +3,15 @@ const express = require('express');
|
|||||||
|
|
||||||
const ProductsService = require('../services/products');
|
const ProductsService = require('../services/products');
|
||||||
const ProductsDBApi = require('../db/api/products');
|
const ProductsDBApi = require('../db/api/products');
|
||||||
const Helpers = require('../helpers');
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
const wrapAsync = Helpers.wrapAsync;
|
|
||||||
|
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const { parse } = require('json2csv');
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
@ -306,16 +307,20 @@ router.get('/', wrapAsync(async (req, res) => {
|
|||||||
const payload = await ProductsDBApi.findAll(
|
const payload = await ProductsDBApi.findAll(
|
||||||
req.query, globalAccess, { currentUser }
|
req.query, globalAccess, { currentUser }
|
||||||
);
|
);
|
||||||
if (filetype && (filetype === 'csv' || filetype === 'xlsx')) {
|
if (filetype && filetype === 'csv') {
|
||||||
let fields = ['id','sku','name','description','uom','currency', 'price'];
|
const fields = ['id','sku','name','description','uom','currency',
|
||||||
if (req.query.fields) {
|
|
||||||
fields = req.query.fields.split(',');
|
'price',
|
||||||
}
|
|
||||||
|
];
|
||||||
|
const opts = { fields };
|
||||||
try {
|
try {
|
||||||
await Helpers.exportData(res, filetype, 'products', fields, payload.rows);
|
const csv = parse(payload.rows, opts);
|
||||||
|
res.status(200).attachment(csv);
|
||||||
|
res.send(csv)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send('Export failed');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
|
|||||||
@ -3,14 +3,15 @@ const express = require('express');
|
|||||||
|
|
||||||
const SuppliersService = require('../services/suppliers');
|
const SuppliersService = require('../services/suppliers');
|
||||||
const SuppliersDBApi = require('../db/api/suppliers');
|
const SuppliersDBApi = require('../db/api/suppliers');
|
||||||
const Helpers = require('../helpers');
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
const wrapAsync = Helpers.wrapAsync;
|
|
||||||
|
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
const { parse } = require('json2csv');
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
@ -303,16 +304,20 @@ router.get('/', wrapAsync(async (req, res) => {
|
|||||||
const payload = await SuppliersDBApi.findAll(
|
const payload = await SuppliersDBApi.findAll(
|
||||||
req.query, globalAccess, { currentUser }
|
req.query, globalAccess, { currentUser }
|
||||||
);
|
);
|
||||||
if (filetype && (filetype === 'csv' || filetype === 'xlsx')) {
|
if (filetype && filetype === 'csv') {
|
||||||
let fields = ['id','supplier_code','name','email','phone','address'];
|
const fields = ['id','supplier_code','name','email','phone','address',
|
||||||
if (req.query.fields) {
|
|
||||||
fields = req.query.fields.split(',');
|
|
||||||
}
|
|
||||||
|
];
|
||||||
|
const opts = { fields };
|
||||||
try {
|
try {
|
||||||
await Helpers.exportData(res, filetype, 'suppliers', fields, payload.rows);
|
const csv = parse(payload.rows, opts);
|
||||||
|
res.status(200).attachment(csv);
|
||||||
|
res.send(csv)
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send('Export failed');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
|
|||||||
3218
backend/yarn.lock
3218
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -7,16 +7,6 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: '/imports',
|
|
||||||
label: 'Import Wizard',
|
|
||||||
icon: icon.mdiFileUploadOutline,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/export-wizard',
|
|
||||||
label: 'Export Wizard',
|
|
||||||
icon: icon.mdiFileExportOutline,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
|
|||||||
@ -65,31 +65,7 @@ const CustomersTablesPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCustomersCSV = async () => {
|
const getCustomersCSV = async () => {
|
||||||
let filterQuery = '';
|
const response = await axios({url: '/customers?filetype=csv', method: 'GET',responseType: 'blob'});
|
||||||
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 type = response.headers['content-type']
|
||||||
const blob = new Blob([response.data], { type: type })
|
const blob = new Blob([response.data], { type: type })
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
|
|||||||
@ -65,31 +65,7 @@ const CustomersTablesPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCustomersCSV = async () => {
|
const getCustomersCSV = async () => {
|
||||||
let filterQuery = '';
|
const response = await axios({url: '/customers?filetype=csv', method: 'GET',responseType: 'blob'});
|
||||||
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 type = response.headers['content-type']
|
||||||
const blob = new Blob([response.data], { type: type })
|
const blob = new Blob([response.data], { type: type })
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
|
|||||||
@ -1,172 +0,0 @@
|
|||||||
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
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
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