Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions RestroHub/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ dependencies {
// Logging (JSON format for production)
implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5'
implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5'

//Excel Dependency
implementation 'org.apache.poi:poi-ooxml:5.2.3'
}

// tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.restroly.qrmenu.category.repository;

import java.util.List;
import java.util.Set;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -16,4 +17,6 @@ public interface CategoryRepository extends JpaRepository<Category, Long> {

Page<Category> findByIsDeleteFalse(Pageable pageable);

List<Category> findByNameInAndIsDeleteFalse(Set<String> names);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.restroly.qrmenu.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import java.util.Map;

@Getter
@Setter
@Configuration
@PropertySource("classpath:templateConfig.properties")
@ConfigurationProperties(prefix = "restroly.excel")
public class ExcelMappingConfig {
private String templatePath;
private Map<String, String> foodMapping;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.restroly.qrmenu.excel.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.restroly.qrmenu.common.dto.ApiResponse;
import com.restroly.qrmenu.excel.service.MenuExcelService;
import com.restroly.qrmenu.exception.ApiErrorResponse;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;

import static com.restroly.qrmenu.common.util.ApiConstants.SECURE_API_VERSION;

@RestController
@RequestMapping(SECURE_API_VERSION + "/excel")
public class ExcelFeatureController {
@Autowired
private MenuExcelService excelService;

@PostMapping("/menu/{branchId}")
@Operation(summary = "Import menu data from Excel file", description = "Uploads and processes an Excel file containing menu data for the specified branch. Existing categories, menu items, variants, and addon mappings are created or updated based on the spreadsheet contents.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Menu imported successfully", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ApiResponse.class), examples = @ExampleObject(value = """
{
"success": true,
"message": "Successfully Imported Menu",
"data": null,
"timestamp": "2024-01-15T10:30:00"
}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid file format or validation failed", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class), examples = @ExampleObject(value = """
{
"status": 400,
"error": "BAD_REQUEST",
"message": "Invalid Excel file format",
"path": "/api/v1/excel/menu/1",
"timestamp": "2024-01-15T10:30:00",
"traceId": "abc123"
}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Branch not found", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "Error while processing Excel file", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class)))
})
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Excel file containing menu data", required = true, content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, schema = @Schema(type = "object", requiredProperties = {
"file" })))
public ResponseEntity<ApiResponse<Object>> importData(@RequestParam("file") MultipartFile file, @PathVariable Long branchId)
throws Exception {
excelService.processImport(file, branchId);
ApiResponse<Object> response = ApiResponse.builder()
.success(true)
.message("Successfully Imported Menu")
.data(null)
.build();
return ResponseEntity.ok(response);
}

