Workflows ETC
Some checks failed
Deploy to Server / deploy (push) Failing after 4s

This commit is contained in:
2025-10-25 18:03:36 -04:00
parent 39e49739c9
commit d8f3edb9fe
8 changed files with 277 additions and 12 deletions

View File

@@ -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]);

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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();