feat: food ordering on coffee page

- Add 'Place Order' button to coffee page
- Order modal with menu item selection by category
- Order type options: pickup, dine in, room delivery (+10%)
- Room delivery requires login (disabled for guests)
- Calculate subtotal, service fee, and total
- Special instructions field
- Admin receives new orders in database
- Create orders and order_items tables
This commit is contained in:
2026-07-01 00:12:34 -05:00
parent d9049d2294
commit f7bb5ed752
3 changed files with 649 additions and 9 deletions
+194
View File
@@ -0,0 +1,194 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { getSession } from '@/lib/auth';
export async function POST(request: Request) {
try {
const user = await getSession();
const { items, order_type, room_id, notes } = await request.json();
// items is array of { menu_item_id, quantity }
if (!items || !Array.isArray(items) || items.length === 0) {
return NextResponse.json({ error: 'Order items are required' }, { status: 400 });
}
if (!order_type || !['pickup', 'dine_in', 'room_delivery'].includes(order_type)) {
return NextResponse.json({ error: 'Valid order type is required (pickup, dine_in, room_delivery)' }, { status: 400 });
}
// Room delivery requires login
if (order_type === 'room_delivery' && !user) {
return NextResponse.json({ error: 'Room delivery requires login. Please log in to continue.' }, { status: 401 });
}
// Room delivery requires room_id
if (order_type === 'room_delivery' && !room_id) {
return NextResponse.json({ error: 'Room selection is required for room delivery' }, { status: 400 });
}
// Get menu item prices
const menuItemIds = items.map((i: { menu_item_id: number }) => i.menu_item_id);
const { rows: menuItems } = await db.query(
'SELECT id, name, price FROM menu_items WHERE id = ANY($1)',
[menuItemIds]
);
if (menuItems.length !== menuItemIds.length) {
return NextResponse.json({ error: 'One or more menu items not found' }, { status: 400 });
}
// Calculate total
const priceMap = new Map(menuItems.map((m: { id: number; price: string }) => [m.id, parseFloat(m.price)]));
let subtotal = 0;
const orderItems = items.map((i: { menu_item_id: number; quantity: number }) => {
const price = priceMap.get(i.menu_item_id) || 0;
const itemTotal = price * i.quantity;
subtotal += itemTotal;
return {
menu_item_id: i.menu_item_id,
quantity: i.quantity,
unit_price: price,
total_price: itemTotal,
};
});
// Room delivery adds 10% service fee
const serviceFee = order_type === 'room_delivery' ? subtotal * 0.1 : 0;
const total = subtotal + serviceFee;
// Create order
const { rows: orderRows } = await db.query(`
INSERT INTO orders (user_id, order_type, room_id, subtotal, service_fee, total, status, notes)
VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7)
RETURNING *
`, [user?.id || null, order_type, order_type === 'room_delivery' ? room_id : null, subtotal, serviceFee, total, notes || null]);
const orderId = orderRows[0].id;
// Insert order items
for (const item of orderItems) {
await db.query(`
INSERT INTO order_items (order_id, menu_item_id, quantity, unit_price, total_price)
VALUES ($1, $2, $3, $4, $5)
`, [orderId, item.menu_item_id, item.quantity, item.unit_price, item.total_price]);
}
// Get full order with items
const { rows: fullOrder } = await db.query(`
SELECT o.*,
json_agg(json_build_object(
'menu_item_id', oi.menu_item_id,
'quantity', oi.quantity,
'unit_price', oi.unit_price,
'total_price', oi.total_price,
'name', m.name
)) as items
FROM orders o
LEFT JOIN order_items oi ON o.id = oi.order_id
LEFT JOIN menu_items m ON oi.menu_item_id = m.id
WHERE o.id = $1
GROUP BY o.id
`, [orderId]);
// Get admin notification targets
const { rows: admins } = await db.query(
'SELECT email, phone FROM users WHERE role = \'admin\' AND (email IS NOT NULL OR phone IS NOT NULL)'
);
// TODO: Send notification to admins (email/SMS)
return NextResponse.json({
success: true,
order: fullOrder[0],
message: order_type === 'room_delivery'
? 'Order placed! We\'ll deliver it to your room shortly.'
: order_type === 'dine_in'
? 'Order placed! We\'ll prepare it for dine-in.'
: 'Order placed! We\'ll have it ready for pickup.'
});
} catch (error) {
console.error('Order error:', error);
return NextResponse.json({ error: 'Failed to create order' }, { status: 500 });
}
}
export async function GET(request: Request) {
try {
const user = await getSession();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const status = searchParams.get('status') || 'all';
const limit = parseInt(searchParams.get('limit') || '50');
let query = `
SELECT o.*, r.name as room_name, u.username, u.first_name, u.last_name
FROM orders o
LEFT JOIN rooms r ON o.room_id = r.id
LEFT JOIN users u ON o.user_id = u.id
`;
const params: any[] = [];
if (status !== 'all') {
query += ' WHERE o.status = $1';
params.push(status);
}
query += ' ORDER BY o.created_at DESC LIMIT $' + (params.length + 1);
params.push(limit);
const { rows } = await db.query(query, params);
// Get items for each order
for (const order of rows) {
const { rows: items } = await db.query(`
SELECT oi.*, m.name, m.description
FROM order_items oi
JOIN menu_items m ON oi.menu_item_id = m.id
WHERE oi.order_id = $1
`, [order.id]);
order.items = items;
}
return NextResponse.json({ orders: rows });
} catch (error) {
console.error('Get orders error:', error);
return NextResponse.json({ error: 'Failed to get orders' }, { status: 500 });
}
}
export async function PATCH(request: Request) {
try {
const user = await getSession();
if (!user || user.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { order_id, status } = await request.json();
if (!order_id || !status) {
return NextResponse.json({ error: 'order_id and status are required' }, { status: 400 });
}
const validStatuses = ['pending', 'preparing', 'ready', 'delivered', 'cancelled'];
if (!validStatuses.includes(status)) {
return NextResponse.json({ error: 'Invalid status' }, { status: 400 });
}
const { rows } = await db.query(
'UPDATE orders SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
[status, order_id]
);
if (rows.length === 0) {
return NextResponse.json({ error: 'Order not found' }, { status: 404 });
}
return NextResponse.json({ success: true, order: rows[0] });
} catch (error) {
console.error('Update order error:', error);
return NextResponse.json({ error: 'Failed to update order' }, { status: 500 });
}
}
+419 -5
View File
@@ -13,6 +13,19 @@ interface MenuItem {
featured_photo?: string | null; featured_photo?: string | null;
} }
interface Room {
id: number;
name: string;
}
interface User {
id: number;
username: string;
role: 'admin' | 'user';
first_name: string | null;
last_name: string | null;
}
const gold = '#E8A849'; const gold = '#E8A849';
const navy = '#010D1E'; const navy = '#010D1E';
const cream = '#f5f0e8'; const cream = '#f5f0e8';
@@ -20,15 +33,39 @@ const warm = '#a6683c';
export default function CoffeePage() { export default function CoffeePage() {
const [items, setItems] = useState<MenuItem[]>([]); const [items, setItems] = useState<MenuItem[]>([]);
const [rooms, setRooms] = useState<Room[]>([]);
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
// Order state
const [showOrderModal, setShowOrderModal] = useState(false);
const [selectedItems, setSelectedItems] = useState<Map<number, number>>(new Map());
const [orderType, setOrderType] = useState<'pickup' | 'dine_in' | 'room_delivery'>('pickup');
const [selectedRoom, setSelectedRoom] = useState<number | null>(null);
const [notes, setNotes] = useState('');
const [submitting, setSubmitting] = useState(false);
const [orderResult, setOrderResult] = useState<{ success: boolean; message: string } | null>(null);
useEffect(() => { useEffect(() => {
fetch('/api/menu') Promise.all([
.then(async (res) => { fetch('/api/menu').then(async (res) => {
if (!res.ok) throw new Error('Failed to load menu'); if (!res.ok) throw new Error('Failed to load menu');
const json = await res.json(); return res.json();
setItems(json.data || []); }),
fetch('/api/auth/session').then(async (res) => {
if (res.ok) return res.json();
return { user: null };
}),
fetch('/api/rooms').then(async (res) => {
if (!res.ok) return { rooms: [] };
return res.json();
}),
])
.then(([menuData, sessionData, roomsData]) => {
setItems(menuData.data || []);
setUser(sessionData.user || null);
setRooms(roomsData.rooms || []);
}) })
.catch((err) => setError(err.message)) .catch((err) => setError(err.message))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -37,12 +74,111 @@ export default function CoffeePage() {
const categories = Array.from(new Set(items.map((i) => i.category))); const categories = Array.from(new Set(items.map((i) => i.category)));
const daily = items.find((i) => i.is_daily_special); const daily = items.find((i) => i.is_daily_special);
// Calculate order totals
const orderItems = items.filter((item) => selectedItems.has(item.id) && (selectedItems.get(item.id) || 0) > 0);
const subtotal = orderItems.reduce((sum, item) => {
const qty = selectedItems.get(item.id) || 0;
return sum + parseFloat(item.price) * qty;
}, 0);
const serviceFee = orderType === 'room_delivery' ? subtotal * 0.1 : 0;
const total = subtotal + serviceFee;
const updateQuantity = (itemId: number, delta: number) => {
setSelectedItems((prev) => {
const newMap = new Map(prev);
const current = newMap.get(itemId) || 0;
const newQty = Math.max(0, current + delta);
if (newQty === 0) {
newMap.delete(itemId);
} else {
newMap.set(itemId, newQty);
}
return newMap;
});
};
const handleSubmitOrder = async () => {
if (selectedItems.size === 0) {
alert('Please select at least one item');
return;
}
if (orderType === 'room_delivery' && !selectedRoom) {
alert('Please select a room for delivery');
return;
}
setSubmitting(true);
try {
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: Array.from(selectedItems.entries()).map(([menu_item_id, quantity]) => ({
menu_item_id,
quantity,
})),
order_type: orderType,
room_id: orderType === 'room_delivery' ? selectedRoom : null,
notes: notes || null,
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Failed to place order');
}
setOrderResult({ success: true, message: data.message });
setSelectedItems(new Map());
setNotes('');
} catch (err) {
setOrderResult({ success: false, message: err instanceof Error ? err.message : 'Failed to place order' });
} finally {
setSubmitting(false);
}
};
const closeModal = () => {
setShowOrderModal(false);
setOrderResult(null);
};
return ( return (
<main style={{ padding: 'clamp(1rem, 5vw, 2rem)', maxWidth: '72rem', margin: '0 auto', minHeight: '60vh' }}> <main style={{ padding: 'clamp(1rem, 5vw, 2rem)', maxWidth: '72rem', margin: '0 auto', minHeight: '60vh' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1.5rem', flexWrap: 'wrap', gap: '1rem' }}>
<div>
<h1 style={{ fontSize: 'clamp(28px, 6vw, 48px)', fontWeight: 400, fontStyle: 'italic', color: navy, margin: '0 0 0.25rem' }}>Coffee &amp; Kitchen</h1> <h1 style={{ fontSize: 'clamp(28px, 6vw, 48px)', fontWeight: 400, fontStyle: 'italic', color: navy, margin: '0 0 0.25rem' }}>Coffee &amp; Kitchen</h1>
<p style={{ color: '#555', marginBottom: '1.5rem', maxWidth: '50ch', fontSize: 'clamp(14px, 3vw, 16px)' }}> <p style={{ color: '#555', marginBottom: 0, maxWidth: '50ch', fontSize: 'clamp(14px, 3vw, 16px)' }}>
Locally sourced coffee, fresh pastries, and seasonal plates. Locally sourced coffee, fresh pastries, and seasonal plates.
</p> </p>
</div>
<button
onClick={() => setShowOrderModal(true)}
style={{
background: `linear-gradient(135deg, ${gold} 0%, ${warm} 100%)`,
color: 'white',
border: 'none',
padding: '0.75rem 1.5rem',
borderRadius: '8px',
fontSize: 'clamp(14px, 3vw, 16px)',
fontWeight: 600,
cursor: 'pointer',
boxShadow: '0 2px 8px rgba(232,168,73,0.3)',
transition: 'transform 0.2s, box-shadow 0.2s',
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(232,168,73,0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(232,168,73,0.3)';
}}
>
Place Order
</button>
</div>
{loading && <p style={{ color: warm }}>Loading menu...</p>} {loading && <p style={{ color: warm }}>Loading menu...</p>}
{error && <p style={{ color: '#c23b22' }}>{error}</p>} {error && <p style={{ color: '#c23b22' }}>{error}</p>}
@@ -130,6 +266,284 @@ export default function CoffeePage() {
</div> </div>
</section> </section>
))} ))}
{/* Order Modal */}
{showOrderModal && (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.6)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '1rem',
zIndex: 1000,
}}
onClick={closeModal}
>
<div
style={{
background: 'white',
borderRadius: '16px',
maxWidth: 'min(95vw, 600px)',
width: '100%',
maxHeight: '90vh',
overflow: 'auto',
padding: 'clamp(1rem, 4vw, 1.5rem)',
}}
onClick={(e) => e.stopPropagation()}
>
{orderResult ? (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<div style={{ fontSize: '48px', marginBottom: '1rem' }}>
{orderResult.success ? '✓' : '✕'}
</div>
<h3 style={{ margin: '0 0 1rem', color: navy }}>{orderResult.success ? 'Order Placed!' : 'Error'}</h3>
<p style={{ color: '#555', marginBottom: '1.5rem' }}>{orderResult.message}</p>
<button
onClick={closeModal}
style={{
background: navy,
color: 'white',
border: 'none',
padding: '0.75rem 2rem',
borderRadius: '8px',
fontSize: '16px',
cursor: 'pointer',
}}
>
Close
</button>
</div>
) : (
<>
<h3 style={{ margin: '0 0 1rem', color: navy, fontSize: '24px' }}>Place Your Order</h3>
{/* Order Type Selection */}
<div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', fontWeight: 600, marginBottom: '0.5rem', color: navy }}>Order Type</label>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{[
{ value: 'pickup', label: 'Pickup' },
{ value: 'dine_in', label: 'Dine In' },
{ value: 'room_delivery', label: 'Room Delivery (+10%)' },
].map((opt) => (
<button
key={opt.value}
onClick={() => setOrderType(opt.value as typeof orderType)}
disabled={opt.value === 'room_delivery' && !user}
style={{
flex: 1,
minWidth: '120px',
padding: '0.75rem 1rem',
border: `2px solid ${orderType === opt.value ? gold : '#ddd'}`,
borderRadius: '8px',
background: orderType === opt.value ? gold : 'white',
color: orderType === opt.value ? 'white' : opt.value === 'room_delivery' && !user ? '#aaa' : navy,
cursor: opt.value === 'room_delivery' && !user ? 'not-allowed' : 'pointer',
opacity: opt.value === 'room_delivery' && !user ? 0.5 : 1,
fontWeight: 500,
fontSize: '14px',
}}
>
{opt.label}
{opt.value === 'room_delivery' && !user && (
<div style={{ fontSize: '11px', marginTop: '2px' }}>Login required</div>
)}
</button>
))}
</div>
</div>
{/* Room Selection for Delivery */}
{orderType === 'room_delivery' && user && (
<div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', fontWeight: 600, marginBottom: '0.5rem', color: navy }}>Select Room</label>
<select
value={selectedRoom || ''}
onChange={(e) => setSelectedRoom(Number(e.target.value) || null)}
style={{
width: '100%',
padding: '0.75rem',
border: '2px solid #ddd',
borderRadius: '8px',
fontSize: '16px',
}}
>
<option value="">Select a room...</option>
{rooms.map((room) => (
<option key={room.id} value={room.id}>{room.name}</option>
))}
</select>
</div>
)}
{/* Menu Items Selection */}
<div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', fontWeight: 600, marginBottom: '0.5rem', color: navy }}>Select Items</label>
<div style={{ maxHeight: '300px', overflow: 'auto', border: '2px solid #ddd', borderRadius: '8px' }}>
{categories.map((category) => (
<div key={category}>
<div style={{ background: '#f5f5f5', padding: '0.5rem 1rem', fontWeight: 600, color: navy, borderBottom: '1px solid #ddd' }}>
{category}
</div>
{items
.filter((i) => i.category === category)
.map((item) => {
const qty = selectedItems.get(item.id) || 0;
return (
<div
key={item.id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0.75rem 1rem',
borderBottom: '1px solid #eee',
}}
>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 500, color: navy }}>{item.name}</div>
<div style={{ color: warm, fontWeight: 600 }}>${item.price}</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<button
onClick={() => updateQuantity(item.id, -1)}
disabled={qty === 0}
style={{
width: '28px',
height: '28px',
border: `2px solid ${qty > 0 ? navy : '#ddd'}`,
borderRadius: '50%',
background: 'white',
color: qty > 0 ? navy : '#aaa',
cursor: qty > 0 ? 'pointer' : 'not-allowed',
fontSize: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
</button>
<span style={{ minWidth: '24px', textAlign: 'center', fontWeight: 600 }}>{qty}</span>
<button
onClick={() => updateQuantity(item.id, 1)}
style={{
width: '28px',
height: '28px',
border: `2px solid ${navy}`,
borderRadius: '50%',
background: navy,
color: 'white',
cursor: 'pointer',
fontSize: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
+
</button>
</div>
</div>
);
})}
</div>
))}
</div>
</div>
{/* Notes */}
<div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', fontWeight: 600, marginBottom: '0.5rem', color: navy }}>Special Instructions (optional)</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Any allergies, preferences, or special requests..."
style={{
width: '100%',
padding: '0.75rem',
border: '2px solid #ddd',
borderRadius: '8px',
fontSize: '14px',
minHeight: '80px',
resize: 'vertical',
}}
/>
</div>
{/* Order Summary */}
{selectedItems.size > 0 && (
<div style={{ background: cream, borderRadius: '8px', padding: '1rem', marginBottom: '1.5rem' }}>
<h4 style={{ margin: '0 0 0.5rem', color: navy }}>Order Summary</h4>
{orderItems.map((item) => (
<div key={item.id} style={{ display: 'flex', justifyContent: 'space-between', fontSize: '14px', marginBottom: '0.25rem' }}>
<span>{item.name} × {selectedItems.get(item.id)}</span>
<span>${(parseFloat(item.price) * (selectedItems.get(item.id) || 0)).toFixed(2)}</span>
</div>
))}
<div style={{ borderTop: '1px solid #ddd', marginTop: '0.5rem', paddingTop: '0.5rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>Subtotal</span>
<span>${subtotal.toFixed(2)}</span>
</div>
{orderType === 'room_delivery' && (
<div style={{ display: 'flex', justifyContent: 'space-between', color: warm }}>
<span>Service Fee (10%)</span>
<span>${serviceFee.toFixed(2)}</span>
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700, marginTop: '0.25rem', fontSize: '16px' }}>
<span>Total</span>
<span>${total.toFixed(2)}</span>
</div>
</div>
</div>
)}
{/* Submit Button */}
<div style={{ display: 'flex', gap: '1rem' }}>
<button
onClick={closeModal}
style={{
flex: 1,
padding: '0.75rem',
border: '2px solid #ddd',
borderRadius: '8px',
background: 'white',
color: navy,
fontSize: '16px',
cursor: 'pointer',
}}
>
Cancel
</button>
<button
onClick={handleSubmitOrder}
disabled={submitting || selectedItems.size === 0}
style={{
flex: 2,
padding: '0.75rem',
border: 'none',
borderRadius: '8px',
background: `linear-gradient(135deg, ${gold} 0%, ${warm} 100%)`,
color: 'white',
fontSize: '16px',
fontWeight: 600,
cursor: submitting || selectedItems.size === 0 ? 'not-allowed' : 'pointer',
opacity: submitting || selectedItems.size === 0 ? 0.5 : 1,
}}
>
{submitting ? 'Placing Order...' : `Place Order • $${total.toFixed(2)}`}
</button>
</div>
</>
)}
</div>
</div>
)}
</main> </main>
); );
} }
+32
View File
@@ -375,6 +375,38 @@ export async function initSchema() {
await db.query(`CREATE INDEX IF NOT EXISTS idx_reservations_room_dates ON reservations(room_id, check_in, check_out)`); await db.query(`CREATE INDEX IF NOT EXISTS idx_reservations_room_dates ON reservations(room_id, check_in, check_out)`);
await db.query(`CREATE INDEX IF NOT EXISTS idx_room_availability_room_date ON room_availability(room_id, date)`); await db.query(`CREATE INDEX IF NOT EXISTS idx_room_availability_room_date ON room_availability(room_id, date)`);
await db.query(`CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC)`); await db.query(`CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at DESC)`);
// Orders tables for coffee/kitchen
await db.query(`
CREATE TABLE IF NOT EXISTS orders (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
order_type VARCHAR(20) NOT NULL,
room_id INTEGER REFERENCES rooms(id),
subtotal DECIMAL(10,2) NOT NULL,
service_fee DECIMAL(10,2) DEFAULT 0,
total DECIMAL(10,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
notes TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
`);
await db.query(`
CREATE TABLE IF NOT EXISTS order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
menu_item_id INTEGER NOT NULL REFERENCES menu_items(id),
quantity INTEGER NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
total_price DECIMAL(10,2) NOT NULL
);
`);
await db.query(`CREATE INDEX IF NOT EXISTS idx_orders_user ON orders(user_id)`);
await db.query(`CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status)`);
await db.query(`CREATE INDEX IF NOT EXISTS idx_order_items_order ON order_items(order_id)`);
} }
export async function migrateFromLegacyPlaces() { export async function migrateFromLegacyPlaces() {