// ==============================================================================================================================================================================================
@GetMapping(value = "/menu/template", produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@Operation(summary = "Download menu import template", description = "Downloads a pre-formatted Excel template that can be used to import menu data into the system.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Template downloaded successfully", content = @Content(mediaType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", schema = @Schema(type = "string", format = "binary"))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "Failed to generate template", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class)))
})
public ResponseEntity<byte[]> getTemplate() throws Exception {
byte[] excelBytes = excelService.getTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=Branch_Menu_Template.xlsx");
return ResponseEntity.ok()
.headers(headers)
.body(excelBytes);
}

// =======================================================================================================================================
@GetMapping(value = "/menu/{branchId}", produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
@Operation(summary = "Export branch menu to Excel", description = "Generates and downloads an Excel file containing all categories, menu items, variants, addons, and related mappings for the specified branch.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Menu exported successfully", content = @Content(mediaType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", schema = @Schema(type = "string", format = "binary"))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "Branch not found", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class), examples = @ExampleObject(value = """
{
"status": 404,
"error": "NOT_FOUND",
"message": "Branch not found with id: 1",
"path": "/api/v1/excel/menu/1",
"timestamp": "2024-01-15T10:30:00",
"traceId": "abc123"
}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "Failed to generate Excel export", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ApiErrorResponse.class)))
})
public ResponseEntity<byte[]> exportData(@PathVariable Long branchId) throws Exception {
byte[] excelBytes = excelService.exportMenuToExcel(branchId);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=Branch_" + branchId + "_Menu.xlsx");
return ResponseEntity.ok()
.headers(headers)
.body(excelBytes);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.restroly.qrmenu.excel.service;

import org.springframework.web.multipart.MultipartFile;

public interface MenuExcelService {
public void processImport(MultipartFile file, Long branchId) throws Exception;
public byte[] exportMenuToExcel(Long branchId);
public byte[] getTemplate();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.restroly.qrmenu.excel.service.generic;

import java.util.List;

public interface GenericExcelExportService<T> {
byte[] exportToExcel(List<T> data);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.restroly.qrmenu.excel.service.generic;

import java.util.List;
import java.util.Set;

import org.springframework.web.multipart.MultipartFile;

public interface GenericExcelImportService<T>{
final Set<String> TRUTHY_VALUES = Set.of(
// --- Core Tech Standards ---
"true", "1", "t",

// --- 🇬🇧 English ---
"yes", "y", "yea", "yeah", "yep", "sure", "ok",

// --- 🇮🇳 Hindi & Urdu ---
"haan", "han", "ji", "हाँ", "जी", "ہاں", "جی",

// --- 🇮🇳 Bengali & Assamese ---
"haa", "hyan", "hya", "hoy", "হ্যাঁ", "হয়",

// --- 🇮🇳 Marathi & Gujarati ---
"ho", "ha", "हो", "હા",

// --- 🇮🇳 Punjabi ---
"aho", "ਹਾਂ",

// --- 🇮🇳 Odia ---
"hna", "aw", "ହଁ",

// --- 🇮🇳 Tamil ---
"aam", "aama", "aamam", "ஆம்", "ஆமாம்",

// --- 🇮🇳 Telugu ---
"avunu", "అవును",

// --- 🇮🇳 Kannada ---
"howdu", "ಹೌದು",

// --- 🇮🇳 Malayalam ---
"athe", "അതെ",

// --- 🇸🇦 Arabic ---
"naam", "na'am", "aywa", "نعم", "أيوا",

// --- 🇨🇳 Chinese (Mandarin) ---
"shi", "dui", "是", "对",

// --- 🇫🇷 French ---
"oui", "vrai",

// --- 🇩🇪 German ---
"ja", "wahr",

// --- 🇪🇸 Spanish & 🇵🇹 Portuguese ---
"si", "sí", "s", "sim", "verdadero", "verdade",

// --- 🇯🇵 Japanese ---
"hai", "はい",

// --- 🇰🇷 Korean ---
"ne", "ye", "네", "예",

// --- 🇹🇭 Thai ---
"chai", "ใช่",

// --- 🇻🇳 Vietnamese ---
"co", "có", "vang", "vâng", "da", "dạ", "đúng",

// --- 🇷🇺 Russian ---
"da", "да",

// --- 🇮🇩 Indonesian & 🇲🇾 Malay ---
"ya", "iya",

// --- 🇹🇷 Turkish ---
"evet", "e"
);
List<T> parseExcel(MultipartFile file) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.restroly.qrmenu.excel.service.generic.impl;

import java.io.ByteArrayOutputStream;
import java.util.List;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import com.restroly.qrmenu.excel.service.generic.GenericExcelExportService;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public abstract class GenericExcelExportServiceImpl<T> implements GenericExcelExportService<T> {

/**
* Child class defines how to build sheets, rows, and cells using the provided data.
*/
protected abstract void buildWorkbookFromData(Workbook workbook, List<T> data) throws Exception;

@Override
public byte[] exportToExcel(List<T> data) {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream out = new ByteArrayOutputStream()) {

log.debug("Created empty workbook. Delegating to child class for data population.");

// Hand control to the child class to build out the sheets and rows
buildWorkbookFromData(workbook, data);

workbook.write(out);
return out.toByteArray();

} catch (Exception e) {
log.error("Failed to generate Excel file", e);
throw new RuntimeException("Failed to generate Excel file: " + e.getMessage(), e);
}
}

// --- Protected Reusable Cell Writing Utilities ---

protected void setCellValue(Cell cell, String value) {
if (value != null) cell.setCellValue(value);
}

protected void setCellValue(Cell cell, Double value) {
if (value != null) cell.setCellValue(value);
}

protected void setCellValue(Cell cell, Integer value) {
if (value != null) cell.setCellValue(value);
}

protected void setCellValue(Cell cell, Boolean value) {
if (value != null) cell.setCellValue(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.restroly.qrmenu.excel.service.generic.impl;

import java.io.InputStream;
import java.util.List;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.multipart.MultipartFile;

import com.restroly.qrmenu.excel.service.generic.GenericExcelImportService;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public abstract class GenericExcelImportServiceImpl<T> implements GenericExcelImportService<T> {

/**
* Child class defines how to traverse the Workbook and extract data into a List.
*/
protected abstract List<T> extractDataFromWorkbook(Workbook workbook) throws Exception;

@Override
public List<T> parseExcel(MultipartFile file) throws Exception {
try (InputStream is = file.getInputStream();
Workbook workbook = new XSSFWorkbook(is)) {

log.debug("Successfully opened Excel file stream. Delegating to child class for extraction.");
return extractDataFromWorkbook(workbook);
}
}

// --- Protected Reusable Cell Extraction Utilities ---

protected String getCellValueAsString(Cell cell) {
if (cell == null)
return null;
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue().trim();
case NUMERIC -> String.valueOf((int) cell.getNumericCellValue());
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
default -> null;
};
}

protected Double getCellValueAsDouble(Cell cell) {
if (cell == null) return null;
if (cell.getCellType() == CellType.NUMERIC) {
return cell.getNumericCellValue();
} else if (cell.getCellType() == CellType.STRING) {
try {
return Double.parseDouble(cell.getStringCellValue().trim());
} catch (NumberFormatException e) {
return 0.0;
}
}
return null;
}

protected Integer getCellValueAsInteger(Cell cell) {
if (cell == null || cell.getCellType() != CellType.NUMERIC)
return null;
return (int) cell.getNumericCellValue();
}

protected Boolean getCellValueAsBoolean(Cell cell) {
if (cell == null)
return null;
if (cell.getCellType() == CellType.BOOLEAN)
return cell.getBooleanCellValue();
if (cell.getCellType() == CellType.STRING) {
String val = cell.getStringCellValue().trim().toLowerCase();
return TRUTHY_VALUES.contains(val);
}
return null;
}
}
Loading