diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/FeedbackController.java b/backend/src/main/java/com/rit/canteen/sales/controller/FeedbackController.java new file mode 100644 index 00000000..457bb8a1 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/controller/FeedbackController.java @@ -0,0 +1,151 @@ +package com.rit.canteen.sales.controller; + +import com.rit.canteen.sales.model.Feedback; +import com.rit.canteen.sales.model.ItemRating; +import com.rit.canteen.sales.model.Order; +import com.rit.canteen.sales.repository.FeedbackRepository; +import com.rit.canteen.sales.repository.OrderRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@RestController +@RequestMapping("/api/feedback") +public class FeedbackController { + + @Autowired + private FeedbackRepository feedbackRepository; + + @Autowired + private OrderRepository orderRepository; + + @GetMapping + public Page getAllFeedback( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + return feedbackRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(page, size)); + } + + @GetMapping("/item-details") + public Page> getItemDetails( + @RequestParam String productName, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size) { + Page ratings = feedbackRepository.findByProductName(productName, PageRequest.of(page, size)); + return ratings.map(r -> { + Map map = new HashMap<>(); + map.put("rating", r.getRating()); + + // Priority: Item-specific comment -> parent Feedback comment -> empty + String comment = r.getComment(); + if (comment == null || comment.isBlank()) { + if (r.getFeedback() != null) { + comment = r.getFeedback().getComment(); + } + } + + map.put("comment", comment != null ? comment : ""); + map.put("date", r.getFeedback() != null ? r.getFeedback().getCreatedAt() : java.time.LocalDateTime.now()); + return map; + }); + } + + @GetMapping("/stats") + public Map getFeedbackStats() { + Map stats = new HashMap<>(); + + Double avg = feedbackRepository.getAverageRating(); + stats.put("averageRating", avg != null ? avg : 0.0); + + List distribution = feedbackRepository.getRatingDistribution(); + List> distList = new ArrayList<>(); + for (Object[] row : distribution) { + Map item = new HashMap<>(); + item.put("rating", row[0]); + item.put("count", row[1]); + distList.add(item); + } + stats.put("distribution", distList); + + List topRated = feedbackRepository.getTopRatedItems(); + List> topList = new ArrayList<>(); + for (Object[] row : topRated) { + Map item = new HashMap<>(); + item.put("name", row[0]); + item.put("average", row[1]); + item.put("count", row[2]); + topList.add(item); + } + stats.put("ratedItems", topList); + + return stats; + } + + @GetMapping("/latest-unrated/{userId}") + public ResponseEntity getLatestUnratedOrder(@PathVariable Long userId) { + // Look for any order that is either PAID or COMPLETED + Optional latestOrderOpt = orderRepository.findFirstByUserIdOrderByCreatedAtDesc(userId); + + if (latestOrderOpt.isPresent()) { + Order order = latestOrderOpt.get(); + String status = order.getStatus().toUpperCase(); + if ((status.equals("PAID") || status.equals("COMPLETED")) && !order.isHasFeedback()) { + return ResponseEntity.ok(order); + } + } + return ResponseEntity.noContent().build(); + } + + @PostMapping("/skip/{orderId}") + @Transactional + public ResponseEntity skipFeedback(@PathVariable Long orderId) { + Optional orderOpt = orderRepository.findById(orderId); + if (orderOpt.isPresent()) { + Order order = orderOpt.get(); + order.setHasFeedback(true); + orderRepository.save(order); + return ResponseEntity.ok().build(); + } + return ResponseEntity.notFound().build(); + } + + @PostMapping("/submit") + @Transactional + public ResponseEntity submitFeedback(@RequestBody Feedback feedback) { + if (feedback.getOrder() == null || feedback.getOrder().getId() == null) { + return ResponseEntity.badRequest().build(); + } + + Optional orderOpt = orderRepository.findById(feedback.getOrder().getId()); + if (orderOpt.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + Order order = orderOpt.get(); + feedback.setOrder(order); + + // Link item ratings to feedback and propagate comment + if (feedback.getItemRatings() != null) { + for (ItemRating rating : feedback.getItemRatings()) { + rating.setFeedback(feedback); + // Propagate the general comment to each item for better visibility in item-wise dashboard + if (rating.getComment() == null || rating.getComment().isBlank()) { + rating.setComment(feedback.getComment()); + } + } + } + + Feedback savedFeedback = feedbackRepository.save(feedback); + + // Mark order as rated + order.setHasFeedback(true); + orderRepository.save(order); + + return ResponseEntity.ok(savedFeedback); + } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Feedback.java b/backend/src/main/java/com/rit/canteen/sales/model/Feedback.java new file mode 100644 index 00000000..98fd9d12 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/Feedback.java @@ -0,0 +1,62 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "canteen_feedbacks") +public class Feedback { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "order_id", nullable = false) + private Order order; + + private Long userId; + private String userName; + + @OneToMany(mappedBy = "feedback", cascade = CascadeType.ALL, orphanRemoval = true) + private List itemRatings = new ArrayList<>(); + + private String comment; + private LocalDateTime createdAt; + + public Feedback() { + this.createdAt = LocalDateTime.now(); + } + + @PrePersist + protected void onCreate() { + if (createdAt == null) createdAt = LocalDateTime.now(); + if (itemRatings != null) { + for (ItemRating rating : itemRatings) { + rating.setFeedback(this); + } + } + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Order getOrder() { return order; } + public void setOrder(Order order) { this.order = order; } + + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + + public String getUserName() { return userName; } + public void setUserName(String userName) { this.userName = userName; } + + public List getItemRatings() { return itemRatings; } + public void setItemRatings(List itemRatings) { this.itemRatings = itemRatings; } + + public String getComment() { return comment; } + public void setComment(String comment) { this.comment = comment; } + + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/ItemRating.java b/backend/src/main/java/com/rit/canteen/sales/model/ItemRating.java new file mode 100644 index 00000000..c5123a06 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/model/ItemRating.java @@ -0,0 +1,44 @@ +package com.rit.canteen.sales.model; + +import jakarta.persistence.*; +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +@Table(name = "canteen_item_ratings") +public class ItemRating { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "feedback_id") + @JsonIgnore + private Feedback feedback; + + private Long productId; + private String productName; + private int rating; // 1 to 5 + + @Column(length = 500) + private String comment; + + public ItemRating() {} + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Feedback getFeedback() { return feedback; } + public void setFeedback(Feedback feedback) { this.feedback = feedback; } + + public Long getProductId() { return productId; } + public void setProductId(Long productId) { this.productId = productId; } + + public String getProductName() { return productName; } + public void setProductName(String productName) { this.productName = productName; } + + public int getRating() { return rating; } + public void setRating(int rating) { this.rating = rating; } + + public String getComment() { return comment; } + public void setComment(String comment) { this.comment = comment; } +} diff --git a/backend/src/main/java/com/rit/canteen/sales/model/Order.java b/backend/src/main/java/com/rit/canteen/sales/model/Order.java index efb0dc79..b693df03 100644 --- a/backend/src/main/java/com/rit/canteen/sales/model/Order.java +++ b/backend/src/main/java/com/rit/canteen/sales/model/Order.java @@ -47,6 +47,9 @@ public class Order { @Column(name = "is_archived", nullable = false) private boolean isArchived = false; + @Column(name = "has_feedback", nullable = false) + private boolean hasFeedback = false; + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) private List items = new ArrayList<>(); @@ -100,6 +103,9 @@ public class Order { public boolean isArchived() { return isArchived; } public void setArchived(boolean archived) { isArchived = archived; } + public boolean isHasFeedback() { return hasFeedback; } + public void setHasFeedback(boolean hasFeedback) { this.hasFeedback = hasFeedback; } + public String getOrderType() { return orderType; } public void setOrderType(String orderType) { this.orderType = orderType; } diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/FeedbackRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/FeedbackRepository.java new file mode 100644 index 00000000..26da3c34 --- /dev/null +++ b/backend/src/main/java/com/rit/canteen/sales/repository/FeedbackRepository.java @@ -0,0 +1,30 @@ +package com.rit.canteen.sales.repository; + +import com.rit.canteen.sales.model.Feedback; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface FeedbackRepository extends JpaRepository { + Optional findByOrderId(Long orderId); + boolean existsByOrderId(Long orderId); + Page findAllByOrderByCreatedAtDesc(Pageable pageable); + + @Query("SELECT AVG(ir.rating) FROM ItemRating ir") + Double getAverageRating(); + + @Query("SELECT ir.rating, COUNT(ir) FROM ItemRating ir GROUP BY ir.rating") + List getRatingDistribution(); + + @Query("SELECT ir.productName, AVG(ir.rating), COUNT(ir) FROM ItemRating ir GROUP BY ir.productName ORDER BY AVG(ir.rating) DESC") + List getTopRatedItems(); + + @Query("SELECT ir FROM ItemRating ir WHERE TRIM(LOWER(ir.productName)) = TRIM(LOWER(:productName)) ORDER BY ir.feedback.createdAt DESC") + Page findByProductName(@org.springframework.data.repository.query.Param("productName") String productName, Pageable pageable); +} diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java index bdb481ba..650cf8aa 100644 --- a/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java +++ b/backend/src/main/java/com/rit/canteen/sales/repository/OrderRepository.java @@ -13,6 +13,9 @@ import java.util.Optional; public interface OrderRepository extends JpaRepository, JpaSpecificationExecutor { List findByUserIdOrderByCreatedAtDesc(Long userId); + Optional findFirstByUserIdOrderByCreatedAtDesc(Long userId); + Optional findFirstByUserIdAndStatusAndHasFeedbackFalseOrderByCreatedAtDesc(Long userId, String status); + Optional findFirstByUserIdAndStatusOrderByCreatedAtDesc(Long userId, String status); long countByCreatedAtGreaterThanEqual(LocalDateTime startOfDay); long countByCreatedAtBetween(LocalDateTime start, LocalDateTime end); List findByIsArchivedFalseAndCreatedAtBefore(LocalDateTime timestamp); diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java b/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java index 949fc659..e6e03677 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/DatabaseSeeder.java @@ -27,11 +27,33 @@ public class DatabaseSeeder implements CommandLineRunner { @Override public void run(String... args) throws Exception { repairStallsSchema(); + repairOrdersSchema(); + repairFeedbackSchema(); repairLobColumns(); seedCategories(); seedProducts(); } + private void repairFeedbackSchema() { + System.out.println("Checking schema consistency for 'canteen_item_ratings' table..."); + try { + jdbcTemplate.execute("ALTER TABLE canteen_item_ratings ADD COLUMN IF NOT EXISTS comment VARCHAR(500)"); + System.out.println("Feedback schema consistency confirmed."); + } catch (Exception e) { + System.err.println("Feedback schema repair notice: " + e.getMessage()); + } + } + + private void repairOrdersSchema() { + System.out.println("Checking schema consistency for 'canteen_orders' table..."); + try { + jdbcTemplate.execute("ALTER TABLE canteen_orders ADD COLUMN IF NOT EXISTS has_feedback BOOLEAN DEFAULT FALSE"); + System.out.println("Orders schema consistency confirmed."); + } catch (Exception e) { + System.err.println("Orders schema repair notice: " + e.getMessage()); + } + } + private void repairStallsSchema() { System.out.println("Checking schema consistency for 'stalls' table..."); try { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 084693a6..cd6c1f41 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,6 +21,7 @@ import PurchaseSummary from './pages/PurchaseSummary.tsx'; import IntentDashboard from './pages/IntentDashboard.tsx'; import IntentList from './pages/IntentList.tsx'; import Reports from './pages/Reports.tsx'; +import Feedback from './pages/Feedback.tsx'; const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const isLoggedIn = sessionStorage.getItem('isLoggedIn') === 'true'; @@ -86,20 +87,13 @@ function App() { {/* Others */} } /> - } /> - } /> - } /> + } /> {/* Stores */} } /> } /> } /> } /> - - {/* Promotions */} - } /> - } /> - } /> diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 905d62cb..6056277b 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -6,7 +6,6 @@ import { CreditCard, Gauge, LayoutGrid, - Megaphone, MessageSquare, ShoppingCart, Store, @@ -114,17 +113,6 @@ const menuItems: MenuItem[] = [ { title: 'Stalls', path: '/stores/stalls' } ] }, - { title: 'Table', icon: Table2, path: '/table' }, - { title: 'Wallet', icon: CreditCard, path: '/wallet' }, - { - title: 'Promotions', - icon: Megaphone, - subMenu: [ - { title: 'Discounts', path: '/promotions/discounts' }, - { title: 'Coupon', path: '/promotions/coupon' }, - { title: 'CouponTemplate', path: '/promotions/coupon-template' } - ] - }, { title: 'Feedback', icon: MessageSquare, path: '/feedback' }, ]; @@ -152,9 +140,6 @@ const Sidebar = () => { 'Expense': 'expense', 'Reports': 'reports', 'Stores': 'stores', - 'Table': 'table', - 'Wallet': 'wallet', - 'Promotions': 'promotions', 'Feedback': 'feedback' }; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 7314bf99..eb95cc5c 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -6,7 +6,8 @@ import { ShoppingBag, ArrowRight, Wallet, - BarChart3 + BarChart3, + MessageSquare } from 'lucide-react'; import { AreaChart, @@ -139,6 +140,14 @@ const Dashboard = () => {
+ + +
+ + {!stats ? ( +
+ +

Aggregating quality data...

+
+ ) : ( +
+ {/* Summary Cards */} +
+
+
+ +
+
+

Global Average

+

{stats.averageRating.toFixed(1)} / 5.0

+
+
+ +
+
+ +
+
+

Rated Products

+

{stats?.ratedItems?.length || 0}

+
+
+ +
+
+ +
+
+

Highest Rated

+

{stats?.ratedItems?.[0]?.name || 'N/A'}

+

{stats?.ratedItems?.[0]?.average?.toFixed(1) || '0.0'} avg rating

+
+
+
+ +
+ {/* Rating Distribution Chart */} +
+
+
+ +
+

Rating Distribution

+
+ +
+ + + + {pieData.map((_, index) => ( + + ))} + + + + +
+
+ + {/* Quality Trend / Top items chart */} +
+
+
+ +
+

Top Performing Foods

+
+ +
+ + + + + + + + + +
+
+
+ + {/* Food Items List */} +
+
+

Product Performance Log

+
+ +
+ + + + + + + + + + + {(stats?.ratedItems || []).map((item, index) => ( + handleItemClick(item)} + > + + + + + + ))} + +
Food ItemAverage RatingTotal ReviewsActions
+
{item.name}
+
Primary Menu
+
+
+ {item.average.toFixed(1)} + {renderStars(Math.round(item.average))} +
+
+
+ {item.count} Feedbacks +
+
+ +
+
+
+
+ )} + + {/* Details Modal */} + + {selectedItem && ( +
+ setSelectedItem(null)} + className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" + /> + +
+
+

{selectedItem.name}

+
+
+ {selectedItem.average.toFixed(1)} +
+ {selectedItem.count} TOTAL REVIEWS +
+
+ +
+ +
+
+ {detailsLoading && itemDetails.length === 0 ? ( +
+ +

Loading reviews...

+
+ ) : itemDetails.map((detail, idx) => ( +
+
+
+
+ +
+ {format(new Date(detail.date), 'dd MMM yyyy, hh:mm a')} +
+ {renderStars(detail.rating, 16)} +
+

+ "{detail.comment || 'No written review provided'}" +

+
+
A
+ Anonymous Customer +
+
+ ))} + + {!detailsLoading && itemDetails.length === 0 && ( +
+ No detailed reviews found. +
+ )} +
+
+ + {detailsTotal > detailsPageSize && ( +
+ +
+ )} +
+
+ )} +
+ + ); +}; + +export default Feedback; diff --git a/ordering_site/src/components/FeedbackModal.css b/ordering_site/src/components/FeedbackModal.css new file mode 100644 index 00000000..cd60c5eb --- /dev/null +++ b/ordering_site/src/components/FeedbackModal.css @@ -0,0 +1,162 @@ +.feedback-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(8px); + z-index: 5000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.feedback-modal { + background: white; + width: 100%; + max-width: 450px; + border-radius: 32px; + overflow: hidden; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + display: flex; + flex-direction: column; + max-height: 90vh; +} + +.feedback-header { + padding: 24px; + text-align: center; + background: linear-gradient(135deg, var(--primary) 0%, #4338ca 100%); + color: white; +} + +.feedback-header h2 { + font-size: 1.5rem; + font-weight: 800; + margin-bottom: 4px; +} + +.feedback-header p { + font-size: 0.9rem; + opacity: 0.9; + font-weight: 500; +} + +.feedback-body { + padding: 24px; + overflow-y: auto; + flex: 1; +} + +.rating-item-card { + background: var(--bg); + border-radius: 20px; + padding: 16px; + margin-bottom: 16px; + border: 1px solid var(--border); +} + +.rating-item-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} + +.rating-item-name { + font-weight: 700; + color: var(--text-dark); + font-size: 1rem; +} + +.stars-container { + display: flex; + gap: 8px; + justify-content: center; +} + +.star-btn { + background: none; + border: none; + padding: 4px; + cursor: pointer; + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + color: #e2e8f0; +} + +.star-btn.active { + color: #fbbf24; + transform: scale(1.1); +} + +.star-btn:active { + transform: scale(0.9); +} + +.feedback-comment-section { + margin-top: 8px; +} + +.feedback-comment-section label { + display: block; + font-size: 0.85rem; + font-weight: 700; + color: var(--text-mid); + margin-bottom: 8px; +} + +.feedback-comment-section textarea { + width: 100%; + border-radius: 16px; + border: 2px solid var(--border); + padding: 12px 16px; + font-family: inherit; + font-size: 0.95rem; + resize: none; + min-height: 80px; + outline: none; + transition: all 0.2s; +} + +.feedback-comment-section textarea:focus { + border-color: var(--primary); + background: white; +} + +.feedback-footer { + padding: 24px; + display: flex; + gap: 12px; + background: white; + border-top: 1px solid var(--border); +} + +.btn-submit-feedback { + flex: 2; + background: var(--primary); + color: white; + border: none; + padding: 16px; + border-radius: 16px; + font-weight: 700; + font-size: 1rem; + box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3); +} + +.btn-submit-feedback:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.btn-skip-feedback { + flex: 1; + background: var(--bg); + color: var(--text-mid); + border: 1px solid var(--border); + padding: 16px; + border-radius: 16px; + font-weight: 700; + font-size: 1rem; +} diff --git a/ordering_site/src/components/FeedbackModal.tsx b/ordering_site/src/components/FeedbackModal.tsx new file mode 100644 index 00000000..ee575ff5 --- /dev/null +++ b/ordering_site/src/components/FeedbackModal.tsx @@ -0,0 +1,141 @@ +import React, { useState } from 'react'; +import { Star, Send, X } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import './FeedbackModal.css'; + +interface OrderItem { + productId: number; + productName: string; + price: number; + quantity: number; +} + +interface Order { + id: number; + orderNumber: string; + items: OrderItem[]; +} + +interface FeedbackModalProps { + order: Order; + userName: string; + userId: number; + onClose: () => void; + onSubmit: (data: any) => Promise; +} + +const FeedbackModal: React.FC = ({ order, userName, userId, onClose, onSubmit }) => { + const [ratings, setRatings] = useState>( + order.items.reduce((acc, item) => ({ ...acc, [item.productId]: 0 }), {}) + ); + const [comment, setComment] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSetRating = (productId: number, rating: number) => { + setRatings(prev => ({ ...prev, [productId]: rating })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Check if at least one item is rated + const hasAtLeastOneRating = Object.values(ratings).some(r => r > 0); + if (!hasAtLeastOneRating) { + alert('Please provide at least one rating'); + return; + } + + setIsSubmitting(true); + + const itemRatings = order.items.map(item => ({ + productId: item.productId, + productName: item.productName, + rating: ratings[item.productId] || 0 + })); + + const feedbackData = { + order: { id: order.id }, + userId: userId, + userName: userName, + itemRatings: itemRatings, + comment: comment + }; + + try { + await onSubmit(feedbackData); + onClose(); + } catch (error) { + console.error('Error submitting feedback:', error); + alert('Failed to submit feedback. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+ e.stopPropagation()} + > +
+

Rate Your Meal

+

How was your experience with Order #{order.orderNumber.split('-')[1]}?

+
+ +
+ {order.items.map((item) => ( +
+
+ {item.productName} + + {item.quantity} {item.quantity > 1 ? 'Units' : 'Unit'} + +
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ ))} + +
+ +