checklist page version1.0
This commit is contained in:
parent
5666191a6b
commit
11940b4684
|
|
@ -2740,4 +2740,94 @@ public class Controller : ControllerBase
|
||||||
return Ok();
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,54 @@
|
||||||
|
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", NoSubtasks),
|
||||||
|
new( 4, "Gateway SD card configured, VPN and gateway name registered", NoSubtasks),
|
||||||
|
new( 5, "Information tab filled out (customer, serials, VPN)",
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{"text":"Customer information (email, address)","checked":false},
|
||||||
|
{"text":"Installation information (external EMS, network provider, data collection)","checked":false},
|
||||||
|
{"text":"Battery serial number","checked":false},
|
||||||
|
{"text":"Inverter serial number","checked":false},
|
||||||
|
{"text":"Data logger serial number","checked":false},
|
||||||
|
{"text":"VPN details","checked":false}
|
||||||
|
]
|
||||||
|
"""),
|
||||||
|
new( 6, "Installation configured and tested electrically / hardware-wise at Vebo",
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{"text":"Inverter firmware and configuration verified","checked":false},
|
||||||
|
{"text":"Battery firmware and configuration verified","checked":false},
|
||||||
|
{"text":"Internet for gateway configured","checked":false},
|
||||||
|
{"text":"Communication cable between gateway and inverter correct","checked":false}
|
||||||
|
]
|
||||||
|
"""),
|
||||||
|
new( 7, "Installation tested software-wise at Vebo",
|
||||||
|
"""
|
||||||
|
[
|
||||||
|
{"text":"S3 bucket number and key credentials copied from Information tab into config.json","checked":false},
|
||||||
|
{"text":"Product ID configured in config.json","checked":false},
|
||||||
|
{"text":"USB ID configured in config.json","checked":false},
|
||||||
|
{"text":"Inverter data reading from inverter tested","checked":false}
|
||||||
|
]
|
||||||
|
"""),
|
||||||
|
new( 8, "Installation delivered to customer site", NoSubtasks),
|
||||||
|
new( 9, "Installation connected to grid", NoSubtasks),
|
||||||
|
new(10, "Hardware verified on site", NoSubtasks),
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -518,4 +518,61 @@ public static class UserMethods
|
||||||
return user.SendEmail(subject, body);
|
return user.SendEmail(subject, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Task SendChecklistAssignedEmail(
|
||||||
|
this User user,
|
||||||
|
ChecklistItem item,
|
||||||
|
Installation installation,
|
||||||
|
String notifiedByName)
|
||||||
|
{
|
||||||
|
var checklistLink = $"https://monitor.inesco.energy/sodiohome_installations/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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +91,9 @@ public static partial class Db
|
||||||
|
|
||||||
// Document storage
|
// Document storage
|
||||||
public static Boolean Create(Document document) => Insert(document);
|
public static Boolean Create(Document document) => Insert(document);
|
||||||
|
|
||||||
|
// Checklist
|
||||||
|
public static Boolean Create(ChecklistItem item) => Insert(item);
|
||||||
|
|
||||||
public static void HandleAction(UserAction newAction)
|
public static void HandleAction(UserAction newAction)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ public static partial class Db
|
||||||
// Document storage
|
// Document storage
|
||||||
public static TableQuery<Document> Documents => Connection.Table<Document>();
|
public static TableQuery<Document> Documents => Connection.Table<Document>();
|
||||||
|
|
||||||
|
// Checklist
|
||||||
|
public static TableQuery<ChecklistItem> ChecklistItems => Connection.Table<ChecklistItem>();
|
||||||
|
|
||||||
|
|
||||||
public static void Init()
|
public static void Init()
|
||||||
{
|
{
|
||||||
|
|
@ -83,6 +86,9 @@ public static partial class Db
|
||||||
|
|
||||||
// Document storage
|
// Document storage
|
||||||
Connection.CreateTable<Document>();
|
Connection.CreateTable<Document>();
|
||||||
|
|
||||||
|
// Checklist
|
||||||
|
Connection.CreateTable<ChecklistItem>();
|
||||||
});
|
});
|
||||||
|
|
||||||
// One-time migration: normalize legacy long-form language values to ISO codes
|
// One-time migration: normalize legacy long-form language values to ISO codes
|
||||||
|
|
@ -150,6 +156,9 @@ public static partial class Db
|
||||||
// Document storage
|
// Document storage
|
||||||
fileConnection.CreateTable<Document>();
|
fileConnection.CreateTable<Document>();
|
||||||
|
|
||||||
|
// Checklist
|
||||||
|
fileConnection.CreateTable<ChecklistItem>();
|
||||||
|
|
||||||
// Migrate new columns: set defaults for existing rows where NULL or empty
|
// 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 ExternalEms = 'No' WHERE ExternalEms IS NULL OR ExternalEms = ''");
|
||||||
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
|
fileConnection.Execute("UPDATE Installation SET InstallationModel = '' WHERE InstallationModel IS NULL");
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,9 @@ public static partial class Db
|
||||||
// Clean up installation-level documents
|
// Clean up installation-level documents
|
||||||
Documents.Delete(d => d.InstallationId == installation.Id);
|
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;
|
return Installations.Delete(i => i.Id == installation.Id) > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,4 +233,18 @@ public static partial class Db
|
||||||
.Where(d => d.InstallationId == installationId && d.Scope == (Int32)DocumentScope.InstallationDocument)
|
.Where(d => d.InstallationId == installationId && d.Scope == (Int32)DocumentScope.InstallationDocument)
|
||||||
.OrderBy(d => d.CreatedAt)
|
.OrderBy(d => d.CreatedAt)
|
||||||
.ToList();
|
.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);
|
||||||
}
|
}
|
||||||
|
|
@ -72,4 +72,7 @@ public static partial class Db
|
||||||
// Ticket system
|
// Ticket system
|
||||||
public static Boolean Update(Ticket ticket) => Update(obj: ticket);
|
public static Boolean Update(Ticket ticket) => Update(obj: ticket);
|
||||||
public static Boolean Update(TicketAiDiagnosis diagnosis) => Update(obj: diagnosis);
|
public static Boolean Update(TicketAiDiagnosis diagnosis) => Update(obj: diagnosis);
|
||||||
|
|
||||||
|
// Checklist
|
||||||
|
public static Boolean Update(ChecklistItem item) => Update(obj: item);
|
||||||
}
|
}
|
||||||
|
|
@ -26,5 +26,6 @@
|
||||||
"report": "report",
|
"report": "report",
|
||||||
"installationTickets": "installationTickets",
|
"installationTickets": "installationTickets",
|
||||||
"documents": "documents",
|
"documents": "documents",
|
||||||
|
"checklist": "checklist",
|
||||||
"tickets": "/tickets/"
|
"tickets": "/tickets/"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
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
|
||||||
|
} from 'src/interfaces/ChecklistTypes';
|
||||||
|
import { AdminUser } from 'src/interfaces/TicketTypes';
|
||||||
|
|
||||||
|
type EmailIconState = 'idle' | 'loading' | 'success' | 'error';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
item: ChecklistItem;
|
||||||
|
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, 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">{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}>
|
||||||
|
{subtasks.map((s, i) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={i}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={s.checked}
|
||||||
|
onChange={() => handleSubtaskToggle(i)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={<Typography variant="body2">{s.text}</Typography>}
|
||||||
|
sx={{ display: 'block', ml: 0 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChecklistStepRow;
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
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}
|
||||||
|
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;
|
||||||
|
|
@ -31,6 +31,8 @@ import Configuration from '../Configuration/Configuration';
|
||||||
import PvView from '../PvView/PvView';
|
import PvView from '../PvView/PvView';
|
||||||
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
|
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
|
||||||
import DocumentsTab from '../Documents/DocumentsTab';
|
import DocumentsTab from '../Documents/DocumentsTab';
|
||||||
|
import InstallationChecklistTab from '../Checklist/InstallationChecklistTab';
|
||||||
|
import { CHECKLIST_ENABLED_PRODUCTS } from 'src/interfaces/ChecklistTypes';
|
||||||
|
|
||||||
interface singleInstallationProps {
|
interface singleInstallationProps {
|
||||||
current_installation?: I_Installation;
|
current_installation?: I_Installation;
|
||||||
|
|
@ -569,6 +571,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
|
<Route
|
||||||
path={'*'}
|
path={'*'}
|
||||||
element={<Navigate to={routes.live}></Navigate>}
|
element={<Navigate to={routes.live}></Navigate>}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
'history',
|
'history',
|
||||||
'pvview',
|
'pvview',
|
||||||
'installationTickets',
|
'installationTickets',
|
||||||
'documents'
|
'documents',
|
||||||
|
'checklist'
|
||||||
];
|
];
|
||||||
|
|
||||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
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
|
// TODO: SodistoreGrid — PV View excluded for product 4, add back when data path is ready
|
||||||
const hidePvView = props.product === 4;
|
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 = (
|
const singleInstallationTabs = (
|
||||||
currentUser.userType == UserType.admin
|
currentUser.userType == UserType.admin
|
||||||
? [
|
? [
|
||||||
|
|
@ -175,6 +180,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
{
|
{
|
||||||
value: 'documents',
|
value: 'documents',
|
||||||
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'checklist',
|
||||||
|
label: <FormattedMessage id="checklist" defaultMessage="Checklist" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: currentUser.userType == UserType.partner
|
: 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 =
|
const tabs =
|
||||||
currentTab != 'list' &&
|
currentTab != 'list' &&
|
||||||
|
|
@ -316,6 +327,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
{
|
{
|
||||||
value: 'documents',
|
value: 'documents',
|
||||||
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'checklist',
|
||||||
|
label: <FormattedMessage id="checklist" defaultMessage="Checklist" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: currentUser.userType == UserType.partner
|
: currentUser.userType == UserType.partner
|
||||||
|
|
@ -410,9 +425,10 @@ function InstallationTabs(props: InstallationTabsProps) {
|
||||||
];
|
];
|
||||||
|
|
||||||
// Filter out PV View for SodistoreGrid
|
// Filter out PV View for SodistoreGrid
|
||||||
const filteredTabs = hidePvView
|
const filteredTabs = (hidePvView
|
||||||
? tabs.filter((tab) => tab.value !== 'pvview')
|
? tabs.filter((tab) => tab.value !== 'pvview')
|
||||||
: tabs;
|
: tabs
|
||||||
|
).filter((tab) => !(!showChecklist && tab.value === 'checklist'));
|
||||||
|
|
||||||
return installations.length > 1 ? (
|
return installations.length > 1 ? (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ import Overview from '../Overview/overview';
|
||||||
import WeeklyReport from './WeeklyReport';
|
import WeeklyReport from './WeeklyReport';
|
||||||
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
|
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
|
||||||
import DocumentsTab from '../Documents/DocumentsTab';
|
import DocumentsTab from '../Documents/DocumentsTab';
|
||||||
|
import InstallationChecklistTab from '../Checklist/InstallationChecklistTab';
|
||||||
|
import { CHECKLIST_ENABLED_PRODUCTS } from 'src/interfaces/ChecklistTypes';
|
||||||
|
|
||||||
interface singleInstallationProps {
|
interface singleInstallationProps {
|
||||||
current_installation?: I_Installation;
|
current_installation?: I_Installation;
|
||||||
|
|
@ -670,6 +672,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
|
<Route
|
||||||
path={'*'}
|
path={'*'}
|
||||||
element={<Navigate to={dataCollectionDisabled ? routes.information : routes.live}></Navigate>}
|
element={<Navigate to={dataCollectionDisabled ? routes.information : routes.live}></Navigate>}
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
'configuration',
|
'configuration',
|
||||||
'report',
|
'report',
|
||||||
'installationTickets',
|
'installationTickets',
|
||||||
'documents'
|
'documents',
|
||||||
|
'checklist'
|
||||||
];
|
];
|
||||||
|
|
||||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||||
|
|
@ -192,6 +193,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
{
|
{
|
||||||
value: 'documents',
|
value: 'documents',
|
||||||
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'checklist',
|
||||||
|
label: <FormattedMessage id="checklist" defaultMessage="Checklist" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: currentUser.userType == UserType.partner
|
: currentUser.userType == UserType.partner
|
||||||
|
|
@ -361,6 +366,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
|
||||||
{
|
{
|
||||||
value: 'documents',
|
value: 'documents',
|
||||||
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'checklist',
|
||||||
|
label: <FormattedMessage id="checklist" defaultMessage="Checklist" />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: inInstallationView && currentUser.userType == UserType.partner
|
: inInstallationView && currentUser.userType == UserType.partner
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
@ -685,5 +685,24 @@
|
||||||
"fileTooLarge": "Datei überschreitet die maximale Grösse von 25 MB.",
|
"fileTooLarge": "Datei überschreitet die maximale Grösse von 25 MB.",
|
||||||
"invalidFileType": "Ungültiger Dateityp.",
|
"invalidFileType": "Ungültiger Dateityp.",
|
||||||
"uploadFailed": "Hochladen fehlgeschlagen.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
@ -433,5 +433,24 @@
|
||||||
"fileTooLarge": "File exceeds maximum size of 25 MB.",
|
"fileTooLarge": "File exceeds maximum size of 25 MB.",
|
||||||
"invalidFileType": "Invalid file type.",
|
"invalidFileType": "Invalid file type.",
|
||||||
"uploadFailed": "Upload failed.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -685,5 +685,24 @@
|
||||||
"fileTooLarge": "Le fichier dépasse la taille maximale de 25 Mo.",
|
"fileTooLarge": "Le fichier dépasse la taille maximale de 25 Mo.",
|
||||||
"invalidFileType": "Type de fichier non valide.",
|
"invalidFileType": "Type de fichier non valide.",
|
||||||
"uploadFailed": "Échec du téléchargement.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
@ -685,5 +685,24 @@
|
||||||
"fileTooLarge": "Il file supera la dimensione massima di 25 MB.",
|
"fileTooLarge": "Il file supera la dimensione massima di 25 MB.",
|
||||||
"invalidFileType": "Tipo di file non valido.",
|
"invalidFileType": "Tipo di file non valido.",
|
||||||
"uploadFailed": "Caricamento fallito.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue