diff --git a/backend/HMIWeb/MachineBridge.cs b/backend/HMIWeb/MachineBridge.cs index c1c4342..9161fc8 100644 --- a/backend/HMIWeb/MachineBridge.cs +++ b/backend/HMIWeb/MachineBridge.cs @@ -153,5 +153,58 @@ namespace HMIWeb return Newtonsoft.Json.JsonConvert.SerializeObject(recipes); } + + public string CopyRecipe(string recipeId, string newName) + { + Console.WriteLine($"[C#] Copying Recipe: {recipeId} as {newName}"); + + try + { + // In real app, copy recipe data from database/file + // Generate new ID + string newId = System.Guid.NewGuid().ToString().Substring(0, 8); + string timestamp = System.DateTime.Now.ToString("yyyy-MM-dd"); + + var response = new { + success = true, + message = "Recipe copied successfully", + newRecipe = new { + id = newId, + name = newName, + lastModified = timestamp + } + }; + return JsonConvert.SerializeObject(response); + } + catch (Exception ex) + { + var response = new { success = false, message = ex.Message }; + return JsonConvert.SerializeObject(response); + } + } + + public string DeleteRecipe(string recipeId) + { + Console.WriteLine($"[C#] Deleting Recipe: {recipeId}"); + + try + { + // In real app, delete recipe from database/file + // Check if recipe is in use + if (recipeId == _host.GetCurrentRecipe()) + { + var response = new { success = false, message = "Cannot delete currently selected recipe" }; + return JsonConvert.SerializeObject(response); + } + + var response = new { success = true, message = "Recipe deleted successfully", recipeId = recipeId }; + return JsonConvert.SerializeObject(response); + } + catch (Exception ex) + { + var response = new { success = false, message = ex.Message }; + return JsonConvert.SerializeObject(response); + } + } } } diff --git a/backend/HMIWeb/WebSocketServer.cs b/backend/HMIWeb/WebSocketServer.cs index 1abdf4f..75f6358 100644 --- a/backend/HMIWeb/WebSocketServer.cs +++ b/backend/HMIWeb/WebSocketServer.cs @@ -177,6 +177,23 @@ namespace HMIWeb var response = new { type = "RECIPE_SELECTED", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) }; await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response)); } + else if (type == "COPY_RECIPE") + { + string recipeId = json.recipeId; + string newName = json.newName; + var bridge = new MachineBridge(_mainForm); + string resultJson = bridge.CopyRecipe(recipeId, newName); + var response = new { type = "RECIPE_COPIED", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) }; + await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response)); + } + else if (type == "DELETE_RECIPE") + { + string recipeId = json.recipeId; + var bridge = new MachineBridge(_mainForm); + string resultJson = bridge.DeleteRecipe(recipeId); + var response = new { type = "RECIPE_DELETED", data = Newtonsoft.Json.JsonConvert.DeserializeObject(resultJson) }; + await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response)); + } } catch (Exception ex) { diff --git a/frontend/communication.ts b/frontend/communication.ts index 2593a66..54ad236 100644 --- a/frontend/communication.ts +++ b/frontend/communication.ts @@ -221,6 +221,66 @@ class CommunicationLayer { }); } } + + public async copyRecipe(recipeId: string, newName: string): Promise<{ success: boolean; message: string; newRecipe?: any }> { + if (isWebView) { + const resultJson = await window.chrome!.webview!.hostObjects.machine.CopyRecipe(recipeId, newName); + return JSON.parse(resultJson); + } else { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + setTimeout(() => { + if (!this.isConnected) reject({ success: false, message: "WebSocket connection timeout" }); + }, 2000); + } + + const timeoutId = setTimeout(() => { + this.listeners = this.listeners.filter(cb => cb !== handler); + reject({ success: false, message: "Recipe copy timeout" }); + }, 10000); + + const handler = (data: any) => { + if (data.type === 'RECIPE_COPIED') { + clearTimeout(timeoutId); + this.listeners = this.listeners.filter(cb => cb !== handler); + resolve(data.data); + } + }; + this.listeners.push(handler); + this.ws?.send(JSON.stringify({ type: 'COPY_RECIPE', recipeId, newName })); + }); + } + } + + public async deleteRecipe(recipeId: string): Promise<{ success: boolean; message: string; recipeId?: string }> { + if (isWebView) { + const resultJson = await window.chrome!.webview!.hostObjects.machine.DeleteRecipe(recipeId); + return JSON.parse(resultJson); + } else { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + setTimeout(() => { + if (!this.isConnected) reject({ success: false, message: "WebSocket connection timeout" }); + }, 2000); + } + + const timeoutId = setTimeout(() => { + this.listeners = this.listeners.filter(cb => cb !== handler); + reject({ success: false, message: "Recipe delete timeout" }); + }, 10000); + + const handler = (data: any) => { + if (data.type === 'RECIPE_DELETED') { + clearTimeout(timeoutId); + this.listeners = this.listeners.filter(cb => cb !== handler); + resolve(data.data); + } + }; + this.listeners.push(handler); + this.ws?.send(JSON.stringify({ type: 'DELETE_RECIPE', recipeId })); + }); + } + } } export const comms = new CommunicationLayer(); diff --git a/frontend/pages/RecipePage.tsx b/frontend/pages/RecipePage.tsx index 0591898..377565c 100644 --- a/frontend/pages/RecipePage.tsx +++ b/frontend/pages/RecipePage.tsx @@ -41,6 +41,66 @@ export const RecipePage: React.FC = () => { // In real app: await comms.saveRecipe(editedRecipe); }; + const handleCopy = async () => { + if (!selectedId) return; + + const selectedRecipe = recipes.find(r => r.id === selectedId); + if (!selectedRecipe) return; + + const newName = prompt(`Copy "${selectedRecipe.name}" as:`, `${selectedRecipe.name}_Copy`); + if (!newName || newName.trim() === '') return; + + try { + const result = await comms.copyRecipe(selectedId, newName.trim()); + if (result.success && result.newRecipe) { + const newRecipeList = [...recipes, result.newRecipe]; + setRecipes(newRecipeList); + setSelectedId(result.newRecipe.id); + setEditedRecipe(result.newRecipe); + console.log("Recipe copied successfully:", result.newRecipe); + } else { + alert(`Failed to copy recipe: ${result.message}`); + } + } catch (error: any) { + alert(`Error copying recipe: ${error.message || 'Unknown error'}`); + console.error('Recipe copy error:', error); + } + }; + + const handleDelete = async () => { + if (!selectedId) return; + + const selectedRecipe = recipes.find(r => r.id === selectedId); + if (!selectedRecipe) return; + + const confirmed = window.confirm(`Are you sure you want to delete "${selectedRecipe.name}"?`); + if (!confirmed) return; + + try { + const result = await comms.deleteRecipe(selectedId); + if (result.success) { + const newRecipeList = recipes.filter(r => r.id !== selectedId); + setRecipes(newRecipeList); + + // Select first recipe or clear selection + if (newRecipeList.length > 0) { + setSelectedId(newRecipeList[0].id); + setEditedRecipe(newRecipeList[0]); + } else { + setSelectedId(null); + setEditedRecipe(null); + } + + console.log("Recipe deleted successfully"); + } else { + alert(`Failed to delete recipe: ${result.message}`); + } + } catch (error: any) { + alert(`Error deleting recipe: ${error.message || 'Unknown error'}`); + console.error('Recipe delete error:', error); + } + }; + return (
{/* Header */} @@ -93,10 +153,22 @@ export const RecipePage: React.FC = () => { - + - +
diff --git a/frontend/types.ts b/frontend/types.ts index f7be9ca..c241bdc 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -60,6 +60,8 @@ declare global { SetIO(id: number, isInput: boolean, state: boolean): Promise; SystemControl(command: string): Promise; SelectRecipe(recipeId: string): Promise; + CopyRecipe(recipeId: string, newName: string): Promise; + DeleteRecipe(recipeId: string): Promise; GetConfig(): Promise; GetIOList(): Promise; GetRecipeList(): Promise;