Compare commits
9 Commits
ca65ce19d8
...
bee5d8e1e7
| Author | SHA1 | Date |
|---|---|---|
|
|
bee5d8e1e7 | |
|
|
78d67d77fd | |
|
|
127e16eb34 | |
|
|
45c816616f | |
|
|
faec16f6fe | |
|
|
7e72940eae | |
|
|
30499df329 | |
|
|
b2814ee216 | |
|
|
11940b4684 |
|
|
@ -5,3 +5,5 @@
|
|||
**/.idea/
|
||||
**/.env
|
||||
.claude/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
|
|
|
|||
|
|
@ -806,7 +806,7 @@ public class Controller : ControllerBase
|
|||
/// <summary>
|
||||
/// Returns an AI-generated diagnosis for a single error/alarm description.
|
||||
/// Responses are cached in memory — repeated calls for the same error code
|
||||
/// do not hit Mistral again.
|
||||
/// do not hit the AI provider again.
|
||||
/// </summary>
|
||||
[HttpGet(nameof(DiagnoseError))]
|
||||
public async Task<ActionResult<DiagnosticResponse>> DiagnoseError(Int64 installationId, string errorDescription, Token authToken)
|
||||
|
|
@ -839,8 +839,11 @@ public class Controller : ControllerBase
|
|||
/// Remove this endpoint in production if not needed.
|
||||
/// </summary>
|
||||
[HttpGet(nameof(TestAlarmKnowledgeBase))]
|
||||
public ActionResult TestAlarmKnowledgeBase()
|
||||
public ActionResult TestAlarmKnowledgeBase(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var testCases = new[]
|
||||
{
|
||||
// Sinexcel alarms (keys match SinexcelRecord.Api.cs property names)
|
||||
|
|
@ -855,7 +858,7 @@ public class Controller : ControllerBase
|
|||
"BmsFault",
|
||||
"OverTemperature",
|
||||
"AFCI Fault",
|
||||
// Unknown alarm (should return null - would call Mistral)
|
||||
// Unknown alarm (should return null - would call AI)
|
||||
"Some unknown alarm XYZ123"
|
||||
};
|
||||
|
||||
|
|
@ -867,7 +870,7 @@ public class Controller : ControllerBase
|
|||
{
|
||||
Alarm = alarm,
|
||||
FoundInKnowledgeBase = diagnosis != null,
|
||||
Explanation = diagnosis?.Explanation ?? "NOT FOUND - Would call Mistral API",
|
||||
Explanation = diagnosis?.Explanation ?? "NOT FOUND - Would call AI API",
|
||||
CausesCount = diagnosis?.Causes.Count ?? 0,
|
||||
NextStepsCount = diagnosis?.NextSteps.Count ?? 0
|
||||
});
|
||||
|
|
@ -878,19 +881,21 @@ public class Controller : ControllerBase
|
|||
TestTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
TotalTests = testCases.Length,
|
||||
FoundInKnowledgeBase = results.Count(r => ((dynamic)r).FoundInKnowledgeBase),
|
||||
WouldCallMistral = results.Count(r => !((dynamic)r).FoundInKnowledgeBase),
|
||||
WouldCallAi = results.Count(r => !((dynamic)r).FoundInKnowledgeBase),
|
||||
Results = results
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test endpoint for the full AI diagnostic flow (knowledge base + Mistral API).
|
||||
/// No auth required. Remove before production.
|
||||
/// Usage: GET /api/TestDiagnoseError?errorDescription=SomeAlarm
|
||||
/// Test endpoint for the full AI diagnostic flow (knowledge base + AI API).
|
||||
/// Admin-only. Usage: GET /api/TestDiagnoseError?errorDescription=SomeAlarm
|
||||
/// </summary>
|
||||
[HttpGet(nameof(TestDiagnoseError))]
|
||||
public async Task<ActionResult> TestDiagnoseError(string errorDescription = "AbnormalGridVoltage", string language = "en")
|
||||
public async Task<ActionResult> TestDiagnoseError(Token authToken, string errorDescription = "AbnormalGridVoltage", string language = "en")
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
// 1. Try static lookup (KB for English, pre-generated translations for others)
|
||||
var staticResult = DiagnosticService.TryGetTranslation(errorDescription, language);
|
||||
if (staticResult is not null)
|
||||
|
|
@ -899,24 +904,24 @@ public class Controller : ControllerBase
|
|||
{
|
||||
Source = "KnowledgeBase",
|
||||
Alarm = errorDescription,
|
||||
MistralEnabled = DiagnosticService.IsEnabled,
|
||||
AiEnabled = DiagnosticService.IsEnabled,
|
||||
staticResult.Explanation,
|
||||
staticResult.Causes,
|
||||
staticResult.NextSteps
|
||||
});
|
||||
}
|
||||
|
||||
// 2. If not found, try Mistral with the correct language
|
||||
// 2. If not found, try AI with the correct language
|
||||
if (!DiagnosticService.IsEnabled)
|
||||
return Ok(new { Source = "None", Alarm = errorDescription, Message = "Not in knowledge base and Mistral API key not configured." });
|
||||
return Ok(new { Source = "None", Alarm = errorDescription, Message = "Not in knowledge base and AI API key not configured." });
|
||||
|
||||
var aiResult = await DiagnosticService.TestCallMistralAsync(errorDescription, language);
|
||||
if (aiResult is null)
|
||||
return Ok(new { Source = "MistralFailed", Alarm = errorDescription, Message = "Mistral API call failed or returned empty." });
|
||||
return Ok(new { Source = "AiFailed", Alarm = errorDescription, Message = "AI API call failed or returned empty." });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Source = "MistralAI",
|
||||
Source = "Ai",
|
||||
Alarm = errorDescription,
|
||||
aiResult.Explanation,
|
||||
aiResult.Causes,
|
||||
|
|
@ -2012,50 +2017,71 @@ public class Controller : ControllerBase
|
|||
// ── Alarm Review Campaign ────────────────────────────────────────────────
|
||||
|
||||
[HttpPost(nameof(SendTestAlarmReview))]
|
||||
public async Task<ActionResult> SendTestAlarmReview()
|
||||
public async Task<ActionResult> SendTestAlarmReview(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
await AlarmReviewService.SendTestBatchAsync();
|
||||
return Ok(new { message = "Test review email sent to liu@inesco.energy. Check your inbox." });
|
||||
}
|
||||
|
||||
[HttpPost(nameof(StartAlarmReviewCampaign))]
|
||||
public ActionResult StartAlarmReviewCampaign()
|
||||
public ActionResult StartAlarmReviewCampaign(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
AlarmReviewService.StartCampaign();
|
||||
return Ok(new { message = "Alarm review campaign started." });
|
||||
}
|
||||
|
||||
[HttpPost(nameof(StopAlarmReviewCampaign))]
|
||||
public ActionResult StopAlarmReviewCampaign()
|
||||
public ActionResult StopAlarmReviewCampaign(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
AlarmReviewService.StopCampaign();
|
||||
return Ok(new { message = "Campaign paused — progress preserved. Use ResumeAlarmReviewCampaign to restart timers." });
|
||||
}
|
||||
|
||||
[HttpPost(nameof(ResumeAlarmReviewCampaign))]
|
||||
public ActionResult ResumeAlarmReviewCampaign()
|
||||
public ActionResult ResumeAlarmReviewCampaign(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
AlarmReviewService.ResumeCampaign();
|
||||
return Ok(new { message = "Campaign resumed — timers restarted from existing progress." });
|
||||
}
|
||||
|
||||
[HttpPost(nameof(ResetAlarmReviewCampaign))]
|
||||
public ActionResult ResetAlarmReviewCampaign()
|
||||
public ActionResult ResetAlarmReviewCampaign(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
AlarmReviewService.ResetCampaign();
|
||||
return Ok(new { message = "Campaign fully reset — all progress deleted. Use StartAlarmReviewCampaign to begin again." });
|
||||
}
|
||||
|
||||
[HttpGet(nameof(CorrectAlarm))]
|
||||
public ActionResult CorrectAlarm(int batch, string key)
|
||||
public ActionResult CorrectAlarm(Token authToken, int batch, string key)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var html = AlarmReviewService.GetCorrectionPage(batch, key);
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpPost(nameof(ApplyAlarmCorrection))]
|
||||
public ActionResult ApplyAlarmCorrection([FromBody] AlarmCorrectionRequest req)
|
||||
public ActionResult ApplyAlarmCorrection(Token authToken, [FromBody] AlarmCorrectionRequest req)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
if (req == null) return BadRequest();
|
||||
var correction = new DiagnosticResponse
|
||||
{
|
||||
|
|
@ -2068,16 +2094,22 @@ public class Controller : ControllerBase
|
|||
}
|
||||
|
||||
[HttpGet(nameof(ReviewAlarms))]
|
||||
public ActionResult ReviewAlarms(int batch, string reviewer)
|
||||
public ActionResult ReviewAlarms(Token authToken, int batch, string reviewer)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var html = AlarmReviewService.GetReviewPage(batch, reviewer);
|
||||
if (html is null) return NotFound("Batch not found or reviewer not recognised.");
|
||||
return Content(html, "text/html");
|
||||
}
|
||||
|
||||
[HttpPost(nameof(SubmitAlarmReview))]
|
||||
public async Task<ActionResult> SubmitAlarmReview(int batch, string? reviewer, [FromBody] List<ReviewFeedback>? feedbacks)
|
||||
public async Task<ActionResult> SubmitAlarmReview(Token authToken, int batch, string? reviewer, [FromBody] List<ReviewFeedback>? feedbacks)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
// Batch 0 = test mode — run dry-run synthesis and return preview HTML (nothing is saved)
|
||||
if (batch == 0)
|
||||
{
|
||||
|
|
@ -2091,14 +2123,20 @@ public class Controller : ControllerBase
|
|||
}
|
||||
|
||||
[HttpGet(nameof(GetAlarmReviewStatus))]
|
||||
public ActionResult GetAlarmReviewStatus()
|
||||
public ActionResult GetAlarmReviewStatus(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
return Ok(AlarmReviewService.GetStatus());
|
||||
}
|
||||
|
||||
[HttpGet(nameof(DownloadCheckedKnowledgeBase))]
|
||||
public ActionResult DownloadCheckedKnowledgeBase()
|
||||
public ActionResult DownloadCheckedKnowledgeBase(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var content = AlarmReviewService.GetCheckedFileContent();
|
||||
if (content is null) return NotFound("AlarmKnowledgeBaseChecked.cs has not been generated yet.");
|
||||
|
||||
|
|
@ -2598,6 +2636,8 @@ public class Controller : ControllerBase
|
|||
[FromQuery] Int64? ticketId,
|
||||
[FromQuery] Int64? ticketCommentId,
|
||||
[FromQuery] Int64? installationId,
|
||||
[FromQuery] Int64? checklistItemId,
|
||||
[FromQuery] String? subtaskKey,
|
||||
[FromQuery] Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
|
|
@ -2676,6 +2716,8 @@ public class Controller : ControllerBase
|
|||
TicketId = ticketId,
|
||||
TicketCommentId = ticketCommentId,
|
||||
InstallationId = installationId,
|
||||
ChecklistItemId = checklistItemId,
|
||||
SubtaskKey = subtaskKey,
|
||||
Scope = scope,
|
||||
S3Key = s3Key,
|
||||
OriginalName = safeFileName,
|
||||
|
|
@ -2725,6 +2767,8 @@ public class Controller : ControllerBase
|
|||
[FromQuery] Int64? ticketId,
|
||||
[FromQuery] Int64? ticketCommentId,
|
||||
[FromQuery] Int64? installationId,
|
||||
[FromQuery] Int64? checklistItemId,
|
||||
[FromQuery] String? subtaskKey,
|
||||
[FromQuery] Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
|
|
@ -2736,6 +2780,12 @@ public class Controller : ControllerBase
|
|||
if (ticketCommentId.HasValue)
|
||||
return Ok(Db.GetDocumentsForComment(ticketCommentId.Value));
|
||||
|
||||
if (checklistItemId.HasValue)
|
||||
{
|
||||
if (user.UserType != 2) return Unauthorized();
|
||||
return Ok(Db.GetDocumentsForChecklistItem(checklistItemId.Value, subtaskKey));
|
||||
}
|
||||
|
||||
if (installationId.HasValue)
|
||||
{
|
||||
// Access control: admin can list all; others need installation access
|
||||
|
|
@ -2747,7 +2797,7 @@ public class Controller : ControllerBase
|
|||
return Ok(Db.GetDocumentsForInstallation(installationId.Value));
|
||||
}
|
||||
|
||||
return BadRequest("Provide ticketId, ticketCommentId, or installationId.");
|
||||
return BadRequest("Provide ticketId, ticketCommentId, installationId, or checklistItemId.");
|
||||
}
|
||||
|
||||
[HttpDelete(nameof(DeleteDocument))]
|
||||
|
|
@ -2774,4 +2824,114 @@ public class Controller : ControllerBase
|
|||
return Ok();
|
||||
}
|
||||
|
||||
// ── Checklist ───────────────────────────────────────────────────────
|
||||
|
||||
[HttpGet(nameof(GetChecklistForInstallation))]
|
||||
public ActionResult<IEnumerable<ChecklistItem>> GetChecklistForInstallation(Int64 installationId, Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var installation = Db.GetInstallationById(installationId);
|
||||
if (installation is null) return NotFound("Installation not found.");
|
||||
|
||||
if (!Db.ChecklistExistsForInstallation(installationId))
|
||||
{
|
||||
foreach (var def in ChecklistStepDefinitions.Steps)
|
||||
{
|
||||
Db.Create(new ChecklistItem
|
||||
{
|
||||
InstallationId = installationId,
|
||||
StepNumber = def.Number,
|
||||
StepTitle = def.Title,
|
||||
Subtasks = def.SubtasksJson,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(Db.GetChecklistForInstallation(installationId));
|
||||
}
|
||||
|
||||
[HttpPut(nameof(UpdateChecklistItem))]
|
||||
public ActionResult<ChecklistItem> UpdateChecklistItem(
|
||||
Int64 checklistItemId,
|
||||
Int32? status,
|
||||
String? comments,
|
||||
Int64? assigneeId,
|
||||
Boolean clearAssignee,
|
||||
String? doneAt,
|
||||
String? subtasks,
|
||||
Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var item = Db.GetChecklistItemById(checklistItemId);
|
||||
if (item is null) return NotFound("Checklist item not found.");
|
||||
|
||||
if (status.HasValue) item.Status = status.Value;
|
||||
if (comments is not null) item.Comments = comments;
|
||||
if (clearAssignee) item.AssigneeId = null;
|
||||
else if (assigneeId.HasValue) item.AssigneeId = assigneeId.Value;
|
||||
if (doneAt is not null) item.DoneAt = String.IsNullOrWhiteSpace(doneAt) ? null : doneAt;
|
||||
if (subtasks is not null) item.Subtasks = subtasks;
|
||||
|
||||
// Auto-fill DoneAt when status transitions to Done and no date provided
|
||||
if (item.Status == (Int32)ChecklistStatus.Done && String.IsNullOrWhiteSpace(item.DoneAt))
|
||||
item.DoneAt = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
|
||||
item.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
return Db.Update(item) ? item : StatusCode(500, "Update failed.");
|
||||
}
|
||||
|
||||
[HttpPost(nameof(NotifyChecklistAssignee))]
|
||||
public async Task<ActionResult> NotifyChecklistAssignee(Int64 checklistItemId, Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var item = Db.GetChecklistItemById(checklistItemId);
|
||||
if (item is null) return NotFound("Checklist item not found.");
|
||||
|
||||
if (item.AssigneeId is null) return BadRequest("No assignee set for this step.");
|
||||
|
||||
var assignee = Db.GetUserById(item.AssigneeId);
|
||||
if (assignee is null) return BadRequest("Assignee user not found.");
|
||||
|
||||
var installation = Db.GetInstallationById(item.InstallationId);
|
||||
if (installation is null) return NotFound("Installation not found.");
|
||||
|
||||
try
|
||||
{
|
||||
await assignee.SendChecklistAssignedEmail(item, installation, user.Name);
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Checklist] Failed to send assignee email: {ex}");
|
||||
return StatusCode(500, "Failed to send notification email.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet(nameof(GetChecklistSummary))]
|
||||
public ActionResult<IEnumerable<Object>> GetChecklistSummary(Token authToken)
|
||||
{
|
||||
var user = Db.GetSession(authToken)?.User;
|
||||
if (user is null || user.UserType != 2) return Unauthorized();
|
||||
|
||||
var summaries = Db.ChecklistItems
|
||||
.ToList()
|
||||
.GroupBy(c => c.InstallationId)
|
||||
.Select(g => new
|
||||
{
|
||||
installationId = g.Key,
|
||||
done = g.Count(c => c.Status == (Int32)ChecklistStatus.Done),
|
||||
total = g.Count()
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(summaries);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
using SQLite;
|
||||
|
||||
namespace InnovEnergy.App.Backend.DataTypes;
|
||||
|
||||
public enum ChecklistStatus { NotStarted = 0, InProgress = 1, Done = 2 }
|
||||
|
||||
public class ChecklistItem
|
||||
{
|
||||
[PrimaryKey, AutoIncrement] public Int64 Id { get; set; }
|
||||
|
||||
[Indexed] public Int64 InstallationId { get; set; }
|
||||
|
||||
public Int32 StepNumber { get; set; }
|
||||
public String StepTitle { get; set; } = "";
|
||||
public Int32 Status { get; set; } = (Int32)ChecklistStatus.NotStarted;
|
||||
public String Comments { get; set; } = "";
|
||||
public Int64? AssigneeId { get; set; }
|
||||
public String? DoneAt { get; set; }
|
||||
public String? Subtasks { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
namespace InnovEnergy.App.Backend.DataTypes;
|
||||
|
||||
public record ChecklistStepDefinition(Int32 Number, String Title, String? SubtasksJson);
|
||||
|
||||
public static class ChecklistStepDefinitions
|
||||
{
|
||||
private const String NoSubtasks = null!;
|
||||
|
||||
public static readonly IReadOnlyList<ChecklistStepDefinition> Steps = new List<ChecklistStepDefinition>
|
||||
{
|
||||
new( 1, "Order created, customer and partner info recorded in CRM", NoSubtasks),
|
||||
new( 2, "Hardware assembled at Vebo", NoSubtasks),
|
||||
new( 3, "Installation created on Monitor under correct product and folder",
|
||||
"""
|
||||
[
|
||||
{"text":"checklistStep3Sub1","checked":false}
|
||||
]
|
||||
"""),
|
||||
new( 4, "Gateway SD card configured, VPN and gateway name registered", NoSubtasks),
|
||||
new( 5, "Information tab filled out (customer, serials, VPN)",
|
||||
"""
|
||||
[
|
||||
{"text":"checklistStep5Sub1","checked":false},
|
||||
{"text":"checklistStep5Sub2","checked":false},
|
||||
{"text":"checklistStep5Sub3","checked":false},
|
||||
{"text":"checklistStep5Sub4","checked":false},
|
||||
{"text":"checklistStep5Sub5","checked":false},
|
||||
{"text":"checklistStep5Sub6","checked":false}
|
||||
]
|
||||
"""),
|
||||
new( 6, "Installation configured and tested electrically / hardware-wise at Vebo",
|
||||
"""
|
||||
[
|
||||
{"text":"checklistStep6Sub1","checked":false},
|
||||
{"text":"checklistStep6Sub2","checked":false},
|
||||
{"text":"checklistStep6Sub3","checked":false},
|
||||
{"text":"checklistStep6Sub4","checked":false}
|
||||
]
|
||||
"""),
|
||||
new( 7, "Installation tested software-wise at Vebo",
|
||||
"""
|
||||
[
|
||||
{"text":"checklistStep7Sub1","checked":false},
|
||||
{"text":"checklistStep7Sub2","checked":false},
|
||||
{"text":"checklistStep7Sub3","checked":false},
|
||||
{"text":"checklistStep7Sub4","checked":false}
|
||||
]
|
||||
"""),
|
||||
new( 8, "Installation delivered to customer site",
|
||||
"""
|
||||
[
|
||||
{"text":"checklistStep8Sub1","checked":false}
|
||||
]
|
||||
"""),
|
||||
new( 9, "Installation connected to grid", NoSubtasks),
|
||||
new(10, "Hardware verified on site",
|
||||
"""
|
||||
[
|
||||
{"text":"checklistStep10Sub1","checked":false},
|
||||
{"text":"checklistStep10Sub2","checked":false}
|
||||
]
|
||||
"""),
|
||||
new(11, "Software verified on site", NoSubtasks),
|
||||
new(12, "Installation online on Monitor", NoSubtasks),
|
||||
new(13, "Customer informed about Monitor account and reports", NoSubtasks),
|
||||
new(14, "User account created with correct folders and access", NoSubtasks),
|
||||
new(15, "Customer follow-up completed, feedback collected", NoSubtasks),
|
||||
new(16, "Further issues tracked via Ticket system", NoSubtasks),
|
||||
};
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ public class Document
|
|||
[Indexed] public Int64? TicketId { get; set; }
|
||||
[Indexed] public Int64? TicketCommentId { get; set; }
|
||||
[Indexed] public Int64? InstallationId { get; set; }
|
||||
[Indexed] public Int64? ChecklistItemId { get; set; }
|
||||
public String? SubtaskKey { get; set; }
|
||||
|
||||
public Int32 Scope { get; set; } = (Int32)DocumentScope.TicketAttachment;
|
||||
public String S3Key { get; set; } = "";
|
||||
|
|
|
|||
|
|
@ -518,4 +518,67 @@ public static class UserMethods
|
|||
return user.SendEmail(subject, body);
|
||||
}
|
||||
|
||||
public static Task SendChecklistAssignedEmail(
|
||||
this User user,
|
||||
ChecklistItem item,
|
||||
Installation installation,
|
||||
String notifiedByName)
|
||||
{
|
||||
var productPath = installation.Product switch
|
||||
{
|
||||
4 => "sodistoregrid_installations",
|
||||
5 => "sodistorepro_installations",
|
||||
_ => "sodiohome_installations"
|
||||
};
|
||||
var checklistLink = $"https://monitor.inesco.energy/{productPath}/installation/{installation.Id}/checklist";
|
||||
var installationName = String.IsNullOrEmpty(installation.Name) ? $"#{installation.Id}" : installation.Name;
|
||||
var commentsBlock = String.IsNullOrWhiteSpace(item.Comments) ? "" : item.Comments;
|
||||
|
||||
var (subject, body) = (user.Language ?? "en") switch
|
||||
{
|
||||
"de" => (
|
||||
"inesco energy – Sie wurden einem Checklisten-Schritt zugewiesen",
|
||||
$"Sehr geehrte/r {user.Name},\n\n" +
|
||||
$"{notifiedByName} hat Sie einem Schritt der Installations-Checkliste zugewiesen:\n\n" +
|
||||
$"Installation: {installationName}\n" +
|
||||
$"Schritt {item.StepNumber}: {item.StepTitle}\n\n" +
|
||||
(commentsBlock.Length > 0 ? $"Kommentare:\n{commentsBlock}\n\n" : "") +
|
||||
$"Checkliste öffnen: {checklistLink}\n\n" +
|
||||
"Mit freundlichen Grüssen\ninesco energy Monitor"
|
||||
),
|
||||
"fr" => (
|
||||
"inesco energy – Une étape de checklist vous a été attribuée",
|
||||
$"Cher/Chère {user.Name},\n\n" +
|
||||
$"{notifiedByName} vous a attribué une étape de la checklist d'installation :\n\n" +
|
||||
$"Installation : {installationName}\n" +
|
||||
$"Étape {item.StepNumber} : {item.StepTitle}\n\n" +
|
||||
(commentsBlock.Length > 0 ? $"Commentaires :\n{commentsBlock}\n\n" : "") +
|
||||
$"Ouvrir la checklist : {checklistLink}\n\n" +
|
||||
"Cordialement,\ninesco energy Monitor"
|
||||
),
|
||||
"it" => (
|
||||
"inesco energy – Le è stato assegnato un passo della checklist",
|
||||
$"Gentile {user.Name},\n\n" +
|
||||
$"{notifiedByName} le ha assegnato un passo della checklist di installazione:\n\n" +
|
||||
$"Installazione: {installationName}\n" +
|
||||
$"Passo {item.StepNumber}: {item.StepTitle}\n\n" +
|
||||
(commentsBlock.Length > 0 ? $"Commenti:\n{commentsBlock}\n\n" : "") +
|
||||
$"Aprire la checklist: {checklistLink}\n\n" +
|
||||
"Cordiali saluti,\ninesco energy Monitor"
|
||||
),
|
||||
_ => (
|
||||
"inesco energy – You have been assigned to a checklist step",
|
||||
$"Dear {user.Name},\n\n" +
|
||||
$"{notifiedByName} has assigned you to an installation checklist step:\n\n" +
|
||||
$"Installation: {installationName}\n" +
|
||||
$"Step {item.StepNumber}: {item.StepTitle}\n\n" +
|
||||
(commentsBlock.Length > 0 ? $"Comments:\n{commentsBlock}\n\n" : "") +
|
||||
$"Open the checklist: {checklistLink}\n\n" +
|
||||
"Best regards,\ninesco energy Monitor"
|
||||
)
|
||||
};
|
||||
|
||||
return user.SendEmail(subject, body);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -92,6 +92,9 @@ public static partial class Db
|
|||
// Document storage
|
||||
public static Boolean Create(Document document) => Insert(document);
|
||||
|
||||
// Checklist
|
||||
public static Boolean Create(ChecklistItem item) => Insert(item);
|
||||
|
||||
public static void HandleAction(UserAction newAction)
|
||||
{
|
||||
//Find the total number of actions for this installation
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ public static partial class Db
|
|||
// Document storage
|
||||
public static TableQuery<Document> Documents => Connection.Table<Document>();
|
||||
|
||||
// Checklist
|
||||
public static TableQuery<ChecklistItem> ChecklistItems => Connection.Table<ChecklistItem>();
|
||||
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
|
|
@ -83,6 +86,9 @@ public static partial class Db
|
|||
|
||||
// Document storage
|
||||
Connection.CreateTable<Document>();
|
||||
|
||||
// Checklist
|
||||
Connection.CreateTable<ChecklistItem>();
|
||||
});
|
||||
|
||||
// One-time migration: normalize legacy long-form language values to ISO codes
|
||||
|
|
@ -103,6 +109,51 @@ public static partial class Db
|
|||
Connection.Execute("UPDATE User SET Name = 'inesco energy Master Admin' WHERE Name = 'InnovEnergy Master Admin'");
|
||||
Connection.Execute("UPDATE User SET Name = 'inesco energy Master Admin' WHERE Name = 'inesco Energy Master Admin'");
|
||||
|
||||
// One-time migration: rewrite early-seeded English subtask text to translation keys so the
|
||||
// frontend can localize them. Idempotent: rows already containing keys match nothing.
|
||||
var subtaskTextToKey = new (String Old, String Key)[]
|
||||
{
|
||||
("Customer information (email, address)", "checklistStep5Sub1"),
|
||||
("Installation information (external EMS, grid provider, data collection)", "checklistStep5Sub2"),
|
||||
("Installation information (external EMS, network provider, data collection)", "checklistStep5Sub2"),
|
||||
("Battery serial number", "checklistStep5Sub3"),
|
||||
("Inverter serial number", "checklistStep5Sub4"),
|
||||
("Data logger serial number", "checklistStep5Sub5"),
|
||||
("VPN details", "checklistStep5Sub6"),
|
||||
("Inverter firmware and configuration verified", "checklistStep6Sub1"),
|
||||
("Battery firmware and configuration verified", "checklistStep6Sub2"),
|
||||
("Internet for gateway configured", "checklistStep6Sub3"),
|
||||
("Communication cable between gateway and inverter correct", "checklistStep6Sub4"),
|
||||
("S3 bucket number and key credentials copied from Information tab into config.json","checklistStep7Sub1"),
|
||||
("Product ID configured in config.json", "checklistStep7Sub2"),
|
||||
("USB ID configured in config.json", "checklistStep7Sub3"),
|
||||
("Inverter data reading from inverter tested", "checklistStep7Sub4")
|
||||
};
|
||||
foreach (var (oldText, key) in subtaskTextToKey)
|
||||
{
|
||||
Connection.Execute(
|
||||
"UPDATE ChecklistItem SET Subtasks = REPLACE(Subtasks, ?, ?) WHERE Subtasks LIKE ?",
|
||||
$"\"{oldText}\"", $"\"{key}\"", $"%\"{oldText}\"%");
|
||||
}
|
||||
|
||||
// One-time backfill: step 3 originally had no subtasks; add the installation serial subtask
|
||||
// to existing rows.
|
||||
Connection.Execute(
|
||||
"UPDATE ChecklistItem SET Subtasks = ? WHERE StepNumber = 3 AND (Subtasks IS NULL OR Subtasks = '')",
|
||||
"[{\"text\":\"checklistStep3Sub1\",\"checked\":false}]");
|
||||
|
||||
// One-time backfill: step 8 originally had no subtasks; add the delivery-receipt subtask
|
||||
// to existing rows so already-seeded installations pick up the new subtask after deploy.
|
||||
Connection.Execute(
|
||||
"UPDATE ChecklistItem SET Subtasks = ? WHERE StepNumber = 8 AND (Subtasks IS NULL OR Subtasks = '')",
|
||||
"[{\"text\":\"checklistStep8Sub1\",\"checked\":false}]");
|
||||
|
||||
// One-time backfill: step 10 originally had no subtasks; add the two upload subtasks
|
||||
// (installation protocol + time & material report) to existing rows.
|
||||
Connection.Execute(
|
||||
"UPDATE ChecklistItem SET Subtasks = ? WHERE StepNumber = 10 AND (Subtasks IS NULL OR Subtasks = '')",
|
||||
"[{\"text\":\"checklistStep10Sub1\",\"checked\":false},{\"text\":\"checklistStep10Sub2\",\"checked\":false}]");
|
||||
|
||||
//UpdateKeys();
|
||||
CleanupSessions().SupressAwaitWarning();
|
||||
DeleteSnapshots().SupressAwaitWarning();
|
||||
|
|
@ -150,6 +201,9 @@ public static partial class Db
|
|||
// Document storage
|
||||
fileConnection.CreateTable<Document>();
|
||||
|
||||
// Checklist
|
||||
fileConnection.CreateTable<ChecklistItem>();
|
||||
|
||||
// Migrate new columns: set defaults for existing rows where NULL or empty
|
||||
fileConnection.Execute("UPDATE Installation SET ExternalEms = 'No' WHERE ExternalEms IS NULL OR ExternalEms = ''");
|
||||
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
|
||||
|
|
|
|||
|
|
@ -145,6 +145,9 @@ public static partial class Db
|
|||
// Clean up installation-level documents
|
||||
Documents.Delete(d => d.InstallationId == installation.Id);
|
||||
|
||||
// Clean up checklist items for this installation
|
||||
ChecklistItems.Delete(c => c.InstallationId == installation.Id);
|
||||
|
||||
return Installations.Delete(i => i.Id == installation.Id) > 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,4 +233,26 @@ public static partial class Db
|
|||
.Where(d => d.InstallationId == installationId && d.Scope == (Int32)DocumentScope.InstallationDocument)
|
||||
.OrderBy(d => d.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
public static List<Document> GetDocumentsForChecklistItem(Int64 checklistItemId, String? subtaskKey = null)
|
||||
=> Documents
|
||||
.Where(d => d.ChecklistItemId == checklistItemId
|
||||
&& d.Scope == (Int32)DocumentScope.InstallationDocument
|
||||
&& (subtaskKey == null || d.SubtaskKey == subtaskKey))
|
||||
.OrderBy(d => d.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
// ── Checklist Queries ───────────────────────────────────────────────
|
||||
|
||||
public static List<ChecklistItem> GetChecklistForInstallation(Int64 installationId)
|
||||
=> ChecklistItems
|
||||
.Where(c => c.InstallationId == installationId)
|
||||
.OrderBy(c => c.StepNumber)
|
||||
.ToList();
|
||||
|
||||
public static Boolean ChecklistExistsForInstallation(Int64 installationId)
|
||||
=> ChecklistItems.Any(c => c.InstallationId == installationId);
|
||||
|
||||
public static ChecklistItem? GetChecklistItemById(Int64 id)
|
||||
=> ChecklistItems.FirstOrDefault(c => c.Id == id);
|
||||
}
|
||||
|
|
@ -73,4 +73,7 @@ public static partial class Db
|
|||
public static Boolean Update(Ticket ticket) => Update(obj: ticket);
|
||||
public static Boolean Update(TicketAiDiagnosis diagnosis) => Update(obj: diagnosis);
|
||||
public static Boolean Update(TicketComment comment) => Update(obj: comment);
|
||||
|
||||
// Checklist
|
||||
public static Boolean Update(ChecklistItem item) => Update(obj: item);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
{
|
||||
"AbnormalGridVoltage": {
|
||||
"Explanation": "Der Wechselrichter hat festgestellt, dass die Netzspannung außerhalb des zulässigen Bereichs liegt. Das System benötigt manuellen Eingriff zur Wiederherstellung.",
|
||||
"Causes": [
|
||||
|
|
@ -28,15 +28,15 @@
|
|||
]
|
||||
},
|
||||
"InvertedSequenceOfGridVoltage": {
|
||||
"Explanation": "Die Phasenreihenfolge der dreiphasigen Netzspannung ist vertauscht. Dies ist ein Verdrahtungsproblem, das einen sicheren Betrieb verhindert.",
|
||||
"Explanation": "Die Reihenfolge der drei Stromphasen vom Netz ist falsch angeschlossen. Das System kann nicht sicher arbeiten.",
|
||||
"Causes": [
|
||||
"Falsche Verdrahtung der Netzphasen während der Installation (L1, L2, L3 vertauscht)",
|
||||
"Nachträgliche Verdrahtungsarbeiten ohne Überprüfung der Phasenfolge"
|
||||
"Falsche Verdrahtung der Netzphasen (L1, L2, L3) bei der Installation",
|
||||
"Nachträgliche Änderungen an der Verdrahtung ohne Prüfung der Phasenfolge"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Schalten Sie das gesamte System sicher aus, bevor Sie die Verdrahtung berühren",
|
||||
"Vertauschen Sie zwei der drei Phasenleitungen an der Netzverbindung, um die Reihenfolge zu korrigieren",
|
||||
"Schalten Sie das System wieder ein und überprüfen Sie, ob die Warnung behoben ist"
|
||||
"Tauschen Sie zwei der drei Phasenleitungen an der Netzverbindung, um die Reihenfolge zu korrigieren",
|
||||
"Schalten Sie das System wieder ein und prüfen Sie, ob die Warnung verschwunden ist"
|
||||
]
|
||||
},
|
||||
"GridVoltagePhaseLoss": {
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
]
|
||||
},
|
||||
"ExcessiveRadiatorTemperature": {
|
||||
"Explanation": "Die Temperatur des Kühlkörpers (Radiator) des Wechselrichters ist zu hoch. Der Kühlkörper dient dazu, Wärme während des Betriebs abzuführen.",
|
||||
"Explanation": "Der Kühlkörper des Wechselrichters ist zu heiß, weil die Wärme nicht richtig abgeführt wird.",
|
||||
"Causes": [
|
||||
"Verstopfte oder blockierte Lüftungsschlitze verhindern die Wärmeabfuhr",
|
||||
"Ausfall des Kühlgebläses reduziert die Luftzirkulation",
|
||||
|
|
@ -145,8 +145,7 @@
|
|||
"NextSteps": [
|
||||
"Reinigen Sie die Lüftungsschlitze und Staubfilter – Staubansammlungen sind eine häufige Ursache",
|
||||
"Überprüfen Sie, ob das Kühlgebläse läuft (Hören Sie auf Gebläsegeräusche während des Betriebs)",
|
||||
"Reduzieren Sie die Last vorübergehend, um die Wärmeentwicklung zu verringern",
|
||||
"Reparieren oder ersetzen Sie das Gebläse, falls es defekt ist, und starten Sie den Wechselrichter neu"
|
||||
"Reduzieren Sie die Last vorübergehend, um die Wärmeentwicklung zu verringern"
|
||||
]
|
||||
},
|
||||
"PcbOvertemperature": {
|
||||
|
|
@ -541,13 +540,14 @@
|
|||
"Battery2OverloadTimeout": {
|
||||
"Explanation": "Batterie 2 läuft seit zu langer Zeit unter Überlast.",
|
||||
"Causes": [
|
||||
"Dauerhafte hohe Last, die die Entladerating von Batterie 2 überschreitet",
|
||||
"Dauerhafte hohe Last, die die Entladeleistung von Batterie 2 überschreitet",
|
||||
"Batterie 2 ist degradiert und kann weniger Leistung bereitstellen"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Den Gesamtstromverbrauch reduzieren",
|
||||
"Prüfen, ob Batterie 2 für die Lastanforderungen richtig dimensioniert ist",
|
||||
"Nach Lastreduzierung den Wechselrichter neu starten"
|
||||
"Nach Lastreduzierung den Wechselrichter neu starten",
|
||||
"Bitte überprüfen Sie die Einstellungen zur Batterie (z. B. Lade-/Entladestrom)"
|
||||
]
|
||||
},
|
||||
"Battery2SoftStartFailure": {
|
||||
|
|
@ -967,9 +967,9 @@
|
|||
]
|
||||
},
|
||||
"Pv3ReverseConnection": {
|
||||
"Explanation": "PV-String 3 ist mit vertauschter Polarität angeschlossen. Dies ist ein Verdrahtungsfehler, der vor dem Betrieb behoben werden muss.",
|
||||
"Explanation": "PV-String 3 ist mit vertauschter Polarität angeschlossen. Dieser Verdrahtungsfehler muss vor dem Betrieb behoben werden.",
|
||||
"Causes": [
|
||||
"Positive und negative Kabel von PV-String 3 wurden während der Installation vertauscht",
|
||||
"Positive und negative Kabel von PV-String 3 wurden womöglich während der Installation vertauscht",
|
||||
"Falsche Kabelverbindung am DC-Eingang des Wechselrichters"
|
||||
],
|
||||
"NextSteps": [
|
||||
|
|
@ -1247,12 +1247,13 @@
|
|||
]
|
||||
},
|
||||
"InverterOverloadTimeout": {
|
||||
"Explanation": "Der Wechselrichter war zu lange überlastet und hat sich abgeschaltet.",
|
||||
"Explanation": "Der Wechselrichter war zu lange überlastet und hat sich automatisch abgeschaltet.",
|
||||
"Causes": [
|
||||
"Dauerhafte Überlastung, die die Kurzzeit-Überlastfähigkeit des Wechselrichters überschreitet",
|
||||
"Der Wechselrichter ist für die tatsächliche Last zu klein dimensioniert"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Die sinnvolle Aufteilung der Lasten zwischen Notstrom- und normalem Hausnetz überprüfen.",
|
||||
"Die angeschlossene Last dauerhaft reduzieren",
|
||||
"Falls die Last notwendig ist, auf einen größeren Wechselrichter umsteigen",
|
||||
"Die Ursache beheben und den Wechselrichter neu starten"
|
||||
|
|
@ -1265,6 +1266,7 @@
|
|||
"Ein neues leistungsstarkes Gerät wurde hinzugefügt, das die Systemleistung übersteigt"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Die sinnvolle Aufteilung der Lasten zwischen Notstrom- und normalem Hausnetz überprüfen.",
|
||||
"Last reduzieren, indem nicht essentielle Geräte ausgeschaltet werden",
|
||||
"Nutzung leistungsstarker Geräte staffeln und den Wechselrichter neu starten"
|
||||
]
|
||||
|
|
@ -1295,16 +1297,15 @@
|
|||
]
|
||||
},
|
||||
"Dsp1ParameterSettingFault": {
|
||||
"Explanation": "DSP 1 (digitaler Signalprozessor) hat eine falsche Parameterkonfiguration erkannt.",
|
||||
"Explanation": "Der Wechselrichter hat eine falsche Einstellung in seinen internen Parametern erkannt.",
|
||||
"Causes": [
|
||||
"Ein oder mehrere Wechselrichterparameter sind außerhalb des zulässigen Bereichs eingestellt",
|
||||
"Firmware-Korruption beeinflusst die Parameterspeicherung",
|
||||
"Konfigurationsinkonsistenz nach einem Firmware-Update"
|
||||
"Ein oder mehrere Parameter des Wechselrichters liegen außerhalb des erlaubten Bereichs.",
|
||||
"Die Firmware ist beschädigt und beeinflusst die Speicherung der Einstellungen.",
|
||||
"Nach einem Firmware-Update stimmen die Einstellungen nicht mehr überein."
|
||||
],
|
||||
"NextSteps": [
|
||||
"Alle Wechselrichter-Parameter überprüfen und eventuell ungültige Werte korrigieren",
|
||||
"Parameter auf Werkseinstellungen zurücksetzen, falls unsicher über die richtigen Werte",
|
||||
"Die Ursache beheben und den Wechselrichter neu starten"
|
||||
"Überprüfen Sie alle Parameter des Wechselrichters und korrigieren Sie ungültige Werte.",
|
||||
"Setzen Sie die Parameter auf Werkseinstellungen zurück, falls Sie unsicher sind."
|
||||
]
|
||||
},
|
||||
"Dsp2ParameterSettingFault": {
|
||||
|
|
@ -1568,14 +1569,15 @@
|
|||
]
|
||||
},
|
||||
"ReverseMeterConnection": {
|
||||
"Explanation": "Der Stromzähler ist falsch installiert oder verdrahtet. Die Zählerstände (Import/Export) sind bis zur Korrektur ungenau.",
|
||||
"Explanation": "Der Stromzähler ist falsch angeschlossen, sodass die Messwerte (Strombezug/Einspeisung) nicht stimmen.",
|
||||
"Causes": [
|
||||
"Der Stromwandler (CT) ist in die falsche Richtung installiert",
|
||||
"Die L- und N-Leitungen des Zählers sind bei der Installation vertauscht"
|
||||
"Der Stromwandler (CT) ist in die falsche Richtung eingebaut",
|
||||
"Die L- und N-Leitungen des Zählers wurden vertauscht",
|
||||
"Andere Messwandler in der Nähe stören – mindestens 30 cm Abstand halten"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Verlassen Sie sich nicht auf die Zählerstände, bis die Korrektur erfolgt ist",
|
||||
"Kontaktieren Sie Ihren Installateur oder einen qualifizierten Elektriker, um den Stromwandler oder die Zählerverkabelung zu korrigieren"
|
||||
"Kontaktieren Sie Ihren Installateur oder einen qualifizierten Elektriker, um den Stromwandler oder die Zählerverkabelung zu prüfen"
|
||||
]
|
||||
},
|
||||
"InverterSealPulse": {
|
||||
|
|
@ -2121,7 +2123,7 @@
|
|||
]
|
||||
},
|
||||
"LithiumBatteryOverload": {
|
||||
"Explanation": "Der Überlastschutz der Lithiumbatterie wurde aktiviert – die Last entnimmt mehr Strom, als die Batterie abgeben kann.",
|
||||
"Explanation": "Der Überlastschutz der Batterie wurde aktiviert – die Last entnimmt mehr Strom, als die Batterie abgeben kann.",
|
||||
"Causes": [
|
||||
"Die Gesamtlastleistung überschreitet die maximale Entladeleistung der Batterie",
|
||||
"Hochstrom beim Einschalten großer Motoren oder Kompressoren übersteigt vorübergehend die Batteriegrenzen"
|
||||
|
|
@ -2220,6 +2222,19 @@
|
|||
"Reparieren oder entfernen Sie den überlastenden Verbraucher, bevor Sie den Wechselrichter neu starten"
|
||||
]
|
||||
},
|
||||
"OffGridBusVoltageTooLow": {
|
||||
"Explanation": "Die Gleichspannung im Inselbetrieb ist zu stark abgesunken, um einen stabilen Betrieb aufrechtzuerhalten.",
|
||||
"Causes": [
|
||||
"Batterieladestand zu niedrig",
|
||||
"Zu hohe Last am Inselausgang",
|
||||
"Defekt oder lockere Verbindung in der DC-Bus-Verdrahtung"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Last am Inselausgang reduzieren",
|
||||
"Batterieladestand prüfen und ggf. aufladen",
|
||||
"DC-Bus-Verdrahtung auf lockere Verbindungen oder Schäden überprüfen"
|
||||
]
|
||||
},
|
||||
"OffGridOutputOverload": {
|
||||
"Explanation": "Der netzunabhängige (EPS/Backup)-Ausgang ist überlastet — es wird mehr Strom angefordert, als der Wechselrichter im Backup-Modus liefern kann.",
|
||||
"Causes": [
|
||||
|
|
@ -2805,18 +2820,5 @@
|
|||
"Messen Sie die tatsächliche DC-Spannung, bevor Sie wieder anschließen",
|
||||
"Überprüfen Sie das String-Design und reduzieren Sie gegebenenfalls die Anzahl der Module in Reihe, um die Wechselrichter-Spannungsgrenzen einzuhalten"
|
||||
]
|
||||
},
|
||||
"OffGridBusVoltageTooLow": {
|
||||
"Explanation": "Die Gleichspannung im Inselbetrieb ist zu stark abgesunken, um einen stabilen Betrieb aufrechtzuerhalten.",
|
||||
"Causes": [
|
||||
"Batterieladestand zu niedrig",
|
||||
"Zu hohe Last am Inselausgang",
|
||||
"Defekt oder lockere Verbindung in der DC-Bus-Verdrahtung"
|
||||
],
|
||||
"NextSteps": [
|
||||
"Last am Inselausgang reduzieren",
|
||||
"Batterieladestand prüfen und ggf. aufladen",
|
||||
"DC-Bus-Verdrahtung auf lockere Verbindungen oder Schäden überprüfen"
|
||||
]
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -279,6 +279,10 @@ public static class AlarmReviewService
|
|||
|
||||
if (current.Synthesized)
|
||||
{
|
||||
// Campaign is fully reviewed → nothing left to do. Without this guard the
|
||||
// recovery branch below re-fires the admin completion email every workday.
|
||||
if (progress.Batches.Count * BatchSize >= AllAlarmKeys.Length) return;
|
||||
|
||||
// Next batch is sent immediately after synthesis — only act here as a safety net
|
||||
// in case the server restarted before SendNextBatchAsync could run.
|
||||
var nextAlreadySent = progress.Batches.Count > current.BatchNumber;
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -4,18 +4,29 @@ generate_alarm_translations.py
|
|||
|
||||
Post-campaign script: reads AlarmTranslationsChecked.de.json (the reviewed and
|
||||
AI-synthesized German content), translates into English, French, and Italian,
|
||||
and writes:
|
||||
and writes preview files for review BEFORE replacing the live translations:
|
||||
|
||||
Resources/AlarmTranslations.de.json ← replace with reviewed German
|
||||
Resources/AlarmTranslations.en.json ← back-translated from German
|
||||
Resources/AlarmTranslations.fr.json ← translated from German
|
||||
Resources/AlarmTranslations.it.json ← translated from German
|
||||
Services/AlarmKnowledgeBase.cs ← updated English source (keeps same structure)
|
||||
Resources/AlarmTranslationsChecked.en.json ← NEW (back-translated from German)
|
||||
Resources/AlarmTranslationsChecked.fr.json ← NEW (translated from German)
|
||||
Resources/AlarmTranslationsChecked.it.json ← NEW (translated from German)
|
||||
Services/AlarmKnowledgeBase.cs ← updated in-place (review via `git diff`)
|
||||
|
||||
Resources/AlarmTranslationsChecked.de.json is the INPUT and is not modified.
|
||||
Live files Resources/AlarmTranslations.{de,fr,it}.json are NOT overwritten —
|
||||
review the *Checked* files, then manually copy them onto the live names when ready:
|
||||
|
||||
cp Resources/AlarmTranslationsChecked.de.json Resources/AlarmTranslations.de.json
|
||||
cp Resources/AlarmTranslationsChecked.en.json Resources/AlarmTranslations.en.json
|
||||
cp Resources/AlarmTranslationsChecked.fr.json Resources/AlarmTranslations.fr.json
|
||||
cp Resources/AlarmTranslationsChecked.it.json Resources/AlarmTranslations.it.json
|
||||
|
||||
Run this AFTER the review campaign is complete:
|
||||
export MISTRAL_API_KEY=your_key_here
|
||||
cd csharp/App/Backend
|
||||
python3 generate_alarm_translations.py
|
||||
|
||||
The script reads MISTRAL_API_KEY from the environment, falling back to the same
|
||||
.env file the C# backend uses (csharp/App/Backend/.env). No `export` needed if
|
||||
the .env file is in place.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
|
@ -23,7 +34,6 @@ import os
|
|||
import re
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
|
|
@ -245,18 +255,17 @@ def main():
|
|||
print(f"ERROR: {CHECKED_FILE} not found. Run the review campaign first.")
|
||||
sys.exit(1)
|
||||
|
||||
with open(CHECKED_FILE, "r", encoding="utf-8") as f:
|
||||
# utf-8-sig strips the BOM that the C# AlarmReviewService writes via Encoding.UTF8
|
||||
with open(CHECKED_FILE, "r", encoding="utf-8-sig") as f:
|
||||
german_source = json.load(f)
|
||||
|
||||
alarm_keys = list(german_source.keys())
|
||||
print(f"Loaded {len(alarm_keys)} alarms from {CHECKED_FILE}.")
|
||||
|
||||
# Step 1: copy reviewed German as the new de.json
|
||||
de_out = os.path.join(RESOURCES_DIR, "AlarmTranslations.de.json")
|
||||
shutil.copy(CHECKED_FILE, de_out)
|
||||
print(f"\n✓ Copied reviewed German → {de_out}")
|
||||
# The reviewed German JSON is already at AlarmTranslationsChecked.de.json — no copy needed.
|
||||
# User will manually replace AlarmTranslations.de.json after reviewing all four Checked files.
|
||||
|
||||
# Step 2: translate to en, fr, it
|
||||
# Translate to en, fr, it → write to AlarmTranslationsChecked.{lang}.json (preview names)
|
||||
all_translations = {} # lang_code → {key → entry}
|
||||
for lang_code, lang_name in TARGET_LANGUAGES.items():
|
||||
print(f"\n── Translating to {lang_name} ({lang_code}) ──")
|
||||
|
|
@ -296,7 +305,7 @@ def main():
|
|||
time.sleep(1)
|
||||
|
||||
all_translations[lang_code] = translations
|
||||
out_file = os.path.join(RESOURCES_DIR, f"AlarmTranslations.{lang_code}.json")
|
||||
out_file = os.path.join(RESOURCES_DIR, f"AlarmTranslationsChecked.{lang_code}.json")
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(translations, f, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ Wrote {len(translations)} entries → {out_file}")
|
||||
|
|
@ -312,8 +321,17 @@ def main():
|
|||
else:
|
||||
print(" Skipped — en.json not generated or AlarmKnowledgeBase.cs not found.")
|
||||
|
||||
print("\n✓ Done. Review the output files before deploying.")
|
||||
print(" Next: cd csharp/App/Backend && dotnet build && ./deploy.sh")
|
||||
print("\n✓ Done. Review these preview files before replacing the live ones:")
|
||||
print(f" - {RESOURCES_DIR}/AlarmTranslationsChecked.de.json (reviewed German — input, unchanged)")
|
||||
print(f" - {RESOURCES_DIR}/AlarmTranslationsChecked.en.json (new)")
|
||||
print(f" - {RESOURCES_DIR}/AlarmTranslationsChecked.fr.json (new)")
|
||||
print(f" - {RESOURCES_DIR}/AlarmTranslationsChecked.it.json (new)")
|
||||
print(f" - {KNOWLEDGE_BASE} (overwritten — review with `git diff`)")
|
||||
print("\nWhen satisfied:")
|
||||
print(" for lang in de en fr it; do")
|
||||
print(f" cp {RESOURCES_DIR}/AlarmTranslationsChecked.$lang.json {RESOURCES_DIR}/AlarmTranslations.$lang.json")
|
||||
print(" done")
|
||||
print(" dotnet build && ./deploy.sh")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
patch_missing_alarms.py
|
||||
|
||||
Re-translates specific keys that failed during a previous run of
|
||||
generate_alarm_translations.py (e.g. due to LLM JSON-format glitches),
|
||||
then regenerates AlarmKnowledgeBase.cs with the now-complete English set.
|
||||
|
||||
Translates one key per API call to dodge the multi-key JSON formatting
|
||||
issue that caused the original failures.
|
||||
|
||||
Edit MISSING below to set which keys to retry per language, then run:
|
||||
cd csharp/App/Backend
|
||||
python3 patch_missing_alarms.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from generate_alarm_translations import (
|
||||
translate_batch,
|
||||
parse_kb_key_sections,
|
||||
write_knowledge_base_cs,
|
||||
load_env_file,
|
||||
CHECKED_FILE,
|
||||
KNOWLEDGE_BASE,
|
||||
RESOURCES_DIR,
|
||||
TARGET_LANGUAGES,
|
||||
)
|
||||
|
||||
# Keys that failed during the 2026-04-28 run.
|
||||
# Update this dict if a new run produces different failures.
|
||||
MISSING = {
|
||||
"en": [
|
||||
"DcBusOvervoltage",
|
||||
"DcBusUndervoltage",
|
||||
"DcBusVoltageUnbalance",
|
||||
"BusSlowOvervoltage",
|
||||
"HardwareBusOvervoltage",
|
||||
],
|
||||
"it": [
|
||||
"NtcTemperatureSensorBroken",
|
||||
"SyncSignalAbnormal",
|
||||
"GridStartupConditionsNotMet",
|
||||
"BatteryCommunicationFailure",
|
||||
"BatteryDisconnected",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
api_key = os.environ.get("MISTRAL_API_KEY", "").strip()
|
||||
if not api_key:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
api_key = load_env_file(os.path.join(script_dir, ".env")).get("MISTRAL_API_KEY", "").strip()
|
||||
if not api_key:
|
||||
print("ERROR: MISTRAL_API_KEY not found in environment or .env file.")
|
||||
sys.exit(1)
|
||||
print("MISTRAL_API_KEY loaded.")
|
||||
|
||||
with open(CHECKED_FILE, encoding="utf-8-sig") as f:
|
||||
de = json.load(f)
|
||||
|
||||
en_translations = None
|
||||
|
||||
for lang_code, missing_keys in MISSING.items():
|
||||
lang_name = TARGET_LANGUAGES[lang_code]
|
||||
out_file = os.path.join(RESOURCES_DIR, f"AlarmTranslationsChecked.{lang_code}.json")
|
||||
with open(out_file, encoding="utf-8") as f:
|
||||
existing = json.load(f)
|
||||
|
||||
# Idempotent: only translate keys that are still genuinely missing from the JSON.
|
||||
actually_missing = [
|
||||
k for k in missing_keys
|
||||
if k in de and (k not in existing or not existing[k].get("Explanation"))
|
||||
]
|
||||
if not actually_missing:
|
||||
print(f"\n── {lang_name} ({lang_code}) already complete ({len(existing)} entries) — skipping translation ──")
|
||||
else:
|
||||
print(f"\n── Patching {lang_name} ({lang_code}) — {len(actually_missing)} keys ──")
|
||||
translated = {}
|
||||
for key in actually_missing:
|
||||
print(f" {key}")
|
||||
result = translate_batch(api_key, {key: de[key]}, lang_name)
|
||||
if result and key in result:
|
||||
r = result[key]
|
||||
translated[key] = {
|
||||
"Explanation": r.get("Explanation", ""),
|
||||
"Causes": r.get("Causes", []),
|
||||
"NextSteps": r.get("NextSteps", []),
|
||||
}
|
||||
snippet = r.get("Explanation", "")[:80]
|
||||
print(f" OK: {snippet}{'...' if len(r.get('Explanation','')) > 80 else ''}")
|
||||
else:
|
||||
print(f" FAILED: {key}")
|
||||
|
||||
existing.update(translated)
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(existing, f, ensure_ascii=False, indent=2)
|
||||
print(f" ✓ Wrote {len(existing)} total entries → {out_file}")
|
||||
|
||||
if lang_code == "en":
|
||||
en_translations = existing
|
||||
|
||||
if en_translations is not None and os.path.exists(KNOWLEDGE_BASE):
|
||||
print("\n── Regenerating AlarmKnowledgeBase.cs ──")
|
||||
key_sections = parse_kb_key_sections(KNOWLEDGE_BASE)
|
||||
write_knowledge_base_cs(KNOWLEDGE_BASE, en_translations, key_sections)
|
||||
|
||||
print("\n✓ Patch done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -26,5 +26,6 @@
|
|||
"report": "report",
|
||||
"installationTickets": "installationTickets",
|
||||
"documents": "documents",
|
||||
"checklist": "checklist",
|
||||
"tickets": "/tickets/"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ interface FileUploadButtonProps {
|
|||
ticketId?: number;
|
||||
ticketCommentId?: number;
|
||||
installationId?: number;
|
||||
checklistItemId?: number;
|
||||
subtaskKey?: string;
|
||||
onUploaded?: (doc: UploadedDocument) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
@ -41,6 +43,8 @@ function FileUploadButton({
|
|||
ticketId,
|
||||
ticketCommentId,
|
||||
installationId,
|
||||
checklistItemId,
|
||||
subtaskKey,
|
||||
onUploaded,
|
||||
disabled = false
|
||||
}: FileUploadButtonProps) {
|
||||
|
|
@ -87,7 +91,7 @@ function FileUploadButton({
|
|||
|
||||
try {
|
||||
const res = await axiosConfig.post('/UploadDocument', formData, {
|
||||
params: { scope, ticketId, ticketCommentId, installationId },
|
||||
params: { scope, ticketId, ticketCommentId, installationId, checklistItemId, subtaskKey },
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
onUploadProgress: (e) => {
|
||||
if (e.total) setProgress(Math.round((e.loaded * 100) / e.total));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,321 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
Chip,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
Stack,
|
||||
TableCell,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import {
|
||||
ChecklistItem,
|
||||
ChecklistStatus,
|
||||
ChecklistSubtask,
|
||||
parseSubtasks,
|
||||
serializeSubtasks,
|
||||
UPLOADABLE_SUBTASK_KEYS
|
||||
} from 'src/interfaces/ChecklistTypes';
|
||||
import { AdminUser } from 'src/interfaces/TicketTypes';
|
||||
import SubtaskDocumentUpload from './SubtaskDocumentUpload';
|
||||
|
||||
type EmailIconState = 'idle' | 'loading' | 'success' | 'error';
|
||||
|
||||
interface Props {
|
||||
item: ChecklistItem;
|
||||
installationId: number;
|
||||
adminUsers: AdminUser[];
|
||||
onUpdate: (
|
||||
id: number,
|
||||
patch: Partial<{
|
||||
status: number;
|
||||
comments: string;
|
||||
assigneeId: number | null;
|
||||
doneAt: string | null;
|
||||
subtasks: string;
|
||||
}>
|
||||
) => Promise<boolean>;
|
||||
onNotify: (id: number) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const statusColors: Record<number, string> = {
|
||||
[ChecklistStatus.NotStarted]: '#9e9e9e',
|
||||
[ChecklistStatus.InProgress]: '#ed6c02',
|
||||
[ChecklistStatus.Done]: '#2e7d32'
|
||||
};
|
||||
|
||||
function ChecklistStepRow({ item, installationId, adminUsers, onUpdate, onNotify }: Props) {
|
||||
const intl = useIntl();
|
||||
const [comments, setComments] = useState(item.comments ?? '');
|
||||
const [emailState, setEmailState] = useState<EmailIconState>('idle');
|
||||
const [subtasksOpen, setSubtasksOpen] = useState(false);
|
||||
const [subtasks, setSubtasks] = useState<ChecklistSubtask[]>(() =>
|
||||
parseSubtasks(item.subtasks)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setComments(item.comments ?? '');
|
||||
}, [item.comments]);
|
||||
|
||||
useEffect(() => {
|
||||
setSubtasks(parseSubtasks(item.subtasks));
|
||||
}, [item.subtasks]);
|
||||
|
||||
const subtaskSummary = useMemo(() => {
|
||||
if (subtasks.length === 0) return null;
|
||||
const done = subtasks.filter((s) => s.checked).length;
|
||||
return `${done}/${subtasks.length}`;
|
||||
}, [subtasks]);
|
||||
|
||||
const handleStatusChange = async (value: number) => {
|
||||
await onUpdate(item.id, { status: value });
|
||||
};
|
||||
|
||||
const handleAssigneeChange = async (rawValue: string) => {
|
||||
if (rawValue === '') {
|
||||
await onUpdate(item.id, { assigneeId: null });
|
||||
} else {
|
||||
await onUpdate(item.id, { assigneeId: Number(rawValue) });
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoneAtChange = async (value: string) => {
|
||||
await onUpdate(item.id, { doneAt: value || null });
|
||||
};
|
||||
|
||||
const handleCommentsBlur = async () => {
|
||||
if (comments !== (item.comments ?? '')) {
|
||||
await onUpdate(item.id, { comments });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubtaskToggle = async (index: number) => {
|
||||
const updated = subtasks.map((s, i) =>
|
||||
i === index ? { ...s, checked: !s.checked } : s
|
||||
);
|
||||
setSubtasks(updated);
|
||||
await onUpdate(item.id, { subtasks: serializeSubtasks(updated) });
|
||||
};
|
||||
|
||||
const handleNotifyClick = async () => {
|
||||
if (!item.assigneeId || emailState === 'loading') return;
|
||||
setEmailState('loading');
|
||||
const ok = await onNotify(item.id);
|
||||
setEmailState(ok ? 'success' : 'error');
|
||||
setTimeout(() => setEmailState('idle'), 2500);
|
||||
};
|
||||
|
||||
const renderEmailIcon = () => {
|
||||
if (emailState === 'success') return <CheckIcon sx={{ color: '#2e7d32' }} />;
|
||||
if (emailState === 'error') return <CloseIcon sx={{ color: '#d32f2f' }} />;
|
||||
return <EmailIcon />;
|
||||
};
|
||||
|
||||
const filteredAdmins = useMemo(
|
||||
() =>
|
||||
adminUsers.filter((u) => {
|
||||
const name = (u.name ?? '').toLowerCase();
|
||||
return (
|
||||
!name.includes('inesco energy master admin') &&
|
||||
!name.includes('paal myhre')
|
||||
);
|
||||
}),
|
||||
[adminUsers]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow hover>
|
||||
<TableCell sx={{ width: 48, verticalAlign: 'top' }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{item.stepNumber}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
<TableCell sx={{ verticalAlign: 'top', minWidth: 240 }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Typography variant="body2">
|
||||
{intl.formatMessage({
|
||||
id: `checklistStep${item.stepNumber}`,
|
||||
defaultMessage: item.stepTitle
|
||||
})}
|
||||
</Typography>
|
||||
{subtasks.length > 0 && (
|
||||
<>
|
||||
<Chip
|
||||
size="small"
|
||||
label={subtaskSummary}
|
||||
color={
|
||||
subtasks.every((s) => s.checked) ? 'success' : 'default'
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setSubtasksOpen((o) => !o)}
|
||||
aria-label={intl.formatMessage({
|
||||
id: 'checklistToggleSubtasks',
|
||||
defaultMessage: 'Toggle subtasks'
|
||||
})}
|
||||
>
|
||||
{subtasksOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell sx={{ verticalAlign: 'top', width: 160 }}>
|
||||
<FormControl size="small" fullWidth>
|
||||
<Select
|
||||
value={item.status}
|
||||
onChange={(e) => handleStatusChange(Number(e.target.value))}
|
||||
sx={{
|
||||
'& .MuiSelect-select': {
|
||||
color: statusColors[item.status] ?? 'inherit',
|
||||
fontWeight: 600
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value={ChecklistStatus.NotStarted}>
|
||||
<FormattedMessage id="checklistNotStarted" defaultMessage="Not Started" />
|
||||
</MenuItem>
|
||||
<MenuItem value={ChecklistStatus.InProgress}>
|
||||
<FormattedMessage id="checklistInProgress" defaultMessage="In Progress" />
|
||||
</MenuItem>
|
||||
<MenuItem value={ChecklistStatus.Done}>
|
||||
<FormattedMessage id="checklistDone" defaultMessage="Done" />
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</TableCell>
|
||||
|
||||
<TableCell sx={{ verticalAlign: 'top', width: 260 }}>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
<FormControl size="small" sx={{ flex: 1 }}>
|
||||
<Select
|
||||
value={item.assigneeId ?? ''}
|
||||
displayEmpty
|
||||
onChange={(e) => handleAssigneeChange(String(e.target.value))}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>
|
||||
<FormattedMessage
|
||||
id="checklistNoAssignee"
|
||||
defaultMessage="Unassigned"
|
||||
/>
|
||||
</em>
|
||||
</MenuItem>
|
||||
{filteredAdmins.map((u) => (
|
||||
<MenuItem key={u.id} value={u.id}>
|
||||
{u.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Tooltip
|
||||
title={
|
||||
!item.assigneeId
|
||||
? intl.formatMessage({
|
||||
id: 'checklistNotifyDisabledTooltip',
|
||||
defaultMessage: 'Assign someone first to send a notification'
|
||||
})
|
||||
: intl.formatMessage({
|
||||
id: 'checklistNotifyTooltip',
|
||||
defaultMessage: 'Send email notification to assignee'
|
||||
})
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
disabled={!item.assigneeId || emailState === 'loading'}
|
||||
onClick={handleNotifyClick}
|
||||
>
|
||||
{renderEmailIcon()}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
<TableCell sx={{ verticalAlign: 'top', width: 160 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
type="date"
|
||||
value={item.doneAt ?? ''}
|
||||
onChange={(e) => handleDoneAtChange(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell sx={{ verticalAlign: 'top', minWidth: 280 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
multiline
|
||||
minRows={1}
|
||||
maxRows={6}
|
||||
value={comments}
|
||||
onChange={(e) => setComments(e.target.value)}
|
||||
onBlur={handleCommentsBlur}
|
||||
fullWidth
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'checklistCommentsPlaceholder',
|
||||
defaultMessage: 'Notes, contact info, observations…'
|
||||
})}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{subtasks.length > 0 && subtasksOpen && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ backgroundColor: '#fafafa', py: 1 }}>
|
||||
<Box pl={6} display="flex" flexDirection="column">
|
||||
{subtasks.map((s, i) => (
|
||||
<Box key={i}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={s.checked}
|
||||
onChange={() => handleSubtaskToggle(i)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2" component="span">
|
||||
{intl.formatMessage({ id: s.text, defaultMessage: s.text })}
|
||||
</Typography>
|
||||
}
|
||||
sx={{ ml: 0 }}
|
||||
/>
|
||||
{UPLOADABLE_SUBTASK_KEYS.has(s.text) && (
|
||||
<SubtaskDocumentUpload
|
||||
installationId={installationId}
|
||||
checklistItemId={item.id}
|
||||
subtaskKey={s.text}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChecklistStepRow;
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
LinearProgress,
|
||||
Snackbar,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Typography,
|
||||
Paper
|
||||
} from '@mui/material';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import {
|
||||
ChecklistItem,
|
||||
ChecklistStatus
|
||||
} from 'src/interfaces/ChecklistTypes';
|
||||
import { AdminUser } from 'src/interfaces/TicketTypes';
|
||||
import ChecklistStepRow from './ChecklistStepRow';
|
||||
|
||||
interface Props {
|
||||
installationId: number;
|
||||
}
|
||||
|
||||
type ToastState = {
|
||||
open: boolean;
|
||||
severity: 'success' | 'error';
|
||||
message: string;
|
||||
};
|
||||
|
||||
function InstallationChecklistTab({ installationId }: Props) {
|
||||
const intl = useIntl();
|
||||
const [items, setItems] = useState<ChecklistItem[]>([]);
|
||||
const [adminUsers, setAdminUsers] = useState<AdminUser[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [toast, setToast] = useState<ToastState>({
|
||||
open: false,
|
||||
severity: 'success',
|
||||
message: ''
|
||||
});
|
||||
|
||||
const fetchItems = useCallback(() => {
|
||||
setLoading(true);
|
||||
axiosConfig
|
||||
.get('/GetChecklistForInstallation', { params: { installationId } })
|
||||
.then((res) => {
|
||||
setItems(Array.isArray(res.data) ? res.data : []);
|
||||
setError('');
|
||||
})
|
||||
.catch(() => setError('Failed to load checklist.'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [installationId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, [fetchItems]);
|
||||
|
||||
useEffect(() => {
|
||||
axiosConfig
|
||||
.get('/GetAdminUsers')
|
||||
.then((res) => {
|
||||
if (Array.isArray(res.data)) setAdminUsers(res.data);
|
||||
})
|
||||
.catch(() => setAdminUsers([]));
|
||||
}, []);
|
||||
|
||||
const progress = useMemo(() => {
|
||||
const total = items.length;
|
||||
const done = items.filter(
|
||||
(i) => i.status === ChecklistStatus.Done
|
||||
).length;
|
||||
const percent = total === 0 ? 0 : Math.round((done / total) * 100);
|
||||
return { total, done, percent };
|
||||
}, [items]);
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
async (
|
||||
id: number,
|
||||
patch: Partial<{
|
||||
status: number;
|
||||
comments: string;
|
||||
assigneeId: number | null;
|
||||
doneAt: string | null;
|
||||
subtasks: string;
|
||||
}>
|
||||
) => {
|
||||
const params: Record<string, unknown> = { checklistItemId: id };
|
||||
if (patch.status !== undefined) params.status = patch.status;
|
||||
if (patch.comments !== undefined) params.comments = patch.comments;
|
||||
if (patch.subtasks !== undefined) params.subtasks = patch.subtasks;
|
||||
|
||||
if ('assigneeId' in patch) {
|
||||
if (patch.assigneeId === null) {
|
||||
params.clearAssignee = true;
|
||||
} else if (typeof patch.assigneeId === 'number') {
|
||||
params.assigneeId = patch.assigneeId;
|
||||
}
|
||||
}
|
||||
|
||||
if ('doneAt' in patch) {
|
||||
params.doneAt = patch.doneAt ?? '';
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axiosConfig.put('/UpdateChecklistItem', null, {
|
||||
params
|
||||
});
|
||||
const updated = res.data as ChecklistItem;
|
||||
setItems((prev) =>
|
||||
prev.map((it) => (it.id === id ? updated : it))
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
setToast({
|
||||
open: true,
|
||||
severity: 'error',
|
||||
message: intl.formatMessage({
|
||||
id: 'checklistSaveFailed',
|
||||
defaultMessage: 'Failed to save change'
|
||||
})
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[intl]
|
||||
);
|
||||
|
||||
const handleNotify = useCallback(
|
||||
async (id: number) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
if (!item || !item.assigneeId) return false;
|
||||
const assignee = adminUsers.find((u) => u.id === item.assigneeId);
|
||||
const assigneeName = assignee?.name ?? '';
|
||||
try {
|
||||
await axiosConfig.post('/NotifyChecklistAssignee', null, {
|
||||
params: { checklistItemId: id }
|
||||
});
|
||||
setToast({
|
||||
open: true,
|
||||
severity: 'success',
|
||||
message: intl.formatMessage(
|
||||
{
|
||||
id: 'checklistEmailSent',
|
||||
defaultMessage: 'Email sent to {name}'
|
||||
},
|
||||
{ name: assigneeName }
|
||||
)
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
setToast({
|
||||
open: true,
|
||||
severity: 'error',
|
||||
message: intl.formatMessage({
|
||||
id: 'checklistEmailFailed',
|
||||
defaultMessage: 'Failed to send email — try again'
|
||||
})
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[items, adminUsers, intl]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box p={3}>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box p={3}>
|
||||
<Alert severity="error">{error}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box p={2}>
|
||||
<Box mb={2}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={1}>
|
||||
<Typography variant="h4">
|
||||
<FormattedMessage
|
||||
id="checklistTitle"
|
||||
defaultMessage="Steps to Bring Installation to Monitor"
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<FormattedMessage
|
||||
id="checklistProgress"
|
||||
defaultMessage="Progress: {done}/{total} ({percent}%)"
|
||||
values={progress}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress.percent}
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>#</TableCell>
|
||||
<TableCell>
|
||||
<FormattedMessage id="checklistStep" defaultMessage="Step" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<FormattedMessage id="checklistStatus" defaultMessage="Status" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<FormattedMessage id="checklistAssignee" defaultMessage="Assignee" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<FormattedMessage id="checklistDateDone" defaultMessage="Date Done" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<FormattedMessage id="checklistComments" defaultMessage="Comments" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map((item) => (
|
||||
<ChecklistStepRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
installationId={installationId}
|
||||
adminUsers={adminUsers}
|
||||
onUpdate={handleUpdate}
|
||||
onNotify={handleNotify}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Snackbar
|
||||
open={toast.open}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setToast((t) => ({ ...t, open: false }))}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert
|
||||
severity={toast.severity}
|
||||
onClose={() => setToast((t) => ({ ...t, open: false }))}
|
||||
>
|
||||
{toast.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default InstallationChecklistTab;
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { Box, LinearProgress, Tooltip, Typography } from '@mui/material';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
interface Props {
|
||||
done: number | null;
|
||||
total: number | null;
|
||||
}
|
||||
|
||||
function phaseId(done: number, total: number): string {
|
||||
if (total === 0) return 'checklistPhaseEmpty';
|
||||
if (done >= total) return 'checklistPhaseComplete';
|
||||
if (done <= 5) return 'checklistPhasePreparation';
|
||||
if (done <= 12) return 'checklistPhaseOnSite';
|
||||
return 'checklistPhaseHandover';
|
||||
}
|
||||
|
||||
function SetupProgress({ done, total }: Props) {
|
||||
const intl = useIntl();
|
||||
|
||||
if (done === null || total === null || total === 0) {
|
||||
return (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontSize: 'small' }}>
|
||||
--
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const percent = Math.round((done / total) * 100);
|
||||
const color = percent >= 100 ? 'success' : percent > 0 ? 'warning' : 'inherit';
|
||||
|
||||
const tooltip = intl.formatMessage({
|
||||
id: phaseId(done, total),
|
||||
defaultMessage: 'Progress'
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip title={tooltip}>
|
||||
<Box display="flex" alignItems="center" sx={{ minWidth: 120 }}>
|
||||
<Box sx={{ width: 70, mr: 1 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={percent}
|
||||
color={color === 'inherit' ? 'primary' : (color as any)}
|
||||
sx={{ height: 6, borderRadius: 3 }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ fontSize: 'small', whiteSpace: 'nowrap' }}>
|
||||
{done}/{total}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default SetupProgress;
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Box, IconButton, Stack, Typography } from '@mui/material';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import FileUploadButton, {
|
||||
UploadedDocument
|
||||
} from 'src/components/FileUploadButton';
|
||||
|
||||
interface DocumentItem {
|
||||
id: number;
|
||||
originalName: string;
|
||||
contentType: string;
|
||||
sizeBytes: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
installationId: number;
|
||||
checklistItemId: number;
|
||||
subtaskKey: string;
|
||||
}
|
||||
|
||||
function fileIcon(contentType: string) {
|
||||
if (contentType === 'application/pdf')
|
||||
return <PictureAsPdfIcon fontSize="small" color="error" />;
|
||||
return <InsertDriveFileIcon fontSize="small" />;
|
||||
}
|
||||
|
||||
function SubtaskDocumentUpload({ installationId, checklistItemId, subtaskKey }: Props) {
|
||||
const [docs, setDocs] = useState<DocumentItem[]>([]);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
const fetchDocs = useCallback(() => {
|
||||
axiosConfig
|
||||
.get('/GetDocuments', { params: { checklistItemId, subtaskKey } })
|
||||
.then((res) => {
|
||||
if (Array.isArray(res.data)) setDocs(res.data);
|
||||
})
|
||||
.catch(() => setDocs([]));
|
||||
}, [checklistItemId, subtaskKey]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocs();
|
||||
}, [fetchDocs, refreshKey]);
|
||||
|
||||
const handleDownload = (doc: DocumentItem) => {
|
||||
axiosConfig
|
||||
.get('/DownloadDocument', {
|
||||
params: { id: doc.id },
|
||||
responseType: 'blob'
|
||||
})
|
||||
.then((res) => {
|
||||
const url = window.URL.createObjectURL(new Blob([res.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', doc.originalName);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const handleUploaded = (_doc: UploadedDocument) => {
|
||||
setRefreshKey((k) => k + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box mt={0.5} mb={1}>
|
||||
{docs.length > 0 && (
|
||||
<Stack spacing={0.25} mb={0.75}>
|
||||
{docs.map((d) => (
|
||||
<Stack
|
||||
key={d.id}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
sx={{ pl: 4 }}
|
||||
>
|
||||
{fileIcon(d.contentType)}
|
||||
<Typography variant="body2" sx={{ flex: 1 }} noWrap>
|
||||
{d.originalName}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleDownload(d)}
|
||||
aria-label="download"
|
||||
>
|
||||
<DownloadIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Box sx={{ pl: 4 }}>
|
||||
<FileUploadButton
|
||||
scope={1}
|
||||
installationId={installationId}
|
||||
checklistItemId={checklistItemId}
|
||||
subtaskKey={subtaskKey}
|
||||
onUploaded={handleUploaded}
|
||||
/>
|
||||
</Box>
|
||||
{docs.length === 0 && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ pl: 4, display: 'block', mt: 0.5 }}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="checklistNoAttachments"
|
||||
defaultMessage="No file attached yet."
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default SubtaskDocumentUpload;
|
||||
|
|
@ -27,6 +27,14 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
|||
import routes from '../../../Resources/routes.json';
|
||||
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
|
||||
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { UserType } from 'src/interfaces/UserTypes';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import {
|
||||
CHECKLIST_ENABLED_PRODUCTS,
|
||||
ChecklistSummary
|
||||
} from 'src/interfaces/ChecklistTypes';
|
||||
import SetupProgress from '../Checklist/SetupProgress';
|
||||
|
||||
interface FlatInstallationViewProps {
|
||||
installations: I_Installation[];
|
||||
|
|
@ -46,6 +54,28 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [sortByStatus, setSortByStatus] = useState('All Installations');
|
||||
const [sortByAction, setSortByAction] = useState('All Installations');
|
||||
const { currentUser } = useContext(UserContext);
|
||||
const showChecklistColumn =
|
||||
currentUser?.userType === UserType.admin &&
|
||||
CHECKLIST_ENABLED_PRODUCTS.has(product ?? -1);
|
||||
const [progressMap, setProgressMap] = useState<Map<number, ChecklistSummary>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showChecklistColumn) return;
|
||||
axiosConfig
|
||||
.get('/GetChecklistSummary')
|
||||
.then((res) => {
|
||||
if (!Array.isArray(res.data)) return;
|
||||
const map = new Map<number, ChecklistSummary>();
|
||||
res.data.forEach((s: ChecklistSummary) => {
|
||||
map.set(s.installationId, s);
|
||||
});
|
||||
setProgressMap(map);
|
||||
})
|
||||
.catch(() => setProgressMap(new Map()));
|
||||
}, [showChecklistColumn, currentLocation.pathname]);
|
||||
|
||||
const HoverableTableRow = styled(TableRow)(({ theme }) => ({
|
||||
cursor: 'pointer',
|
||||
|
|
@ -315,6 +345,14 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
<TableCell>
|
||||
<FormattedMessage id="status" defaultMessage="Status" />
|
||||
</TableCell>
|
||||
{showChecklistColumn && (
|
||||
<TableCell>
|
||||
<FormattedMessage
|
||||
id="setupProgress"
|
||||
defaultMessage="Setup Progress"
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -461,6 +499,19 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
{showChecklistColumn && (
|
||||
<TableCell>
|
||||
{(() => {
|
||||
const summary = progressMap.get(installation.id);
|
||||
return (
|
||||
<SetupProgress
|
||||
done={summary?.done ?? 0}
|
||||
total={summary?.total ?? 16}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import Configuration from '../Configuration/Configuration';
|
|||
import PvView from '../PvView/PvView';
|
||||
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
|
||||
import DocumentsTab from '../Documents/DocumentsTab';
|
||||
import InstallationChecklistTab from '../Checklist/InstallationChecklistTab';
|
||||
import { CHECKLIST_ENABLED_PRODUCTS } from 'src/interfaces/ChecklistTypes';
|
||||
|
||||
interface singleInstallationProps {
|
||||
current_installation?: I_Installation;
|
||||
|
|
@ -386,7 +388,8 @@ function Installation(props: singleInstallationProps) {
|
|||
currentTab != 'history' &&
|
||||
// currentTab != 'manage' &&
|
||||
currentTab != 'log' &&
|
||||
currentTab != 'installationTickets' && (
|
||||
currentTab != 'installationTickets' &&
|
||||
currentTab != 'checklist' && (
|
||||
<Container
|
||||
maxWidth="xl"
|
||||
sx={{
|
||||
|
|
@ -569,6 +572,18 @@ function Installation(props: singleInstallationProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{currentUser.userType == UserType.admin &&
|
||||
CHECKLIST_ENABLED_PRODUCTS.has(props.current_installation.product) && (
|
||||
<Route
|
||||
path={routes.checklist}
|
||||
element={
|
||||
<InstallationChecklistTab
|
||||
installationId={props.current_installation.id}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Route
|
||||
path={'*'}
|
||||
element={<Navigate to={routes.live}></Navigate>}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
'history',
|
||||
'pvview',
|
||||
'installationTickets',
|
||||
'documents'
|
||||
'documents',
|
||||
'checklist'
|
||||
];
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||
|
|
@ -105,6 +106,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
// TODO: SodistoreGrid — PV View excluded for product 4, add back when data path is ready
|
||||
const hidePvView = props.product === 4;
|
||||
|
||||
// Checklist is only shown for Sodistore Grid (product=4) in the Installations view.
|
||||
// Salimax (0) / Salidomo (1) / SodistoreMax (3) use different onboarding flows.
|
||||
const showChecklist = props.product === 4;
|
||||
|
||||
const singleInstallationTabs = (
|
||||
currentUser.userType == UserType.admin
|
||||
? [
|
||||
|
|
@ -175,6 +180,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
{
|
||||
value: 'documents',
|
||||
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
||||
},
|
||||
{
|
||||
value: 'checklist',
|
||||
label: <FormattedMessage id="checklist" defaultMessage="Checklist" />
|
||||
}
|
||||
]
|
||||
: currentUser.userType == UserType.partner
|
||||
|
|
@ -229,7 +238,9 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
)
|
||||
}
|
||||
]
|
||||
).filter((tab) => !(hidePvView && tab.value === 'pvview'));
|
||||
)
|
||||
.filter((tab) => !(hidePvView && tab.value === 'pvview'))
|
||||
.filter((tab) => !(!showChecklist && tab.value === 'checklist'));
|
||||
|
||||
const tabs =
|
||||
currentTab != 'list' &&
|
||||
|
|
@ -316,6 +327,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
{
|
||||
value: 'documents',
|
||||
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
||||
},
|
||||
{
|
||||
value: 'checklist',
|
||||
label: <FormattedMessage id="checklist" defaultMessage="Checklist" />
|
||||
}
|
||||
]
|
||||
: currentUser.userType == UserType.partner
|
||||
|
|
@ -410,9 +425,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
|||
];
|
||||
|
||||
// Filter out PV View for SodistoreGrid
|
||||
const filteredTabs = hidePvView
|
||||
const filteredTabs = (hidePvView
|
||||
? tabs.filter((tab) => tab.value !== 'pvview')
|
||||
: tabs;
|
||||
: tabs
|
||||
).filter((tab) => !(!showChecklist && tab.value === 'checklist'));
|
||||
|
||||
return installations.length > 1 ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
CircularProgress,
|
||||
|
|
@ -20,6 +20,14 @@ import routes from '../../../Resources/routes.json';
|
|||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import { getDeviceTypeName } from '../Information/installationSetupUtils';
|
||||
import { UserContext } from 'src/contexts/userContext';
|
||||
import { UserType } from 'src/interfaces/UserTypes';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import {
|
||||
CHECKLIST_ENABLED_PRODUCTS,
|
||||
ChecklistSummary
|
||||
} from 'src/interfaces/ChecklistTypes';
|
||||
import SetupProgress from '../Checklist/SetupProgress';
|
||||
|
||||
interface FlatInstallationViewProps {
|
||||
installations: I_Installation[];
|
||||
|
|
@ -32,6 +40,32 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
const currentLocation = useLocation();
|
||||
const baseRoute = props.product === 5 ? routes.sodistorepro_installations : routes.sodiohome_installations;
|
||||
|
||||
const { currentUser } = useContext(UserContext);
|
||||
const showChecklistColumn =
|
||||
currentUser?.userType === UserType.admin &&
|
||||
CHECKLIST_ENABLED_PRODUCTS.has(props.product ?? -1);
|
||||
const isListViewPath =
|
||||
currentLocation.pathname === baseRoute + 'list' ||
|
||||
currentLocation.pathname === baseRoute + routes.list;
|
||||
const [progressMap, setProgressMap] = useState<Map<number, ChecklistSummary>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showChecklistColumn || !isListViewPath) return;
|
||||
axiosConfig
|
||||
.get('/GetChecklistSummary')
|
||||
.then((res) => {
|
||||
if (!Array.isArray(res.data)) return;
|
||||
const map = new Map<number, ChecklistSummary>();
|
||||
res.data.forEach((s: ChecklistSummary) => {
|
||||
map.set(s.installationId, s);
|
||||
});
|
||||
setProgressMap(map);
|
||||
})
|
||||
.catch(() => setProgressMap(new Map()));
|
||||
}, [showChecklistColumn, isListViewPath]);
|
||||
|
||||
const sortedInstallations = useMemo(() => {
|
||||
return [...props.installations].sort((a, b) => {
|
||||
// Data-collection-disabled installations sink below everything (even offline).
|
||||
|
|
@ -83,9 +117,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
}
|
||||
}));
|
||||
|
||||
const isListView =
|
||||
currentLocation.pathname === baseRoute + 'list' ||
|
||||
currentLocation.pathname === baseRoute + routes.list;
|
||||
const isListView = isListViewPath;
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} sx={{ marginTop: 0.1 }}>
|
||||
|
|
@ -118,6 +150,14 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
<TableCell>
|
||||
<FormattedMessage id="status" defaultMessage="Status" />
|
||||
</TableCell>
|
||||
{showChecklistColumn && (
|
||||
<TableCell>
|
||||
<FormattedMessage
|
||||
id="setupProgress"
|
||||
defaultMessage="Setup Progress"
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -282,6 +322,19 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
|
|||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
{showChecklistColumn && (
|
||||
<TableCell>
|
||||
{(() => {
|
||||
const summary = progressMap.get(installation.id);
|
||||
return (
|
||||
<SetupProgress
|
||||
done={summary?.done ?? 0}
|
||||
total={summary?.total ?? 16}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</TableCell>
|
||||
)}
|
||||
</HoverableTableRow>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ import Overview from '../Overview/overview';
|
|||
import WeeklyReport from './WeeklyReport';
|
||||
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
|
||||
import DocumentsTab from '../Documents/DocumentsTab';
|
||||
import InstallationChecklistTab from '../Checklist/InstallationChecklistTab';
|
||||
import { CHECKLIST_ENABLED_PRODUCTS } from 'src/interfaces/ChecklistTypes';
|
||||
|
||||
interface singleInstallationProps {
|
||||
current_installation?: I_Installation;
|
||||
|
|
@ -496,7 +498,8 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
currentTab != 'log' &&
|
||||
currentTab != 'report' &&
|
||||
currentTab != 'installationTickets' &&
|
||||
currentTab != 'documents' && (
|
||||
currentTab != 'documents' &&
|
||||
currentTab != 'checklist' && (
|
||||
<Container
|
||||
maxWidth="xl"
|
||||
sx={{
|
||||
|
|
@ -670,6 +673,18 @@ function SodioHomeInstallation(props: singleInstallationProps) {
|
|||
/>
|
||||
)}
|
||||
|
||||
{currentUser.userType == UserType.admin &&
|
||||
CHECKLIST_ENABLED_PRODUCTS.has(props.current_installation.product) && (
|
||||
<Route
|
||||
path={routes.checklist}
|
||||
element={
|
||||
<InstallationChecklistTab
|
||||
installationId={props.current_installation.id}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Route
|
||||
path={'*'}
|
||||
element={<Navigate to={dataCollectionDisabled ? routes.information : routes.live}></Navigate>}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,8 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
'configuration',
|
||||
'report',
|
||||
'installationTickets',
|
||||
'documents'
|
||||
'documents',
|
||||
'checklist'
|
||||
];
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||
|
|
@ -192,6 +193,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
{
|
||||
value: 'documents',
|
||||
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
||||
},
|
||||
{
|
||||
value: 'checklist',
|
||||
label: <FormattedMessage id="checklist" defaultMessage="Checklist" />
|
||||
}
|
||||
]
|
||||
: currentUser.userType == UserType.partner
|
||||
|
|
@ -279,7 +284,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
const dataCollectionDisabled =
|
||||
currentInstallation?.dataCollectionEnabled === false
|
||||
|| (installations.length === 1 && installations[0].dataCollectionEnabled === false);
|
||||
const allowedWhenDisabled = ['list', 'tree', 'information', 'history', 'installationTickets', 'documents'];
|
||||
const allowedWhenDisabled = ['list', 'tree', 'information', 'history', 'installationTickets', 'documents', 'checklist'];
|
||||
|
||||
const tabs = inInstallationView && currentUser.userType == UserType.admin
|
||||
? [
|
||||
|
|
@ -361,6 +366,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
|||
{
|
||||
value: 'documents',
|
||||
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
||||
},
|
||||
{
|
||||
value: 'checklist',
|
||||
label: <FormattedMessage id="checklist" defaultMessage="Checklist" />
|
||||
}
|
||||
]
|
||||
: inInstallationView && currentUser.userType == UserType.partner
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
export enum ChecklistStatus {
|
||||
NotStarted = 0,
|
||||
InProgress = 1,
|
||||
Done = 2
|
||||
}
|
||||
|
||||
export type ChecklistSubtask = {
|
||||
text: string;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
export type ChecklistItem = {
|
||||
id: number;
|
||||
installationId: number;
|
||||
stepNumber: number;
|
||||
stepTitle: string;
|
||||
status: number;
|
||||
comments: string;
|
||||
assigneeId: number | null;
|
||||
doneAt: string | null;
|
||||
subtasks: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export const CHECKLIST_ENABLED_PRODUCTS: ReadonlySet<number> = new Set([2, 4, 5]);
|
||||
|
||||
export const UPLOADABLE_SUBTASK_KEYS: ReadonlySet<string> = new Set([
|
||||
'checklistStep8Sub1',
|
||||
'checklistStep10Sub1',
|
||||
'checklistStep10Sub2'
|
||||
]);
|
||||
|
||||
export type ChecklistSummary = {
|
||||
installationId: number;
|
||||
done: number;
|
||||
total: number;
|
||||
};
|
||||
|
||||
export function parseSubtasks(raw: string | null | undefined): ChecklistSubtask[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed
|
||||
.filter((x) => x && typeof x.text === 'string')
|
||||
.map((x) => ({ text: String(x.text), checked: Boolean(x.checked) }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeSubtasks(subtasks: ChecklistSubtask[]): string {
|
||||
return JSON.stringify(subtasks);
|
||||
}
|
||||
|
|
@ -693,5 +693,65 @@
|
|||
"fileTooLarge": "Datei überschreitet die maximale Grösse von 25 MB.",
|
||||
"invalidFileType": "Ungültiger Dateityp.",
|
||||
"uploadFailed": "Hochladen fehlgeschlagen.",
|
||||
"uploadSuccess": "Hochladen erfolgreich."
|
||||
"uploadSuccess": "Hochladen erfolgreich.",
|
||||
"checklist": "Checkliste",
|
||||
"checklistTitle": "Schritte zur Anbindung der Installation an Monitor",
|
||||
"checklistProgress": "Fortschritt: {done}/{total} ({percent}%)",
|
||||
"checklistStep": "Schritt",
|
||||
"checklistStatus": "Status",
|
||||
"checklistAssignee": "Zuständig",
|
||||
"checklistDateDone": "Erledigungsdatum",
|
||||
"checklistComments": "Kommentare",
|
||||
"checklistNotStarted": "Nicht gestartet",
|
||||
"checklistInProgress": "In Bearbeitung",
|
||||
"checklistDone": "Erledigt",
|
||||
"checklistNoAssignee": "Nicht zugewiesen",
|
||||
"checklistNotifyTooltip": "E-Mail-Benachrichtigung an Zuständige senden",
|
||||
"checklistNotifyDisabledTooltip": "Weisen Sie zuerst jemanden zu, um eine Benachrichtigung zu senden",
|
||||
"checklistToggleSubtasks": "Unteraufgaben umschalten",
|
||||
"checklistCommentsPlaceholder": "Notizen, Kontaktinformationen, Beobachtungen…",
|
||||
"checklistEmailSent": "E-Mail an {name} gesendet",
|
||||
"checklistEmailFailed": "E-Mail-Versand fehlgeschlagen — bitte erneut versuchen",
|
||||
"checklistSaveFailed": "Änderung konnte nicht gespeichert werden",
|
||||
"checklistStep1": "Auftrag erstellt, Kunden- und Partnerinformationen im CRM erfasst",
|
||||
"checklistStep2": "Hardware bei Vebo zusammengebaut",
|
||||
"checklistStep3": "Installation auf Monitor unter korrektem Produkt und Ordner erstellt",
|
||||
"checklistStep4": "Gateway-SD-Karte konfiguriert, VPN und Gateway-Name registriert",
|
||||
"checklistStep5": "Informations-Tab ausgefüllt (Kunde, Seriennummern, VPN)",
|
||||
"checklistStep6": "Installation bei Vebo elektrisch/hardwareseitig konfiguriert und getestet",
|
||||
"checklistStep7": "Installation bei Vebo softwareseitig getestet",
|
||||
"checklistStep8": "Installation am Kundenstandort ausgeliefert",
|
||||
"checklistStep9": "Installation ans Netz angeschlossen",
|
||||
"checklistStep10": "Hardware vor Ort verifiziert",
|
||||
"checklistStep11": "Software vor Ort verifiziert",
|
||||
"checklistStep12": "Installation online auf Monitor",
|
||||
"checklistStep13": "Kunde über Monitor-Konto und Reports informiert",
|
||||
"checklistStep14": "Benutzerkonto mit richtigen Ordnern und Zugriffen erstellt",
|
||||
"checklistStep15": "Kundennachverfolgung abgeschlossen, Feedback eingeholt",
|
||||
"checklistStep16": "Weitere Anliegen werden über das Ticket-System verfolgt",
|
||||
"checklistStep3Sub1": "Installations-Seriennummer",
|
||||
"checklistStep5Sub1": "Kundeninformationen (E-Mail, Adresse)",
|
||||
"checklistStep5Sub2": "Installationsinformationen (externes EMS, Stromanbieter, Datenerfassung)",
|
||||
"checklistStep5Sub3": "Batterie-Seriennummer",
|
||||
"checklistStep5Sub4": "Wechselrichter-Seriennummer",
|
||||
"checklistStep5Sub5": "Datenlogger-Seriennummer",
|
||||
"checklistStep5Sub6": "VPN",
|
||||
"checklistStep6Sub1": "Wechselrichter-Firmware und Konfiguration geprüft",
|
||||
"checklistStep6Sub2": "Batterie-Firmware und Konfiguration geprüft",
|
||||
"checklistStep6Sub3": "Internet für Gateway konfiguriert",
|
||||
"checklistStep6Sub4": "Kommunikationskabel zwischen Gateway und Wechselrichter korrekt",
|
||||
"checklistStep7Sub1": "S3-Bucket-Nummer und Schlüssel-Credentials aus Informations-Tab in config.json übernommen",
|
||||
"checklistStep7Sub2": "Produkt-ID in config.json konfiguriert",
|
||||
"checklistStep7Sub3": "USB-ID in config.json konfiguriert",
|
||||
"checklistStep7Sub4": "Datenlesung vom Wechselrichter getestet",
|
||||
"checklistStep8Sub1": "Lieferschein mit Kundenunterschrift erhalten und hochgeladen",
|
||||
"checklistStep10Sub1": "Installationsprotokoll hochgeladen",
|
||||
"checklistStep10Sub2": "Zeit- und Materialbericht in Monitoring hochgeladen",
|
||||
"checklistNoAttachments": "Noch keine Datei angehängt.",
|
||||
"setupProgress": "Setup-Fortschritt",
|
||||
"checklistPhaseEmpty": "Nicht gestartet",
|
||||
"checklistPhasePreparation": "Vorbereitung",
|
||||
"checklistPhaseOnSite": "Vor Ort",
|
||||
"checklistPhaseHandover": "Kundenübergabe",
|
||||
"checklistPhaseComplete": "Abgeschlossen"
|
||||
}
|
||||
|
|
@ -441,5 +441,65 @@
|
|||
"fileTooLarge": "File exceeds maximum size of 25 MB.",
|
||||
"invalidFileType": "Invalid file type.",
|
||||
"uploadFailed": "Upload failed.",
|
||||
"uploadSuccess": "Upload successful."
|
||||
"uploadSuccess": "Upload successful.",
|
||||
"checklist": "Checklist",
|
||||
"checklistTitle": "Steps to Bring Installation to Monitor",
|
||||
"checklistProgress": "Progress: {done}/{total} ({percent}%)",
|
||||
"checklistStep": "Step",
|
||||
"checklistStatus": "Status",
|
||||
"checklistAssignee": "Assignee",
|
||||
"checklistDateDone": "Date Done",
|
||||
"checklistComments": "Comments",
|
||||
"checklistNotStarted": "Not Started",
|
||||
"checklistInProgress": "In Progress",
|
||||
"checklistDone": "Done",
|
||||
"checklistNoAssignee": "Unassigned",
|
||||
"checklistNotifyTooltip": "Send email notification to assignee",
|
||||
"checklistNotifyDisabledTooltip": "Assign someone first to send a notification",
|
||||
"checklistToggleSubtasks": "Toggle subtasks",
|
||||
"checklistCommentsPlaceholder": "Notes, contact info, observations…",
|
||||
"checklistEmailSent": "Email sent to {name}",
|
||||
"checklistEmailFailed": "Failed to send email — try again",
|
||||
"checklistSaveFailed": "Failed to save change",
|
||||
"checklistStep1": "Order created, customer and partner info recorded in CRM",
|
||||
"checklistStep2": "Hardware assembled at Vebo",
|
||||
"checklistStep3": "Installation created on Monitor under correct product and folder",
|
||||
"checklistStep4": "Gateway SD card configured, VPN and gateway name registered",
|
||||
"checklistStep5": "Information tab filled out (customer, serials, VPN)",
|
||||
"checklistStep6": "Installation configured and tested electrically / hardware-wise at Vebo",
|
||||
"checklistStep7": "Installation tested software-wise at Vebo",
|
||||
"checklistStep8": "Installation delivered to customer site",
|
||||
"checklistStep9": "Installation connected to grid",
|
||||
"checklistStep10": "Hardware verified on site",
|
||||
"checklistStep11": "Software verified on site",
|
||||
"checklistStep12": "Installation online on Monitor",
|
||||
"checklistStep13": "Customer informed about Monitor account and reports",
|
||||
"checklistStep14": "User account created with correct folders and access",
|
||||
"checklistStep15": "Customer follow-up completed, feedback collected",
|
||||
"checklistStep16": "Further issues tracked via Ticket system",
|
||||
"checklistStep3Sub1": "Installation serial number",
|
||||
"checklistStep5Sub1": "Customer information (email, address)",
|
||||
"checklistStep5Sub2": "Installation information (external EMS, grid provider, data collection)",
|
||||
"checklistStep5Sub3": "Battery serial number",
|
||||
"checklistStep5Sub4": "Inverter serial number",
|
||||
"checklistStep5Sub5": "Data logger serial number",
|
||||
"checklistStep5Sub6": "VPN",
|
||||
"checklistStep6Sub1": "Inverter firmware and configuration verified",
|
||||
"checklistStep6Sub2": "Battery firmware and configuration verified",
|
||||
"checklistStep6Sub3": "Internet for gateway configured",
|
||||
"checklistStep6Sub4": "Communication cable between gateway and inverter correct",
|
||||
"checklistStep7Sub1": "S3 bucket number and key credentials copied from Information tab into config.json",
|
||||
"checklistStep7Sub2": "Product ID configured in config.json",
|
||||
"checklistStep7Sub3": "USB ID configured in config.json",
|
||||
"checklistStep7Sub4": "Inverter data reading from inverter tested",
|
||||
"checklistStep8Sub1": "Delivery receipt with customer signature received and uploaded",
|
||||
"checklistStep10Sub1": "Installation protocol uploaded",
|
||||
"checklistStep10Sub2": "Time and material report uploaded to Monitoring",
|
||||
"checklistNoAttachments": "No file attached yet.",
|
||||
"setupProgress": "Setup Progress",
|
||||
"checklistPhaseEmpty": "Not started",
|
||||
"checklistPhasePreparation": "Preparation",
|
||||
"checklistPhaseOnSite": "On-site",
|
||||
"checklistPhaseHandover": "Customer handover",
|
||||
"checklistPhaseComplete": "Complete"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -693,5 +693,65 @@
|
|||
"fileTooLarge": "Le fichier dépasse la taille maximale de 25 Mo.",
|
||||
"invalidFileType": "Type de fichier non valide.",
|
||||
"uploadFailed": "Échec du téléchargement.",
|
||||
"uploadSuccess": "Téléchargement réussi."
|
||||
"uploadSuccess": "Téléchargement réussi.",
|
||||
"checklist": "Checklist",
|
||||
"checklistTitle": "Étapes pour connecter l'installation à Monitor",
|
||||
"checklistProgress": "Progression : {done}/{total} ({percent}%)",
|
||||
"checklistStep": "Étape",
|
||||
"checklistStatus": "Statut",
|
||||
"checklistAssignee": "Responsable",
|
||||
"checklistDateDone": "Date de réalisation",
|
||||
"checklistComments": "Commentaires",
|
||||
"checklistNotStarted": "Non commencé",
|
||||
"checklistInProgress": "En cours",
|
||||
"checklistDone": "Terminé",
|
||||
"checklistNoAssignee": "Non attribué",
|
||||
"checklistNotifyTooltip": "Envoyer une notification par e-mail au responsable",
|
||||
"checklistNotifyDisabledTooltip": "Attribuez d'abord un responsable pour envoyer une notification",
|
||||
"checklistToggleSubtasks": "Afficher/masquer les sous-tâches",
|
||||
"checklistCommentsPlaceholder": "Notes, coordonnées, observations…",
|
||||
"checklistEmailSent": "E-mail envoyé à {name}",
|
||||
"checklistEmailFailed": "Échec de l'envoi — veuillez réessayer",
|
||||
"checklistSaveFailed": "Échec de l'enregistrement",
|
||||
"checklistStep1": "Commande créée, informations client et partenaire enregistrées dans le CRM",
|
||||
"checklistStep2": "Matériel assemblé chez Vebo",
|
||||
"checklistStep3": "Installation créée sur Monitor sous le bon produit et dossier",
|
||||
"checklistStep4": "Carte SD du gateway configurée, VPN et nom du gateway enregistrés",
|
||||
"checklistStep5": "Onglet Informations rempli (client, numéros de série, VPN)",
|
||||
"checklistStep6": "Installation configurée et testée électriquement/matériel chez Vebo",
|
||||
"checklistStep7": "Installation testée côté logiciel chez Vebo",
|
||||
"checklistStep8": "Installation livrée sur le site du client",
|
||||
"checklistStep9": "Installation raccordée au réseau",
|
||||
"checklistStep10": "Matériel vérifié sur site",
|
||||
"checklistStep11": "Logiciel vérifié sur site",
|
||||
"checklistStep12": "Installation en ligne sur Monitor",
|
||||
"checklistStep13": "Client informé du compte Monitor et des rapports",
|
||||
"checklistStep14": "Compte utilisateur créé avec les dossiers et accès corrects",
|
||||
"checklistStep15": "Suivi client effectué, retour recueilli",
|
||||
"checklistStep16": "Problèmes ultérieurs suivis via le système de tickets",
|
||||
"checklistStep3Sub1": "Numéro de série de l'installation",
|
||||
"checklistStep5Sub1": "Informations client (e-mail, adresse)",
|
||||
"checklistStep5Sub2": "Informations d'installation (EMS externe, fournisseur réseau, collecte de données)",
|
||||
"checklistStep5Sub3": "Numéro de série de la batterie",
|
||||
"checklistStep5Sub4": "Numéro de série de l'onduleur",
|
||||
"checklistStep5Sub5": "Numéro de série de l'enregistreur de données",
|
||||
"checklistStep5Sub6": "VPN",
|
||||
"checklistStep6Sub1": "Firmware et configuration de l'onduleur vérifiés",
|
||||
"checklistStep6Sub2": "Firmware et configuration de la batterie vérifiés",
|
||||
"checklistStep6Sub3": "Internet pour le gateway configuré",
|
||||
"checklistStep6Sub4": "Câble de communication entre gateway et onduleur correct",
|
||||
"checklistStep7Sub1": "Numéro de bucket S3 et identifiants copiés depuis l'onglet Informations dans config.json",
|
||||
"checklistStep7Sub2": "ID produit configuré dans config.json",
|
||||
"checklistStep7Sub3": "ID USB configuré dans config.json",
|
||||
"checklistStep7Sub4": "Lecture des données de l'onduleur testée",
|
||||
"checklistStep8Sub1": "Bon de livraison signé par le client reçu et téléversé",
|
||||
"checklistStep10Sub1": "Procès-verbal d'installation téléversé",
|
||||
"checklistStep10Sub2": "Rapport de temps et matériaux téléversé dans Monitoring",
|
||||
"checklistNoAttachments": "Aucun fichier joint pour le moment.",
|
||||
"setupProgress": "Progression installation",
|
||||
"checklistPhaseEmpty": "Non commencé",
|
||||
"checklistPhasePreparation": "Préparation",
|
||||
"checklistPhaseOnSite": "Sur site",
|
||||
"checklistPhaseHandover": "Transfert client",
|
||||
"checklistPhaseComplete": "Terminé"
|
||||
}
|
||||
|
|
@ -693,5 +693,65 @@
|
|||
"fileTooLarge": "Il file supera la dimensione massima di 25 MB.",
|
||||
"invalidFileType": "Tipo di file non valido.",
|
||||
"uploadFailed": "Caricamento fallito.",
|
||||
"uploadSuccess": "Caricamento riuscito."
|
||||
"uploadSuccess": "Caricamento riuscito.",
|
||||
"checklist": "Checklist",
|
||||
"checklistTitle": "Passi per collegare l'installazione a Monitor",
|
||||
"checklistProgress": "Avanzamento: {done}/{total} ({percent}%)",
|
||||
"checklistStep": "Passo",
|
||||
"checklistStatus": "Stato",
|
||||
"checklistAssignee": "Assegnatario",
|
||||
"checklistDateDone": "Data completamento",
|
||||
"checklistComments": "Commenti",
|
||||
"checklistNotStarted": "Non avviato",
|
||||
"checklistInProgress": "In corso",
|
||||
"checklistDone": "Completato",
|
||||
"checklistNoAssignee": "Non assegnato",
|
||||
"checklistNotifyTooltip": "Invia notifica e-mail all'assegnatario",
|
||||
"checklistNotifyDisabledTooltip": "Assegnare prima qualcuno per inviare una notifica",
|
||||
"checklistToggleSubtasks": "Mostra/nascondi sottoattività",
|
||||
"checklistCommentsPlaceholder": "Note, contatti, osservazioni…",
|
||||
"checklistEmailSent": "E-mail inviata a {name}",
|
||||
"checklistEmailFailed": "Invio e-mail non riuscito — riprovare",
|
||||
"checklistSaveFailed": "Salvataggio non riuscito",
|
||||
"checklistStep1": "Ordine creato, informazioni cliente e partner registrate nel CRM",
|
||||
"checklistStep2": "Hardware assemblato presso Vebo",
|
||||
"checklistStep3": "Installazione creata su Monitor sotto il prodotto e la cartella corretti",
|
||||
"checklistStep4": "Scheda SD del gateway configurata, VPN e nome gateway registrati",
|
||||
"checklistStep5": "Scheda Informazioni compilata (cliente, seriali, VPN)",
|
||||
"checklistStep6": "Installazione configurata e testata elettricamente/hardware presso Vebo",
|
||||
"checklistStep7": "Installazione testata a livello software presso Vebo",
|
||||
"checklistStep8": "Installazione consegnata presso il sito del cliente",
|
||||
"checklistStep9": "Installazione collegata alla rete",
|
||||
"checklistStep10": "Hardware verificato in sito",
|
||||
"checklistStep11": "Software verificato in sito",
|
||||
"checklistStep12": "Installazione online su Monitor",
|
||||
"checklistStep13": "Cliente informato su account Monitor e report",
|
||||
"checklistStep14": "Account utente creato con cartelle e accessi corretti",
|
||||
"checklistStep15": "Follow-up cliente completato, feedback raccolto",
|
||||
"checklistStep16": "Ulteriori problemi tracciati tramite il sistema di ticket",
|
||||
"checklistStep3Sub1": "Numero di serie dell'installazione",
|
||||
"checklistStep5Sub1": "Informazioni cliente (e-mail, indirizzo)",
|
||||
"checklistStep5Sub2": "Informazioni installazione (EMS esterno, fornitore di rete, raccolta dati)",
|
||||
"checklistStep5Sub3": "Numero di serie batteria",
|
||||
"checklistStep5Sub4": "Numero di serie inverter",
|
||||
"checklistStep5Sub5": "Numero di serie data logger",
|
||||
"checklistStep5Sub6": "VPN",
|
||||
"checklistStep6Sub1": "Firmware e configurazione inverter verificati",
|
||||
"checklistStep6Sub2": "Firmware e configurazione batteria verificati",
|
||||
"checklistStep6Sub3": "Internet per gateway configurato",
|
||||
"checklistStep6Sub4": "Cavo di comunicazione tra gateway e inverter corretto",
|
||||
"checklistStep7Sub1": "Numero bucket S3 e credenziali copiati dalla scheda Informazioni in config.json",
|
||||
"checklistStep7Sub2": "ID prodotto configurato in config.json",
|
||||
"checklistStep7Sub3": "ID USB configurato in config.json",
|
||||
"checklistStep7Sub4": "Lettura dati inverter testata",
|
||||
"checklistStep8Sub1": "Bolla di consegna con firma del cliente ricevuta e caricata",
|
||||
"checklistStep10Sub1": "Verbale di installazione caricato",
|
||||
"checklistStep10Sub2": "Rapporto tempi e materiali caricato su Monitoring",
|
||||
"checklistNoAttachments": "Nessun file allegato.",
|
||||
"setupProgress": "Avanzamento installazione",
|
||||
"checklistPhaseEmpty": "Non avviato",
|
||||
"checklistPhasePreparation": "Preparazione",
|
||||
"checklistPhaseOnSite": "In sito",
|
||||
"checklistPhaseHandover": "Consegna cliente",
|
||||
"checklistPhaseComplete": "Completato"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue