diff --git a/packages/backend/src/modules/auth/auth.service.ts b/packages/backend/src/modules/auth/auth.service.ts
index 45f96de..a105d76 100644
--- a/packages/backend/src/modules/auth/auth.service.ts
+++ b/packages/backend/src/modules/auth/auth.service.ts
@@ -71,8 +71,9 @@ export class AuthService {
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) throw new UnauthorizedException('Invalid credentials');
- const { password_hash, ...safeUser } = user;
- return { user: safeUser, token: this.signToken(user) };
+ // Return full profile including restaurantId / driverId so frontend can use them
+ const fullUser = await this.validateToken({ sub: user.id, role: user.role });
+ return { user: fullUser, token: this.signToken(user) };
}
async validateToken(payload: { sub: string; role: string }) {
diff --git a/packages/backend/src/modules/restaurants/restaurants.controller.ts b/packages/backend/src/modules/restaurants/restaurants.controller.ts
index 593dac1..4a346d6 100644
--- a/packages/backend/src/modules/restaurants/restaurants.controller.ts
+++ b/packages/backend/src/modules/restaurants/restaurants.controller.ts
@@ -1,4 +1,4 @@
-import { Controller, Get, Post, Patch, Body, Param, Query, UseGuards, Request, BadRequestException } from '@nestjs/common';
+import { Controller, Get, Post, Patch, Delete, Body, Param, Query, UseGuards, Request, BadRequestException } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard, Roles } from '../auth/guards/roles.guard';
import { RestaurantsService } from './restaurants.service';
@@ -62,6 +62,44 @@ export class RestaurantsController {
return this.restaurantsService.updateHours(req.user.restaurantId, isOpen);
}
+ // ---- Menu management (restaurant owner) ----
+ // Must be before :slug to avoid route conflict
+
+ @Get('menu/items')
+ @UseGuards(JwtAuthGuard, RolesGuard)
+ @Roles('restaurant_owner')
+ getMenu(@Request() req) {
+ return this.restaurantsService.getMenu(req.user.restaurantId);
+ }
+
+ @Post('menu/categories')
+ @UseGuards(JwtAuthGuard, RolesGuard)
+ @Roles('restaurant_owner')
+ addCategory(@Request() req, @Body('name') name: string) {
+ return this.restaurantsService.addCategory(req.user.restaurantId, name);
+ }
+
+ @Post('menu/items')
+ @UseGuards(JwtAuthGuard, RolesGuard)
+ @Roles('restaurant_owner')
+ addMenuItem(@Request() req, @Body() dto: any) {
+ return this.restaurantsService.addMenuItem(req.user.restaurantId, dto);
+ }
+
+ @Patch('menu/items/:id')
+ @UseGuards(JwtAuthGuard, RolesGuard)
+ @Roles('restaurant_owner')
+ updateMenuItem(@Request() req, @Param('id') id: string, @Body() dto: any) {
+ return this.restaurantsService.updateMenuItem(req.user.restaurantId, id, dto);
+ }
+
+ @Delete('menu/items/:id')
+ @UseGuards(JwtAuthGuard, RolesGuard)
+ @Roles('restaurant_owner')
+ deleteMenuItem(@Request() req, @Param('id') id: string) {
+ return this.restaurantsService.deleteMenuItem(req.user.restaurantId, id);
+ }
+
@Get(':slug')
findBySlug(@Param('slug') slug: string) {
return this.restaurantsService.findBySlug(slug);
diff --git a/packages/backend/src/modules/restaurants/restaurants.service.ts b/packages/backend/src/modules/restaurants/restaurants.service.ts
index 820f399..f4f1b63 100644
--- a/packages/backend/src/modules/restaurants/restaurants.service.ts
+++ b/packages/backend/src/modules/restaurants/restaurants.service.ts
@@ -185,6 +185,109 @@ export class RestaurantsService {
return { lat: 43.6389, lng: -79.4196 }; // fallback: Liberty Village centre
}
+ // ============================================================
+ // MENU MANAGEMENT (restaurant owner)
+ // ============================================================
+
+ async getMenu(restaurantId: string) {
+ const categories = await this.db.queryMany(
+ `SELECT mc.id, mc.name, mc.sort_order, mc.is_active
+ FROM menu_categories mc
+ WHERE mc.restaurant_id = $1
+ ORDER BY mc.sort_order, mc.name`,
+ [restaurantId],
+ );
+
+ const items = await this.db.queryMany(
+ `SELECT mi.id, mi.category_id, mi.name, mi.description, mi.price,
+ mi.is_available, mi.is_featured, mi.dietary_tags, mi.sort_order
+ FROM menu_items mi
+ JOIN menu_categories mc ON mc.id = mi.category_id
+ WHERE mc.restaurant_id = $1
+ ORDER BY mi.sort_order, mi.name`,
+ [restaurantId],
+ );
+
+ return categories.map((cat) => ({
+ ...cat,
+ items: items.filter((i) => i.category_id === cat.id),
+ }));
+ }
+
+ async addCategory(restaurantId: string, name: string) {
+ const existing = await this.db.queryMany(
+ `SELECT sort_order FROM menu_categories WHERE restaurant_id = $1`,
+ [restaurantId],
+ );
+ const nextSort = existing.length + 1;
+ return this.db.queryOne(
+ `INSERT INTO menu_categories (restaurant_id, name, sort_order)
+ VALUES ($1, $2, $3) RETURNING *`,
+ [restaurantId, name.trim(), nextSort],
+ );
+ }
+
+ async addMenuItem(restaurantId: string, dto: {
+ categoryId: string;
+ name: string;
+ description?: string;
+ price: number;
+ dietaryTags?: string[];
+ }) {
+ // Verify category belongs to this restaurant
+ const cat = await this.db.queryOne(
+ `SELECT id FROM menu_categories WHERE id = $1 AND restaurant_id = $2`,
+ [dto.categoryId, restaurantId],
+ );
+ if (!cat) throw new Error('Category not found');
+
+ const count = await this.db.queryOne(
+ `SELECT COUNT(*) AS n FROM menu_items WHERE category_id = $1`,
+ [dto.categoryId],
+ );
+ const nextSort = Number(count?.n || 0) + 1;
+
+ return this.db.queryOne(
+ `INSERT INTO menu_items (restaurant_id, category_id, name, description, price, dietary_tags, sort_order, is_available)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, TRUE) RETURNING *`,
+ [restaurantId, dto.categoryId, dto.name.trim(), dto.description || null, dto.price, dto.dietaryTags || [], nextSort],
+ );
+ }
+
+ async updateMenuItem(restaurantId: string, itemId: string, dto: {
+ name?: string;
+ description?: string;
+ price?: number;
+ isAvailable?: boolean;
+ isFeatured?: boolean;
+ }) {
+ const sets: string[] = [];
+ const params: any[] = [itemId, restaurantId];
+ let idx = 3;
+
+ if (dto.name !== undefined) { sets.push(`name = $${idx++}`); params.push(dto.name.trim()); }
+ if (dto.description !== undefined) { sets.push(`description = $${idx++}`); params.push(dto.description); }
+ if (dto.price !== undefined) { sets.push(`price = $${idx++}`); params.push(dto.price); }
+ if (dto.isAvailable !== undefined) { sets.push(`is_available = $${idx++}`); params.push(dto.isAvailable); }
+ if (dto.isFeatured !== undefined) { sets.push(`is_featured = $${idx++}`); params.push(dto.isFeatured); }
+
+ if (sets.length === 0) return this.db.queryOne(`SELECT * FROM menu_items WHERE id = $1`, [itemId]);
+
+ sets.push(`updated_at = NOW()`);
+ return this.db.queryOne(
+ `UPDATE menu_items SET ${sets.join(', ')}
+ WHERE id = $1 AND restaurant_id = $2 RETURNING *`,
+ params,
+ );
+ }
+
+ async deleteMenuItem(restaurantId: string, itemId: string) {
+ return this.db.queryOne(
+ `DELETE FROM menu_items WHERE id = $1 AND restaurant_id = $2 RETURNING id`,
+ [itemId, restaurantId],
+ );
+ }
+
// ============================================================
// SAVINGS CALCULATOR (standalone - for marketing page)
// ============================================================
diff --git a/packages/web/src/app/restaurant/dashboard/page.tsx b/packages/web/src/app/restaurant/dashboard/page.tsx
index 9981ac7..acc82a6 100644
--- a/packages/web/src/app/restaurant/dashboard/page.tsx
+++ b/packages/web/src/app/restaurant/dashboard/page.tsx
@@ -171,6 +171,9 @@ export default function RestaurantDashboardPage() {
Restaurant Dashboard
+
+ Menu
+
([])
+ const [loading, setLoading] = useState(true)
+ const [showAddCategory, setShowAddCategory] = useState(false)
+ const [newCategoryName, setNewCategoryName] = useState('')
+ const [addingCategory, setAddingCategory] = useState(false)
+ const [addingToCategory, setAddingToCategory] = useState
(null)
+ const [editingItem, setEditingItem] = useState