This commit is contained in:
@@ -22,13 +22,20 @@ async function ensureSchema() {
|
||||
description text not null,
|
||||
item_condition varchar(50) not null,
|
||||
has_box boolean not null,
|
||||
image_path text not null,
|
||||
image_path text null,
|
||||
image_data longblob null,
|
||||
image_mime varchar(100) null,
|
||||
created_at timestamp default current_timestamp
|
||||
) engine=InnoDB;
|
||||
`;
|
||||
await pool.query(sql);
|
||||
// backfill for older tables without sku
|
||||
await pool.query(`alter table items add column if not exists sku varchar(64) null` as any);
|
||||
// backfill nullable image_path
|
||||
await pool.query(`alter table items modify column image_path text null` as any);
|
||||
// add image_data and image_mime if missing
|
||||
await pool.query(`alter table items add column if not exists image_data longblob null` as any);
|
||||
await pool.query(`alter table items add column if not exists image_mime varchar(100) null` as any);
|
||||
}
|
||||
|
||||
let schemaEnsured: Promise<void> | null = null;
|
||||
@@ -41,9 +48,20 @@ export const db = {
|
||||
async insertItem(item: Omit<TrainItem, 'id'>) {
|
||||
await getReady();
|
||||
const [result] = await pool.execute(
|
||||
`insert into items (manufacturer, model, sku, quantity, description, item_condition, has_box, image_path)
|
||||
values (?,?,?,?,?,?,?,?)`,
|
||||
[item.manufacturer, item.model, item.sku || null, item.quantity, item.description, item.condition, item.hasBox, item.imagePath]
|
||||
`insert into items (manufacturer, model, sku, quantity, description, item_condition, has_box, image_path, image_data, image_mime)
|
||||
values (?,?,?,?,?,?,?,?,?,?)`,
|
||||
[
|
||||
item.manufacturer,
|
||||
item.model,
|
||||
item.sku || null,
|
||||
item.quantity,
|
||||
item.description,
|
||||
item.condition,
|
||||
item.hasBox,
|
||||
item.imagePath || null,
|
||||
(item as any).imageData || null,
|
||||
(item as any).imageMime || null
|
||||
]
|
||||
);
|
||||
const insertId = (result as any).insertId as number;
|
||||
const [rows] = await pool.query(
|
||||
@@ -79,6 +97,27 @@ export const db = {
|
||||
if (row) { row.condition = row.cond; delete row.cond; }
|
||||
return row;
|
||||
},
|
||||
async getItemWithImage(id: number) {
|
||||
await getReady();
|
||||
const [rows] = await pool.query(
|
||||
`select id, manufacturer, model, sku, quantity, description, item_condition as cond, has_box as hasBox, image_path as imagePath, image_data as imageData, image_mime as imageMime, created_at as createdAt
|
||||
from items where id = ?`,
|
||||
[id]
|
||||
);
|
||||
const row = (rows as any[])[0] || null;
|
||||
if (row) { row.condition = row.cond; delete row.cond; }
|
||||
return row;
|
||||
},
|
||||
async listItemsWithImage() {
|
||||
await getReady();
|
||||
const [rows] = await pool.query(
|
||||
`select id, manufacturer, model, sku, quantity, description, item_condition as cond, has_box as hasBox, image_path as imagePath, image_data as imageData, image_mime as imageMime, created_at as createdAt
|
||||
from items order by id asc`
|
||||
);
|
||||
const arr = rows as any[];
|
||||
for (const r of arr) { r.condition = r.cond; delete r.cond; }
|
||||
return arr;
|
||||
},
|
||||
async deleteItem(id: number) {
|
||||
await getReady();
|
||||
const [res] = await pool.query('delete from items where id = ?', [id]);
|
||||
|
||||
@@ -21,6 +21,10 @@ router.post('/upload', async (req, res) => {
|
||||
|
||||
const analysis = await analyzeImageToMetadata(savePath);
|
||||
|
||||
const imageBuffer = await fs.readFile(savePath);
|
||||
const imageMime: string = file.mimetype || 'application/octet-stream';
|
||||
|
||||
// Insert with image bytes and mime; do not persist path
|
||||
const saved = await db.insertItem({
|
||||
manufacturer: analysis.manufacturer,
|
||||
model: analysis.model,
|
||||
@@ -29,8 +33,13 @@ router.post('/upload', async (req, res) => {
|
||||
description: analysis.description,
|
||||
condition: analysis.condition,
|
||||
hasBox: analysis.hasBox,
|
||||
imagePath: savePath,
|
||||
});
|
||||
imagePath: null as any,
|
||||
imageData: imageBuffer as any,
|
||||
imageMime: imageMime as any
|
||||
} as any);
|
||||
|
||||
// Remove uploaded file after DB storage
|
||||
try { await fs.unlink(savePath); } catch {}
|
||||
|
||||
res.json({ item: saved });
|
||||
} catch (err: any) {
|
||||
@@ -53,7 +62,7 @@ router.get('/items', async (req, res) => {
|
||||
router.get('/items/:id/pdf', async (req, res) => {
|
||||
try {
|
||||
const id = Number(req.params.id);
|
||||
const item = await db.getItem(id);
|
||||
const item = await db.getItemWithImage(id);
|
||||
if (!item) return res.status(404).json({ error: 'Not found' });
|
||||
const pdfPath = await generatePdfForItem(item);
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
@@ -79,11 +88,41 @@ router.get('/export/xlsx', async (_req, res) => {
|
||||
{ header: 'Condition', key: 'condition', width: 14 },
|
||||
{ header: 'Description', key: 'description', width: 50 },
|
||||
{ header: 'Has Box', key: 'hasBox', width: 10 },
|
||||
{ header: 'Image Path', key: 'imagePath', width: 50 },
|
||||
{ header: 'Image', key: 'image', width: 25 },
|
||||
{ header: 'Created', key: 'createdAt', width: 24 }
|
||||
];
|
||||
const items = await db.listItems();
|
||||
for (const it of items) ws.addRow(it);
|
||||
const items = await db.listItemsWithImage();
|
||||
let rowIndex = 2; // 1-based, row 1 is header
|
||||
for (const it of items) {
|
||||
// Add row without imagePath, we have an Image column placeholder
|
||||
const row = ws.addRow({
|
||||
id: it.id,
|
||||
manufacturer: it.manufacturer,
|
||||
model: it.model,
|
||||
sku: it.sku,
|
||||
quantity: it.quantity,
|
||||
condition: it.condition,
|
||||
description: it.description,
|
||||
hasBox: it.hasBox ? 'Yes' : 'No',
|
||||
image: '',
|
||||
createdAt: it.createdAt
|
||||
});
|
||||
// Adjust row height for image display
|
||||
row.height = 90;
|
||||
// Embed image if available
|
||||
const buf = (it as any).imageData as unknown as Uint8Array | null;
|
||||
const mime: string | null = (it as any).imageMime || null;
|
||||
if (buf && mime && (mime.includes('png') || mime.includes('jpeg') || mime.includes('jpg'))) {
|
||||
const imgId = wb.addImage({ buffer: Buffer.from(buf as any) as any, extension: mime.includes('png') ? 'png' : 'jpeg' });
|
||||
// Place image in the Image column (index 9, zero-based col 8) anchored to this row
|
||||
const colIndexZeroBased = 8; // 0-based; column I (Image) since A=0
|
||||
ws.addImage(imgId, {
|
||||
tl: { col: colIndexZeroBased, row: rowIndex - 1 },
|
||||
ext: { width: 120, height: 80 }
|
||||
});
|
||||
}
|
||||
rowIndex++;
|
||||
}
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="train_inventory.xlsx"');
|
||||
await wb.xlsx.write(res);
|
||||
|
||||
@@ -11,7 +11,10 @@ export type TrainItem = {
|
||||
description: string; // 1 sentence
|
||||
condition: string; // e.g., New, Excellent, Good, Fair, Poor
|
||||
hasBox: boolean;
|
||||
imagePath: string;
|
||||
// Prefer in-DB storage; path retained for legacy/backfill
|
||||
imagePath?: string;
|
||||
imageData?: Buffer;
|
||||
imageMime?: string;
|
||||
id?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,12 @@ export async function generatePdfForItem(item: TrainItem & { id: number }): Prom
|
||||
try {
|
||||
doc.moveDown();
|
||||
doc.text('Photo:', { continued: false });
|
||||
doc.image(item.imagePath, { width: 300 });
|
||||
const imageBuffer: Buffer | undefined = (item as any).imageData;
|
||||
if (imageBuffer && imageBuffer.length > 0) {
|
||||
doc.image(imageBuffer, { width: 300 });
|
||||
} else if (item.imagePath) {
|
||||
doc.image(item.imagePath, { width: 300 });
|
||||
}
|
||||
} catch {}
|
||||
|
||||
doc.end();
|
||||
|
||||
Reference in New Issue
Block a user