misc (ia assistant, SectionParcours + others fixs)
This commit is contained in:
parent
674655eb80
commit
3cd5f7573d
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "manager-service (debug)",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/ManagerService/ManagerService.csproj"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -88,6 +88,7 @@ namespace ManagerService.Controllers
|
||||
return Forbid();
|
||||
|
||||
// Vérifie que l'app concernée a activé l'assistant
|
||||
// Pour AppType.Voice : fallback sur Mobile si pas d'instance Voice dédiée
|
||||
var appInstance = _context.ApplicationInstances
|
||||
.FirstOrDefault(ai => ai.InstanceId == request.InstanceId && ai.AppType == request.AppType);
|
||||
|
||||
|
||||
@ -229,7 +229,7 @@ namespace ManagerService.Controllers
|
||||
break;
|
||||
case SectionType.Game:
|
||||
Resource resource = _myInfoMateDbContext.Resources.FirstOrDefault(r => r.Id == (dto as GameDTO).puzzleImageId);
|
||||
(dto as GameDTO).puzzleImage = resource.ToDTO();
|
||||
(dto as GameDTO).puzzleImage = resource?.ToDTO();
|
||||
break;
|
||||
case SectionType.Map:
|
||||
var geoPoints = _myInfoMateDbContext.GeoPoints.Where(gp => gp.SectionMapId == section.Id)/*.OrderBy(gp => gp.or)*/.ToList();
|
||||
@ -348,6 +348,15 @@ namespace ManagerService.Controllers
|
||||
}
|
||||
(dto as MenuDTO).sections = subSectionToReturn;
|
||||
break;
|
||||
case SectionType.Parcours:
|
||||
var parcoursPaths = _myInfoMateDbContext.GuidedPaths
|
||||
.Include(gp => gp.ImageResource)
|
||||
.Include(gp => gp.Steps).ThenInclude(s => s.QuizQuestions)
|
||||
.Where(gp => gp.SectionParcoursId == section.Id)
|
||||
.OrderBy(gp => gp.Order)
|
||||
.ToList();
|
||||
(dto as ParcoursDTO).guidedPaths = parcoursPaths.Select(gp => gp.ToDTO()).ToList();
|
||||
break;
|
||||
}
|
||||
|
||||
sectionsToReturn.Add(dto);
|
||||
@ -612,6 +621,12 @@ namespace ManagerService.Controllers
|
||||
WebSource = "",
|
||||
};
|
||||
break;
|
||||
case SectionType.Parcours:
|
||||
section = new SectionParcours
|
||||
{
|
||||
GuidedPaths = new List<GuidedPath>()
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
section.InstanceId = newSection.instanceId;
|
||||
@ -936,13 +951,6 @@ namespace ManagerService.Controllers
|
||||
// REMOVE ALL POINTS
|
||||
var geoPoints = _myInfoMateDbContext.GeoPoints.Where(gp => gp.SectionMapId == section.Id).ToList();
|
||||
_myInfoMateDbContext.RemoveRange(geoPoints);
|
||||
|
||||
var guidedPaths = _myInfoMateDbContext.GuidedPaths.Include(gp => gp.Steps).Where(gp => gp.SectionMapId == id);
|
||||
foreach (var guidedPath in guidedPaths)
|
||||
{
|
||||
_myInfoMateDbContext.RemoveRange(guidedPath.Steps);
|
||||
_myInfoMateDbContext.Remove(guidedPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (section.Type == SectionType.Quiz)
|
||||
@ -952,6 +960,16 @@ namespace ManagerService.Controllers
|
||||
_myInfoMateDbContext.RemoveRange(quizQuestions);
|
||||
}
|
||||
|
||||
if (section.Type == SectionType.Parcours)
|
||||
{
|
||||
var guidedPaths = _myInfoMateDbContext.GuidedPaths.Include(gp => gp.Steps).Where(gp => gp.SectionParcoursId == id);
|
||||
foreach (var guidedPath in guidedPaths)
|
||||
{
|
||||
_myInfoMateDbContext.RemoveRange(guidedPath.Steps);
|
||||
_myInfoMateDbContext.Remove(guidedPath);
|
||||
}
|
||||
}
|
||||
|
||||
_myInfoMateDbContext.Remove(section);
|
||||
|
||||
var configuration = _myInfoMateDbContext.Configurations.FirstOrDefault(c => c.Id == section.ConfigurationId);
|
||||
|
||||
@ -339,9 +339,8 @@ namespace ManagerService.Controllers
|
||||
try
|
||||
{
|
||||
List<GuidedPath> guidedPaths = _myInfoMateDbContext.GuidedPaths
|
||||
.Include(gp => gp.Steps).ThenInclude(gp => gp.QuizQuestions)
|
||||
.Include(gp => gp.Steps).ThenInclude(gp => gp.TriggerGeoPoint)
|
||||
.Where(gp => gp.SectionMapId == sectionMapId).ToList();
|
||||
.Include(gp => gp.Steps).ThenInclude(gp => gp.QuizQuestions).ThenInclude(q => q.PuzzleImage)
|
||||
.Where(gp => gp.SectionEventId == sectionMapId).ToList();
|
||||
|
||||
/*List<ProgrammeBlock> programmeBlocks = new List<ProgrammeBlock>();
|
||||
foreach (var program in sectionEvent.Programme)
|
||||
@ -387,13 +386,14 @@ namespace ManagerService.Controllers
|
||||
if (guidedPathDTO == null)
|
||||
throw new ArgumentNullException("GuidedPath param is null");
|
||||
|
||||
var existingSection = _myInfoMateDbContext.Sections.OfType<SectionMap>().FirstOrDefault(sm => sm.Id == sectionMapId);
|
||||
if (existingSection == null)
|
||||
throw new KeyNotFoundException("Section map does not exist");
|
||||
var existingSectionEvent = _myInfoMateDbContext.Sections.OfType<SectionEvent>().FirstOrDefault(se => se.Id == sectionMapId);
|
||||
|
||||
if (existingSectionEvent == null)
|
||||
throw new KeyNotFoundException("Section event does not exist");
|
||||
|
||||
GuidedPath guidedPath = new GuidedPath().FromDTO(guidedPathDTO);
|
||||
guidedPath.Id = idService.GenerateHexId();
|
||||
guidedPath.SectionMapId = sectionMapId;
|
||||
guidedPath.SectionEventId = existingSectionEvent?.Id;
|
||||
guidedPath.Steps = new List<GuidedStep>();
|
||||
foreach (var stepDTO in guidedPathDTO.steps ?? new List<GuidedStepDTO>())
|
||||
{
|
||||
@ -438,7 +438,7 @@ namespace ManagerService.Controllers
|
||||
throw new ArgumentNullException("GuidedPath param is null");
|
||||
|
||||
var existingGuidedPath = _myInfoMateDbContext.GuidedPaths
|
||||
.Include(gp => gp.Steps)
|
||||
.Include(gp => gp.Steps).ThenInclude(s => s.QuizQuestions)
|
||||
.FirstOrDefault(gp => gp.Id == guidedPathDTO.id);
|
||||
if (existingGuidedPath == null)
|
||||
throw new KeyNotFoundException("GuidedPath does not exist");
|
||||
@ -446,9 +446,7 @@ namespace ManagerService.Controllers
|
||||
existingGuidedPath.InstanceId = guidedPathDTO.instanceId;
|
||||
existingGuidedPath.Title = guidedPathDTO.title ?? new List<TranslationDTO>();
|
||||
existingGuidedPath.Description = guidedPathDTO.description ?? new List<TranslationDTO>();
|
||||
existingGuidedPath.SectionMapId = guidedPathDTO.sectionMapId;
|
||||
existingGuidedPath.SectionEventId = guidedPathDTO.sectionEventId;
|
||||
existingGuidedPath.SectionGameId = guidedPathDTO.sectionGameId;
|
||||
existingGuidedPath.IsLinear = guidedPathDTO.isLinear;
|
||||
existingGuidedPath.RequireSuccessToAdvance = guidedPathDTO.requireSuccessToAdvance;
|
||||
existingGuidedPath.HideNextStepsUntilComplete = guidedPathDTO.hideNextStepsUntilComplete;
|
||||
@ -553,7 +551,6 @@ namespace ManagerService.Controllers
|
||||
{
|
||||
List<GuidedStep> guidedSteps = _myInfoMateDbContext.GuidedSteps
|
||||
.Include(gs => gs.QuizQuestions)
|
||||
.Include(gs => gs.TriggerGeoPoint)
|
||||
.Where(gs => gs.GuidedPathId == guidedPathId).ToList();
|
||||
|
||||
/*List<ProgrammeBlock> programmeBlocks = new List<ProgrammeBlock>();
|
||||
@ -657,7 +654,6 @@ namespace ManagerService.Controllers
|
||||
}
|
||||
existingGuidedStep.ZoneRadiusMeters = guidedStepDTO.zoneRadiusMeters;
|
||||
existingGuidedStep.ImageUrl = guidedStepDTO.imageUrl;
|
||||
existingGuidedStep.TriggerGeoPointId = guidedStepDTO.triggerGeoPointId;
|
||||
existingGuidedStep.IsHiddenInitially = guidedStepDTO.isHiddenInitially;
|
||||
existingGuidedStep.QuizQuestions = guidedStepDTO.quizQuestions; // <20> convertir si besoin ?
|
||||
existingGuidedStep.IsStepTimer = guidedStepDTO.isStepTimer;
|
||||
|
||||
382
ManagerService/Controllers/SectionParcoursController.cs
Normal file
382
ManagerService/Controllers/SectionParcoursController.cs
Normal file
@ -0,0 +1,382 @@
|
||||
using Manager.DTOs;
|
||||
using ManagerService.Data;
|
||||
using ManagerService.Data.SubSection;
|
||||
using ManagerService.DTOs;
|
||||
using ManagerService.Helpers;
|
||||
using ManagerService.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSwag.Annotations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ManagerService.Controllers
|
||||
{
|
||||
[Authorize(Policy = ManagerService.Service.Security.Policies.ContentEditor)]
|
||||
[ApiController, Route("api/[controller]")]
|
||||
[OpenApiTag("Section parcours", Description = "Section parcours management")]
|
||||
public class SectionParcoursController : ControllerBase
|
||||
{
|
||||
private readonly MyInfoMateDbContext _myInfoMateDbContext;
|
||||
private readonly ILogger<SectionParcoursController> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
IHexIdGeneratorService idService = new HexIdGeneratorService();
|
||||
|
||||
public SectionParcoursController(IConfiguration configuration, ILogger<SectionParcoursController> logger, MyInfoMateDbContext myInfoMateDbContext)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
_myInfoMateDbContext = myInfoMateDbContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all guided paths from a parcours section
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(List<GuidedPathDTO>), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpGet("{sectionParcoursId}/guided-path")]
|
||||
public ObjectResult GetAllGuidedPathFromSection(string sectionParcoursId)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<GuidedPath> guidedPaths = _myInfoMateDbContext.GuidedPaths
|
||||
.Include(gp => gp.ImageResource)
|
||||
.Include(gp => gp.Steps).ThenInclude(s => s.QuizQuestions).ThenInclude(q => q.PuzzleImage)
|
||||
.Where(gp => gp.SectionParcoursId == sectionParcoursId)
|
||||
.OrderBy(gp => gp.Order)
|
||||
.ToList();
|
||||
|
||||
return new OkObjectResult(guidedPaths.Select(gp => gp.ToDTO()));
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new NotFoundObjectResult(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new guided path in a parcours section
|
||||
/// </summary>
|
||||
[ProducesResponseType(typeof(GuidedPathDTO), 200)]
|
||||
[ProducesResponseType(typeof(string), 400)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpPost("{sectionParcoursId}/guided-path")]
|
||||
public ObjectResult CreateGuidedPath(string sectionParcoursId, [FromBody] GuidedPathDTO guidedPathDTO)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (sectionParcoursId == null)
|
||||
throw new ArgumentNullException("SectionParcoursId param is null");
|
||||
|
||||
if (guidedPathDTO == null)
|
||||
throw new ArgumentNullException("GuidedPath param is null");
|
||||
|
||||
var sectionParcours = _myInfoMateDbContext.Sections.OfType<SectionParcours>().FirstOrDefault(sp => sp.Id == sectionParcoursId);
|
||||
if (sectionParcours == null)
|
||||
throw new KeyNotFoundException("SectionParcours does not exist");
|
||||
|
||||
GuidedPath guidedPath = new GuidedPath().FromDTO(guidedPathDTO);
|
||||
guidedPath.Id = idService.GenerateHexId();
|
||||
guidedPath.SectionParcoursId = sectionParcoursId;
|
||||
guidedPath.Order = _myInfoMateDbContext.GuidedPaths.Count(gp => gp.SectionParcoursId == sectionParcoursId);
|
||||
guidedPath.Steps = new List<GuidedStep>();
|
||||
|
||||
foreach (var stepDTO in guidedPathDTO.steps ?? new List<GuidedStepDTO>())
|
||||
{
|
||||
GuidedStep step = new GuidedStep().FromDTO(stepDTO);
|
||||
step.Id = idService.GenerateHexId();
|
||||
step.GuidedPathId = guidedPath.Id;
|
||||
guidedPath.Steps.Add(step);
|
||||
}
|
||||
|
||||
_myInfoMateDbContext.GuidedPaths.Add(guidedPath);
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
|
||||
return new OkObjectResult(guidedPath.ToDTO());
|
||||
}
|
||||
catch (ArgumentNullException ex)
|
||||
{
|
||||
return new BadRequestObjectResult(ex.Message);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new NotFoundObjectResult(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a guided path
|
||||
/// </summary>
|
||||
[ProducesResponseType(typeof(GuidedPathDTO), 200)]
|
||||
[ProducesResponseType(typeof(string), 400)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpPut("guided-path")]
|
||||
public ObjectResult UpdateGuidedPath([FromBody] GuidedPathDTO guidedPathDTO)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (guidedPathDTO == null)
|
||||
throw new ArgumentNullException("GuidedPath param is null");
|
||||
|
||||
var existingGuidedPath = _myInfoMateDbContext.GuidedPaths
|
||||
.Include(gp => gp.Steps).ThenInclude(s => s.QuizQuestions)
|
||||
.FirstOrDefault(gp => gp.Id == guidedPathDTO.id);
|
||||
if (existingGuidedPath == null)
|
||||
throw new KeyNotFoundException("GuidedPath does not exist");
|
||||
|
||||
existingGuidedPath.InstanceId = guidedPathDTO.instanceId;
|
||||
existingGuidedPath.Title = guidedPathDTO.title ?? new List<TranslationDTO>();
|
||||
existingGuidedPath.Description = guidedPathDTO.description ?? new List<TranslationDTO>();
|
||||
existingGuidedPath.IsLinear = guidedPathDTO.isLinear;
|
||||
existingGuidedPath.RequireSuccessToAdvance = guidedPathDTO.requireSuccessToAdvance;
|
||||
existingGuidedPath.HideNextStepsUntilComplete = guidedPathDTO.hideNextStepsUntilComplete;
|
||||
existingGuidedPath.EstimatedDurationMinutes = guidedPathDTO.estimatedDurationMinutes;
|
||||
existingGuidedPath.ImageResourceId = guidedPathDTO.imageResourceId;
|
||||
existingGuidedPath.Order = guidedPathDTO.order.GetValueOrDefault();
|
||||
existingGuidedPath.IsGameMode = guidedPathDTO.isGameMode;
|
||||
existingGuidedPath.GameMessageDebut = guidedPathDTO.gameMessageDebut ?? new List<TranslationAndResourceDTO>();
|
||||
existingGuidedPath.GameMessageFin = guidedPathDTO.gameMessageFin ?? new List<TranslationAndResourceDTO>();
|
||||
|
||||
// Sync steps
|
||||
var dtoStepIds = (guidedPathDTO.steps ?? new List<GuidedStepDTO>())
|
||||
.Where(s => s.id != null)
|
||||
.Select(s => s.id)
|
||||
.ToHashSet();
|
||||
|
||||
var stepsToRemove = existingGuidedPath.Steps
|
||||
.Where(s => !dtoStepIds.Contains(s.Id))
|
||||
.ToList();
|
||||
foreach (var s in stepsToRemove)
|
||||
existingGuidedPath.Steps.Remove(s);
|
||||
|
||||
foreach (var stepDTO in guidedPathDTO.steps ?? new List<GuidedStepDTO>())
|
||||
{
|
||||
var existing = existingGuidedPath.Steps.FirstOrDefault(s => s.Id == stepDTO.id);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.FromDTO(stepDTO);
|
||||
existing.GuidedPathId = existingGuidedPath.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
var newStep = new GuidedStep().FromDTO(stepDTO);
|
||||
newStep.Id = idService.GenerateHexId();
|
||||
newStep.GuidedPathId = existingGuidedPath.Id;
|
||||
existingGuidedPath.Steps.Add(newStep);
|
||||
}
|
||||
}
|
||||
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
|
||||
return new OkObjectResult(existingGuidedPath.ToDTO());
|
||||
}
|
||||
catch (ArgumentNullException ex)
|
||||
{
|
||||
return new BadRequestObjectResult(ex.Message);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new NotFoundObjectResult(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a guided path
|
||||
/// </summary>
|
||||
[ProducesResponseType(typeof(string), 202)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpDelete("guided-path/{guidedPathId}")]
|
||||
public ObjectResult DeleteGuidedPath(string guidedPathId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var guidedPath = _myInfoMateDbContext.GuidedPaths.Include(gp => gp.Steps).FirstOrDefault(gp => gp.Id == guidedPathId);
|
||||
if (guidedPath == null)
|
||||
throw new KeyNotFoundException("GuidedPath does not exist");
|
||||
|
||||
_myInfoMateDbContext.RemoveRange(guidedPath.Steps);
|
||||
_myInfoMateDbContext.Remove(guidedPath);
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
|
||||
return new ObjectResult("The guidedPath has been deleted") { StatusCode = 202 };
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new NotFoundObjectResult(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all steps from a guided path
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(List<GuidedStepDTO>), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpGet("guided-path/{guidedPathId}/guided-step")]
|
||||
public ObjectResult GetAllGuidedStepFromGuidedPath(string guidedPathId)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<GuidedStep> guidedSteps = _myInfoMateDbContext.GuidedSteps
|
||||
.Include(gs => gs.QuizQuestions)
|
||||
.Where(gs => gs.GuidedPathId == guidedPathId)
|
||||
.OrderBy(gs => gs.Order)
|
||||
.ToList();
|
||||
|
||||
return new OkObjectResult(guidedSteps.Select(gs => gs.ToDTO()));
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new NotFoundObjectResult(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new step in a guided path
|
||||
/// </summary>
|
||||
[ProducesResponseType(typeof(GuidedStepDTO), 200)]
|
||||
[ProducesResponseType(typeof(string), 400)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpPost("guided-path/{guidedPathId}/guided-step")]
|
||||
public ObjectResult CreateGuidedStep(string guidedPathId, [FromBody] GuidedStepDTO guidedStepDTO)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (guidedPathId == null)
|
||||
throw new ArgumentNullException("GuidedPathId param is null");
|
||||
|
||||
if (guidedStepDTO == null)
|
||||
throw new ArgumentNullException("GuidedStep param is null");
|
||||
|
||||
var guidedPath = _myInfoMateDbContext.GuidedPaths.FirstOrDefault(gp => gp.Id == guidedPathId);
|
||||
if (guidedPath == null)
|
||||
throw new KeyNotFoundException("GuidedPath does not exist");
|
||||
|
||||
GuidedStep guidedStep = new GuidedStep().FromDTO(guidedStepDTO);
|
||||
guidedStep.Id = idService.GenerateHexId();
|
||||
guidedStep.GuidedPathId = guidedPathId;
|
||||
guidedStep.Order = _myInfoMateDbContext.GuidedSteps.Count(gs => gs.GuidedPathId == guidedPathId);
|
||||
|
||||
_myInfoMateDbContext.GuidedSteps.Add(guidedStep);
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
|
||||
return new OkObjectResult(guidedStep.ToDTO());
|
||||
}
|
||||
catch (ArgumentNullException ex)
|
||||
{
|
||||
return new BadRequestObjectResult(ex.Message);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new NotFoundObjectResult(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a guided step
|
||||
/// </summary>
|
||||
[ProducesResponseType(typeof(GuidedStepDTO), 200)]
|
||||
[ProducesResponseType(typeof(string), 400)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpPut("guided-step")]
|
||||
public ObjectResult UpdateGuidedStep([FromBody] GuidedStepDTO guidedStepDTO)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (guidedStepDTO == null)
|
||||
throw new ArgumentNullException("GuidedStep param is null");
|
||||
|
||||
var existingGuidedStep = _myInfoMateDbContext.GuidedSteps
|
||||
.Include(gs => gs.QuizQuestions)
|
||||
.FirstOrDefault(gs => gs.Id == guidedStepDTO.id);
|
||||
if (existingGuidedStep == null)
|
||||
throw new KeyNotFoundException("GuidedStep does not exist");
|
||||
|
||||
existingGuidedStep.FromDTO(guidedStepDTO);
|
||||
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
|
||||
return new OkObjectResult(existingGuidedStep.ToDTO());
|
||||
}
|
||||
catch (ArgumentNullException ex)
|
||||
{
|
||||
return new BadRequestObjectResult(ex.Message);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new NotFoundObjectResult(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a guided step
|
||||
/// </summary>
|
||||
[ProducesResponseType(typeof(string), 202)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
[HttpDelete("guided-step/{guidedStepId}")]
|
||||
public ObjectResult DeleteGuidedStep(string guidedStepId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var guidedStep = _myInfoMateDbContext.GuidedSteps.Include(gs => gs.QuizQuestions).FirstOrDefault(gs => gs.Id == guidedStepId);
|
||||
if (guidedStep == null)
|
||||
throw new KeyNotFoundException("GuidedStep does not exist");
|
||||
|
||||
guidedStep.QuizQuestions?.Clear();
|
||||
_myInfoMateDbContext.Remove(guidedStep);
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
|
||||
return new ObjectResult("The guidedStep has been deleted") { StatusCode = 202 };
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new NotFoundObjectResult(ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ObjectResult(ex.Message) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,12 @@ namespace ManagerService.DTOs
|
||||
public string ConfigurationId { get; set; } // null = scope instance, fourni = scope configuration
|
||||
public string Language { get; set; }
|
||||
public List<AiChatMessage> History { get; set; } = new();
|
||||
/// <summary>
|
||||
/// true = interaction vocale (lunettes, assistant vocal).
|
||||
/// Le backend adapte le prompt : pas de markdown, pas de navigation,
|
||||
/// dates en toutes lettres, réponses courtes audio-friendly.
|
||||
/// </summary>
|
||||
public bool IsVoice { get; set; } = false;
|
||||
}
|
||||
|
||||
public class AiChatMessage
|
||||
@ -39,6 +45,11 @@ namespace ManagerService.DTOs
|
||||
public string Reply { get; set; }
|
||||
public List<AiCardDTO>? Cards { get; set; }
|
||||
public NavigationActionDTO? Navigation { get; set; }
|
||||
/// <summary>
|
||||
/// false = le LLM ne s'attend pas à une réponse du visiteur (info pure, hors-sujet, politesse, beacon).
|
||||
/// Le front peut désactiver l'écoute automatique après le TTS.
|
||||
/// </summary>
|
||||
public bool ExpectsReply { get; set; } = true;
|
||||
}
|
||||
|
||||
public class AiTranslateRequest
|
||||
|
||||
@ -8,13 +8,18 @@ namespace ManagerService.DTOs
|
||||
public string instanceId { get; set; }
|
||||
public List<TranslationDTO> title { get; set; }
|
||||
public List<TranslationDTO> description { get; set; }
|
||||
public string? sectionMapId { get; set; }
|
||||
public string? sectionEventId { get; set; }
|
||||
public string? sectionGameId { get; set; }
|
||||
public string? sectionParcoursId { get; set; }
|
||||
public bool isLinear { get; set; }
|
||||
public bool requireSuccessToAdvance { get; set; }
|
||||
public bool hideNextStepsUntilComplete { get; set; }
|
||||
public int? estimatedDurationMinutes { get; set; }
|
||||
public string? imageResourceId { get; set; }
|
||||
public string? imageUrl { get; set; }
|
||||
public int? order { get; set; }
|
||||
public bool isGameMode { get; set; }
|
||||
public List<TranslationAndResourceDTO> gameMessageDebut { get; set; }
|
||||
public List<TranslationAndResourceDTO> gameMessageFin { get; set; }
|
||||
public List<GuidedStepDTO> steps { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Manager.DTOs;
|
||||
using ManagerService.Data.SubSection;
|
||||
using ManagerService.DTOs;
|
||||
|
||||
namespace ManagerService.DTOs
|
||||
{
|
||||
@ -12,10 +13,12 @@ namespace ManagerService.DTOs
|
||||
public List<TranslationDTO> title { get; set; }
|
||||
public List<TranslationDTO> description { get; set; }
|
||||
public GeometryDTO geometry { get; set; }
|
||||
public bool isGeoTriggered { get; set; }
|
||||
public double? zoneRadiusMeters { get; set; }
|
||||
public string imageUrl { get; set; }
|
||||
public int? triggerGeoPointId { get; set; }
|
||||
public GeoPointDTO? triggerGeoPoint { get; set; }
|
||||
public List<TranslationDTO> audioIds { get; set; }
|
||||
public List<ContentDTO> contents { get; set; }
|
||||
public List<TranslationDTO> factContent { get; set; }
|
||||
public bool isHiddenInitially { get; set; }
|
||||
public bool isStepTimer { get; set; }
|
||||
public bool isStepLocked { get; set; }
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
Game,
|
||||
Agenda,
|
||||
Weather,
|
||||
Event
|
||||
Event,
|
||||
Parcours
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,5 @@ namespace Manager.DTOs
|
||||
public int rows { get; set; } = 3;
|
||||
public int cols { get; set; } = 3;
|
||||
public GameTypes gameType { get; set; } = GameTypes.Puzzle;
|
||||
public List<GuidedPathDTO> guidedPaths { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
11
ManagerService/DTOs/SubSection/ParcoursDTO.cs
Normal file
11
ManagerService/DTOs/SubSection/ParcoursDTO.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ManagerService.DTOs
|
||||
{
|
||||
public class ParcoursDTO : SectionDTO
|
||||
{
|
||||
public bool showMap { get; set; }
|
||||
public string? baseSectionMapId { get; set; }
|
||||
public List<GuidedPathDTO> guidedPaths { get; set; }
|
||||
}
|
||||
}
|
||||
@ -7,8 +7,8 @@ namespace Manager.DTOs
|
||||
{
|
||||
public class SectionEventDTO : SectionDTO
|
||||
{
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public string? BaseSectionMapId { get; set; }
|
||||
public List<string> ParcoursIds { get; set; }
|
||||
public List<MapAnnotationDTO> GlobalMapAnnotations { get; set; } = new();
|
||||
|
||||
@ -105,7 +105,8 @@ namespace ManagerService.Data
|
||||
Mobile,
|
||||
Tablet,
|
||||
Web,
|
||||
VR
|
||||
VR,
|
||||
Voice // Lunettes Ray-Ban Meta / interface vocale — pas de navigation UI
|
||||
}
|
||||
|
||||
public enum LayoutMainPageType
|
||||
|
||||
@ -186,7 +186,8 @@ namespace ManagerService.Data
|
||||
.HasValue<SectionSlider>("Slider")
|
||||
.HasValue<SectionVideo>("Video")
|
||||
.HasValue<SectionWeather>("Weather")
|
||||
.HasValue<SectionWeb>("Web");
|
||||
.HasValue<SectionWeb>("Web")
|
||||
.HasValue<SectionParcours>("Parcours");
|
||||
|
||||
/*modelBuilder.Entity<GeoPoint>(entity =>
|
||||
{
|
||||
@ -283,6 +284,20 @@ namespace ManagerService.Data
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
|
||||
|
||||
modelBuilder.Entity<GuidedPath>()
|
||||
.Property(gp => gp.GameMessageDebut)
|
||||
.HasColumnType("jsonb")
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<TranslationAndResourceDTO>>(v, options));
|
||||
|
||||
modelBuilder.Entity<GuidedPath>()
|
||||
.Property(gp => gp.GameMessageFin)
|
||||
.HasColumnType("jsonb")
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<TranslationAndResourceDTO>>(v, options));
|
||||
|
||||
// Configurations JSON pour GuidedStep
|
||||
modelBuilder.Entity<GuidedStep>()
|
||||
.Property(gs => gs.Title)
|
||||
@ -305,6 +320,43 @@ namespace ManagerService.Data
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
|
||||
|
||||
modelBuilder.Entity<GuidedStep>()
|
||||
.Property(gs => gs.AudioIds)
|
||||
.HasColumnType("jsonb")
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
|
||||
|
||||
modelBuilder.Entity<GuidedStep>()
|
||||
.Property(gs => gs.Contents)
|
||||
.HasColumnType("jsonb")
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<ContentDTO>>(v, options));
|
||||
|
||||
modelBuilder.Entity<GuidedStep>()
|
||||
.Property(gs => gs.FactContent)
|
||||
.HasColumnType("jsonb")
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, options),
|
||||
v => JsonSerializer.Deserialize<List<TranslationDTO>>(v, options));
|
||||
|
||||
// SectionParcours: lien optionnel vers une SectionMap de base
|
||||
modelBuilder.Entity<SectionParcours>()
|
||||
.HasOne(sp => sp.BaseMap)
|
||||
.WithMany()
|
||||
.HasForeignKey(sp => sp.BaseSectionMapId)
|
||||
.IsRequired(false)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// GuidedPath: lien vers SectionParcours
|
||||
modelBuilder.Entity<GuidedPath>()
|
||||
.HasOne(gp => gp.SectionParcours)
|
||||
.WithMany(sp => sp.GuidedPaths)
|
||||
.HasForeignKey(gp => gp.SectionParcoursId)
|
||||
.IsRequired(false)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<EventAgenda>()
|
||||
.Property(s => s.Label)
|
||||
.HasColumnType("jsonb")
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using ManagerService.DTOs;
|
||||
using ManagerService.Data;
|
||||
using ManagerService.DTOs;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
@ -21,28 +22,38 @@ namespace ManagerService.Data.SubSection
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<TranslationDTO> Description { get; set; }
|
||||
|
||||
// Lié à une carte (optionnel)
|
||||
public string? SectionMapId { get; set; }
|
||||
[ForeignKey("SectionMapId")]
|
||||
public SectionMap? SectionMap { get; set; }
|
||||
|
||||
// Lié à un événement (optionnel)
|
||||
public string? SectionEventId { get; set; }
|
||||
[ForeignKey("SectionEventId")]
|
||||
public SectionEvent? SectionEvent { get; set; }
|
||||
|
||||
// Lié à une section game (optionnel) => Escape game / geolocalisé ou non
|
||||
public string? SectionGameId { get; set; }
|
||||
[ForeignKey("SectionGameId")]
|
||||
public SectionGame? SectionGame { get; set; }
|
||||
// Lié à une section parcours (FK principale)
|
||||
public string? SectionParcoursId { get; set; }
|
||||
[ForeignKey("SectionParcoursId")]
|
||||
public SectionParcours? SectionParcours { get; set; }
|
||||
|
||||
// Type de parcours
|
||||
public bool IsLinear { get; set; } = true; // Avancer dans l’ordre - Ordre obligatoire
|
||||
public bool RequireSuccessToAdvance { get; set; } = false; // Par exemple: résoudre une énigme
|
||||
public bool HideNextStepsUntilComplete { get; set; } = false; // Étapes cachées tant que non terminées
|
||||
|
||||
public int? EstimatedDurationMinutes { get; set; }
|
||||
|
||||
public string? ImageResourceId { get; set; }
|
||||
|
||||
[ForeignKey("ImageResourceId")]
|
||||
public Resource? ImageResource { get; set; }
|
||||
|
||||
public int Order { get; set; }
|
||||
|
||||
public bool IsGameMode { get; set; } = false;
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<TranslationAndResourceDTO> GameMessageDebut { get; set; } = new();
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<TranslationAndResourceDTO> GameMessageFin { get; set; } = new();
|
||||
|
||||
public List<GuidedStep> Steps { get; set; } = new();
|
||||
|
||||
public GuidedPathDTO ToDTO()
|
||||
@ -53,13 +64,19 @@ namespace ManagerService.Data.SubSection
|
||||
instanceId = InstanceId,
|
||||
title = Title,
|
||||
description = Description,
|
||||
sectionMapId = SectionMapId,
|
||||
sectionEventId = SectionEventId,
|
||||
sectionParcoursId = SectionParcoursId,
|
||||
isLinear = IsLinear,
|
||||
requireSuccessToAdvance = RequireSuccessToAdvance,
|
||||
hideNextStepsUntilComplete = HideNextStepsUntilComplete,
|
||||
estimatedDurationMinutes = EstimatedDurationMinutes,
|
||||
imageResourceId = ImageResourceId,
|
||||
imageUrl = ImageResource?.Url,
|
||||
order = Order,
|
||||
steps = Steps?.Select(s => s.ToDTO()).ToList()
|
||||
isGameMode = IsGameMode,
|
||||
gameMessageDebut = GameMessageDebut,
|
||||
gameMessageFin = GameMessageFin,
|
||||
steps = Steps?.OrderBy(s => s.Order).Select(s => s.ToDTO()).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
@ -70,12 +87,17 @@ namespace ManagerService.Data.SubSection
|
||||
InstanceId = dto.instanceId;
|
||||
Title = dto.title ?? new List<TranslationDTO>();
|
||||
Description = dto.description ?? new List<TranslationDTO>();
|
||||
SectionMapId = dto.sectionMapId;
|
||||
SectionEventId = dto.sectionEventId;
|
||||
SectionParcoursId = dto.sectionParcoursId;
|
||||
IsLinear = dto.isLinear;
|
||||
RequireSuccessToAdvance = dto.requireSuccessToAdvance;
|
||||
HideNextStepsUntilComplete = dto.hideNextStepsUntilComplete;
|
||||
EstimatedDurationMinutes = dto.estimatedDurationMinutes;
|
||||
ImageResourceId = dto.imageResourceId;
|
||||
Order = dto.order.GetValueOrDefault();
|
||||
IsGameMode = dto.isGameMode;
|
||||
GameMessageDebut = dto.gameMessageDebut ?? new List<TranslationAndResourceDTO>();
|
||||
GameMessageFin = dto.gameMessageFin ?? new List<TranslationAndResourceDTO>();
|
||||
//Steps = dto.Steps?.Select(s => s.FromDTO()).ToList() ?? new List<GuidedStep>() // Other method
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
using ManagerService.DTOs;
|
||||
using Manager.DTOs;
|
||||
using ManagerService.DTOs;
|
||||
using NetTopologySuite.Geometries;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using ManagerService.Helpers;
|
||||
|
||||
namespace ManagerService.Data.SubSection
|
||||
@ -29,14 +31,22 @@ namespace ManagerService.Data.SubSection
|
||||
|
||||
public Geometry? Geometry { get; set; } // Polygon ou centre du cercle
|
||||
|
||||
// Option : si true, cette étape doit être atteinte géographiquement (zone/rayon) pour être validée
|
||||
public bool IsGeoTriggered { get; set; } = false;
|
||||
|
||||
public double? ZoneRadiusMeters { get; set; } // Optionnel, utile si zone cercle ou point
|
||||
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
public int? TriggerGeoPointId { get; set; } // Lieu lié à l'étape et genre si on veut plus d'info ou pourrait afficher les infos du geopoint.
|
||||
// Rich content (calqué sur SectionArticle)
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<TranslationDTO> AudioIds { get; set; } = new();
|
||||
|
||||
[ForeignKey("TriggerGeoPointId")]
|
||||
public GeoPoint? TriggerGeoPoint { get; set; }
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<ContentDTO> Contents { get; set; } = new();
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<TranslationDTO> FactContent { get; set; } = new();
|
||||
|
||||
public bool IsHiddenInitially { get; set; } = false;
|
||||
|
||||
@ -66,10 +76,12 @@ namespace ManagerService.Data.SubSection
|
||||
title = Title,
|
||||
description = Description,
|
||||
geometry = Geometry?.ToDto(),
|
||||
isGeoTriggered = IsGeoTriggered,
|
||||
zoneRadiusMeters = ZoneRadiusMeters,
|
||||
imageUrl = ImageUrl,
|
||||
triggerGeoPointId = TriggerGeoPointId,
|
||||
triggerGeoPoint = TriggerGeoPoint?.ToDTO(),
|
||||
audioIds = AudioIds,
|
||||
contents = Contents,
|
||||
factContent = FactContent,
|
||||
isHiddenInitially = IsHiddenInitially,
|
||||
isStepTimer = IsStepTimer,
|
||||
isStepLocked = IsStepLocked,
|
||||
@ -87,19 +99,58 @@ namespace ManagerService.Data.SubSection
|
||||
Title = dto.title ?? new List<TranslationDTO>();
|
||||
Description = dto.description ?? new List<TranslationDTO>();
|
||||
Geometry = dto.geometry.FromDto();
|
||||
IsGeoTriggered = dto.isGeoTriggered;
|
||||
ZoneRadiusMeters = dto.zoneRadiusMeters;
|
||||
ImageUrl = dto.imageUrl;
|
||||
TriggerGeoPointId = dto.triggerGeoPointId;
|
||||
AudioIds = dto.audioIds ?? new List<TranslationDTO>();
|
||||
Contents = dto.contents ?? new List<ContentDTO>();
|
||||
FactContent = dto.factContent ?? new List<TranslationDTO>();
|
||||
IsHiddenInitially = dto.isHiddenInitially;
|
||||
IsStepTimer = dto.isStepTimer;
|
||||
IsStepLocked = dto.isStepLocked;
|
||||
TimerSeconds = dto.timerSeconds;
|
||||
TimerExpiredMessage = dto.timerExpiredMessage ?? new List<TranslationDTO>();
|
||||
Order = dto.order.GetValueOrDefault();
|
||||
QuizQuestions = dto.quizQuestions;
|
||||
SyncQuizQuestions(dto.quizQuestions);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Merges incoming quiz questions into the tracked QuizQuestions collection by Id,
|
||||
// instead of replacing the reference outright — a blind reassignment would make EF
|
||||
// treat every incoming question (including already-persisted ones) as a brand new
|
||||
// row to insert, which fails with a duplicate key error as soon as a step already
|
||||
// has a saved question and a new one is added alongside it.
|
||||
public void SyncQuizQuestions(List<QuizQuestion>? dtoQuestions)
|
||||
{
|
||||
dtoQuestions ??= new List<QuizQuestion>();
|
||||
QuizQuestions ??= new List<QuizQuestion>();
|
||||
|
||||
var dtoIds = dtoQuestions.Where(q => q.Id != 0).Select(q => q.Id).ToHashSet();
|
||||
QuizQuestions.RemoveAll(q => !dtoIds.Contains(q.Id));
|
||||
|
||||
foreach (var qDto in dtoQuestions)
|
||||
{
|
||||
var existing = qDto.Id != 0 ? QuizQuestions.FirstOrDefault(q => q.Id == qDto.Id) : null;
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Label = qDto.Label;
|
||||
existing.ResourceId = qDto.ResourceId;
|
||||
existing.Order = qDto.Order;
|
||||
existing.Responses = qDto.Responses;
|
||||
existing.ValidationQuestionType = qDto.ValidationQuestionType;
|
||||
existing.PuzzleImageId = qDto.PuzzleImageId;
|
||||
existing.PuzzleRows = qDto.PuzzleRows;
|
||||
existing.PuzzleCols = qDto.PuzzleCols;
|
||||
existing.IsSlidingPuzzle = qDto.IsSlidingPuzzle;
|
||||
}
|
||||
else
|
||||
{
|
||||
qDto.Id = 0;
|
||||
QuizQuestions.Add(qDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ManagerService.Data.SubSection
|
||||
{
|
||||
@ -33,13 +34,16 @@ namespace ManagerService.Data.SubSection
|
||||
public string? SectionQuizId { get; set; }
|
||||
|
||||
[ForeignKey("SectionQuizId")]
|
||||
[JsonIgnore]
|
||||
public SectionQuiz? SectionQuiz { get; set; }
|
||||
|
||||
public string? GuidedStepId { get; set; }
|
||||
|
||||
[ForeignKey("GuidedStepId")]
|
||||
[JsonIgnore]
|
||||
public GuidedStep? GuidedStep { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonNumberEnumConverter<QuestionType>))]
|
||||
public QuestionType ValidationQuestionType { get; set; } = QuestionType.Simple;
|
||||
|
||||
public string? PuzzleImageId { get; set; }
|
||||
@ -49,7 +53,7 @@ namespace ManagerService.Data.SubSection
|
||||
|
||||
public int? PuzzleCols { get; set; } = 3;
|
||||
|
||||
public bool IsSlidingPuzzle { get; set; } = false;
|
||||
public bool? IsSlidingPuzzle { get; set; } = false;
|
||||
|
||||
// TODO
|
||||
/*public TranslationDTO ToDTO()
|
||||
|
||||
@ -15,8 +15,8 @@ namespace ManagerService.Data.SubSection
|
||||
/// </summary>
|
||||
public class SectionEvent : Section
|
||||
{
|
||||
public DateTime StartDate { get; set; }
|
||||
public DateTime EndDate { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
public string? BaseSectionMapId { get; set; }
|
||||
[ForeignKey(nameof(BaseSectionMapId))]
|
||||
public SectionMap? BaseMap { get; set; }
|
||||
|
||||
@ -68,8 +68,7 @@ namespace ManagerService.Data.SubSection
|
||||
public enum GameTypes
|
||||
{
|
||||
Puzzle,
|
||||
SlidingPuzzle,
|
||||
Escape
|
||||
SlidingPuzzle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
ManagerService/Data/SubSection/SectionParcours.cs
Normal file
48
ManagerService/Data/SubSection/SectionParcours.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using Manager.DTOs;
|
||||
using ManagerService.DTOs;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
|
||||
namespace ManagerService.Data.SubSection
|
||||
{
|
||||
public class SectionParcours : Section
|
||||
{
|
||||
public bool ShowMap { get; set; } = true;
|
||||
|
||||
public string? BaseSectionMapId { get; set; }
|
||||
[ForeignKey(nameof(BaseSectionMapId))]
|
||||
public SectionMap? BaseMap { get; set; }
|
||||
|
||||
public List<GuidedPath> GuidedPaths { get; set; } = new();
|
||||
|
||||
public ParcoursDTO ToDTO()
|
||||
{
|
||||
return new ParcoursDTO
|
||||
{
|
||||
id = Id,
|
||||
label = Label,
|
||||
title = Title.ToList(),
|
||||
description = Description.ToList(),
|
||||
order = Order,
|
||||
type = Type,
|
||||
imageId = ImageId,
|
||||
imageSource = ImageSource,
|
||||
configurationId = ConfigurationId,
|
||||
isSubSection = IsSubSection,
|
||||
parentId = ParentId,
|
||||
dateCreation = DateCreation,
|
||||
instanceId = InstanceId,
|
||||
isBeacon = IsBeacon,
|
||||
beaconId = BeaconId,
|
||||
latitude = Latitude,
|
||||
longitude = Longitude,
|
||||
meterZoneGPS = MeterZoneGPS,
|
||||
showMap = ShowMap,
|
||||
baseSectionMapId = BaseSectionMapId,
|
||||
guidedPaths = GuidedPaths?.Select(gp => gp.ToDTO()).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -41,10 +41,11 @@ namespace ManagerService.Helpers
|
||||
};
|
||||
}
|
||||
|
||||
public static Geometry FromDto(this GeometryDTO dto, GeometryFactory factory = null!)
|
||||
public static Geometry? FromDto(this GeometryDTO? dto, GeometryFactory factory = null!)
|
||||
{
|
||||
factory ??= NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326);
|
||||
if (dto == null) return null;
|
||||
|
||||
factory ??= NtsGeometryServices.Instance.CreateGeometryFactory(srid: 4326);
|
||||
|
||||
return dto.type switch
|
||||
{
|
||||
|
||||
1584
ManagerService/Migrations/20260510203954_MakeIsSlidingPuzzleNullable.Designer.cs
generated
Normal file
1584
ManagerService/Migrations/20260510203954_MakeIsSlidingPuzzleNullable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakeIsSlidingPuzzleNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<bool>(
|
||||
name: "IsSlidingPuzzle",
|
||||
table: "QuizQuestions",
|
||||
type: "boolean",
|
||||
nullable: true,
|
||||
oldClrType: typeof(bool),
|
||||
oldType: "boolean");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<bool>(
|
||||
name: "IsSlidingPuzzle",
|
||||
table: "QuizQuestions",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false,
|
||||
oldClrType: typeof(bool),
|
||||
oldType: "boolean",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
1651
ManagerService/Migrations/20260612145724_AddSectionParcours.Designer.cs
generated
Normal file
1651
ManagerService/Migrations/20260612145724_AddSectionParcours.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
184
ManagerService/Migrations/20260612145724_AddSectionParcours.cs
Normal file
184
ManagerService/Migrations/20260612145724_AddSectionParcours.cs
Normal file
@ -0,0 +1,184 @@
|
||||
using System.Collections.Generic;
|
||||
using ManagerService.DTOs;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSectionParcours : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GuidedPaths_Sections_SectionGameId",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "SectionGameId",
|
||||
table: "GuidedPaths",
|
||||
newName: "SectionParcoursId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_GuidedPaths_SectionGameId",
|
||||
table: "GuidedPaths",
|
||||
newName: "IX_GuidedPaths_SectionParcoursId");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsGameMode",
|
||||
table: "Sections",
|
||||
type: "boolean",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SectionParcours_BaseSectionMapId",
|
||||
table: "Sections",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<List<TranslationAndResourceDTO>>(
|
||||
name: "SectionParcours_GameMessageDebut",
|
||||
table: "Sections",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<List<TranslationAndResourceDTO>>(
|
||||
name: "SectionParcours_GameMessageFin",
|
||||
table: "Sections",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "ShowMap",
|
||||
table: "Sections",
|
||||
type: "boolean",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AudioIds",
|
||||
table: "GuidedSteps",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Contents",
|
||||
table: "GuidedSteps",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "FactContent",
|
||||
table: "GuidedSteps",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "EstimatedDurationMinutes",
|
||||
table: "GuidedPaths",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ImageResourceId",
|
||||
table: "GuidedPaths",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sections_SectionParcours_BaseSectionMapId",
|
||||
table: "Sections",
|
||||
column: "SectionParcours_BaseSectionMapId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GuidedPaths_Sections_SectionParcoursId",
|
||||
table: "GuidedPaths",
|
||||
column: "SectionParcoursId",
|
||||
principalTable: "Sections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Sections_Sections_SectionParcours_BaseSectionMapId",
|
||||
table: "Sections",
|
||||
column: "SectionParcours_BaseSectionMapId",
|
||||
principalTable: "Sections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GuidedPaths_Sections_SectionParcoursId",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Sections_Sections_SectionParcours_BaseSectionMapId",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Sections_SectionParcours_BaseSectionMapId",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsGameMode",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SectionParcours_BaseSectionMapId",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SectionParcours_GameMessageDebut",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SectionParcours_GameMessageFin",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ShowMap",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AudioIds",
|
||||
table: "GuidedSteps");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Contents",
|
||||
table: "GuidedSteps");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "FactContent",
|
||||
table: "GuidedSteps");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EstimatedDurationMinutes",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ImageResourceId",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "SectionParcoursId",
|
||||
table: "GuidedPaths",
|
||||
newName: "SectionGameId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_GuidedPaths_SectionParcoursId",
|
||||
table: "GuidedPaths",
|
||||
newName: "IX_GuidedPaths_SectionGameId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GuidedPaths_Sections_SectionGameId",
|
||||
table: "GuidedPaths",
|
||||
column: "SectionGameId",
|
||||
principalTable: "Sections",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
1653
ManagerService/Migrations/20260629150749_MoveGameModeToGuidedPath.Designer.cs
generated
Normal file
1653
ManagerService/Migrations/20260629150749_MoveGameModeToGuidedPath.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,101 @@
|
||||
using System.Collections.Generic;
|
||||
using ManagerService.DTOs;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MoveGameModeToGuidedPath : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsGameMode",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SectionParcours_GameMessageDebut",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SectionParcours_GameMessageFin",
|
||||
table: "Sections");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GameMessageDebut",
|
||||
table: "GuidedPaths",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GameMessageFin",
|
||||
table: "GuidedPaths",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsGameMode",
|
||||
table: "GuidedPaths",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GuidedPaths_ImageResourceId",
|
||||
table: "GuidedPaths",
|
||||
column: "ImageResourceId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GuidedPaths_Resources_ImageResourceId",
|
||||
table: "GuidedPaths",
|
||||
column: "ImageResourceId",
|
||||
principalTable: "Resources",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GuidedPaths_Resources_ImageResourceId",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_GuidedPaths_ImageResourceId",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GameMessageDebut",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GameMessageFin",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsGameMode",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsGameMode",
|
||||
table: "Sections",
|
||||
type: "boolean",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<List<TranslationAndResourceDTO>>(
|
||||
name: "SectionParcours_GameMessageDebut",
|
||||
table: "Sections",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<List<TranslationAndResourceDTO>>(
|
||||
name: "SectionParcours_GameMessageFin",
|
||||
table: "Sections",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
1642
ManagerService/Migrations/20260708094421_RemoveGuidedStepTriggerGeoPoint.Designer.cs
generated
Normal file
1642
ManagerService/Migrations/20260708094421_RemoveGuidedStepTriggerGeoPoint.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveGuidedStepTriggerGeoPoint : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GuidedSteps_GeoPoints_TriggerGeoPointId",
|
||||
table: "GuidedSteps");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_GuidedSteps_TriggerGeoPointId",
|
||||
table: "GuidedSteps");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TriggerGeoPointId",
|
||||
table: "GuidedSteps");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "TriggerGeoPointId",
|
||||
table: "GuidedSteps",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GuidedSteps_TriggerGeoPointId",
|
||||
table: "GuidedSteps",
|
||||
column: "TriggerGeoPointId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GuidedSteps_GeoPoints_TriggerGeoPointId",
|
||||
table: "GuidedSteps",
|
||||
column: "TriggerGeoPointId",
|
||||
principalTable: "GeoPoints",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
1645
ManagerService/Migrations/20260708123715_AddGuidedStepIsGeoTriggered.Designer.cs
generated
Normal file
1645
ManagerService/Migrations/20260708123715_AddGuidedStepIsGeoTriggered.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddGuidedStepIsGeoTriggered : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsGeoTriggered",
|
||||
table: "GuidedSteps",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsGeoTriggered",
|
||||
table: "GuidedSteps");
|
||||
}
|
||||
}
|
||||
}
|
||||
1634
ManagerService/Migrations/20260708143339_RemoveGuidedPathSectionMapRelation.Designer.cs
generated
Normal file
1634
ManagerService/Migrations/20260708143339_RemoveGuidedPathSectionMapRelation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,48 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveGuidedPathSectionMapRelation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_GuidedPaths_Sections_SectionMapId",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_GuidedPaths_SectionMapId",
|
||||
table: "GuidedPaths");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SectionMapId",
|
||||
table: "GuidedPaths");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "SectionMapId",
|
||||
table: "GuidedPaths",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_GuidedPaths_SectionMapId",
|
||||
table: "GuidedPaths",
|
||||
column: "SectionMapId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_GuidedPaths_Sections_SectionMapId",
|
||||
table: "GuidedPaths",
|
||||
column: "SectionMapId",
|
||||
principalTable: "Sections",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -711,13 +711,28 @@ namespace ManagerService.Migrations
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int?>("EstimatedDurationMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("GameMessageDebut")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("GameMessageFin")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<bool>("HideNextStepsUntilComplete")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("ImageResourceId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("InstanceId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsGameMode")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsLinear")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@ -730,10 +745,7 @@ namespace ManagerService.Migrations
|
||||
b.Property<string>("SectionEventId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SectionGameId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SectionMapId")
|
||||
b.Property<string>("SectionParcoursId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
@ -742,11 +754,11 @@ namespace ManagerService.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ImageResourceId");
|
||||
|
||||
b.HasIndex("SectionEventId");
|
||||
|
||||
b.HasIndex("SectionGameId");
|
||||
|
||||
b.HasIndex("SectionMapId");
|
||||
b.HasIndex("SectionParcoursId");
|
||||
|
||||
b.ToTable("GuidedPaths");
|
||||
});
|
||||
@ -756,9 +768,18 @@ namespace ManagerService.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("AudioIds")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Contents")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("FactContent")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<Geometry>("Geometry")
|
||||
.HasColumnType("geometry");
|
||||
|
||||
@ -769,6 +790,9 @@ namespace ManagerService.Migrations
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsGeoTriggered")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsHiddenInitially")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@ -791,9 +815,6 @@ namespace ManagerService.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<int?>("TriggerGeoPointId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<double?>("ZoneRadiusMeters")
|
||||
.HasColumnType("double precision");
|
||||
|
||||
@ -801,8 +822,6 @@ namespace ManagerService.Migrations
|
||||
|
||||
b.HasIndex("GuidedPathId");
|
||||
|
||||
b.HasIndex("TriggerGeoPointId");
|
||||
|
||||
b.ToTable("GuidedSteps");
|
||||
});
|
||||
|
||||
@ -817,7 +836,7 @@ namespace ManagerService.Migrations
|
||||
b.Property<string>("GuidedStepId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsSlidingPuzzle")
|
||||
b.Property<bool?>("IsSlidingPuzzle")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<List<TranslationAndResourceDTO>>("Label")
|
||||
@ -1127,13 +1146,13 @@ namespace ManagerService.Migrations
|
||||
b.Property<string>("BaseSectionMapId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("EndDate")
|
||||
b.Property<DateTime?>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<List<string>>("ParcoursIds")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<DateTime>("StartDate")
|
||||
b.Property<DateTime?>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasIndex("BaseSectionMapId");
|
||||
@ -1214,6 +1233,27 @@ namespace ManagerService.Migrations
|
||||
b.HasDiscriminator().HasValue("Menu");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b =>
|
||||
{
|
||||
b.HasBaseType("ManagerService.Data.Section");
|
||||
|
||||
b.Property<string>("BaseSectionMapId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("ShowMap")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasIndex("BaseSectionMapId");
|
||||
|
||||
b.ToTable("Sections", t =>
|
||||
{
|
||||
t.Property("BaseSectionMapId")
|
||||
.HasColumnName("SectionParcours_BaseSectionMapId");
|
||||
});
|
||||
|
||||
b.HasDiscriminator().HasValue("Parcours");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.SubSection.SectionPdf", b =>
|
||||
{
|
||||
b.HasBaseType("ManagerService.Data.Section");
|
||||
@ -1413,23 +1453,24 @@ namespace ManagerService.Migrations
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.SubSection.GuidedPath", b =>
|
||||
{
|
||||
b.HasOne("ManagerService.Data.Resource", "ImageResource")
|
||||
.WithMany()
|
||||
.HasForeignKey("ImageResourceId");
|
||||
|
||||
b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent")
|
||||
.WithMany()
|
||||
.HasForeignKey("SectionEventId");
|
||||
|
||||
b.HasOne("ManagerService.Data.SubSection.SectionGame", "SectionGame")
|
||||
.WithMany()
|
||||
.HasForeignKey("SectionGameId");
|
||||
b.HasOne("ManagerService.Data.SubSection.SectionParcours", "SectionParcours")
|
||||
.WithMany("GuidedPaths")
|
||||
.HasForeignKey("SectionParcoursId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ManagerService.Data.SubSection.SectionMap", "SectionMap")
|
||||
.WithMany()
|
||||
.HasForeignKey("SectionMapId");
|
||||
b.Navigation("ImageResource");
|
||||
|
||||
b.Navigation("SectionEvent");
|
||||
|
||||
b.Navigation("SectionGame");
|
||||
|
||||
b.Navigation("SectionMap");
|
||||
b.Navigation("SectionParcours");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b =>
|
||||
@ -1440,13 +1481,7 @@ namespace ManagerService.Migrations
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ManagerService.Data.SubSection.GeoPoint", "TriggerGeoPoint")
|
||||
.WithMany()
|
||||
.HasForeignKey("TriggerGeoPointId");
|
||||
|
||||
b.Navigation("GuidedPath");
|
||||
|
||||
b.Navigation("TriggerGeoPoint");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.SubSection.QuizQuestion", b =>
|
||||
@ -1529,6 +1564,16 @@ namespace ManagerService.Migrations
|
||||
b.Navigation("MapResource");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b =>
|
||||
{
|
||||
b.HasOne("ManagerService.Data.SubSection.SectionMap", "BaseMap")
|
||||
.WithMany()
|
||||
.HasForeignKey("BaseSectionMapId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("BaseMap");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.ApplicationInstance", b =>
|
||||
{
|
||||
b.Navigation("Configurations");
|
||||
@ -1571,6 +1616,11 @@ namespace ManagerService.Migrations
|
||||
b.Navigation("MenuSections");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b =>
|
||||
{
|
||||
b.Navigation("GuidedPaths");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.SubSection.SectionQuiz", b =>
|
||||
{
|
||||
b.Navigation("QuizQuestions");
|
||||
|
||||
@ -63,6 +63,70 @@ namespace ManagerService.Services
|
||||
private static string StripHtml(string? html) =>
|
||||
string.IsNullOrEmpty(html) ? "" : System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "").Trim();
|
||||
|
||||
private record DateWords(string Today, string Tomorrow, string This, string From, string To, string The, string NoDate);
|
||||
|
||||
private static readonly System.Collections.Generic.Dictionary<string, (string culture, DateWords words)> LangConfig = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["FR"] = ("fr-FR", new("aujourd'hui", "demain", "ce", "du", "au", "le", "date non précisée")),
|
||||
["NL"] = ("nl-BE", new("vandaag", "morgen", "deze", "van", "tot", "op", "datum niet bepaald")),
|
||||
["EN"] = ("en-GB", new("today", "tomorrow", "this", "from", "to", "on", "date unknown")),
|
||||
["DE"] = ("de-DE", new("heute", "morgen", "diesen", "vom", "bis", "am", "Datum unbekannt")),
|
||||
["IT"] = ("it-IT", new("oggi", "domani", "questo", "dal", "al", "il", "data non precisata")),
|
||||
["ES"] = ("es-ES", new("hoy", "mañana", "este", "del", "al", "el", "fecha no especificada")),
|
||||
["PL"] = ("pl-PL", new("dzisiaj", "jutro", "w ten", "od", "do", "w", "data nieznana")),
|
||||
};
|
||||
|
||||
private static (System.Globalization.CultureInfo culture, DateWords words) GetLangConfig(string lang)
|
||||
{
|
||||
if (LangConfig.TryGetValue(lang ?? "FR", out var cfg))
|
||||
return (new System.Globalization.CultureInfo(cfg.culture), cfg.words);
|
||||
return (new System.Globalization.CultureInfo("fr-FR"), LangConfig["FR"].words);
|
||||
}
|
||||
|
||||
private static (string text, bool expectsReply) StripMarkdownForVoice(string text)
|
||||
{
|
||||
var expectsReply = true;
|
||||
if (text.Contains("[FIN]"))
|
||||
{
|
||||
expectsReply = false;
|
||||
text = text.Replace("[FIN]", "").TrimEnd();
|
||||
}
|
||||
text = Regex.Replace(text, @"(?m)^\s*[-*•]\s+", "");
|
||||
text = Regex.Replace(text, @"(?m)^\s*\d+\.\s+", "");
|
||||
text = Regex.Replace(text, @"\*{1,2}([^*]+)\*{1,2}", "$1");
|
||||
text = Regex.Replace(text, @"_{1,2}([^_]+)_{1,2}", "$1");
|
||||
text = Regex.Replace(text, @"(?m)^#{1,6}\s+", "");
|
||||
text = text.Replace("`", "");
|
||||
text = Regex.Replace(text, @"\n{3,}", "\n\n").Trim();
|
||||
return (text, expectsReply);
|
||||
}
|
||||
|
||||
private static string FormatDateVoice(DateTime date, DateTime today, string lang)
|
||||
{
|
||||
var (culture, w) = GetLangConfig(lang);
|
||||
var diff = (date.Date - today.Date).Days;
|
||||
var dayName = culture.DateTimeFormat.GetDayName(date.DayOfWeek).ToLower();
|
||||
var monthName = culture.DateTimeFormat.GetMonthName(date.Month).ToLower();
|
||||
var yearSuffix = date.Year != today.Year ? $" {date.Year}" : "";
|
||||
var absolute = $"{dayName} {date.Day} {monthName}{yearSuffix}";
|
||||
|
||||
return diff switch
|
||||
{
|
||||
0 => $"{w.Today} {date.Day} {monthName}{yearSuffix}",
|
||||
1 => $"{w.Tomorrow} {dayName} {date.Day} {monthName}{yearSuffix}",
|
||||
>= 2 and <= 7 => $"{w.This} {absolute}",
|
||||
_ => absolute,
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatDateRangeVoice(DateTime? from, DateTime? to, DateTime today, string lang)
|
||||
{
|
||||
var (_, w) = GetLangConfig(lang);
|
||||
if (from == null) return w.NoDate;
|
||||
if (to == null) return $"{w.The} {FormatDateVoice(from.Value, today, lang)}";
|
||||
return $"{w.From} {FormatDateVoice(from.Value, today, lang)} {w.To} {FormatDateVoice(to.Value, today, lang)}";
|
||||
}
|
||||
|
||||
private static DateTime? ParseFilterDate(string? s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return null;
|
||||
@ -107,7 +171,33 @@ namespace ManagerService.Services
|
||||
return $"- id:{s.Id} | type:{s.Type} | titre:\"{title}\"";
|
||||
}));
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.System, $"""
|
||||
var isVoiceMode = request.IsVoice;
|
||||
|
||||
var systemPrompt = isVoiceMode ? $"""
|
||||
Tu es le guide audio de visite de "{config?.Label ?? "cette application"}".
|
||||
Aujourd'hui nous sommes le {DateTime.Now:dddd dd MMMM yyyy}.
|
||||
Le visiteur t'écoute via des lunettes connectées Ray-Ban — il ne voit PAS d'écran.
|
||||
Tu réponds en {request.Language}. Tu es chaleureux, naturel et très concis (2-3 phrases max).
|
||||
|
||||
Voici les sections disponibles :
|
||||
{sectionsSummary}
|
||||
|
||||
RÈGLES STRICTES pour le mode vocal — chaque violation rend la réponse inutilisable :
|
||||
1. ZÉRO markdown : absolument aucun astérisque (*), tiret de liste (- item ou • item), dièse (#), backtick (`), ou crochet ([lien]). Écris uniquement des phrases.
|
||||
2. Les dates dans les résultats des outils sont DÉJÀ formatées dans la langue de la réponse — recopie-les telles quelles, ne les reformate JAMAIS.
|
||||
3. ZÉRO liste. Cite 2-3 éléments maximum directement dans une phrase fluide, séparés par des virgules.
|
||||
4. Pour les événements → appelle "GetUpcomingEvents", puis cite exactement 2-3 des plus pertinents dans une seule phrase fluide. Même si l'outil en retourne 50, n'en cite que 2-3. Ajoute [FIN] à la fin.
|
||||
5. Pour les détails d'un item → appelle "GetItemDetails" et résume en 2-3 phrases fluides. Ajoute [FIN] à la fin.
|
||||
6. Pour les sections de type Article → appelle "GetSectionDetail", résume le contenu en 2-3 phrases, puis demande "Veux-tu en savoir plus ?" (sans ajouter [FIN] car tu attends une réponse).
|
||||
7. Les sections de type Video, PDF, Slider, Quiz, Web, Game sont des contenus visuels. Si le visiteur en parle, mentionne leur existence mais précise qu'il doit les consulter sur l'écran.
|
||||
8. NE propose JAMAIS de navigation — le visiteur ne peut pas toucher l'écran.
|
||||
9. Ne mentionne JAMAIS les identifiants techniques (id, guid).
|
||||
10. Chaque réponse doit tenir en moins de 20 secondes à l'oral.
|
||||
11. Termine TOUJOURS par une phrase complète.
|
||||
12. Tu réponds UNIQUEMENT aux questions liées à cette visite et au contenu disponible dans les sections ci-dessus. Pour toute question hors-sujet (politique, actualité, recettes, vie personnelle…), décline poliment en une phrase courte et naturelle — varie la formulation à chaque fois (ex: "Ma spécialité c'est cette visite, pas ce sujet !", "Je suis ton guide ici, pas un moteur de recherche !", "Bonne question, mais là je sèche — demande-moi plutôt ce qu'il y a à voir ici !") — et ajoute [FIN].
|
||||
13. Politesses et fin de conversation ("merci", "au revoir", "c'est tout", "ok merci") → réponds chaleureusement en une phrase courte ("Bonne visite !" ou "Avec plaisir, bonne visite !") et ajoute [FIN].
|
||||
14. Signal [FIN] : ajoute le token [FIN] à la toute fin quand ta réponse est une information pure sans question posée au visiteur. Ne l'ajoute PAS quand tu poses une question (article, proposition de détails).
|
||||
""" : $"""
|
||||
Tu es l'assistant de visite de l'application "{config?.Label ?? "cette application"}".
|
||||
Aujourd'hui nous sommes le {DateTime.Now:dddd dd MMMM yyyy}.
|
||||
Le weekend en cours (ou prochain) : samedi {weekendSat:dd/MM/yyyy} – dimanche {weekendSun:dd/MM/yyyy}.
|
||||
@ -126,7 +216,9 @@ namespace ManagerService.Services
|
||||
- Ne mentionne JAMAIS les identifiants techniques (id, guid) dans tes réponses finales.
|
||||
- NE POSE JAMAIS de question à la fin de ta réponse après avoir présenté des résultats.
|
||||
- NE TE RÉPÈTE PAS : dis les choses une seule fois de manière fluide.
|
||||
"""));
|
||||
""";
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.System, systemPrompt));
|
||||
|
||||
foreach (var h in request.History.TakeLast(MaxHistoryMessages))
|
||||
messages.Add(new ChatMessage(h.Role == "user" ? ChatRole.User : ChatRole.Assistant, h.Content));
|
||||
@ -141,14 +233,37 @@ namespace ManagerService.Services
|
||||
AIFunctionFactory.Create(
|
||||
(string sectionId) =>
|
||||
{
|
||||
var section = sections.FirstOrDefault(s => s.Id == sectionId);
|
||||
var section = _context.Sections
|
||||
.FirstOrDefault(s => s.Id == sectionId && s.ConfigurationId == request.ConfigurationId);
|
||||
if (section == null) return "Section non trouvée.";
|
||||
var title = section.Title?.FirstOrDefault(t => t.language == request.Language)?.value ?? section.Label;
|
||||
var desc = section.Description?.FirstOrDefault(t => t.language == request.Language)?.value ?? "";
|
||||
return $"Titre: {title}\nDescription: {desc}\nType: {section.Type}";
|
||||
var lang = request.Language;
|
||||
var title = StripHtml(section.Title?.FirstOrDefault(t => t.language == lang)?.value
|
||||
?? section.Title?.FirstOrDefault()?.value
|
||||
?? section.Label);
|
||||
var desc = StripHtml(section.Description?.FirstOrDefault(t => t.language == lang)?.value
|
||||
?? section.Description?.FirstOrDefault()?.value
|
||||
?? "");
|
||||
|
||||
// Requêtes explicites par type pour contourner les problèmes de cache EF Core
|
||||
var extra = "";
|
||||
var asArticle = _context.Sections
|
||||
.OfType<ManagerService.Data.SubSection.SectionArticle>()
|
||||
.FirstOrDefault(s => s.Id == sectionId);
|
||||
if (asArticle != null)
|
||||
{
|
||||
extra = StripHtml(asArticle.ArticleContent?.FirstOrDefault(t => t.language == lang)?.value
|
||||
?? asArticle.ArticleContent?.FirstOrDefault()?.value ?? "");
|
||||
}
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"Titre: {title}");
|
||||
if (!string.IsNullOrWhiteSpace(desc)) sb.AppendLine($"Description: {desc}");
|
||||
if (!string.IsNullOrWhiteSpace(extra)) sb.AppendLine($"Contenu: {extra}");
|
||||
sb.AppendLine($"Type: {section.Type}");
|
||||
return sb.ToString().Trim();
|
||||
},
|
||||
"GetSectionDetail",
|
||||
"Récupère le titre et la description d'une section."
|
||||
"Récupère le titre, la description et le contenu complet d'une section (article, vidéo, etc.)."
|
||||
),
|
||||
AIFunctionFactory.Create(
|
||||
async (string? dateFrom, string? dateTo) =>
|
||||
@ -193,10 +308,14 @@ namespace ManagerService.Services
|
||||
if (end == null || end.Value.Date < today || dtFrom?.Date > horizon) continue;
|
||||
if (filterFrom.HasValue && end.Value.Date < filterFrom.Value) continue;
|
||||
if (filterTo.HasValue && dtFrom?.Date > filterTo.Value) continue;
|
||||
var dateStr = dtFrom.HasValue ? dtFrom.Value.ToString("dd/MM/yyyy") : "Date non précisée";
|
||||
if (dtTo.HasValue && dtTo.Value.Date != dtFrom?.Date)
|
||||
dateStr += $" → {dtTo.Value.ToString("dd/MM/yyyy")}";
|
||||
allEvents.Add((dtFrom ?? DateTime.MaxValue, $"- [En ligne] AgendaId:{agenda.Id} | Titre:{e.name ?? "Sans titre"} | Date:{dateStr}"));
|
||||
var dtToOpt = dtTo.HasValue && dtTo.Value.Date != dtFrom?.Date ? dtTo : null;
|
||||
var dateStr = isVoiceMode
|
||||
? FormatDateRangeVoice(dtFrom, dtToOpt, today, request.Language)
|
||||
: (dtFrom.HasValue ? dtFrom.Value.ToString("dd/MM/yyyy") : "Date non précisée")
|
||||
+ (dtToOpt.HasValue ? $" → {dtToOpt.Value:dd/MM/yyyy}" : "");
|
||||
allEvents.Add((dtFrom ?? DateTime.MaxValue, isVoiceMode
|
||||
? $"{e.name ?? "Sans titre"} ({dateStr})"
|
||||
: $"- [En ligne] AgendaId:{agenda.Id} | Titre:{e.name ?? "Sans titre"} | Date:{dateStr}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,10 +332,14 @@ namespace ManagerService.Services
|
||||
foreach (var e in events)
|
||||
{
|
||||
var title = e.Label?.FirstOrDefault(t => t.language == request.Language)?.value ?? e.Label?.FirstOrDefault()?.value ?? "Sans titre";
|
||||
var dateStr = e.DateFrom.HasValue ? e.DateFrom.Value.ToString("dd/MM/yyyy") : "Date non précisée";
|
||||
if (e.DateTo.HasValue && e.DateTo.Value.Date != e.DateFrom?.Date)
|
||||
dateStr += $" → {e.DateTo.Value.ToString("dd/MM/yyyy")}";
|
||||
allEvents.Add((e.DateFrom ?? DateTime.MaxValue, $"- SectionId:{agenda.Id} | EventId:{e.Id} | Titre:{title} | Date:{dateStr}"));
|
||||
var dtToOpt2 = e.DateTo.HasValue && e.DateTo.Value.Date != e.DateFrom?.Date ? e.DateTo : null;
|
||||
var dateStr = isVoiceMode
|
||||
? FormatDateRangeVoice(e.DateFrom, dtToOpt2, today, request.Language)
|
||||
: (e.DateFrom.HasValue ? e.DateFrom.Value.ToString("dd/MM/yyyy") : "Date non précisée")
|
||||
+ (dtToOpt2.HasValue ? $" → {dtToOpt2.Value:dd/MM/yyyy}" : "");
|
||||
allEvents.Add((e.DateFrom ?? DateTime.MaxValue, isVoiceMode
|
||||
? $"{title} ({dateStr})"
|
||||
: $"- SectionId:{agenda.Id} | EventId:{e.Id} | Titre:{title} | Date:{dateStr}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -324,7 +447,12 @@ namespace ManagerService.Services
|
||||
"GetItemDetails",
|
||||
"Récupère les détails complets (prix, horaires, contact, site) d'un événement (type='event') ou d'un point d'intérêt (type='poi') via son ID."
|
||||
),
|
||||
AIFunctionFactory.Create(
|
||||
};
|
||||
|
||||
// Navigation et cards : uniquement en mode UI (pas Voice)
|
||||
if (!isVoiceMode)
|
||||
{
|
||||
tools.Add(AIFunctionFactory.Create(
|
||||
(string sectionId, string sectionTitle, string sectionType) =>
|
||||
{
|
||||
var imageUrl = sections.FirstOrDefault(s => s.Id == sectionId)?.ImageSource;
|
||||
@ -333,8 +461,8 @@ namespace ManagerService.Services
|
||||
},
|
||||
"navigate_to_section",
|
||||
"Propose navigation to a specific section when the user wants to go there or see it."
|
||||
),
|
||||
AIFunctionFactory.Create(
|
||||
));
|
||||
tools.Add(AIFunctionFactory.Create(
|
||||
(string[] titles, string[] subtitles, string[]? icons) =>
|
||||
{
|
||||
cards = titles.Select((t, i) => new AiCardDTO
|
||||
@ -347,14 +475,25 @@ namespace ManagerService.Services
|
||||
},
|
||||
"show_cards",
|
||||
"Display structured info cards in the chat. Use for lists of items (events, activities...) that benefit from visual presentation."
|
||||
)
|
||||
};
|
||||
));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
options = new ChatOptions { Tools = tools };
|
||||
var response = await _chatClient.GetResponseAsync(messages, options);
|
||||
return new AiChatResponse { Reply = response.Text ?? "", Cards = cards, Navigation = navigation };
|
||||
string reply;
|
||||
bool expectsReply;
|
||||
if (isVoiceMode)
|
||||
{
|
||||
(reply, expectsReply) = StripMarkdownForVoice(response.Text ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
reply = response.Text ?? "";
|
||||
expectsReply = true;
|
||||
}
|
||||
return new AiChatResponse { Reply = reply, Cards = cards, Navigation = navigation, ExpectsReply = expectsReply };
|
||||
}
|
||||
catch (System.ClientModel.ClientResultException ex)
|
||||
{
|
||||
@ -391,7 +530,27 @@ namespace ManagerService.Services
|
||||
return $"- id:{c.Id} | titre:\"{title}\"{typesStr}";
|
||||
}));
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.System, $"""
|
||||
var isVoiceMode = request.IsVoice;
|
||||
|
||||
var instanceSystemPrompt = isVoiceMode ? $"""
|
||||
Tu es le guide audio de visite.
|
||||
Aujourd'hui nous sommes le {DateTime.Now:dddd dd MMMM yyyy}.
|
||||
Le visiteur t'écoute via des lunettes connectées — il ne voit PAS d'écran.
|
||||
Tu réponds en {request.Language}. Tu es chaleureux, naturel et très concis (2-3 phrases max).
|
||||
|
||||
Voici les expériences (visites) disponibles :
|
||||
{configsSummary}
|
||||
|
||||
RÈGLES STRICTES pour le mode vocal :
|
||||
1. ZÉRO markdown : aucun astérisque, tiret de liste, dièse, backtick. Écris uniquement des phrases.
|
||||
2. ZÉRO liste. Cite 2-3 éléments maximum dans une phrase fluide séparés par des virgules.
|
||||
3. Pour les événements → appelle "SearchEventsGlobal", puis cite exactement 2-3 des plus pertinents dans une phrase fluide. Ajoute [FIN].
|
||||
4. NE propose JAMAIS de navigation. Ne mentionne JAMAIS les identifiants techniques.
|
||||
5. Chaque réponse doit tenir en moins de 20 secondes à l'oral.
|
||||
6. Tu réponds UNIQUEMENT aux questions liées à cette visite. Pour toute question hors-sujet, réponds : "Je suis uniquement là pour t'accompagner dans ta visite." et ajoute [FIN].
|
||||
7. Politesses ("merci", "au revoir") → réponse courte et chaleureuse + [FIN].
|
||||
8. Signal [FIN] : ajoute-le quand ta réponse est une information pure sans question au visiteur.
|
||||
""" : $"""
|
||||
Tu es l'assistant de visite principal. Ton rôle est d'orienter l'utilisateur vers la bonne expérience de visite.
|
||||
Aujourd'hui nous sommes le {DateTime.Now:dddd dd MMMM yyyy}.
|
||||
Le weekend en cours (ou prochain) : samedi {weekendSat:dd/MM/yyyy} – dimanche {weekendSun:dd/MM/yyyy}.
|
||||
@ -409,7 +568,9 @@ namespace ManagerService.Services
|
||||
6. Pour les détails d'un item → "GetItemDetailsGlobal".
|
||||
- NE POSE JAMAIS de question à la fin de ta réponse après avoir présenté des résultats.
|
||||
- NE TE RÉPÈTE PAS : dis les choses une seule fois de manière fluide.
|
||||
"""));
|
||||
""";
|
||||
|
||||
messages.Add(new ChatMessage(ChatRole.System, instanceSystemPrompt));
|
||||
|
||||
foreach (var h in request.History.TakeLast(MaxHistoryMessages))
|
||||
messages.Add(new ChatMessage(h.Role == "user" ? ChatRole.User : ChatRole.Assistant, h.Content));
|
||||
@ -617,7 +778,18 @@ namespace ManagerService.Services
|
||||
{
|
||||
options = new ChatOptions { Tools = tools };
|
||||
var response = await _chatClient.GetResponseAsync(messages, options);
|
||||
return new AiChatResponse { Reply = response.Text ?? "", Navigation = navigation };
|
||||
string reply;
|
||||
bool expectsReply;
|
||||
if (isVoiceMode)
|
||||
{
|
||||
(reply, expectsReply) = StripMarkdownForVoice(response.Text ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
reply = response.Text ?? "";
|
||||
expectsReply = true;
|
||||
}
|
||||
return new AiChatResponse { Reply = reply, Navigation = navigation, ExpectsReply = expectsReply };
|
||||
}
|
||||
catch (System.ClientModel.ClientResultException ex)
|
||||
{
|
||||
|
||||
@ -25,6 +25,7 @@ namespace ManagerService.Services
|
||||
VideoDTO videoDTO = new VideoDTO();
|
||||
WeatherDTO weatherDTO = new WeatherDTO();
|
||||
WebDTO webDTO = new WebDTO();
|
||||
ParcoursDTO parcoursDTO = new ParcoursDTO();
|
||||
|
||||
switch (dto.type)
|
||||
{
|
||||
@ -35,7 +36,10 @@ namespace ManagerService.Services
|
||||
articleDTO = JsonConvert.DeserializeObject<ArticleDTO>(jsonElement.ToString());
|
||||
break;
|
||||
case SectionType.Event:
|
||||
sectionEventDTO = JsonConvert.DeserializeObject<SectionEventDTO>(jsonElement.ToString());
|
||||
sectionEventDTO = JsonConvert.DeserializeObject<SectionEventDTO>(
|
||||
jsonElement.ToString(),
|
||||
new JsonSerializerSettings { Error = (_, args) => args.ErrorContext.Handled = true }
|
||||
);
|
||||
break;
|
||||
case SectionType.Map:
|
||||
mapDTO = JsonConvert.DeserializeObject<MapDTO>(jsonElement.ToString());
|
||||
@ -64,6 +68,9 @@ namespace ManagerService.Services
|
||||
case SectionType.Web:
|
||||
webDTO = JsonConvert.DeserializeObject<WebDTO>(jsonElement.ToString());
|
||||
break;
|
||||
case SectionType.Parcours:
|
||||
parcoursDTO = JsonConvert.DeserializeObject<ParcoursDTO>(jsonElement.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
return dto.type switch
|
||||
@ -139,8 +146,8 @@ namespace ManagerService.Services
|
||||
Longitude = dto.longitude,
|
||||
MeterZoneGPS = dto.meterZoneGPS,
|
||||
Type = dto.type,
|
||||
StartDate = sectionEventDTO.StartDate,
|
||||
EndDate = sectionEventDTO.EndDate,
|
||||
StartDate = sectionEventDTO.StartDate?.ToUniversalTime(),
|
||||
EndDate = sectionEventDTO.EndDate?.ToUniversalTime(),
|
||||
ParcoursIds = sectionEventDTO.ParcoursIds,
|
||||
BaseSectionMapId = sectionEventDTO.BaseSectionMapId,
|
||||
//Programmes = // TODO specific
|
||||
@ -363,6 +370,29 @@ namespace ManagerService.Services
|
||||
Type = dto.type,
|
||||
WebSource = webDTO.source,
|
||||
},
|
||||
SectionType.Parcours => new SectionParcours
|
||||
{
|
||||
Id = dto.id,
|
||||
DateCreation = dto.dateCreation.Value,
|
||||
ConfigurationId = dto.configurationId,
|
||||
InstanceId = dto.instanceId,
|
||||
Label = dto.label,
|
||||
Title = dto.title,
|
||||
Description = dto.description,
|
||||
Order = dto.order.Value,
|
||||
ImageId = dto.imageId,
|
||||
ImageSource = dto.imageSource,
|
||||
IsSubSection = dto.isSubSection,
|
||||
ParentId = dto.parentId,
|
||||
IsBeacon = dto.isBeacon,
|
||||
BeaconId = dto.beaconId,
|
||||
Latitude = dto.latitude,
|
||||
Longitude = dto.longitude,
|
||||
MeterZoneGPS = dto.meterZoneGPS,
|
||||
Type = dto.type,
|
||||
ShowMap = parcoursDTO.showMap,
|
||||
BaseSectionMapId = parcoursDTO.baseSectionMapId,
|
||||
},
|
||||
_ => throw new NotImplementedException("Section type not handled")
|
||||
};
|
||||
}
|
||||
@ -446,8 +476,8 @@ namespace ManagerService.Services
|
||||
longitude = sectionEvent.Longitude,
|
||||
meterZoneGPS = sectionEvent.MeterZoneGPS,
|
||||
type = sectionEvent.Type,
|
||||
StartDate = sectionEvent.StartDate,
|
||||
EndDate = sectionEvent.EndDate,
|
||||
StartDate = sectionEvent.StartDate?.Year > 1000 ? sectionEvent.StartDate : null,
|
||||
EndDate = sectionEvent.EndDate?.Year > 1000 ? sectionEvent.EndDate : null,
|
||||
ParcoursIds = sectionEvent.ParcoursIds,
|
||||
BaseSectionMapId = sectionEvent.BaseSectionMapId,
|
||||
GlobalMapAnnotations = sectionEvent.GlobalMapAnnotations?.Select(ma => ma.ToDTO()).ToList() ?? new(),
|
||||
@ -681,6 +711,31 @@ namespace ManagerService.Services
|
||||
type = web.Type,
|
||||
source = web.WebSource
|
||||
},
|
||||
SectionParcours parcours => new ParcoursDTO
|
||||
{
|
||||
id = parcours.Id,
|
||||
dateCreation = parcours.DateCreation,
|
||||
configurationId = parcours.ConfigurationId,
|
||||
instanceId = parcours.InstanceId,
|
||||
label = parcours.Label,
|
||||
title = parcours.Title,
|
||||
description = parcours.Description,
|
||||
order = parcours.Order,
|
||||
imageId = parcours.ImageId,
|
||||
imageSource = parcours.ImageSource,
|
||||
isActive = parcours.IsActive,
|
||||
isSubSection = parcours.IsSubSection,
|
||||
parentId = parcours.ParentId,
|
||||
isBeacon = parcours.IsBeacon,
|
||||
beaconId = parcours.BeaconId,
|
||||
latitude = parcours.Latitude,
|
||||
longitude = parcours.Longitude,
|
||||
meterZoneGPS = parcours.MeterZoneGPS,
|
||||
type = parcours.Type,
|
||||
showMap = parcours.ShowMap,
|
||||
baseSectionMapId = parcours.BaseSectionMapId,
|
||||
// guidedPaths chargés spécifiquement dans GetFromConfigurationDetail
|
||||
},
|
||||
_ => throw new NotImplementedException("Section type not handled")
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user