diff --git a/Prototyping/Cod50100.PVSCaseHistoryMgmt.al b/Prototyping/Cod50100.PVSCaseHistoryMgmt.al
new file mode 100644
index 0000000..3b443bc
--- /dev/null
+++ b/Prototyping/Cod50100.PVSCaseHistoryMgmt.al
@@ -0,0 +1,111 @@
+///
+/// Populates the PVS Case History table from historical PrintVis cases.
+///
+codeunit 50100 "PVS Case History Mgmt"
+{
+ Access = Public;
+
+ trigger OnRun()
+ begin
+ PopulateHistory();
+ end;
+
+ ///
+ /// Clears and repopulates the Case History table with cases from the last N months
+ /// as configured in PVS AI Case Setup.
+ ///
+ procedure PopulateHistory()
+ var
+ Setup: Record "PVS AI Case Setup";
+ CaseHistory: Record "PVS Case History";
+ PVSCaseHeader: Record "PVS Case";
+ Customer: Record Customer;
+ PatternAnalysis: Codeunit "PVS Pattern Analysis";
+ FromDate: Date;
+ InsertCount: Integer;
+ begin
+ GetOrCreateSetup(Setup);
+ FromDate := CalcDate('<-' + Format(Setup."History Months") + 'M>', Today());
+
+ CaseHistory.DeleteAll(false);
+
+ PVSCaseHeader.SetFilter(PVSCaseHeader."Order Date", '>=%1', FromDate);
+ if PVSCaseHeader.FindSet() then
+ repeat
+ CaseHistory.Init();
+ CaseHistory."Case No." := CopyStr(Format(PVSCaseHeader.ID), 1, MaxStrLen(CaseHistory."Case No."));
+ CaseHistory."Customer No." := PVSCaseHeader."Customer No.";
+ CaseHistory."Job Type Code" := PVSCaseHeader."Job Type Code";
+ CaseHistory."Order Date" := PVSCaseHeader."Order Date";
+ CaseHistory."Due Date" := PVSCaseHeader."Due Date";
+ CaseHistory."Quantity" := PVSCaseHeader."Quantity 1";
+ CaseHistory."Total Amount" := PVSCaseHeader."Total Sales Price";
+ CaseHistory."Status" := Format(PVSCaseHeader.Status);
+ CaseHistory."Created Date" := PVSCaseHeader."Order Date";
+
+ if (CaseHistory."Order Date" <> 0D) and (CaseHistory."Due Date" <> 0D) and
+ (CaseHistory."Due Date" >= CaseHistory."Order Date")
+ then
+ CaseHistory."Turnaround Days" :=
+ CaseHistory."Due Date" - CaseHistory."Order Date";
+
+ if Customer.Get(PVSCaseHeader."Customer No.") then begin
+ CaseHistory."Customer Name" :=
+ CopyStr(Customer.Name, 1, MaxStrLen(CaseHistory."Customer Name"));
+ CaseHistory."Customer Segment" := Customer."Customer Posting Group";
+ end;
+
+ CaseHistory.Insert(false);
+ InsertCount += 1;
+ until PVSCaseHeader.Next() = 0;
+
+ Setup."Total Cases Loaded" := InsertCount;
+ Setup."Last Refresh Date" := CurrentDateTime();
+ Setup.Modify(true);
+
+ PatternAnalysis.AnalyzePatterns();
+ end;
+
+ ///
+ /// Creates a recurring Job Queue Entry that refreshes case history nightly.
+ ///
+ procedure CreateJobQueueEntry()
+ var
+ Setup: Record "PVS AI Case Setup";
+ JobQueueEntry: Record "Job Queue Entry";
+ begin
+ GetOrCreateSetup(Setup);
+
+ JobQueueEntry.Init();
+ JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Codeunit;
+ JobQueueEntry."Object ID to Run" := Codeunit::"PVS Case History Mgmt";
+ JobQueueEntry."Job Queue Category Code" := Setup."Job Queue Category Code";
+ JobQueueEntry."Recurring Job" := true;
+ JobQueueEntry."No. of Minutes between Runs" := 1440;
+ JobQueueEntry."Starting Time" := 020000T;
+ JobQueueEntry."Run on Mondays" := true;
+ JobQueueEntry."Run on Tuesdays" := true;
+ JobQueueEntry."Run on Wednesdays" := true;
+ JobQueueEntry."Run on Thursdays" := true;
+ JobQueueEntry."Run on Fridays" := true;
+ JobQueueEntry."Run on Saturdays" := false;
+ JobQueueEntry."Run on Sundays" := false;
+ JobQueueEntry.Description := CopyStr(
+ 'Refresh PVS Case History (AI)', 1, MaxStrLen(JobQueueEntry.Description));
+ JobQueueEntry.Insert(true);
+
+ Codeunit.Run(Codeunit::"Job Queue - Enqueue", JobQueueEntry);
+
+ Message('Job Queue Entry created. Case history will refresh nightly at 02:00.');
+ end;
+
+ local procedure GetOrCreateSetup(var Setup: Record "PVS AI Case Setup")
+ begin
+ if not Setup.Get('DEFAULT') then begin
+ Setup.Init();
+ Setup.Code := 'DEFAULT';
+ Setup."History Months" := 12;
+ Setup.Insert(true);
+ end;
+ end;
+}
diff --git a/Prototyping/Cod50101.PVSPatternAnalysis.al b/Prototyping/Cod50101.PVSPatternAnalysis.al
new file mode 100644
index 0000000..f1c10d2
--- /dev/null
+++ b/Prototyping/Cod50101.PVSPatternAnalysis.al
@@ -0,0 +1,158 @@
+///
+/// Analyses Case History records to produce pattern summaries stored in PVS Case Pattern.
+/// Patterns group cases by Job Type + Material + Customer Segment and compute statistical
+/// summaries (averages, min/max, confidence score) used to guide Copilot suggestions.
+///
+codeunit 50101 "PVS Pattern Analysis"
+{
+ Access = Public;
+
+ ///
+ /// Rebuilds the PVS Case Pattern table from all current PVS Case History records.
+ ///
+ procedure AnalyzePatterns()
+ var
+ Setup: Record "PVS AI Case Setup";
+ CasePattern: Record "PVS Case Pattern";
+ CaseHistory: Record "PVS Case History";
+ TotalAmount: Decimal;
+ MinAmount: Decimal;
+ MaxAmount: Decimal;
+ TotalQuantity: Decimal;
+ TotalTurnaround: Decimal;
+ CaseCount: Integer;
+ PatternCount: Integer;
+ CurrentJobType: Code[20];
+ CurrentMaterial: Code[20];
+ CurrentSegment: Text[50];
+ begin
+ CasePattern.DeleteAll(false);
+
+ CaseHistory.SetCurrentKey(CaseHistory."Job Type Code", CaseHistory."Material Code", CaseHistory."Customer Segment");
+ if not CaseHistory.FindSet() then
+ exit;
+
+ CurrentJobType := CaseHistory."Job Type Code";
+ CurrentMaterial := CaseHistory."Material Code";
+ CurrentSegment := CaseHistory."Customer Segment";
+ InitAccumulators(TotalAmount, MinAmount, MaxAmount, TotalQuantity, TotalTurnaround, CaseCount);
+
+ repeat
+ if (CaseHistory."Job Type Code" <> CurrentJobType) or
+ (CaseHistory."Material Code" <> CurrentMaterial) or
+ (CaseHistory."Customer Segment" <> CurrentSegment)
+ then begin
+ WritePattern(
+ CasePattern, CurrentJobType, CurrentMaterial, CurrentSegment,
+ TotalAmount, MinAmount, MaxAmount, TotalQuantity, TotalTurnaround, CaseCount);
+ PatternCount += 1;
+
+ CurrentJobType := CaseHistory."Job Type Code";
+ CurrentMaterial := CaseHistory."Material Code";
+ CurrentSegment := CaseHistory."Customer Segment";
+ InitAccumulators(
+ TotalAmount, MinAmount, MaxAmount, TotalQuantity, TotalTurnaround, CaseCount);
+ end;
+
+ AccumulateCase(
+ CaseHistory, TotalAmount, MinAmount, MaxAmount,
+ TotalQuantity, TotalTurnaround, CaseCount);
+ until CaseHistory.Next() = 0;
+
+ // Write the last group
+ if CaseCount > 0 then begin
+ WritePattern(
+ CasePattern, CurrentJobType, CurrentMaterial, CurrentSegment,
+ TotalAmount, MinAmount, MaxAmount, TotalQuantity, TotalTurnaround, CaseCount);
+ PatternCount += 1;
+ end;
+
+ if Setup.Get('DEFAULT') then begin
+ Setup."Total Patterns Found" := PatternCount;
+ Setup.Modify(true);
+ end;
+ end;
+
+ local procedure InitAccumulators(
+ var TotalAmount: Decimal;
+ var MinAmount: Decimal;
+ var MaxAmount: Decimal;
+ var TotalQuantity: Decimal;
+ var TotalTurnaround: Decimal;
+ var CaseCount: Integer)
+ begin
+ TotalAmount := 0;
+ MinAmount := 0;
+ MaxAmount := 0;
+ TotalQuantity := 0;
+ TotalTurnaround := 0;
+ CaseCount := 0;
+ end;
+
+ local procedure AccumulateCase(
+ CaseHistory: Record "PVS Case History";
+ var TotalAmount: Decimal;
+ var MinAmount: Decimal;
+ var MaxAmount: Decimal;
+ var TotalQuantity: Decimal;
+ var TotalTurnaround: Decimal;
+ var CaseCount: Integer)
+ begin
+ CaseCount += 1;
+ TotalAmount += CaseHistory."Total Amount";
+ TotalQuantity += CaseHistory.Quantity;
+ TotalTurnaround += CaseHistory."Turnaround Days";
+
+ if CaseCount = 1 then begin
+ MinAmount := CaseHistory."Total Amount";
+ MaxAmount := CaseHistory."Total Amount";
+ end else begin
+ if CaseHistory."Total Amount" < MinAmount then
+ MinAmount := CaseHistory."Total Amount";
+ if CaseHistory."Total Amount" > MaxAmount then
+ MaxAmount := CaseHistory."Total Amount";
+ end;
+ end;
+
+ local procedure WritePattern(
+ var CasePattern: Record "PVS Case Pattern";
+ JobTypeCode: Code[20];
+ MaterialCode: Code[20];
+ CustomerSegment: Text[50];
+ TotalAmount: Decimal;
+ MinAmount: Decimal;
+ MaxAmount: Decimal;
+ TotalQuantity: Decimal;
+ TotalTurnaround: Decimal;
+ CaseCount: Integer)
+ begin
+ CasePattern.Init();
+ CasePattern."Job Type Code" := JobTypeCode;
+ CasePattern."Material Code" := MaterialCode;
+ CasePattern."Customer Segment" :=
+ CopyStr(CustomerSegment, 1, MaxStrLen(CasePattern."Customer Segment"));
+ CasePattern."Case Count" := CaseCount;
+ CasePattern."Min Total Amount" := MinAmount;
+ CasePattern."Max Total Amount" := MaxAmount;
+ CasePattern."Last Updated" := CurrentDateTime();
+
+ if CaseCount > 0 then begin
+ CasePattern."Avg Total Amount" := Round(TotalAmount / CaseCount, 0.01);
+ CasePattern."Avg Quantity" := Round(TotalQuantity / CaseCount, 0.01);
+ CasePattern."Avg Turnaround Days" := Round(TotalTurnaround / CaseCount, 0.1);
+ end;
+
+ // Confidence = min(100, sqrt(CaseCount) * 20) — more cases = higher confidence
+ CasePattern."Confidence Score" :=
+ Round(MinDecimal(100, Power(CaseCount, 0.5) * 20), 0.1);
+
+ CasePattern.Insert(false);
+ end;
+
+ local procedure MinDecimal(A: Decimal; B: Decimal): Decimal
+ begin
+ if A < B then
+ exit(A);
+ exit(B);
+ end;
+}
diff --git a/Prototyping/Cod50102.PVSCopilotCaseGen.al b/Prototyping/Cod50102.PVSCopilotCaseGen.al
new file mode 100644
index 0000000..950675f
--- /dev/null
+++ b/Prototyping/Cod50102.PVSCopilotCaseGen.al
@@ -0,0 +1,210 @@
+///
+/// Handles AI-powered case generation using Azure OpenAI.
+/// Uses historical patterns from PVS Case Pattern to enrich the prompt with context,
+/// then calls the AOAI chat completions API and maps the response back to case fields.
+///
+codeunit 50102 "PVS Copilot Case Gen"
+{
+ Access = Public;
+
+ ///
+ /// Generates a suggested case record based on the user's natural-language prompt
+ /// and historical patterns. The result is stored in a temporary PVS Case History record.
+ ///
+ procedure GenerateCaseSuggestion(UserPrompt: Text; var SuggestedCase: Record "PVS Case History")
+ var
+ Setup: Record "PVS AI Case Setup";
+ AzureOpenAI: Codeunit "Azure OpenAI";
+ AOAIChatMessages: Codeunit "AOAI Chat Messages";
+ AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params";
+ AOAIOperationResponse: Codeunit "AOAI Operation Response";
+ SystemPrompt: Text;
+ ResponseText: Text;
+ ApiKey: Text;
+ begin
+ if not Setup.Get('DEFAULT') then
+ Error('PVS AI Case Setup not found. Please configure the AI settings first.');
+ if not Setup."Enable Copilot" then
+ Error('Copilot is not enabled. Please enable it in PVS AI Case Setup.');
+ if Setup."AOAI Endpoint" = '' then
+ Error('Azure OpenAI Endpoint is not configured in PVS AI Case Setup.');
+ if Setup."AOAI Deployment Name" = '' then
+ Error('Azure OpenAI Deployment Name is not configured in PVS AI Case Setup.');
+
+ GetApiKey(ApiKey);
+ if ApiKey = '' then
+ Error('Azure OpenAI API Key is not stored. Please enter it in PVS AI Case Setup.');
+
+ SystemPrompt := BuildSystemPrompt(UserPrompt);
+
+ AzureOpenAI.SetAuthorization(
+ Enum::"AOAI Model Type"::"Chat Completions",
+ Setup."AOAI Endpoint",
+ Setup."AOAI Deployment Name",
+ ApiKey);
+ AzureOpenAI.SetCopilotCapability(Enum::"Copilot Capability"::"PVS Case Assistant");
+
+ AOAIChatCompletionParams.SetMaxTokens(800);
+ AOAIChatCompletionParams.SetTemperature(0.3);
+
+ AOAIChatMessages.AddSystemMessage(SystemPrompt);
+ AOAIChatMessages.AddUserMessage(UserPrompt);
+
+ AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAIChatCompletionParams, AOAIOperationResponse);
+
+ if not AOAIOperationResponse.IsSuccess() then
+ Error('Copilot generation failed: %1', AOAIOperationResponse.GetError());
+
+ ResponseText := AOAIChatMessages.GetLastMessage();
+ ParseAOAIResponse(ResponseText, SuggestedCase);
+ end;
+
+ ///
+ /// Creates a new PVS Case from a Copilot suggestion and opens it for review.
+ ///
+ procedure CreateCaseFromSuggestion(
+ JobTypeCode: Code[20];
+ CustomerNo: Code[20];
+ Description: Text[200];
+ MaterialCode: Code[20];
+ Quantity: Decimal;
+ DueDays: Integer)
+ var
+ PVSCaseHeader: Record "PVS Case";
+ begin
+ PVSCaseHeader.Init();
+ PVSCaseHeader."Customer No." := CustomerNo;
+ PVSCaseHeader."Job Type Code" := JobTypeCode;
+ PVSCaseHeader."Quantity 1" := Quantity;
+ PVSCaseHeader."Order Date" := Today();
+ if DueDays > 0 then
+ PVSCaseHeader."Due Date" := Today() + DueDays;
+
+ PVSCaseHeader."Copilot Assisted" := true;
+ PVSCaseHeader."Copilot Assisted At" := CurrentDateTime();
+
+ PVSCaseHeader.Insert(true);
+
+ Page.Run(Page::"PVS Case Card", PVSCaseHeader);
+ end;
+
+ local procedure BuildSystemPrompt(UserPrompt: Text): Text
+ var
+ PatternContext: Text;
+ SystemPromptBuilder: TextBuilder;
+ begin
+ PatternContext := BuildPatternContext(UserPrompt);
+
+ SystemPromptBuilder.Append(
+ 'You are a PrintVis case management assistant. ' +
+ 'Based on the historical patterns provided and the user''s request, ' +
+ 'generate a new case specification.' +
+ 'Respond ONLY with valid JSON using this exact structure:' + NewLine() +
+ '{' + NewLine() +
+ ' "job_type_code": "", ' + NewLine() +
+ ' "customer_no": "", ' + NewLine() +
+ ' "description": "", ' + NewLine() +
+ ' "quantity": , ' + NewLine() +
+ ' "material_code": "", ' + NewLine() +
+ ' "turnaround_days": , ' + NewLine() +
+ ' "estimated_amount": ' + NewLine() +
+ '}' + NewLine());
+
+ if PatternContext <> '' then begin
+ SystemPromptBuilder.Append(NewLine() + 'Historical patterns relevant to this request:' + NewLine());
+ SystemPromptBuilder.Append(PatternContext);
+ end;
+
+ exit(SystemPromptBuilder.ToText());
+ end;
+
+ local procedure BuildPatternContext(UserPrompt: Text): Text
+ var
+ CasePattern: Record "PVS Case Pattern";
+ ContextBuilder: TextBuilder;
+ LineCount: Integer;
+ begin
+ // Return the top 5 patterns by confidence score as context for the AI
+ CasePattern.SetCurrentKey(CasePattern."Confidence Score");
+ CasePattern.Ascending(false);
+ if not CasePattern.FindSet() then
+ exit('');
+
+ repeat
+ ContextBuilder.Append(
+ StrSubstNo(
+ '- Job Type: %1, Material: %2, Segment: %3, Avg Qty: %4, ' +
+ 'Avg Amount: %5, Avg Days: %6, Cases: %7' + NewLine(),
+ CasePattern."Job Type Code",
+ CasePattern."Material Code",
+ CasePattern."Customer Segment",
+ Format(CasePattern."Avg Quantity", 0, ''),
+ Format(CasePattern."Avg Total Amount", 0, ''),
+ Format(CasePattern."Avg Turnaround Days", 0, ''),
+ Format(CasePattern."Case Count")));
+ LineCount += 1;
+ until (CasePattern.Next() = 0) or (LineCount >= 5);
+
+ exit(ContextBuilder.ToText());
+ end;
+
+ local procedure ParseAOAIResponse(ResponseText: Text; var SuggestedCase: Record "PVS Case History")
+ var
+ JsonObj: JsonObject;
+ JsonTok: JsonToken;
+ CleanedResponse: Text;
+ StartPos: Integer;
+ EndPos: Integer;
+ begin
+ // Extract JSON block if the response contains surrounding text
+ StartPos := ResponseText.IndexOf('{');
+ EndPos := ResponseText.LastIndexOf('}');
+ if (StartPos > 0) and (EndPos > StartPos) then
+ CleanedResponse := ResponseText.Substring(StartPos, EndPos - StartPos + 1)
+ else
+ CleanedResponse := ResponseText;
+
+ if not JsonObj.ReadFrom(CleanedResponse) then
+ exit;
+
+ SuggestedCase.Init();
+
+ if JsonObj.Get('job_type_code', JsonTok) then
+ SuggestedCase."Job Type Code" :=
+ CopyStr(JsonTok.AsValue().AsText(), 1, MaxStrLen(SuggestedCase."Job Type Code"));
+
+ if JsonObj.Get('customer_no', JsonTok) then
+ SuggestedCase."Customer No." :=
+ CopyStr(JsonTok.AsValue().AsText(), 1, MaxStrLen(SuggestedCase."Customer No."));
+
+ if JsonObj.Get('description', JsonTok) then
+ SuggestedCase.Description :=
+ CopyStr(JsonTok.AsValue().AsText(), 1, MaxStrLen(SuggestedCase.Description));
+
+ if JsonObj.Get('quantity', JsonTok) then
+ SuggestedCase.Quantity := JsonTok.AsValue().AsDecimal();
+
+ if JsonObj.Get('material_code', JsonTok) then
+ SuggestedCase."Material Code" :=
+ CopyStr(JsonTok.AsValue().AsText(), 1, MaxStrLen(SuggestedCase."Material Code"));
+
+ if JsonObj.Get('turnaround_days', JsonTok) then
+ SuggestedCase."Turnaround Days" := JsonTok.AsValue().AsInteger();
+
+ if JsonObj.Get('estimated_amount', JsonTok) then
+ SuggestedCase."Total Amount" := JsonTok.AsValue().AsDecimal();
+ end;
+
+ local procedure GetApiKey(var ApiKey: Text)
+ begin
+ if not IsolatedStorage.Get('PVSAOAIApiKey', DataScope::Module, ApiKey) then
+ ApiKey := '';
+ end;
+
+ local procedure NewLine(): Text
+ var
+ TypeHelper: Codeunit "Type Helper";
+ begin
+ exit(TypeHelper.NewLine());
+ end;
+}
diff --git a/Prototyping/Cod50103.PVSAICaseInstall.al b/Prototyping/Cod50103.PVSAICaseInstall.al
new file mode 100644
index 0000000..0bd1ec4
--- /dev/null
+++ b/Prototyping/Cod50103.PVSAICaseInstall.al
@@ -0,0 +1,40 @@
+codeunit 50103 "PVS AI Case Install"
+{
+ Subtype = Install;
+ Access = Internal;
+
+ trigger OnInstallAppPerDatabase()
+ begin
+ RegisterCopilotCapability();
+ end;
+
+ trigger OnInstallAppPerCompany()
+ begin
+ InitializeSetup();
+ end;
+
+ local procedure RegisterCopilotCapability()
+ var
+ CopilotCapability: Codeunit "Copilot Capability";
+ LearnMoreUrl: Text[2048];
+ begin
+ LearnMoreUrl := 'https://www.printvis.com';
+ if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"PVS Case Assistant") then
+ CopilotCapability.RegisterCapability(
+ Enum::"Copilot Capability"::"PVS Case Assistant",
+ LearnMoreUrl);
+ end;
+
+ local procedure InitializeSetup()
+ var
+ Setup: Record "PVS AI Case Setup";
+ begin
+ if not Setup.Get('DEFAULT') then begin
+ Setup.Init();
+ Setup.Code := 'DEFAULT';
+ Setup."History Months" := 12;
+ Setup."Enable Copilot" := false;
+ Setup.Insert(true);
+ end;
+ end;
+}
diff --git a/Prototyping/EnumExt50100.PVSCopilotCapabilities.al b/Prototyping/EnumExt50100.PVSCopilotCapabilities.al
new file mode 100644
index 0000000..f091b7f
--- /dev/null
+++ b/Prototyping/EnumExt50100.PVSCopilotCapabilities.al
@@ -0,0 +1,7 @@
+enumextension 50100 "PVS Copilot Capabilities Ext" extends "Copilot Capability"
+{
+ value(50100; "PVS Case Assistant")
+ {
+ Caption = 'PrintVis Case Assistant';
+ }
+}
diff --git a/Prototyping/Pag50100.PVSAICaseSetup.al b/Prototyping/Pag50100.PVSAICaseSetup.al
new file mode 100644
index 0000000..339aca8
--- /dev/null
+++ b/Prototyping/Pag50100.PVSAICaseSetup.al
@@ -0,0 +1,164 @@
+page 50100 "PVS AI Case Setup"
+{
+ PageType = Card;
+ Caption = 'PVS AI Case Setup';
+ ApplicationArea = All;
+ UsageCategory = Administration;
+ SourceTable = "PVS AI Case Setup";
+ InsertAllowed = false;
+ DeleteAllowed = false;
+
+ layout
+ {
+ area(Content)
+ {
+ group(General)
+ {
+ Caption = 'General';
+
+ field("History Months"; Rec."History Months")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Number of months of case history to load for pattern analysis.';
+ }
+ field("Last Refresh Date"; Rec."Last Refresh Date")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Date and time the case history was last refreshed.';
+ }
+ field("Total Cases Loaded"; Rec."Total Cases Loaded")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Total number of cases loaded into the history table.';
+ }
+ field("Total Patterns Found"; Rec."Total Patterns Found")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Total number of patterns identified from case history.';
+ }
+ field("Job Queue Category Code"; Rec."Job Queue Category Code")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Job Queue Category used for the scheduled history refresh job.';
+ }
+ }
+ group(CopilotSettings)
+ {
+ Caption = 'Copilot / Azure OpenAI';
+
+ field("Enable Copilot"; Rec."Enable Copilot")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Enable the Copilot Case Assistant feature.';
+ }
+ field("AOAI Endpoint"; Rec."AOAI Endpoint")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Your Azure OpenAI endpoint URL (e.g. https://.openai.azure.com/).';
+ }
+ field("AOAI Deployment Name"; Rec."AOAI Deployment Name")
+ {
+ ApplicationArea = All;
+ ToolTip = 'The deployment name of the GPT model in Azure OpenAI Studio.';
+ }
+ field(APIKeyInput; APIKeyDisplayText)
+ {
+ ApplicationArea = All;
+ Caption = 'Azure OpenAI API Key';
+ ExtendedDatatype = Masked;
+ ToolTip = 'Enter the Azure OpenAI API key. The value is stored securely and cannot be read back.';
+
+ trigger OnValidate()
+ begin
+ if APIKeyDisplayText <> '' then begin
+ IsolatedStorage.Set('PVSAOAIApiKey', APIKeyDisplayText, DataScope::Module);
+ APIKeyDisplayText := '********';
+ Message('API Key saved securely.');
+ end;
+ end;
+ }
+ }
+ }
+ }
+
+ actions
+ {
+ area(Processing)
+ {
+ action(RefreshHistory)
+ {
+ ApplicationArea = All;
+ Caption = 'Refresh Case History';
+ ToolTip = 'Reload the last N months of cases from PrintVis into the history table and re-run pattern analysis.';
+ Image = Refresh;
+ Promoted = true;
+ PromotedCategory = Process;
+ PromotedIsBig = true;
+
+ trigger OnAction()
+ var
+ HistoryMgmt: Codeunit "PVS Case History Mgmt";
+ begin
+ HistoryMgmt.PopulateHistory();
+ Rec.Get(Rec.Code);
+ Message('Case history refreshed. %1 cases loaded, %2 patterns found.',
+ Rec."Total Cases Loaded", Rec."Total Patterns Found");
+ end;
+ }
+ action(AnalyzePatterns)
+ {
+ ApplicationArea = All;
+ Caption = 'Re-analyse Patterns';
+ ToolTip = 'Re-run pattern analysis on the existing history without reloading cases.';
+ Image = Alerts;
+ Promoted = true;
+ PromotedCategory = Process;
+
+ trigger OnAction()
+ var
+ PatternAnalysis: Codeunit "PVS Pattern Analysis";
+ begin
+ PatternAnalysis.AnalyzePatterns();
+ Rec.Get(Rec.Code);
+ Message('%1 patterns found.', Rec."Total Patterns Found");
+ end;
+ }
+ action(ViewPatterns)
+ {
+ ApplicationArea = All;
+ Caption = 'View Patterns';
+ ToolTip = 'Open the Case Patterns list to inspect all discovered patterns.';
+ Image = List;
+ RunObject = Page "PVS Case Patterns";
+ }
+ action(CreateJobQueue)
+ {
+ ApplicationArea = All;
+ Caption = 'Schedule Nightly Refresh';
+ ToolTip = 'Create a Job Queue Entry to refresh case history every night at 02:00.';
+ Image = JobQueueEntry;
+
+ trigger OnAction()
+ var
+ HistoryMgmt: Codeunit "PVS Case History Mgmt";
+ begin
+ HistoryMgmt.CreateJobQueueEntry();
+ end;
+ }
+ }
+ }
+
+ trigger OnOpenPage()
+ begin
+ if not Rec.Get('DEFAULT') then begin
+ Rec.Init();
+ Rec.Code := 'DEFAULT';
+ Rec."History Months" := 12;
+ Rec.Insert(true);
+ end;
+ APIKeyDisplayText := '********';
+ end;
+
+ var
+ APIKeyDisplayText: Text;
+}
diff --git a/Prototyping/Pag50101.PVSCasePatterns.al b/Prototyping/Pag50101.PVSCasePatterns.al
new file mode 100644
index 0000000..0535587
--- /dev/null
+++ b/Prototyping/Pag50101.PVSCasePatterns.al
@@ -0,0 +1,135 @@
+page 50101 "PVS Case Patterns"
+{
+ PageType = List;
+ Caption = 'PVS Case Patterns';
+ ApplicationArea = All;
+ UsageCategory = Lists;
+ SourceTable = "PVS Case Pattern";
+ Editable = false;
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ DeleteAllowed = false;
+
+ layout
+ {
+ area(Content)
+ {
+ repeater(Patterns)
+ {
+ field("Job Type Code"; Rec."Job Type Code")
+ {
+ ApplicationArea = All;
+ ToolTip = 'The job type common to this pattern.';
+ }
+ field("Job Type Description"; Rec."Job Type Description")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Description of the job type.';
+ }
+ field("Material Code"; Rec."Material Code")
+ {
+ ApplicationArea = All;
+ ToolTip = 'The primary material associated with this pattern.';
+ }
+ field("Customer Segment"; Rec."Customer Segment")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Customer posting group representing the customer segment.';
+ }
+ field("Case Count"; Rec."Case Count")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Number of historical cases matching this pattern.';
+ }
+ field("Avg Quantity"; Rec."Avg Quantity")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Average order quantity for this pattern.';
+ }
+ field("Avg Total Amount"; Rec."Avg Total Amount")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Average total sales amount for this pattern.';
+ }
+ field("Min Total Amount"; Rec."Min Total Amount")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Minimum total sales amount observed for this pattern.';
+ }
+ field("Max Total Amount"; Rec."Max Total Amount")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Maximum total sales amount observed for this pattern.';
+ }
+ field("Avg Turnaround Days"; Rec."Avg Turnaround Days")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Average number of days from order date to due date.';
+ }
+ field("Confidence Score"; Rec."Confidence Score")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Confidence score (0-100) based on the number of matching cases.';
+ StyleExpr = ConfidenceStyle;
+ }
+ field("Last Updated"; Rec."Last Updated")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Date and time this pattern was last calculated.';
+ }
+ }
+ }
+ area(FactBoxes)
+ {
+ part(CaseHistorySubform; "PVS Case History Subform")
+ {
+ ApplicationArea = All;
+ Caption = 'Matching Cases';
+ SubPageLink = "Job Type Code" = field("Job Type Code"),
+ "Material Code" = field("Material Code");
+ }
+ }
+ }
+
+ actions
+ {
+ area(Processing)
+ {
+ action(ReanalyzePatterns)
+ {
+ ApplicationArea = All;
+ Caption = 'Re-analyse Patterns';
+ ToolTip = 'Rebuild all patterns from current case history.';
+ Image = Refresh;
+ Promoted = true;
+ PromotedCategory = Process;
+
+ trigger OnAction()
+ var
+ PatternAnalysis: Codeunit "PVS Pattern Analysis";
+ begin
+ PatternAnalysis.AnalyzePatterns();
+ CurrPage.Update(false);
+ end;
+ }
+ }
+ }
+
+ trigger OnAfterGetRecord()
+ begin
+ SetConfidenceStyle();
+ end;
+
+ local procedure SetConfidenceStyle()
+ begin
+ if Rec."Confidence Score" >= 70 then
+ ConfidenceStyle := 'Favorable'
+ else if Rec."Confidence Score" >= 40 then
+ ConfidenceStyle := 'Ambiguous'
+ else
+ ConfidenceStyle := 'Unfavorable';
+ end;
+
+ var
+ ConfidenceStyle: Text;
+}
diff --git a/Prototyping/Pag50102.PVSCopilotCaseDialog.al b/Prototyping/Pag50102.PVSCopilotCaseDialog.al
new file mode 100644
index 0000000..0c2c582
--- /dev/null
+++ b/Prototyping/Pag50102.PVSCopilotCaseDialog.al
@@ -0,0 +1,173 @@
+page 50102 "PVS Copilot Case Dialog"
+{
+ PageType = PromptDialog;
+ Extensible = false;
+ Caption = 'New Case with Copilot';
+ ApplicationArea = All;
+ IsPreview = true;
+ PromptMode = Prompt;
+
+ layout
+ {
+ area(Prompt)
+ {
+ field(UserPromptField; UserPromptTxt)
+ {
+ ApplicationArea = All;
+ Caption = 'Describe the new case';
+ MultiLine = true;
+ InstructionalText = 'Describe the print job you need to quote — for example: "4-page brochure, 5000 copies, coated paper, customer segment EXPORT, needed within 2 weeks".';
+ }
+ }
+ area(Content)
+ {
+ group(ProposedCaseGroup)
+ {
+ Caption = 'Proposed Case Details';
+ InstructionalText = 'Review and adjust the values proposed by Copilot before creating the case.';
+
+ field(ProposedJobTypeField; ProposedJobTypeCode)
+ {
+ ApplicationArea = All;
+ Caption = 'Job Type';
+ ToolTip = 'Proposed job type code based on historical patterns.';
+ }
+ field(ProposedCustomerNoField; ProposedCustomerNo)
+ {
+ ApplicationArea = All;
+ Caption = 'Customer No.';
+ ToolTip = 'Proposed customer number (if the description included a customer reference).';
+ }
+ field(ProposedDescriptionField; ProposedDescription)
+ {
+ ApplicationArea = All;
+ Caption = 'Description';
+ MultiLine = true;
+ ToolTip = 'Proposed case description generated by Copilot.';
+ }
+ field(ProposedQuantityField; ProposedQuantity)
+ {
+ ApplicationArea = All;
+ Caption = 'Quantity';
+ ToolTip = 'Proposed order quantity.';
+ }
+ field(ProposedMaterialCodeField; ProposedMaterialCode)
+ {
+ ApplicationArea = All;
+ Caption = 'Material Code';
+ ToolTip = 'Proposed primary material code.';
+ }
+ field(ProposedTurnaroundDaysField; ProposedTurnaroundDays)
+ {
+ ApplicationArea = All;
+ Caption = 'Turnaround Days';
+ ToolTip = 'Proposed number of production days.';
+ }
+ field(ProposedTotalAmountField; ProposedTotalAmount)
+ {
+ ApplicationArea = All;
+ Caption = 'Estimated Amount';
+ ToolTip = 'Estimated total sales amount based on historical patterns.';
+ }
+ }
+ }
+ }
+
+ actions
+ {
+ area(SystemActions)
+ {
+ systemaction(Generate)
+ {
+ Caption = 'Generate';
+ ToolTip = 'Generate a case suggestion based on your description and historical patterns.';
+
+ trigger OnAction()
+ begin
+ RunGeneration();
+ end;
+ }
+ systemaction(Regenerate)
+ {
+ Caption = 'Regenerate';
+ ToolTip = 'Request a new suggestion from Copilot.';
+
+ trigger OnAction()
+ begin
+ RunGeneration();
+ end;
+ }
+ systemaction(OK)
+ {
+ Caption = 'Create Case';
+ ToolTip = 'Accept the suggestion and create the new PrintVis case.';
+ }
+ systemaction(Cancel)
+ {
+ Caption = 'Discard';
+ ToolTip = 'Close the dialog without creating a case.';
+ }
+ }
+ }
+
+ trigger OnQueryClosePage(CloseAction: Action): Boolean
+ begin
+ if CloseAction = Action::OK then
+ if not HasGenerated then
+ Error('Please use the Generate action to get a suggestion before creating a case.');
+ exit(true);
+ end;
+
+ ///
+ /// Returns the suggested case field values to the caller after the dialog is accepted.
+ ///
+ procedure GetSuggestedValues(
+ var JobTypeCode: Code[20];
+ var CustomerNo: Code[20];
+ var Description: Text[200];
+ var MaterialCode: Code[20];
+ var Quantity: Decimal;
+ var TurnaroundDays: Integer)
+ begin
+ JobTypeCode := ProposedJobTypeCode;
+ CustomerNo := ProposedCustomerNo;
+ Description := ProposedDescription;
+ MaterialCode := ProposedMaterialCode;
+ Quantity := ProposedQuantity;
+ TurnaroundDays := ProposedTurnaroundDays;
+ end;
+
+ local procedure RunGeneration()
+ var
+ CopilotCaseGen: Codeunit "PVS Copilot Case Gen";
+ SuggestedCase: Record "PVS Case History" temporary;
+ begin
+ if UserPromptTxt = '' then
+ Error('Please enter a description for the new case before generating.');
+
+ CopilotCaseGen.GenerateCaseSuggestion(UserPromptTxt, SuggestedCase);
+
+ ProposedJobTypeCode := SuggestedCase."Job Type Code";
+ ProposedCustomerNo := SuggestedCase."Customer No.";
+ ProposedDescription := SuggestedCase.Description;
+ ProposedMaterialCode := SuggestedCase."Material Code";
+ ProposedQuantity := SuggestedCase.Quantity;
+ ProposedTurnaroundDays := SuggestedCase."Turnaround Days";
+ ProposedTotalAmount := SuggestedCase."Total Amount";
+ HasGenerated := true;
+
+ CurrPage.PromptMode := PromptMode::Content;
+ CurrPage.Update(false);
+ end;
+
+ var
+ UserPromptTxt: Text;
+ ProposedJobTypeCode: Code[20];
+ ProposedCustomerNo: Code[20];
+ ProposedDescription: Text[200];
+ ProposedMaterialCode: Code[20];
+ ProposedQuantity: Decimal;
+ ProposedTurnaroundDays: Integer;
+ ProposedTotalAmount: Decimal;
+ HasGenerated: Boolean;
+}
diff --git a/Prototyping/Pag50103.PVSCaseHistorySubform.al b/Prototyping/Pag50103.PVSCaseHistorySubform.al
new file mode 100644
index 0000000..eba18a9
--- /dev/null
+++ b/Prototyping/Pag50103.PVSCaseHistorySubform.al
@@ -0,0 +1,61 @@
+page 50103 "PVS Case History Subform"
+{
+ PageType = ListPart;
+ Caption = 'Case History';
+ ApplicationArea = All;
+ SourceTable = "PVS Case History";
+ Editable = false;
+ InsertAllowed = false;
+ ModifyAllowed = false;
+ DeleteAllowed = false;
+
+ layout
+ {
+ area(Content)
+ {
+ repeater(History)
+ {
+ field("Case No."; Rec."Case No.")
+ {
+ ApplicationArea = All;
+ ToolTip = 'The PrintVis case number.';
+ }
+ field("Customer Name"; Rec."Customer Name")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Customer name.';
+ }
+ field("Description"; Rec.Description)
+ {
+ ApplicationArea = All;
+ ToolTip = 'Case description.';
+ }
+ field("Order Date"; Rec."Order Date")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Order date.';
+ }
+ field("Quantity"; Rec.Quantity)
+ {
+ ApplicationArea = All;
+ ToolTip = 'Order quantity.';
+ }
+ field("Total Amount"; Rec."Total Amount")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Total sales amount.';
+ }
+ field("Turnaround Days"; Rec."Turnaround Days")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Days from order to due date.';
+ }
+ field("Copilot Assisted"; Rec."Copilot Assisted")
+ {
+ ApplicationArea = All;
+ ToolTip = 'Indicates the case was originally created with Copilot assistance.';
+ }
+ }
+ }
+ }
+}
diff --git a/Prototyping/PagExt50100.PVSCaseCardExt.al b/Prototyping/PagExt50100.PVSCaseCardExt.al
new file mode 100644
index 0000000..942f2db
--- /dev/null
+++ b/Prototyping/PagExt50100.PVSCaseCardExt.al
@@ -0,0 +1,43 @@
+///
+/// Extends the PrintVis Case Card with a Copilot action that opens the AI case generation dialog.
+/// NOTE: If the base page name differs from "PVS Case Card" in your environment, update the
+/// extends clause accordingly.
+///
+pageextension 50100 "PVS Case Card Ext" extends "PVS Case Card"
+{
+ actions
+ {
+ addlast(Processing)
+ {
+ action(NewCaseWithCopilot)
+ {
+ ApplicationArea = All;
+ Caption = 'New Case with Copilot';
+ ToolTip = 'Use AI to draft a new case based on historical patterns and your description.';
+ Image = Sparkle;
+ Promoted = true;
+ PromotedCategory = New;
+ PromotedIsBig = true;
+
+ trigger OnAction()
+ var
+ CopilotDialog: Page "PVS Copilot Case Dialog";
+ CopilotCaseGen: Codeunit "PVS Copilot Case Gen";
+ JobTypeCode: Code[20];
+ CustomerNo: Code[20];
+ Description: Text[200];
+ MaterialCode: Code[20];
+ Quantity: Decimal;
+ TurnaroundDays: Integer;
+ begin
+ if CopilotDialog.RunModal() = Action::OK then begin
+ CopilotDialog.GetSuggestedValues(
+ JobTypeCode, CustomerNo, Description, MaterialCode, Quantity, TurnaroundDays);
+ CopilotCaseGen.CreateCaseFromSuggestion(
+ JobTypeCode, CustomerNo, Description, MaterialCode, Quantity, TurnaroundDays);
+ end;
+ end;
+ }
+ }
+ }
+}
diff --git a/Prototyping/PermSet50100.PVSAICaseUser.al b/Prototyping/PermSet50100.PVSAICaseUser.al
new file mode 100644
index 0000000..484adab
--- /dev/null
+++ b/Prototyping/PermSet50100.PVSAICaseUser.al
@@ -0,0 +1,20 @@
+permissionset 50100 "PVS AI Case User"
+{
+ Caption = 'PVS AI Case User';
+ Assignable = true;
+
+ Permissions =
+ tabledata "PVS Case History" = RIMD,
+ tabledata "PVS Case Pattern" = RIMD,
+ tabledata "PVS AI Case Setup" = RIMD,
+ table "PVS Case History" = X,
+ table "PVS Case Pattern" = X,
+ table "PVS AI Case Setup" = X,
+ codeunit "PVS Case History Mgmt" = X,
+ codeunit "PVS Pattern Analysis" = X,
+ codeunit "PVS Copilot Case Gen" = X,
+ page "PVS AI Case Setup" = X,
+ page "PVS Case Patterns" = X,
+ page "PVS Copilot Case Dialog" = X,
+ page "PVS Case History Subform" = X;
+}
diff --git a/Prototyping/Tab50100.PVSCaseHistory.al b/Prototyping/Tab50100.PVSCaseHistory.al
new file mode 100644
index 0000000..590aa77
--- /dev/null
+++ b/Prototyping/Tab50100.PVSCaseHistory.al
@@ -0,0 +1,99 @@
+table 50100 "PVS Case History"
+{
+ Caption = 'PVS Case History';
+ DataClassification = CustomerContent;
+
+ fields
+ {
+ field(1; "Entry No."; Integer)
+ {
+ Caption = 'Entry No.';
+ AutoIncrement = true;
+ }
+ field(2; "Case No."; Code[20])
+ {
+ Caption = 'Case No.';
+ }
+ field(3; "Customer No."; Code[20])
+ {
+ Caption = 'Customer No.';
+ }
+ field(4; "Customer Name"; Text[100])
+ {
+ Caption = 'Customer Name';
+ }
+ field(5; "Job Type Code"; Code[20])
+ {
+ Caption = 'Job Type Code';
+ }
+ field(6; "Job Type Description"; Text[100])
+ {
+ Caption = 'Job Type Description';
+ }
+ field(7; "Material Code"; Code[20])
+ {
+ Caption = 'Material Code';
+ }
+ field(8; "Material Description"; Text[50])
+ {
+ Caption = 'Material Description';
+ }
+ field(9; "Description"; Text[200])
+ {
+ Caption = 'Description';
+ }
+ field(10; "Order Date"; Date)
+ {
+ Caption = 'Order Date';
+ }
+ field(11; "Due Date"; Date)
+ {
+ Caption = 'Due Date';
+ }
+ field(12; "Quantity"; Decimal)
+ {
+ Caption = 'Quantity';
+ DecimalPlaces = 0 : 2;
+ }
+ field(13; "Total Amount"; Decimal)
+ {
+ Caption = 'Total Amount';
+ DecimalPlaces = 2 : 2;
+ }
+ field(14; "Turnaround Days"; Integer)
+ {
+ Caption = 'Turnaround Days';
+ }
+ field(15; "Status"; Text[50])
+ {
+ Caption = 'Status';
+ }
+ field(16; "Created Date"; Date)
+ {
+ Caption = 'Created Date';
+ }
+ field(17; "Customer Segment"; Text[50])
+ {
+ Caption = 'Customer Segment';
+ }
+ field(18; "Copilot Assisted"; Boolean)
+ {
+ Caption = 'Copilot Assisted';
+ }
+ field(19; "Copilot Assisted At"; DateTime)
+ {
+ Caption = 'Copilot Assisted At';
+ }
+ }
+
+ keys
+ {
+ key(PK; "Entry No.")
+ {
+ Clustered = true;
+ }
+ key(CaseNo; "Case No.") { }
+ key(CustomerDate; "Customer No.", "Created Date") { }
+ key(JobTypeMaterial; "Job Type Code", "Material Code") { }
+ }
+}
diff --git a/Prototyping/Tab50101.PVSCasePattern.al b/Prototyping/Tab50101.PVSCasePattern.al
new file mode 100644
index 0000000..e2c0395
--- /dev/null
+++ b/Prototyping/Tab50101.PVSCasePattern.al
@@ -0,0 +1,84 @@
+table 50101 "PVS Case Pattern"
+{
+ Caption = 'PVS Case Pattern';
+ DataClassification = CustomerContent;
+
+ fields
+ {
+ field(1; "Pattern ID"; Integer)
+ {
+ Caption = 'Pattern ID';
+ AutoIncrement = true;
+ }
+ field(2; "Job Type Code"; Code[20])
+ {
+ Caption = 'Job Type Code';
+ }
+ field(3; "Job Type Description"; Text[100])
+ {
+ Caption = 'Job Type Description';
+ }
+ field(4; "Material Code"; Code[20])
+ {
+ Caption = 'Material Code';
+ }
+ field(5; "Customer Segment"; Text[50])
+ {
+ Caption = 'Customer Segment';
+ }
+ field(6; "Avg Quantity"; Decimal)
+ {
+ Caption = 'Avg Quantity';
+ DecimalPlaces = 0 : 2;
+ }
+ field(7; "Avg Total Amount"; Decimal)
+ {
+ Caption = 'Avg Total Amount';
+ DecimalPlaces = 2 : 2;
+ }
+ field(8; "Min Total Amount"; Decimal)
+ {
+ Caption = 'Min Total Amount';
+ DecimalPlaces = 2 : 2;
+ }
+ field(9; "Max Total Amount"; Decimal)
+ {
+ Caption = 'Max Total Amount';
+ DecimalPlaces = 2 : 2;
+ }
+ field(10; "Avg Turnaround Days"; Decimal)
+ {
+ Caption = 'Avg Turnaround Days';
+ DecimalPlaces = 1 : 1;
+ }
+ field(11; "Case Count"; Integer)
+ {
+ Caption = 'Case Count';
+ }
+ field(12; "Confidence Score"; Decimal)
+ {
+ Caption = 'Confidence Score (%)';
+ DecimalPlaces = 0 : 1;
+ MinValue = 0;
+ MaxValue = 100;
+ }
+ field(13; "Last Updated"; DateTime)
+ {
+ Caption = 'Last Updated';
+ }
+ field(14; "Common Description"; Text[200])
+ {
+ Caption = 'Common Description';
+ }
+ }
+
+ keys
+ {
+ key(PK; "Pattern ID")
+ {
+ Clustered = true;
+ }
+ key(JobTypeMaterialSegment; "Job Type Code", "Material Code", "Customer Segment") { }
+ key(ConfidenceScore; "Confidence Score") { }
+ }
+}
diff --git a/Prototyping/Tab50102.PVSAICaseSetup.al b/Prototyping/Tab50102.PVSAICaseSetup.al
new file mode 100644
index 0000000..121efe9
--- /dev/null
+++ b/Prototyping/Tab50102.PVSAICaseSetup.al
@@ -0,0 +1,60 @@
+table 50102 "PVS AI Case Setup"
+{
+ Caption = 'PVS AI Case Setup';
+ DataClassification = CustomerContent;
+
+ fields
+ {
+ field(1; "Code"; Code[10])
+ {
+ Caption = 'Code';
+ }
+ field(2; "History Months"; Integer)
+ {
+ Caption = 'History Months';
+ InitValue = 12;
+ MinValue = 1;
+ MaxValue = 60;
+ }
+ field(3; "Last Refresh Date"; DateTime)
+ {
+ Caption = 'Last Refresh Date';
+ Editable = false;
+ }
+ field(4; "AOAI Endpoint"; Text[250])
+ {
+ Caption = 'Azure OpenAI Endpoint';
+ }
+ field(5; "AOAI Deployment Name"; Text[100])
+ {
+ Caption = 'Azure OpenAI Deployment Name';
+ }
+ field(6; "Enable Copilot"; Boolean)
+ {
+ Caption = 'Enable Copilot';
+ }
+ field(7; "Job Queue Category Code"; Code[10])
+ {
+ Caption = 'Job Queue Category Code';
+ TableRelation = "Job Queue Category".Code;
+ }
+ field(8; "Total Cases Loaded"; Integer)
+ {
+ Caption = 'Total Cases Loaded';
+ Editable = false;
+ }
+ field(9; "Total Patterns Found"; Integer)
+ {
+ Caption = 'Total Patterns Found';
+ Editable = false;
+ }
+ }
+
+ keys
+ {
+ key(PK; "Code")
+ {
+ Clustered = true;
+ }
+ }
+}
diff --git a/Prototyping/TabExt50100.PVSCaseHeaderExt.al b/Prototyping/TabExt50100.PVSCaseHeaderExt.al
new file mode 100644
index 0000000..a4c524c
--- /dev/null
+++ b/Prototyping/TabExt50100.PVSCaseHeaderExt.al
@@ -0,0 +1,23 @@
+///
+/// Extends the PrintVis Case table with Copilot audit fields.
+/// NOTE: If the base table name differs from "PVS Case" in your environment,
+/// update the extends clause accordingly.
+///
+tableextension 50100 "PVS Case Header Ext" extends "PVS Case"
+{
+ fields
+ {
+ field(50100; "Copilot Assisted"; Boolean)
+ {
+ Caption = 'Copilot Assisted';
+ DataClassification = CustomerContent;
+ ToolTip = 'Indicates this case was created with the PrintVis Copilot assistant.';
+ }
+ field(50101; "Copilot Assisted At"; DateTime)
+ {
+ Caption = 'Copilot Assisted At';
+ DataClassification = CustomerContent;
+ ToolTip = 'Date and time the Copilot assistant was used to create this case.';
+ }
+ }
+}