From b6771961f34817f7faafde30b5dba6bd210de8e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Feb 2026 20:36:34 +0000 Subject: [PATCH 1/2] Update Launchpad for Shuttle STS-2.01, add PO open button, bump v2.0.2 - Update shuttle version from STS-2.00 to STS-2.01 across backend and frontend - Add hotel_collection (textbox) checkout field alongside hotel_list (dropdown) - Make hotel collection input a full resizable textbox (8 rows) instead of compact field - Update Hotel Event Shop preset to use hotel_collection by default - Add dedicated "Open PO" button column in orders table for each order's PO file - Update backend PO endpoint to handle Excel (.xlsx/.xls), PDF, CSV, Word, and image files with correct Content-Type headers (was previously PDF-only) - Bump platform version from 2.0.1 to 2.0.2 https://claude.ai/code/session_01UA9w5W1iRtPsSifcqFHFnS --- backend/package.json | 2 +- backend/src/orders.js | 26 +++++++++++++-- backend/src/shops.js | 13 ++++---- frontend/package.json | 2 +- frontend/src/components/OrderTable.jsx | 44 +++++++++++++++++++------- frontend/src/pages/NewShop.jsx | 37 ++++++++++++---------- frontend/src/pages/Settings.jsx | 42 +++++++++++++----------- 7 files changed, 110 insertions(+), 56 deletions(-) diff --git a/backend/package.json b/backend/package.json index 048818b..90b95db 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "shuttle-platform-backend", - "version": "2.0.1", + "version": "2.0.2", "private": true, "scripts": { "start": "node src/index.js", diff --git a/backend/src/orders.js b/backend/src/orders.js index bc7a827..8ca37ed 100644 --- a/backend/src/orders.js +++ b/backend/src/orders.js @@ -89,7 +89,20 @@ router.post('/:slug/orders/wipe', (req, res) => { } }); -// GET /api/shops/:slug/orders/po/:filename — Download a PO file (PDF) +// Content-type map for common PO file extensions +const PO_CONTENT_TYPES = { + '.pdf': 'application/pdf', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xls': 'application/vnd.ms-excel', + '.csv': 'text/csv', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', +}; + +// GET /api/shops/:slug/orders/po/:filename — Download/open a PO file router.get('/:slug/orders/po/:filename', (req, res) => { const { slug, filename } = req.params; @@ -114,8 +127,15 @@ router.get('/:slug/orders/po/:filename', (req, res) => { return res.status(404).json({ error: 'PO file not found' }); } - res.setHeader('Content-Type', 'application/pdf'); - res.setHeader('Content-Disposition', `inline; filename="${safeName}"`); + const ext = path.extname(safeName).toLowerCase(); + const contentType = PO_CONTENT_TYPES[ext] || 'application/octet-stream'; + + // PDFs and images can be shown inline; everything else triggers download + const isInline = ext === '.pdf' || ext === '.png' || ext === '.jpg' || ext === '.jpeg'; + const disposition = isInline ? 'inline' : 'attachment'; + + res.setHeader('Content-Type', contentType); + res.setHeader('Content-Disposition', `${disposition}; filename="${safeName}"`); fs.createReadStream(filePath).pipe(res); }); diff --git a/backend/src/shops.js b/backend/src/shops.js index cd7dd08..e6950f3 100644 --- a/backend/src/shops.js +++ b/backend/src/shops.js @@ -477,14 +477,15 @@ router.post('/', (req, res) => { `details: ${dr.details !== false}`, `extra_notes: ${dr.extra_notes !== false}`, `shipping_handler: ${dr.shipping_handler !== false}`, - `hotel_list: ${dr.hotel_list === true}`, + `hotel_list: ${dr.hotel_list === true || dr.hotel_collection === true}`, + `hotel_collection: ${dr.hotel_collection === true}`, ].join('\n'); fs.writeFileSync( path.join(shopDir, 'DATABASE', 'Presets', 'DataRequired.txt'), drContent ); - if (dr.hotel_list && hotelList) { + if ((dr.hotel_list || dr.hotel_collection) && hotelList) { fs.mkdirSync(path.join(shopDir, 'DATABASE', 'Design', 'Details'), { recursive: true }); fs.writeFileSync( path.join(shopDir, 'DATABASE', 'Design', 'Details', 'Hotels.txt'), @@ -523,7 +524,7 @@ router.post('/', (req, res) => { // Register in database db.prepare( 'INSERT INTO shops (slug, name, description, status, port, subdomain, shuttle_version) VALUES (?, ?, ?, ?, ?, ?, ?)' - ).run(slug, name, description || '', 'stopped', port, subdomain, 'STS-2.00'); + ).run(slug, name, description || '', 'stopped', port, subdomain, 'STS-2.01'); // Start the container using container-readable path const hostComposeFile = getComposeFilePath(slug); @@ -981,7 +982,7 @@ router.get('/:slug/version', (req, res) => { result.currentVersion = shop.shuttle_version; } - result.latestAvailable = 'STS-2.00'; + result.latestAvailable = 'STS-2.01'; res.json(result); } catch (err) { @@ -1078,7 +1079,7 @@ router.post('/:slug/update-template', (req, res) => { }).trim(); } catch { /* ignore */ } - db.prepare('UPDATE shops SET shuttle_version = ? WHERE slug = ?').run('STS-2.00', slug); + db.prepare('UPDATE shops SET shuttle_version = ? WHERE slug = ?').run('STS-2.01', slug); log.push(`Update complete. Now at commit ${newCommit}.`); res.json({ message: `Shop "${slug}" updated successfully`, commit: newCommit, log: log.join('\n') }); @@ -1169,7 +1170,7 @@ router.post('/:slug/upgrade', (req, res) => { } // Update version in DB - db.prepare('UPDATE shops SET shuttle_version = ?, status = ? WHERE slug = ?').run('STS-2.00', 'running', slug); + db.prepare('UPDATE shops SET shuttle_version = ?, status = ? WHERE slug = ?').run('STS-2.01', 'running', slug); log.push('Updated shop version to STS-2.00.'); res.json({ message: `Shop "${slug}" upgraded to STS-2.00`, log: log.join('\n') }); diff --git a/frontend/package.json b/frontend/package.json index 909c06b..576b644 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "shuttle-platform-frontend", "private": true, - "version": "2.0.1", + "version": "2.0.2", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/OrderTable.jsx b/frontend/src/components/OrderTable.jsx index 32f609b..1dfbe4f 100644 --- a/frontend/src/components/OrderTable.jsx +++ b/frontend/src/components/OrderTable.jsx @@ -1,5 +1,5 @@ import { useState, useMemo } from 'react'; -import { ArrowUpDown, FileDown } from 'lucide-react'; +import { ArrowUpDown, FileDown, FileSpreadsheet } from 'lucide-react'; import { getPoFileUrl } from '../lib/api'; export default function OrderTable({ orders, slug }) { @@ -11,6 +11,11 @@ export default function OrderTable({ orders, slug }) { return Object.keys(orders[0]); }, [orders]); + // Check if any order has a PO File value + const hasPo = useMemo(() => { + return orders.some(row => row['PO File'] && row['PO File'].trim()); + }, [orders]); + const sorted = useMemo(() => { if (!sortKey) return orders; return [...orders].sort((a, b) => { @@ -35,18 +40,10 @@ export default function OrderTable({ orders, slug }) { } const renderCell = (col, value) => { - // Render PO File column as a download link + // Render PO File column as the filename text (the button is in the dedicated column) if (col === 'PO File' && value && slug) { return ( - - - {value} - + {value} ); } return value; @@ -69,6 +66,14 @@ export default function OrderTable({ orders, slug }) { ))} + {hasPo && ( + + + + Open PO + + + )} @@ -79,6 +84,23 @@ export default function OrderTable({ orders, slug }) { {renderCell(col, row[col])} ))} + {hasPo && ( + + {row['PO File'] && row['PO File'].trim() ? ( + + + Open PO + + ) : ( + + )} + + )} ))} diff --git a/frontend/src/pages/NewShop.jsx b/frontend/src/pages/NewShop.jsx index 7fa325d..5731054 100644 --- a/frontend/src/pages/NewShop.jsx +++ b/frontend/src/pages/NewShop.jsx @@ -8,22 +8,22 @@ const SHOP_PRESETS = [ { label: 'Basic Free Shop', shopType: 'free', - dataRequired: { address: true, details: true, extra_notes: true, shipping_handler: true, hotel_list: false }, + dataRequired: { address: true, details: true, extra_notes: true, shipping_handler: true, hotel_list: false, hotel_collection: false }, }, { label: 'PO Required Shop', shopType: 'po', - dataRequired: { address: true, details: true, extra_notes: true, shipping_handler: true, hotel_list: false }, + dataRequired: { address: true, details: true, extra_notes: true, shipping_handler: true, hotel_list: false, hotel_collection: false }, }, { label: 'Hotel Event Shop', shopType: 'free', - dataRequired: { address: true, details: true, extra_notes: true, shipping_handler: false, hotel_list: true }, + dataRequired: { address: true, details: true, extra_notes: true, shipping_handler: false, hotel_list: false, hotel_collection: true }, }, { label: 'Minimal Free Shop', shopType: 'free', - dataRequired: { address: false, details: false, extra_notes: false, shipping_handler: true, hotel_list: false }, + dataRequired: { address: false, details: false, extra_notes: false, shipping_handler: true, hotel_list: false, hotel_collection: false }, }, ]; @@ -43,7 +43,7 @@ export default function NewShop() { const navigate = useNavigate(); const queryClient = useQueryClient(); - // STS-2.00: Shop type & preset state + // STS-2.01: Shop type & preset state const [shopType, setShopType] = useState('free'); const [dataRequired, setDataRequired] = useState({ address: true, @@ -51,6 +51,7 @@ export default function NewShop() { extra_notes: true, shipping_handler: true, hotel_list: false, + hotel_collection: false, }); const [hotelList, setHotelList] = useState(''); @@ -106,13 +107,16 @@ export default function NewShop() { const applyPreset = (preset) => { setShopType(preset.shopType); setDataRequired({ ...preset.dataRequired }); - if (!preset.dataRequired.hotel_list) setHotelList(''); + if (!preset.dataRequired.hotel_list && !preset.dataRequired.hotel_collection) setHotelList(''); }; const toggleDataRequired = (key) => { setDataRequired(prev => { const next = { ...prev, [key]: !prev[key] }; - if (key === 'hotel_list' && !next.hotel_list) setHotelList(''); + if ((key === 'hotel_list' && !next.hotel_list && !next.hotel_collection) || + (key === 'hotel_collection' && !next.hotel_collection && !next.hotel_list)) { + setHotelList(''); + } return next; }); }; @@ -128,7 +132,7 @@ export default function NewShop() { if (customSlug.trim()) payload.slug = customSlug.trim(); if (description.trim()) payload.description = description.trim(); if (folderPath.trim()) payload.folderPath = folderPath.trim(); - if (dataRequired.hotel_list && hotelList.trim()) payload.hotelList = hotelList.trim(); + if ((dataRequired.hotel_list || dataRequired.hotel_collection) && hotelList.trim()) payload.hotelList = hotelList.trim(); mutation.mutate(payload); }; @@ -141,7 +145,8 @@ export default function NewShop() { details: 'Details', extra_notes: 'Extra Notes', shipping_handler: 'Shipping Handler', - hotel_list: 'Hotel List', + hotel_list: 'Hotel List (Dropdown)', + hotel_collection: 'Hotel Collection (Textbox)', }; return ( @@ -287,24 +292,24 @@ export default function NewShop() { - {/* Hotel List (conditional) */} - {dataRequired.hotel_list && ( + {/* Hotel Collection (conditional) */} + {(dataRequired.hotel_list || dataRequired.hotel_collection) && (