on site checklist on monitor

This commit is contained in:
Yinyin Liu 2026-05-28 08:27:25 +02:00
parent 646b6c0e20
commit ed5ec0afa2
37 changed files with 1659 additions and 22 deletions

View File

@ -3047,4 +3047,160 @@ public class Controller : ControllerBase
return Ok(summaries); return Ok(summaries);
} }
// ── On-Site Installer Checklist (sodistore home only) ──────────────
[HttpGet(nameof(GetOnSiteChecklistForInstallation))]
public ActionResult<Object> GetOnSiteChecklistForInstallation(Int64 installationId, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null) return Unauthorized();
var installation = Db.GetInstallationById(installationId);
if (installation is null || !user.HasAccessTo(installation)) return Unauthorized();
// Sodistore home only (product == 2)
if (installation.Product != 2) return BadRequest("On-site checklist is only available for sodistore home installations.");
if (!Db.OnSiteChecklistExistsForInstallation(installationId))
{
foreach (var def in OnSiteChecklistStepDefinitions.Steps)
{
Db.Create(new OnSiteChecklistItem
{
InstallationId = installationId,
StepNumber = def.Number,
StepKey = def.StepKey,
Subtasks = def.SubtasksJson,
});
}
}
var items = Db.GetOnSiteChecklistForInstallation(installationId);
var signature = Db.GetOnSiteChecklistSignature(installationId);
if (signature is null)
{
signature = new OnSiteChecklistSignature { InstallationId = installationId };
Db.Create(signature);
}
return Ok(new { items, signature });
}
[HttpPut(nameof(UpdateOnSiteChecklistItem))]
public ActionResult<OnSiteChecklistItem> UpdateOnSiteChecklistItem(
Int64 onSiteChecklistItemId,
Boolean? @checked,
String? comments,
String? subtasks,
Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null) return Unauthorized();
var item = Db.GetOnSiteChecklistItemById(onSiteChecklistItemId);
if (item is null) return NotFound("On-site checklist item not found.");
var installation = Db.GetInstallationById(item.InstallationId);
if (installation is null || !user.HasAccessTo(installation)) return Unauthorized();
// Lock edits once signed (admins can still edit to support reset workflow).
var sig = Db.GetOnSiteChecklistSignature(item.InstallationId);
var isSigned = sig is not null && !String.IsNullOrEmpty(sig.SignedAt);
if (isSigned && user.UserType != 2) return BadRequest("Checklist is signed and locked.");
if (@checked.HasValue) item.Checked = @checked.Value;
if (comments is not null) item.Comments = comments;
if (subtasks is not null) item.Subtasks = subtasks;
item.UpdatedAt = DateTime.UtcNow;
return Db.Update(item) ? item : StatusCode(500, "Update failed.");
}
[HttpPut(nameof(UpdateOnSiteChecklistRemarks))]
public ActionResult<OnSiteChecklistSignature> UpdateOnSiteChecklistRemarks(
Int64 installationId,
String? remarks,
Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null) return Unauthorized();
var installation = Db.GetInstallationById(installationId);
if (installation is null || !user.HasAccessTo(installation)) return Unauthorized();
// Skip writes when the param is omitted — prevents an empty/missing query
// string from silently wiping previously-saved remarks.
if (remarks is null) return BadRequest("Remarks parameter is required.");
var sig = Db.GetOnSiteChecklistSignature(installationId);
if (sig is null)
{
sig = new OnSiteChecklistSignature { InstallationId = installationId };
Db.Create(sig);
sig = Db.GetOnSiteChecklistSignature(installationId)!;
}
var isSigned = !String.IsNullOrEmpty(sig.SignedAt);
if (isSigned && user.UserType != 2) return BadRequest("Checklist is signed and locked.");
sig.Remarks = remarks;
sig.UpdatedAt = DateTime.UtcNow;
return Db.Update(sig) ? sig : StatusCode(500, "Update failed.");
}
[HttpPost(nameof(SignOnSiteChecklist))]
public ActionResult<OnSiteChecklistSignature> SignOnSiteChecklist(
Int64 installationId,
String signedByName,
Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user is null) return Unauthorized();
var installation = Db.GetInstallationById(installationId);
if (installation is null || !user.HasAccessTo(installation)) return Unauthorized();
if (String.IsNullOrWhiteSpace(signedByName)) return BadRequest("Signed-by name is required.");
var sig = Db.GetOnSiteChecklistSignature(installationId);
if (sig is null)
{
sig = new OnSiteChecklistSignature { InstallationId = installationId };
Db.Create(sig);
sig = Db.GetOnSiteChecklistSignature(installationId)!;
}
sig.SignedByName = signedByName.Trim();
sig.SignedByUserId = user.Id;
sig.SignedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
sig.UpdatedAt = DateTime.UtcNow;
return Db.Update(sig) ? sig : StatusCode(500, "Sign failed.");
}
[HttpPost(nameof(ResetOnSiteChecklistSignature))]
public ActionResult<OnSiteChecklistSignature> ResetOnSiteChecklistSignature(
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 || !user.HasAccessTo(installation)) return Unauthorized();
var sig = Db.GetOnSiteChecklistSignature(installationId);
if (sig is null) return NotFound("Signature row not found.");
sig.SignedByName = null;
sig.SignedByUserId = null;
sig.SignedAt = null;
sig.UpdatedAt = DateTime.UtcNow;
return Db.Update(sig) ? sig : StatusCode(500, "Reset failed.");
}
} }

View File

@ -0,0 +1,22 @@
using SQLite;
namespace InnovEnergy.App.Backend.DataTypes;
// Installer-facing on-site installation checklist. One row per (installation, step).
// Distinct from ChecklistItem (internal monitor-onboarding workflow) — no assignee,
// no done-date; comments and substep state still persist per installation.
public class OnSiteChecklistItem
{
[PrimaryKey, AutoIncrement] public Int64 Id { get; set; }
[Indexed] public Int64 InstallationId { get; set; }
public Int32 StepNumber { get; set; }
public String StepKey { get; set; } = "";
public Boolean Checked { get; set; } = false;
public String Comments { get; set; } = "";
public String? Subtasks { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@ -0,0 +1,21 @@
using SQLite;
namespace InnovEnergy.App.Backend.DataTypes;
// One row per installation. Holds the free-text installer remarks ("Bemerkungen vom
// Installateur") plus the e-signature (typed name + timestamp + signer user id).
// Existence is lazy: row is created on first read if absent.
public class OnSiteChecklistSignature
{
[PrimaryKey, AutoIncrement] public Int64 Id { get; set; }
[Indexed] public Int64 InstallationId { get; set; }
public String Remarks { get; set; } = "";
public String? SignedByName { get; set; }
public Int64? SignedByUserId{ get; set; }
public String? SignedAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@ -0,0 +1,104 @@
namespace InnovEnergy.App.Backend.DataTypes;
public record OnSiteChecklistStepDefinition(Int32 Number, String StepKey, String? SubtasksJson);
// Static definitions for the installer-facing on-site checklist (sodistore home only).
// Step titles, warning text, and image filenames live in the frontend (translations +
// static assets) and are keyed by Number — the backend only stores per-step state.
// Subtask JSON items can be either a checkable substep `{"text":"key","checked":false}`
// or a non-checkable sub-heading `{"heading":"key"}` (e.g. "Notstromanforderungen").
public static class OnSiteChecklistStepDefinitions
{
public static readonly IReadOnlyList<OnSiteChecklistStepDefinition> Steps = new List<OnSiteChecklistStepDefinition>
{
new(1, "auspacken",
"""
[
{"text":"onSiteStep1Sub1","checked":false},
{"text":"onSiteStep1Sub2","checked":false}
]
"""),
new(2, "wechselrichterMontieren",
"""
[
{"text":"onSiteStep2Sub1","checked":false},
{"text":"onSiteStep2Sub2","checked":false},
{"text":"onSiteStep2Sub3","checked":false}
]
"""),
new(3, "hauptverteilungAcSeitig",
"""
[
{"text":"onSiteStep3Sub1","checked":false},
{"text":"onSiteStep3Sub2","checked":false},
{"text":"onSiteStep3Sub3","checked":false},
{"text":"onSiteStep3Sub4","checked":false},
{"text":"onSiteStep3Sub5","checked":false},
{"text":"onSiteStep3Sub6","checked":false},
{"heading":"onSiteStep3Subheading1"},
{"text":"onSiteStep3Sub7","checked":false},
{"text":"onSiteStep3Sub8","checked":false},
{"text":"onSiteStep3Sub9","checked":false},
{"text":"onSiteStep3Sub10","checked":false}
]
"""),
new(4, "meterAnschliessen",
"""
[
{"text":"onSiteStep4Sub1","checked":false},
{"text":"onSiteStep4Sub2","checked":false},
{"text":"onSiteStep4Sub3","checked":false},
{"text":"onSiteStep4Sub4","checked":false}
]
"""),
new(5, "gatewayInternet",
"""
[
{"text":"onSiteStep5Sub1","checked":false},
{"text":"onSiteStep5Sub2","checked":false},
{"text":"onSiteStep5Sub3","checked":false},
{"text":"onSiteStep5Sub4","checked":false}
]
"""),
new(6, "systemverbindungen",
"""
[
{"text":"onSiteStep6Sub1","checked":false},
{"text":"onSiteStep6Sub2","checked":false},
{"text":"onSiteStep6Sub3","checked":false},
{"text":"onSiteStep6Sub4","checked":false},
{"text":"onSiteStep6Sub5","checked":false},
{"text":"onSiteStep6Sub6","checked":false},
{"text":"onSiteStep6Sub7","checked":false}
]
"""),
new(7, "pvWechselrichter",
"""
[
{"text":"onSiteStep7Sub1","checked":false}
]
"""),
new(8, "inbetriebnahme",
"""
[
{"text":"onSiteStep8Sub1","checked":false},
{"text":"onSiteStep8Sub2","checked":false},
{"text":"onSiteStep8Sub3","checked":false},
{"text":"onSiteStep8Sub4","checked":false},
{"text":"onSiteStep8Sub5","checked":false},
{"text":"onSiteStep8Sub6","checked":false},
{"text":"onSiteStep8Sub7","checked":false},
{"text":"onSiteStep8Sub8","checked":false},
{"text":"onSiteStep8Sub9","checked":false},
{"text":"onSiteStep8Sub10","checked":false},
{"text":"onSiteStep8Sub11","checked":false}
]
"""),
new(9, "abschluss",
"""
[
{"text":"onSiteStep9Sub1","checked":false}
]
"""),
};
}

View File

@ -95,6 +95,10 @@ public static partial class Db
// Checklist // Checklist
public static Boolean Create(ChecklistItem item) => Insert(item); public static Boolean Create(ChecklistItem item) => Insert(item);
// On-site installer checklist
public static Boolean Create(OnSiteChecklistItem item) => Insert(item);
public static Boolean Create(OnSiteChecklistSignature sig) => Insert(sig);
public static void HandleAction(UserAction newAction) public static void HandleAction(UserAction newAction)
{ {
//Find the total number of actions for this installation //Find the total number of actions for this installation

View File

@ -46,6 +46,10 @@ public static partial class Db
// Checklist // Checklist
public static TableQuery<ChecklistItem> ChecklistItems => Connection.Table<ChecklistItem>(); public static TableQuery<ChecklistItem> ChecklistItems => Connection.Table<ChecklistItem>();
// On-site installer checklist (sodistore home)
public static TableQuery<OnSiteChecklistItem> OnSiteChecklistItems => Connection.Table<OnSiteChecklistItem>();
public static TableQuery<OnSiteChecklistSignature> OnSiteChecklistSignatures => Connection.Table<OnSiteChecklistSignature>();
public static void Init() public static void Init()
{ {
@ -90,6 +94,10 @@ public static partial class Db
// Checklist // Checklist
Connection.CreateTable<ChecklistItem>(); Connection.CreateTable<ChecklistItem>();
// On-site installer checklist (sodistore home)
Connection.CreateTable<OnSiteChecklistItem>();
Connection.CreateTable<OnSiteChecklistSignature>();
}); });
// One-time migration: normalize legacy long-form language values to ISO codes // One-time migration: normalize legacy long-form language values to ISO codes
@ -232,6 +240,10 @@ public static partial class Db
// Checklist // Checklist
fileConnection.CreateTable<ChecklistItem>(); fileConnection.CreateTable<ChecklistItem>();
// On-site installer checklist (sodistore home)
fileConnection.CreateTable<OnSiteChecklistItem>();
fileConnection.CreateTable<OnSiteChecklistSignature>();
// 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");

View File

@ -255,4 +255,21 @@ public static partial class Db
public static ChecklistItem? GetChecklistItemById(Int64 id) public static ChecklistItem? GetChecklistItemById(Int64 id)
=> ChecklistItems.FirstOrDefault(c => c.Id == id); => ChecklistItems.FirstOrDefault(c => c.Id == id);
// ── On-Site Checklist Queries ────────────────────────────────────────
public static List<OnSiteChecklistItem> GetOnSiteChecklistForInstallation(Int64 installationId)
=> OnSiteChecklistItems
.Where(c => c.InstallationId == installationId)
.OrderBy(c => c.StepNumber)
.ToList();
public static Boolean OnSiteChecklistExistsForInstallation(Int64 installationId)
=> OnSiteChecklistItems.Any(c => c.InstallationId == installationId);
public static OnSiteChecklistItem? GetOnSiteChecklistItemById(Int64 id)
=> OnSiteChecklistItems.FirstOrDefault(c => c.Id == id);
public static OnSiteChecklistSignature? GetOnSiteChecklistSignature(Int64 installationId)
=> OnSiteChecklistSignatures.FirstOrDefault(s => s.InstallationId == installationId);
} }

View File

@ -76,4 +76,8 @@ public static partial class Db
// Checklist // Checklist
public static Boolean Update(ChecklistItem item) => Update(obj: item); public static Boolean Update(ChecklistItem item) => Update(obj: item);
// On-site installer checklist
public static Boolean Update(OnSiteChecklistItem item) => Update(obj: item);
public static Boolean Update(OnSiteChecklistSignature sig) => Update(obj: sig);
} }

View File

@ -27,5 +27,6 @@
"installationTickets": "installationTickets", "installationTickets": "installationTickets",
"documents": "documents", "documents": "documents",
"checklist": "checklist", "checklist": "checklist",
"onSiteChecklist": "onsiteChecklist",
"tickets": "/tickets/" "tickets": "/tickets/"
} }

View File

@ -0,0 +1,32 @@
// Static image imports for the sodistore home on-site installation checklist.
// Webpack/CRA resolves each import to a bundled URL string.
import step1Auspacken from './step1-auspacken.jpg';
import step2WrMontiert from './step2-wr-montiert.jpg';
import step2AcAnschluesse from './step2-ac-anschluesse.jpg';
import step2AcStecker from './step2-ac-stecker.jpg';
import step3Hauptverteilung from './step3-hauptverteilung.jpg';
import step3Hauptverteilung2 from './step3-hauptverteilung-2.jpg';
import step4Meter from './step4-meter.jpg';
import step4Wandler from './step4-wandler.jpg';
import step5Gateway from './step5-gateway.jpg';
import step6Batterien from './step6-batterien.jpg';
import step6Splitter from './step6-splitter.jpg';
import step7PvSwitch from './step7-pv-switch.jpg';
import step8BatterieBreaker from './step8-batterie-breaker.jpg';
export const ON_SITE_IMAGES: Readonly<Record<string, string>> = {
'step1-auspacken.jpg': step1Auspacken,
'step2-wr-montiert.jpg': step2WrMontiert,
'step2-ac-anschluesse.jpg': step2AcAnschluesse,
'step2-ac-stecker.jpg': step2AcStecker,
'step3-hauptverteilung.jpg': step3Hauptverteilung,
'step3-hauptverteilung-2.jpg': step3Hauptverteilung2,
'step4-meter.jpg': step4Meter,
'step4-wandler.jpg': step4Wandler,
'step5-gateway.jpg': step5Gateway,
'step6-batterien.jpg': step6Batterien,
'step6-splitter.jpg': step6Splitter,
'step7-pv-switch.jpg': step7PvSwitch,
'step8-batterie-breaker.jpg': step8BatterieBreaker
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

View File

@ -361,7 +361,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
<TableCell> <TableCell>
<FormattedMessage <FormattedMessage
id="setupProgress" id="setupProgress"
defaultMessage="Setup Progress" defaultMessage="Monitor Onboarding Progress"
/> />
</TableCell> </TableCell>
)} )}

View File

@ -183,7 +183,7 @@ function InstallationTabs(props: InstallationTabsProps) {
}, },
{ {
value: 'checklist', value: 'checklist',
label: <FormattedMessage id="checklist" defaultMessage="Checklist" /> label: <FormattedMessage id="checklist" defaultMessage="Monitor Onboarding Checklist" />
} }
] ]
: currentUser.userType == UserType.partner : currentUser.userType == UserType.partner
@ -330,7 +330,7 @@ function InstallationTabs(props: InstallationTabsProps) {
}, },
{ {
value: 'checklist', value: 'checklist',
label: <FormattedMessage id="checklist" defaultMessage="Checklist" /> label: <FormattedMessage id="checklist" defaultMessage="Monitor Onboarding Checklist" />
} }
] ]
: currentUser.userType == UserType.partner : currentUser.userType == UserType.partner

View File

@ -0,0 +1,203 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Alert,
Box,
Checkbox,
FormControlLabel,
Stack,
TableCell,
TableRow,
TextField,
Typography
} from '@mui/material';
import { FormattedMessage, useIntl } from 'react-intl';
import {
ON_SITE_STEP_META,
OnSiteChecklistItem,
OnSiteChecklistSubtask,
parseOnSiteSubtasks,
serializeOnSiteSubtasks
} from 'src/interfaces/OnSiteChecklistTypes';
import { ON_SITE_IMAGES } from 'src/assets/installation-checklist/sodistore-home';
interface Props {
item: OnSiteChecklistItem;
disabled: boolean;
onUpdate: (
id: number,
patch: Partial<{ checked: boolean; comments: string; subtasks: string }>
) => Promise<boolean>;
onImageClick: (filename: string, alt: string) => void;
}
function OnSiteChecklistStepRow({ item, disabled, onUpdate, onImageClick }: Props) {
const intl = useIntl();
const [comments, setComments] = useState(item.comments ?? '');
const [subtasks, setSubtasks] = useState<OnSiteChecklistSubtask[]>(() =>
parseOnSiteSubtasks(item.subtasks)
);
useEffect(() => {
setComments(item.comments ?? '');
}, [item.comments]);
useEffect(() => {
setSubtasks(parseOnSiteSubtasks(item.subtasks));
}, [item.subtasks]);
const meta = useMemo(
() => ON_SITE_STEP_META.find((m) => m.number === item.stepNumber),
[item.stepNumber]
);
const taskCount = subtasks.filter((s) => s.kind === 'task').length;
const checkedCount = subtasks.filter((s) => s.kind === 'task' && s.checked).length;
const allDone = taskCount > 0 && checkedCount === taskCount;
// Auto-mirror item.checked from all-substasks-done.
useEffect(() => {
if (disabled) return;
if (taskCount === 0) return;
if (allDone && !item.checked) {
onUpdate(item.id, { checked: true });
} else if (!allDone && item.checked) {
onUpdate(item.id, { checked: false });
}
}, [allDone, taskCount, item.checked, disabled, item.id, onUpdate]);
const handleSubtaskToggle = async (index: number) => {
if (disabled) return;
const updated = subtasks.map((s, i) => {
if (i !== index) return s;
if (s.kind !== 'task') return s;
return { ...s, checked: !s.checked };
});
setSubtasks(updated);
await onUpdate(item.id, { subtasks: serializeOnSiteSubtasks(updated) });
};
const handleCommentsBlur = async () => {
if (disabled) return;
if (comments !== (item.comments ?? '')) {
await onUpdate(item.id, { comments });
}
};
const stepTitle = meta
? intl.formatMessage({ id: meta.titleKey, defaultMessage: item.stepKey })
: item.stepKey;
return (
<TableRow hover sx={{ verticalAlign: 'top' }}>
<TableCell sx={{ width: 48, verticalAlign: 'top', pt: 2 }}>
<Typography variant="body1" fontWeight={700}>
{item.stepNumber}
</Typography>
</TableCell>
<TableCell sx={{ verticalAlign: 'top', minWidth: 360, pt: 2 }}>
<Stack spacing={1}>
<Stack direction="row" alignItems="center" spacing={1}>
<Typography variant="h5" fontWeight={700}>
{stepTitle}
</Typography>
{taskCount > 0 && (
<Typography variant="body2" color={allDone ? 'success.main' : 'text.secondary'}>
({checkedCount}/{taskCount})
</Typography>
)}
</Stack>
<Box>
{subtasks.map((s, i) => {
if (s.kind === 'heading') {
return (
<Typography
key={`h-${i}`}
variant="subtitle2"
fontWeight={700}
sx={{ mt: 1.5, mb: 0.5 }}
>
<FormattedMessage id={s.heading} defaultMessage={s.heading} />
</Typography>
);
}
return (
<FormControlLabel
key={`t-${i}`}
control={
<Checkbox
size="small"
checked={s.checked}
disabled={disabled}
onChange={() => handleSubtaskToggle(i)}
/>
}
label={
<Typography variant="body2" component="span">
<FormattedMessage id={s.text} defaultMessage={s.text} />
</Typography>
}
sx={{ display: 'flex', alignItems: 'flex-start', ml: 0 }}
/>
);
})}
</Box>
{(meta?.warnings ?? []).map((w) => (
<Alert key={w.key} severity={w.severity} variant="outlined" sx={{ mt: 1 }}>
<FormattedMessage id={w.key} defaultMessage="" />
</Alert>
))}
<TextField
size="small"
multiline
minRows={1}
maxRows={6}
value={comments}
disabled={disabled}
onChange={(e) => setComments(e.target.value)}
onBlur={handleCommentsBlur}
fullWidth
placeholder={intl.formatMessage({
id: 'onSiteCommentsPlaceholder',
defaultMessage: 'Notizen, Beobachtungen…'
})}
sx={{ mt: 1, maxWidth: 600 }}
/>
</Stack>
</TableCell>
<TableCell sx={{ verticalAlign: 'top', width: 260, pt: 2 }}>
<Stack spacing={1}>
{(meta?.images ?? []).map((filename) => {
const src = ON_SITE_IMAGES[filename];
if (!src) return null;
return (
<Box
key={filename}
component="img"
src={src}
alt={stepTitle}
onClick={() => onImageClick(filename, stepTitle)}
sx={{
width: '100%',
maxWidth: 240,
height: 'auto',
borderRadius: 1,
cursor: 'zoom-in',
border: '1px solid #e0e0e0',
transition: 'transform 0.1s',
'&:hover': { transform: 'scale(1.02)' }
}}
/>
);
})}
</Stack>
</TableCell>
</TableRow>
);
}
export default OnSiteChecklistStepRow;

View File

@ -0,0 +1,289 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import {
Alert,
Box,
LinearProgress,
Paper,
Snackbar,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from '@mui/material';
import { FormattedMessage, useIntl } from 'react-intl';
import axiosConfig from 'src/Resources/axiosConfig';
import {
OnSiteChecklistItem,
OnSiteChecklistResponse,
OnSiteChecklistSignature
} from 'src/interfaces/OnSiteChecklistTypes';
import { UserContext } from 'src/contexts/userContext';
import { UserType } from 'src/interfaces/UserTypes';
import OnSiteChecklistStepRow from './OnSiteChecklistStepRow';
import OnSiteSignatureBlock from './OnSiteSignatureBlock';
import OnSiteImageLightbox from './OnSiteImageLightbox';
import { ON_SITE_IMAGES } from 'src/assets/installation-checklist/sodistore-home';
interface Props {
installationId: number;
}
type ToastState = {
open: boolean;
severity: 'success' | 'error';
message: string;
};
function OnSiteChecklistTab({ installationId }: Props) {
const intl = useIntl();
const { currentUser } = useContext(UserContext);
const [items, setItems] = useState<OnSiteChecklistItem[]>([]);
const [signature, setSignature] = useState<OnSiteChecklistSignature | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [toast, setToast] = useState<ToastState>({
open: false,
severity: 'success',
message: ''
});
const [lightbox, setLightbox] = useState<{ open: boolean; src: string | null; alt: string }>({
open: false,
src: null,
alt: ''
});
const fetchAll = useCallback(() => {
setLoading(true);
axiosConfig
.get('/GetOnSiteChecklistForInstallation', { params: { installationId } })
.then((res) => {
const data = res.data as OnSiteChecklistResponse;
setItems(Array.isArray(data?.items) ? data.items : []);
setSignature(data?.signature ?? null);
setError('');
})
.catch(() => setError('Failed to load on-site checklist.'))
.finally(() => setLoading(false));
}, [installationId]);
useEffect(() => {
fetchAll();
}, [fetchAll]);
const isSigned = !!signature?.signedAt;
const isAdmin = currentUser.userType === UserType.admin;
const disabled = isSigned && !isAdmin;
const progress = useMemo(() => {
const total = items.length;
const done = items.filter((i) => i.checked).length;
const percent = total === 0 ? 0 : Math.round((done / total) * 100);
return { total, done, percent };
}, [items]);
const showSaveError = useCallback(() => {
setToast({
open: true,
severity: 'error',
message: intl.formatMessage({
id: 'onSiteSaveFailed',
defaultMessage: 'Änderung konnte nicht gespeichert werden'
})
});
}, [intl]);
const handleItemUpdate = useCallback(
async (
id: number,
patch: Partial<{ checked: boolean; comments: string; subtasks: string }>
) => {
const params: Record<string, unknown> = { onSiteChecklistItemId: id };
if (patch.checked !== undefined) params.checked = patch.checked;
if (patch.comments !== undefined) params.comments = patch.comments;
if (patch.subtasks !== undefined) params.subtasks = patch.subtasks;
try {
const res = await axiosConfig.put('/UpdateOnSiteChecklistItem', null, { params });
const updated = res.data as OnSiteChecklistItem;
setItems((prev) => prev.map((it) => (it.id === id ? updated : it)));
return true;
} catch {
showSaveError();
return false;
}
},
[showSaveError]
);
const handleRemarksChange = useCallback(
async (remarks: string) => {
try {
const res = await axiosConfig.put('/UpdateOnSiteChecklistRemarks', null, {
params: { installationId, remarks }
});
setSignature(res.data as OnSiteChecklistSignature);
return true;
} catch {
showSaveError();
return false;
}
},
[installationId, showSaveError]
);
const handleSign = useCallback(
async (signedByName: string) => {
try {
const res = await axiosConfig.post('/SignOnSiteChecklist', null, {
params: { installationId, signedByName }
});
setSignature(res.data as OnSiteChecklistSignature);
setToast({
open: true,
severity: 'success',
message: intl.formatMessage({
id: 'onSiteSignedSuccess',
defaultMessage: 'Checkliste erfolgreich unterzeichnet'
})
});
return true;
} catch {
showSaveError();
return false;
}
},
[installationId, showSaveError, intl]
);
const handleResetSignature = useCallback(async () => {
try {
const res = await axiosConfig.post('/ResetOnSiteChecklistSignature', null, {
params: { installationId }
});
setSignature(res.data as OnSiteChecklistSignature);
return true;
} catch {
showSaveError();
return false;
}
}, [installationId, showSaveError]);
const handleImageClick = (filename: string, alt: string) => {
setLightbox({ open: true, src: ON_SITE_IMAGES[filename] ?? null, alt });
};
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}>
<Stack direction="row" justifyContent="space-between" alignItems="center" mb={1}>
<Typography variant="h4">
<FormattedMessage
id="onSiteChecklistTitle"
defaultMessage="Vor-Ort-Installations-Checkliste"
/>
</Typography>
<Typography variant="body2" color="text.secondary">
<FormattedMessage
id="onSiteChecklistProgress"
defaultMessage="Fortschritt: {done}/{total} ({percent}%)"
values={progress}
/>
</Typography>
</Stack>
<LinearProgress
variant="determinate"
value={progress.percent}
sx={{ height: 8, borderRadius: 4 }}
/>
{disabled && (
<Alert severity="info" sx={{ mt: 2 }}>
<FormattedMessage
id="onSiteLocked"
defaultMessage="Diese Checkliste wurde unterzeichnet und ist gesperrt."
/>
</Alert>
)}
</Box>
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell sx={{ width: 48 }}>#</TableCell>
<TableCell>
<FormattedMessage id="onSiteStepHeading" defaultMessage="Schritt" />
</TableCell>
<TableCell sx={{ width: 260 }}>
<FormattedMessage id="onSiteImagesHeading" defaultMessage="Bilder" />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => (
<OnSiteChecklistStepRow
key={item.id}
item={item}
disabled={disabled}
onUpdate={handleItemUpdate}
onImageClick={handleImageClick}
/>
))}
</TableBody>
</Table>
</TableContainer>
{signature && (
<OnSiteSignatureBlock
signature={signature}
disabled={disabled}
currentUserType={currentUser.userType}
onRemarksChange={handleRemarksChange}
onSign={handleSign}
onResetSignature={handleResetSignature}
/>
)}
<OnSiteImageLightbox
open={lightbox.open}
src={lightbox.src}
alt={lightbox.alt}
onClose={() => setLightbox({ open: false, src: null, alt: '' })}
/>
<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 OnSiteChecklistTab;

View File

@ -0,0 +1,49 @@
import React from 'react';
import { Dialog, IconButton, Box } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
interface Props {
open: boolean;
src: string | null;
alt: string;
onClose: () => void;
}
function OnSiteImageLightbox({ open, src, alt, onClose }: Props) {
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<Box sx={{ position: 'relative', backgroundColor: '#000' }}>
<IconButton
onClick={onClose}
sx={{
position: 'absolute',
top: 8,
right: 8,
color: '#fff',
backgroundColor: 'rgba(0,0,0,0.4)',
'&:hover': { backgroundColor: 'rgba(0,0,0,0.7)' }
}}
>
<CloseIcon />
</IconButton>
{src && (
<Box
component="img"
src={src}
alt={alt}
onClick={onClose}
sx={{
display: 'block',
maxWidth: '100%',
maxHeight: '85vh',
margin: '0 auto',
cursor: 'zoom-out'
}}
/>
)}
</Box>
</Dialog>
);
}
export default OnSiteImageLightbox;

View File

@ -0,0 +1,183 @@
import React, { useEffect, useState } from 'react';
import {
Alert,
Box,
Button,
Collapse,
Link,
Paper,
Stack,
TextField,
Typography
} from '@mui/material';
import { FormattedMessage, useIntl } from 'react-intl';
import { OnSiteChecklistSignature } from 'src/interfaces/OnSiteChecklistTypes';
import { UserType } from 'src/interfaces/UserTypes';
interface Props {
signature: OnSiteChecklistSignature;
disabled: boolean;
currentUserType: UserType;
onRemarksChange: (remarks: string) => Promise<boolean>;
onSign: (signedByName: string) => Promise<boolean>;
onResetSignature: () => Promise<boolean>;
}
function OnSiteSignatureBlock({
signature,
disabled,
currentUserType,
onRemarksChange,
onSign,
onResetSignature
}: Props) {
const intl = useIntl();
const [remarks, setRemarks] = useState(signature.remarks ?? '');
const [signedByName, setSignedByName] = useState('');
const [consentExpanded, setConsentExpanded] = useState(false);
useEffect(() => {
setRemarks(signature.remarks ?? '');
}, [signature.remarks]);
const isSigned = !!signature.signedAt;
const canResetSignature = isSigned && currentUserType === UserType.admin;
const handleRemarksBlur = async () => {
if (disabled) return;
if (remarks !== (signature.remarks ?? '')) {
await onRemarksChange(remarks);
}
};
const handleSign = async () => {
if (!signedByName.trim()) return;
await onSign(signedByName.trim());
};
const formatSignedDate = (iso: string | null): string => {
if (!iso) return '';
try {
return new Date(iso).toLocaleString();
} catch {
return iso;
}
};
return (
<Paper variant="outlined" sx={{ p: 3, mt: 3 }}>
<Stack spacing={2}>
<Box>
<Typography variant="h5" fontWeight={700} gutterBottom>
<FormattedMessage
id="onSiteRemarksTitle"
defaultMessage="Bemerkungen vom Installateur"
/>
</Typography>
<TextField
multiline
minRows={4}
maxRows={12}
fullWidth
value={remarks}
disabled={disabled}
onChange={(e) => setRemarks(e.target.value)}
onBlur={handleRemarksBlur}
placeholder={intl.formatMessage({
id: 'onSiteRemarksPlaceholder',
defaultMessage: 'Zusätzliche Bemerkungen…'
})}
/>
</Box>
<Box>
<Typography variant="h5" fontWeight={700} gutterBottom>
<FormattedMessage id="onSiteSignatureTitle" defaultMessage="E-Signatur" />
</Typography>
{isSigned ? (
<Alert
severity="success"
action={
canResetSignature ? (
<Button color="inherit" size="small" onClick={onResetSignature}>
<FormattedMessage
id="onSiteResetSignature"
defaultMessage="Unterschrift zurücksetzen"
/>
</Button>
) : undefined
}
>
<FormattedMessage
id="onSiteSignedBy"
defaultMessage="Unterzeichnet von {name} am {date}"
values={{
name: signature.signedByName ?? '',
date: formatSignedDate(signature.signedAt)
}}
/>
</Alert>
) : (
<Stack spacing={2}>
<Box>
<Typography variant="body2" sx={{ whiteSpace: 'pre-line' }}>
<FormattedMessage
id="onSiteConsentShort"
defaultMessage="Ich bestätige, dass die Installation gemäss obiger Checkliste fachgerecht ausgeführt und in Betrieb genommen wurde."
/>
</Typography>
<Link
component="button"
type="button"
variant="body2"
onClick={() => setConsentExpanded((v) => !v)}
sx={{ mt: 0.5 }}
>
{consentExpanded ? (
<FormattedMessage id="onSiteShowLess" defaultMessage="Weniger anzeigen" />
) : (
<FormattedMessage id="onSiteShowMore" defaultMessage="Mehr anzeigen" />
)}
</Link>
<Collapse in={consentExpanded}>
<Typography variant="body2" sx={{ mt: 1, whiteSpace: 'pre-line' }}>
<FormattedMessage
id="onSiteConsentLong"
defaultMessage="Mit der elektronischen Unterschrift bestätigt der ausführende Installateur, dass die Installation gemäss obiger Checkliste und den anerkannten Regeln der Technik ausgeführt und in Betrieb genommen wurde. Die obenstehenden Angaben sind vollständig und wahrheitsgetreu. Diese Bestätigung ist Teil der Inbetriebnahme und wird zusammen mit der Checkliste an inesco energy übermittelt."
/>
</Typography>
</Collapse>
</Box>
<TextField
label={intl.formatMessage({
id: 'onSiteFullName',
defaultMessage: 'Vollständiger Name'
})}
required
value={signedByName}
onChange={(e) => setSignedByName(e.target.value)}
disabled={disabled}
sx={{ maxWidth: 480 }}
/>
<Box>
<Button
variant="contained"
color="primary"
disabled={disabled || !signedByName.trim()}
onClick={handleSign}
>
<FormattedMessage id="onSiteSignButton" defaultMessage="Unterschreiben" />
</Button>
</Box>
</Stack>
)}
</Box>
</Stack>
</Paper>
);
}
export default OnSiteSignatureBlock;

View File

@ -154,7 +154,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => {
<TableCell> <TableCell>
<FormattedMessage <FormattedMessage
id="setupProgress" id="setupProgress"
defaultMessage="Setup Progress" defaultMessage="Monitor Onboarding Progress"
/> />
</TableCell> </TableCell>
)} )}

View File

@ -32,6 +32,7 @@ import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
import DocumentsTab from '../Documents/DocumentsTab'; import DocumentsTab from '../Documents/DocumentsTab';
import InstallationChecklistTab from '../Checklist/InstallationChecklistTab'; import InstallationChecklistTab from '../Checklist/InstallationChecklistTab';
import { CHECKLIST_ENABLED_PRODUCTS } from 'src/interfaces/ChecklistTypes'; import { CHECKLIST_ENABLED_PRODUCTS } from 'src/interfaces/ChecklistTypes';
import OnSiteChecklistTab from '../OnSiteChecklist/OnSiteChecklistTab';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -499,7 +500,8 @@ function SodioHomeInstallation(props: singleInstallationProps) {
currentTab != 'report' && currentTab != 'report' &&
currentTab != 'installationTickets' && currentTab != 'installationTickets' &&
currentTab != 'documents' && currentTab != 'documents' &&
currentTab != 'checklist' && ( currentTab != 'checklist' &&
currentTab != 'onSiteChecklist' && (
<Container <Container
maxWidth="xl" maxWidth="xl"
sx={{ sx={{
@ -685,6 +687,18 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/> />
)} )}
{(currentUser.userType == UserType.admin || currentUser.userType == UserType.partner) &&
props.current_installation.product === 2 && (
<Route
path={routes.onSiteChecklist}
element={
<OnSiteChecklistTab
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>}

View File

@ -54,7 +54,8 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
'report', 'report',
'installationTickets', 'installationTickets',
'documents', 'documents',
'checklist' 'checklist',
'onSiteChecklist'
]; ];
const [currentTab, setCurrentTab] = useState<string>(undefined); const [currentTab, setCurrentTab] = useState<string>(undefined);
@ -196,7 +197,11 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
}, },
{ {
value: 'checklist', value: 'checklist',
label: <FormattedMessage id="checklist" defaultMessage="Checklist" /> label: <FormattedMessage id="checklist" defaultMessage="Monitor Onboarding Checklist" />
},
{
value: 'onSiteChecklist',
label: <FormattedMessage id="onSiteChecklist" defaultMessage="On-Site Checklist" />
} }
] ]
: currentUser.userType == UserType.partner : currentUser.userType == UserType.partner
@ -240,6 +245,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
{ {
value: 'documents', value: 'documents',
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" /> label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
},
{
value: 'onSiteChecklist',
label: <FormattedMessage id="onSiteChecklist" defaultMessage="On-Site Checklist" />
} }
] ]
: [ : [
@ -284,7 +293,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
const dataCollectionDisabled = const dataCollectionDisabled =
currentInstallation?.dataCollectionEnabled === false currentInstallation?.dataCollectionEnabled === false
|| (installations.length === 1 && installations[0].dataCollectionEnabled === false); || (installations.length === 1 && installations[0].dataCollectionEnabled === false);
const allowedWhenDisabled = ['list', 'tree', 'information', 'history', 'installationTickets', 'documents', 'checklist']; const allowedWhenDisabled = ['list', 'tree', 'information', 'history', 'installationTickets', 'documents', 'checklist', 'onSiteChecklist'];
const tabs = inInstallationView && currentUser.userType == UserType.admin const tabs = inInstallationView && currentUser.userType == UserType.admin
? [ ? [
@ -369,7 +378,11 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
}, },
{ {
value: 'checklist', value: 'checklist',
label: <FormattedMessage id="checklist" defaultMessage="Checklist" /> label: <FormattedMessage id="checklist" defaultMessage="Monitor Onboarding Checklist" />
},
{
value: 'onSiteChecklist',
label: <FormattedMessage id="onSiteChecklist" defaultMessage="On-Site Checklist" />
} }
] ]
: inInstallationView && currentUser.userType == UserType.partner : inInstallationView && currentUser.userType == UserType.partner
@ -421,6 +434,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
{ {
value: 'documents', value: 'documents',
label: <FormattedMessage id="documentsTab" defaultMessage="Documents" /> label: <FormattedMessage id="documentsTab" defaultMessage="Documents" />
},
{
value: 'onSiteChecklist',
label: <FormattedMessage id="onSiteChecklist" defaultMessage="On-Site Checklist" />
} }
] ]
: inInstallationView && currentUser.userType == UserType.client : inInstallationView && currentUser.userType == UserType.client
@ -486,6 +503,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
> >
{tabs {tabs
.filter((tab) => !(isGrowatt && tab.value === 'report')) .filter((tab) => !(isGrowatt && tab.value === 'report'))
.filter((tab) => !(tab.value === 'onSiteChecklist' && props.product !== 2))
.filter((tab) => !dataCollectionDisabled || allowedWhenDisabled.includes(tab.value)) .filter((tab) => !dataCollectionDisabled || allowedWhenDisabled.includes(tab.value))
.map((tab) => ( .map((tab) => (
<Tab <Tab
@ -560,6 +578,7 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
> >
{singleInstallationTabs {singleInstallationTabs
.filter((tab) => !(isGrowatt && tab.value === 'report')) .filter((tab) => !(isGrowatt && tab.value === 'report'))
.filter((tab) => !(tab.value === 'onSiteChecklist' && props.product !== 2))
.filter((tab) => !dataCollectionDisabled || allowedWhenDisabled.includes(tab.value)) .filter((tab) => !dataCollectionDisabled || allowedWhenDisabled.includes(tab.value))
.map((tab) => ( .map((tab) => (
<Tab <Tab

View File

@ -0,0 +1,143 @@
// Installer-facing on-site installation checklist (sodistore home only).
// Distinct from the internal monitor-onboarding checklist (ChecklistTypes.tsx) —
// no assignee, no done-date; per-step substeps + comments + per-installation
// signature ("Bemerkungen" + typed-name e-signature).
export type OnSiteChecklistSubtask =
| { kind: 'task'; text: string; checked: boolean }
| { kind: 'heading'; heading: string };
export type OnSiteChecklistItem = {
id: number;
installationId: number;
stepNumber: number;
stepKey: string;
checked: boolean;
comments: string;
subtasks: string | null;
createdAt: string;
updatedAt: string;
};
export type OnSiteChecklistSignature = {
id: number;
installationId: number;
remarks: string;
signedByName: string | null;
signedByUserId: number | null;
signedAt: string | null;
createdAt: string;
updatedAt: string;
};
export type OnSiteChecklistResponse = {
items: OnSiteChecklistItem[];
signature: OnSiteChecklistSignature;
};
// Static per-step metadata not stored in the DB (title key + image filenames +
// optional warning key). Lookup table keyed by step number. Image paths are
// resolved at render time against the static assets folder.
export type OnSiteStepMeta = {
number: number;
titleKey: string;
warnings?: OnSiteWarning[];
images: string[];
};
// A single "Achtung" callout. severity 'error' renders red (used for the most
// critical warnings that appear in red in the source PDF).
export type OnSiteWarning = {
key: string;
severity: 'warning' | 'error';
};
export const ON_SITE_STEP_META: ReadonlyArray<OnSiteStepMeta> = [
{
number: 1,
titleKey: 'onSiteStep1Title',
warnings: [{ key: 'onSiteStep1Warning', severity: 'warning' }],
images: ['step1-auspacken.jpg']
},
{
number: 2,
titleKey: 'onSiteStep2Title',
images: ['step2-wr-montiert.jpg', 'step2-ac-anschluesse.jpg', 'step2-ac-stecker.jpg']
},
{
number: 3,
titleKey: 'onSiteStep3Title',
warnings: [{ key: 'onSiteStep3Warning', severity: 'warning' }],
images: ['step3-hauptverteilung.jpg', 'step3-hauptverteilung-2.jpg']
},
{
number: 4,
titleKey: 'onSiteStep4Title',
warnings: [{ key: 'onSiteStep4Warning', severity: 'warning' }],
images: ['step4-meter.jpg', 'step4-wandler.jpg']
},
{
number: 5,
titleKey: 'onSiteStep5Title',
warnings: [{ key: 'onSiteStep5Warning', severity: 'warning' }],
images: ['step5-gateway.jpg']
},
{
number: 6,
titleKey: 'onSiteStep6Title',
warnings: [
{ key: 'onSiteStep6Warning1', severity: 'warning' },
{ key: 'onSiteStep6Warning2', severity: 'error' },
{ key: 'onSiteStep6Warning3', severity: 'warning' },
{ key: 'onSiteStep6Warning4', severity: 'warning' }
],
images: ['step6-batterien.jpg', 'step6-splitter.jpg']
},
{
number: 7,
titleKey: 'onSiteStep7Title',
warnings: [{ key: 'onSiteStep7Warning', severity: 'warning' }],
images: ['step7-pv-switch.jpg']
},
{
number: 8,
titleKey: 'onSiteStep8Title',
images: ['step8-batterie-breaker.jpg']
},
{
number: 9,
titleKey: 'onSiteStep9Title',
images: []
}
];
export function parseOnSiteSubtasks(raw: string | null | undefined): OnSiteChecklistSubtask[] {
if (!raw) return [];
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
return parsed
.map((x): OnSiteChecklistSubtask | null => {
if (!x || typeof x !== 'object') return null;
if (typeof x.heading === 'string') {
return { kind: 'heading', heading: String(x.heading) };
}
if (typeof x.text === 'string') {
return { kind: 'task', text: String(x.text), checked: Boolean(x.checked) };
}
return null;
})
.filter((x): x is OnSiteChecklistSubtask => x !== null);
} catch {
return [];
}
}
export function serializeOnSiteSubtasks(subtasks: OnSiteChecklistSubtask[]): string {
return JSON.stringify(
subtasks.map((s) => {
if (s.kind === 'heading') return { heading: s.heading };
return { text: s.text, checked: s.checked };
})
);
}

View File

@ -745,7 +745,7 @@
"invalidFileType": "Ungültiger Dateityp.", "invalidFileType": "Ungültiger Dateityp.",
"uploadFailed": "Hochladen fehlgeschlagen.", "uploadFailed": "Hochladen fehlgeschlagen.",
"uploadSuccess": "Hochladen erfolgreich.", "uploadSuccess": "Hochladen erfolgreich.",
"checklist": "Checkliste", "checklist": "Monitor-Onboarding-Checkliste",
"checklistTitle": "Schritte zur Anbindung der Installation an Monitor", "checklistTitle": "Schritte zur Anbindung der Installation an Monitor",
"checklistProgress": "Fortschritt: {done}/{total} ({percent}%)", "checklistProgress": "Fortschritt: {done}/{total} ({percent}%)",
"checklistStep": "Schritt", "checklistStep": "Schritt",
@ -800,7 +800,7 @@
"checklistStep10Sub2": "Zeit- und Materialbericht in Monitoring hochgeladen", "checklistStep10Sub2": "Zeit- und Materialbericht in Monitoring hochgeladen",
"checklistStep10Sub3": "Atef kontaktieren, falls externes EMS vorhanden ist", "checklistStep10Sub3": "Atef kontaktieren, falls externes EMS vorhanden ist",
"checklistNoAttachments": "Noch keine Datei angehängt.", "checklistNoAttachments": "Noch keine Datei angehängt.",
"setupProgress": "Setup-Fortschritt", "setupProgress": "Monitor-Onboarding-Fortschritt",
"checklistPhaseEmpty": "Nicht gestartet", "checklistPhaseEmpty": "Nicht gestartet",
"checklistPhasePreparation": "Vorbereitung", "checklistPhasePreparation": "Vorbereitung",
"checklistPhaseOnSite": "Vor Ort", "checklistPhaseOnSite": "Vor Ort",
@ -810,5 +810,96 @@
"minDischargeVoltageV": "Min. Entladespannung (V)", "minDischargeVoltageV": "Min. Entladespannung (V)",
"maxDischargeCurrentA": "Max. Entladestrom (A)", "maxDischargeCurrentA": "Max. Entladestrom (A)",
"maxChargeCurrentA": "Max. Ladestrom (A)", "maxChargeCurrentA": "Max. Ladestrom (A)",
"maxChargeVoltageV": "Max. Ladespannung (V)" "maxChargeVoltageV": "Max. Ladespannung (V)",
"onSiteChecklist": "Vor-Ort-Checkliste",
"onSiteChecklistTitle": "Vor-Ort-Installations-Checkliste",
"onSiteChecklistProgress": "Fortschritt: {done}/{total} ({percent}%)",
"onSiteStepHeading": "Schritt",
"onSiteImagesHeading": "Bilder zur Installation",
"onSiteCommentsPlaceholder": "Notizen, Beobachtungen…",
"onSiteSaveFailed": "Änderung konnte nicht gespeichert werden",
"onSiteLocked": "Diese Checkliste wurde unterzeichnet und ist gesperrt.",
"onSiteRemarksTitle": "Bemerkungen vom Installateur",
"onSiteRemarksPlaceholder": "Zusätzliche Bemerkungen…",
"onSiteSignatureTitle": "E-Signatur",
"onSiteFullName": "Vollständiger Name",
"onSiteSignButton": "Unterschreiben",
"onSiteSignedBy": "Unterzeichnet von {name} am {date}",
"onSiteSignedSuccess": "Checkliste erfolgreich unterzeichnet",
"onSiteResetSignature": "Unterschrift zurücksetzen",
"onSiteShowMore": "Mehr anzeigen",
"onSiteShowLess": "Weniger anzeigen",
"onSiteConsentShort": "Ich bestätige, dass die Installation gemäss obiger Checkliste fachgerecht ausgeführt und in Betrieb genommen wurde.",
"onSiteConsentLong": "Mit der elektronischen Unterschrift bestätigt der ausführende Installateur, dass die Installation gemäss obiger Checkliste und den anerkannten Regeln der Technik ausgeführt und in Betrieb genommen wurde. Die obenstehenden Angaben sind vollständig und wahrheitsgetreu. Diese Bestätigung ist Teil der Inbetriebnahme und wird zusammen mit der Checkliste an inesco energy übermittelt.",
"onSiteStep1Title": "Auspacken",
"onSiteStep1Sub1": "sodistore home vorsichtig auspacken",
"onSiteStep1Sub2": "Standortbedingungen prüfen (Trockene Umgebung 20 bis +55°C, feste Ebene)",
"onSiteStep1Warning": "Wenn Transportschäden vorhanden sind, bitte mit Fotos dokumentieren und inesco melden.",
"onSiteStep2Title": "inesco Wechselrichter montieren",
"onSiteStep2Sub1": "WR wieder an der Vorrichtung montieren",
"onSiteStep2Sub2": "Überprüfen ob durch Transport Kabel beschädigt wurden",
"onSiteStep2Sub3": "AC-Kabel wieder einstecken GRID zu GRID und BACK-UP zu Load",
"onSiteStep3Title": "Arbeiten in der Hauptverteilung AC-seitig",
"onSiteStep3Sub1": "AC-Installation gemäss Schema installieren",
"onSiteStep3Sub2": "Netzumschalter der inesco (empfohlen) installieren",
"onSiteStep3Sub3": "LS für WR installieren je nach Modell 25A/40A",
"onSiteStep3Sub4": "Anlageschalter für sodistore home installieren",
"onSiteStep3Sub5": "Zul. Kabel (Grid) auf sodistore home anschliessen",
"onSiteStep3Sub6": "Load Kabel auf sodistore home anschliessen",
"onSiteStep3Subheading1": "Notstromanforderungen",
"onSiteStep3Sub7": "Notstromversorgte Gruppen in der HV definieren",
"onSiteStep3Sub8": "Achtung TNS oder TNC-Gruppen dürfen nicht gemischt werden!!!",
"onSiteStep3Sub9": "WR muss je nach System TNS oder TNC eingestellt werden",
"onSiteStep3Sub10": "Notstromgruppen mit Typ LS Typ B und 300mA FI absichern",
"onSiteStep3Warning": "Die inesco energy empfiehlt einen Netzumschalter beim Notstrom Betrieb. So kann im Störungsfall die Anlage auf Netz umgeschaltet und das Haus trotzdem mit Energie versorgt werden.",
"onSiteStep4Title": "Anschliessen des Meters",
"onSiteStep4Sub1": "Einbau des Meters in der HV oder im Sodistore home",
"onSiteStep4Sub2": "LS/3LN installieren für Referenzspannung Meter",
"onSiteStep4Sub3": "Wandler um die Zuleitung klemmen, beachten der Richtigen Richtung",
"onSiteStep4Sub4": "Bus-Kabel von Meter auf WR installieren und einstecken, dies darf verlängert werden",
"onSiteStep4Warning": "Die Messwandler-Kabel dürfen nicht verlängert oder gekürzt werden, da dies zu falschen Messungen führt. Die Wandler sollten im Hochvoltbereich einen Mindestabstand von 15 cm zu anderen Wandlern oder hochfrequenten Signalen haben, da sonst ebenfalls falsche Messwerte auftreten können.",
"onSiteStep5Title": "inesco Gateway und Internet",
"onSiteStep5Sub1": "Internetkabel zu inesco Gateway ziehen",
"onSiteStep5Sub2": "Verbindung inesco Gateway zu WR-Prüfen (USB zu BMS)",
"onSiteStep5Sub3": "inesco Gateway mit Spannung versorgen",
"onSiteStep5Sub4": "WLAN-Verbindung zu WR herstellen",
"onSiteStep5Warning": "Die WR brauchen zwingend WLAN-Verbindung kein RJ45 LAN, ansonsten können diese nicht kommunizieren.",
"onSiteStep6Title": "Systemverbindungen",
"onSiteStep6Sub1": "Erdverbindungen",
"onSiteStep6Sub2": "inesco Gateway (USB) mit WR verbunden (über Splitter)",
"onSiteStep6Sub3": "Batterien + / - zu WR gemäss Schema",
"onSiteStep6Sub4": "CAN von Batterie zu WR auf BMS (über Splitter)",
"onSiteStep6Sub5": "Batterien abschlaufen RS485 2 zu RS485 2",
"onSiteStep6Sub6": "Von Splitter auf WR",
"onSiteStep6Sub7": "bei 27/36 müssen die WR über PARA verbunden werden",
"onSiteStep6Warning1": "Systemverbindungen müssen gemäss Schema verdrahtet werden. Jede Baugruppe (9, 18, 27 und 36) hat ein eigenes Schema, das zu beachten ist.",
"onSiteStep6Warning2": "Die RJ45-Kabel dürfen nicht vertauscht werden. Das schwarze Kabel ist mit Kontakt A verbunden und führt zum inesco Gateway über USB. Kontakt B ist mit der Batterie verbunden.",
"onSiteStep6Warning3": "Die Kabel haben unterschiedliche Pinbelegungen und sind entsprechend gekrimpt. Daher dürfen sie nicht vertauscht werden.",
"onSiteStep6Warning4": "Maximum 2 Batterien + / - abschlaufen.",
"onSiteStep7Title": "Anschluss PV-Wechselrichter",
"onSiteStep7Sub1": "DC-Leitungen auf inesco WR anschliessen. Beachten der String Spannungen.",
"onSiteStep7Warning": "Wenn die Anlage AC-gekoppelt ist, bleibt der bestehende WR und der inesco WR wird nicht mit den DC-Leitungen der PV-Anlage verbunden.",
"onSiteStep8Title": "Inbetriebnahme",
"onSiteStep8Sub1": "IBN via Telefon mit inesco",
"onSiteStep8Sub2": "Sichtprüfung",
"onSiteStep8Sub3": "Messungen nach Norm DC und AC",
"onSiteStep8Sub4": "Spannung zuschalten",
"onSiteStep8Sub5": "DC-Schalter beim WR einschalten",
"onSiteStep8Sub6": "Batterien einschalten Knopf auf ON stellen",
"onSiteStep8Sub7": "Breaker an den Batterien auf ON stellen",
"onSiteStep8Sub8": "Prüfen, ob alle Geräte angezeigt werden (WR, Batterie, inesco Gateway)",
"onSiteStep8Sub9": "Anlage via ESS Link App eröffnen",
"onSiteStep8Sub10": "Konfigurieren für den Kunden via ESS Link App",
"onSiteStep8Sub11": "EVU-Zähler vergleichen (gleiche kW-Werte wie im Portal)",
"onSiteStep9Title": "Abschluss",
"onSiteStep9Sub1": "Checkliste unterschreiben und an inesco energy versenden (kontakt@inesco.energy)"
} }

View File

@ -493,7 +493,7 @@
"invalidFileType": "Invalid file type.", "invalidFileType": "Invalid file type.",
"uploadFailed": "Upload failed.", "uploadFailed": "Upload failed.",
"uploadSuccess": "Upload successful.", "uploadSuccess": "Upload successful.",
"checklist": "Checklist", "checklist": "Monitor Onboarding Checklist",
"checklistTitle": "Steps to Bring Installation to Monitor", "checklistTitle": "Steps to Bring Installation to Monitor",
"checklistProgress": "Progress: {done}/{total} ({percent}%)", "checklistProgress": "Progress: {done}/{total} ({percent}%)",
"checklistStep": "Step", "checklistStep": "Step",
@ -548,7 +548,7 @@
"checklistStep10Sub2": "Time and material report uploaded to Monitoring", "checklistStep10Sub2": "Time and material report uploaded to Monitoring",
"checklistStep10Sub3": "Contact Atef if there is external EMS", "checklistStep10Sub3": "Contact Atef if there is external EMS",
"checklistNoAttachments": "No file attached yet.", "checklistNoAttachments": "No file attached yet.",
"setupProgress": "Setup Progress", "setupProgress": "Monitor Onboarding Progress",
"checklistPhaseEmpty": "Not started", "checklistPhaseEmpty": "Not started",
"checklistPhasePreparation": "Preparation", "checklistPhasePreparation": "Preparation",
"checklistPhaseOnSite": "On-site", "checklistPhaseOnSite": "On-site",
@ -558,5 +558,96 @@
"minDischargeVoltageV": "Min Discharge Voltage (V)", "minDischargeVoltageV": "Min Discharge Voltage (V)",
"maxDischargeCurrentA": "Max Discharge Current (A)", "maxDischargeCurrentA": "Max Discharge Current (A)",
"maxChargeCurrentA": "Max Charge Current (A)", "maxChargeCurrentA": "Max Charge Current (A)",
"maxChargeVoltageV": "Max Charge Voltage (V)" "maxChargeVoltageV": "Max Charge Voltage (V)",
"onSiteChecklist": "On-Site Checklist",
"onSiteChecklistTitle": "On-Site Installation Checklist",
"onSiteChecklistProgress": "Progress: {done}/{total} ({percent}%)",
"onSiteStepHeading": "Step",
"onSiteImagesHeading": "Installation pictures",
"onSiteCommentsPlaceholder": "Notes, observations…",
"onSiteSaveFailed": "Could not save change",
"onSiteLocked": "This checklist has been signed and is locked.",
"onSiteRemarksTitle": "Installer remarks",
"onSiteRemarksPlaceholder": "Additional remarks…",
"onSiteSignatureTitle": "E-signature",
"onSiteFullName": "Full name",
"onSiteSignButton": "Sign",
"onSiteSignedBy": "Signed by {name} on {date}",
"onSiteSignedSuccess": "Checklist signed successfully",
"onSiteResetSignature": "Reset signature",
"onSiteShowMore": "Show more",
"onSiteShowLess": "Show less",
"onSiteConsentShort": "I confirm that the installation has been carried out and commissioned in accordance with the checklist above.",
"onSiteConsentLong": "By signing electronically, the executing installer confirms that the installation has been carried out and commissioned in accordance with the checklist above and the recognised rules of technology. The information above is complete and truthful. This confirmation is part of the commissioning and is submitted together with the checklist to inesco energy.",
"onSiteStep1Title": "Unpacking",
"onSiteStep1Sub1": "Carefully unpack sodistore home",
"onSiteStep1Sub2": "Check site conditions (dry environment 20 to +55°C, flat surface)",
"onSiteStep1Warning": "If there is transport damage, please document it with photos and report it to inesco.",
"onSiteStep2Title": "Mount the inesco inverter",
"onSiteStep2Sub1": "Re-mount the inverter on the bracket",
"onSiteStep2Sub2": "Verify cables have not been damaged during transport",
"onSiteStep2Sub3": "Re-connect AC cable: GRID to GRID and BACK-UP to Load",
"onSiteStep3Title": "Work in the main distribution board (AC side)",
"onSiteStep3Sub1": "Install AC according to the schematic",
"onSiteStep3Sub2": "Install the inesco transfer switch (recommended)",
"onSiteStep3Sub3": "Install the inverter MCB (25A or 40A depending on model)",
"onSiteStep3Sub4": "Install the system breaker for sodistore home",
"onSiteStep3Sub5": "Connect the supply (grid) cable to sodistore home",
"onSiteStep3Sub6": "Connect the load cable to sodistore home",
"onSiteStep3Subheading1": "Backup power requirements",
"onSiteStep3Sub7": "Define backup-supplied groups in the main distribution",
"onSiteStep3Sub8": "Caution: TNS and TNC groups must not be mixed!!!",
"onSiteStep3Sub9": "Inverter must be set to TNS or TNC depending on the system",
"onSiteStep3Sub10": "Protect backup groups with Type B MCB and 300mA RCD",
"onSiteStep3Warning": "inesco energy recommends a transfer switch when running on backup. In a fault scenario the system can be switched to grid and the house can still be supplied with energy.",
"onSiteStep4Title": "Connect the meter",
"onSiteStep4Sub1": "Install the meter in the main distribution or inside sodistore home",
"onSiteStep4Sub2": "Install MCB / 3LN for the meter reference voltage",
"onSiteStep4Sub3": "Clamp the CTs around the supply line — observe the correct direction",
"onSiteStep4Sub4": "Connect the bus cable from meter to inverter (extension allowed)",
"onSiteStep4Warning": "The CT cables must not be extended or shortened — this leads to incorrect measurements. CTs should keep at least 15 cm distance from other CTs or high-frequency signals in the high-voltage area, otherwise measurements may also be wrong.",
"onSiteStep5Title": "inesco Gateway and internet",
"onSiteStep5Sub1": "Run an internet cable to the inesco Gateway",
"onSiteStep5Sub2": "Verify inesco Gateway connection to inverter (USB to BMS)",
"onSiteStep5Sub3": "Power up the inesco Gateway",
"onSiteStep5Sub4": "Establish WiFi connection to the inverter",
"onSiteStep5Warning": "Inverters strictly require a WiFi connection — no RJ45 LAN — otherwise they cannot communicate.",
"onSiteStep6Title": "System connections",
"onSiteStep6Sub1": "Earth connections",
"onSiteStep6Sub2": "inesco Gateway (USB) connected to inverter (via splitter)",
"onSiteStep6Sub3": "Batteries +/- to inverter according to schematic",
"onSiteStep6Sub4": "CAN from battery to inverter on BMS (via splitter)",
"onSiteStep6Sub5": "Daisy-chain batteries RS485 2 to RS485 2",
"onSiteStep6Sub6": "From splitter to inverter",
"onSiteStep6Sub7": "For 27/36 assemblies the inverters must be connected via PARA",
"onSiteStep6Warning1": "System connections must be wired according to the schematic. Each assembly (9, 18, 27 and 36) has its own schematic that must be followed.",
"onSiteStep6Warning2": "The RJ45 cables must not be swapped. The black cable is connected to contact A and runs to the inesco Gateway via USB. Contact B is connected to the battery.",
"onSiteStep6Warning3": "The cables have different pinouts and are crimped accordingly — they must not be swapped.",
"onSiteStep6Warning4": "Daisy-chain a maximum of 2 batteries +/-.",
"onSiteStep7Title": "PV inverter connection",
"onSiteStep7Sub1": "Connect DC lines to the inesco inverter — observe the string voltages.",
"onSiteStep7Warning": "If the system is AC-coupled, the existing inverter remains and the inesco inverter is not connected to the DC lines of the PV system.",
"onSiteStep8Title": "Commissioning",
"onSiteStep8Sub1": "Commission via phone with inesco",
"onSiteStep8Sub2": "Visual inspection",
"onSiteStep8Sub3": "Measurements according to standard — DC and AC",
"onSiteStep8Sub4": "Switch on voltage",
"onSiteStep8Sub5": "Switch on the DC switch on the inverter",
"onSiteStep8Sub6": "Switch on batteries — set the button to ON",
"onSiteStep8Sub7": "Set the breaker on the batteries to ON",
"onSiteStep8Sub8": "Check that all devices are shown (inverter, battery, inesco Gateway)",
"onSiteStep8Sub9": "Open the system via the ESS Link app",
"onSiteStep8Sub10": "Configure for the customer via the ESS Link app",
"onSiteStep8Sub11": "Compare utility meter (same kW values as on the portal)",
"onSiteStep9Title": "Completion",
"onSiteStep9Sub1": "Sign the checklist and send it to inesco energy (kontakt@inesco.energy)"
} }

View File

@ -745,7 +745,7 @@
"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", "checklist": "Check-list d'onboarding Monitor",
"checklistTitle": "Étapes pour connecter l'installation à Monitor", "checklistTitle": "Étapes pour connecter l'installation à Monitor",
"checklistProgress": "Progression : {done}/{total} ({percent}%)", "checklistProgress": "Progression : {done}/{total} ({percent}%)",
"checklistStep": "Étape", "checklistStep": "Étape",
@ -800,7 +800,7 @@
"checklistStep10Sub2": "Rapport de temps et matériaux téléversé dans Monitoring", "checklistStep10Sub2": "Rapport de temps et matériaux téléversé dans Monitoring",
"checklistStep10Sub3": "Contacter Atef en cas d'EMS externe", "checklistStep10Sub3": "Contacter Atef en cas d'EMS externe",
"checklistNoAttachments": "Aucun fichier joint pour le moment.", "checklistNoAttachments": "Aucun fichier joint pour le moment.",
"setupProgress": "Progression installation", "setupProgress": "Progression d'onboarding Monitor",
"checklistPhaseEmpty": "Non commencé", "checklistPhaseEmpty": "Non commencé",
"checklistPhasePreparation": "Préparation", "checklistPhasePreparation": "Préparation",
"checklistPhaseOnSite": "Sur site", "checklistPhaseOnSite": "Sur site",
@ -810,5 +810,96 @@
"minDischargeVoltageV": "Tension de décharge min. (V)", "minDischargeVoltageV": "Tension de décharge min. (V)",
"maxDischargeCurrentA": "Courant de décharge max. (A)", "maxDischargeCurrentA": "Courant de décharge max. (A)",
"maxChargeCurrentA": "Courant de charge max. (A)", "maxChargeCurrentA": "Courant de charge max. (A)",
"maxChargeVoltageV": "Tension de charge max. (V)" "maxChargeVoltageV": "Tension de charge max. (V)",
"onSiteChecklist": "Check-list sur site",
"onSiteChecklistTitle": "Check-list d'installation sur site",
"onSiteChecklistProgress": "Progression : {done}/{total} ({percent}%)",
"onSiteStepHeading": "Étape",
"onSiteImagesHeading": "Photos d'installation",
"onSiteCommentsPlaceholder": "Notes, observations…",
"onSiteSaveFailed": "Impossible d'enregistrer la modification",
"onSiteLocked": "Cette check-list a été signée et est verrouillée.",
"onSiteRemarksTitle": "Remarques de l'installateur",
"onSiteRemarksPlaceholder": "Remarques supplémentaires…",
"onSiteSignatureTitle": "Signature électronique",
"onSiteFullName": "Nom complet",
"onSiteSignButton": "Signer",
"onSiteSignedBy": "Signé par {name} le {date}",
"onSiteSignedSuccess": "Check-list signée avec succès",
"onSiteResetSignature": "Réinitialiser la signature",
"onSiteShowMore": "Afficher plus",
"onSiteShowLess": "Afficher moins",
"onSiteConsentShort": "Je confirme que l'installation a été réalisée et mise en service conformément à la check-list ci-dessus.",
"onSiteConsentLong": "Par la signature électronique, l'installateur exécutant confirme que l'installation a été réalisée et mise en service conformément à la check-list ci-dessus et aux règles techniques reconnues. Les informations ci-dessus sont complètes et véridiques. Cette confirmation fait partie de la mise en service et est transmise avec la check-list à inesco energy.",
"onSiteStep1Title": "Déballage",
"onSiteStep1Sub1": "Déballer sodistore home avec précaution",
"onSiteStep1Sub2": "Vérifier les conditions du site (environnement sec 20 à +55°C, surface plane)",
"onSiteStep1Warning": "En cas de dommages de transport, veuillez les documenter avec des photos et les signaler à inesco.",
"onSiteStep2Title": "Monter l'onduleur inesco",
"onSiteStep2Sub1": "Remonter l'onduleur sur son support",
"onSiteStep2Sub2": "Vérifier que les câbles n'ont pas été endommagés pendant le transport",
"onSiteStep2Sub3": "Reconnecter le câble AC : GRID vers GRID et BACK-UP vers Load",
"onSiteStep3Title": "Travaux dans le tableau principal (côté AC)",
"onSiteStep3Sub1": "Installer la partie AC selon le schéma",
"onSiteStep3Sub2": "Installer l'inverseur de réseau inesco (recommandé)",
"onSiteStep3Sub3": "Installer le disjoncteur de l'onduleur (25A ou 40A selon le modèle)",
"onSiteStep3Sub4": "Installer l'interrupteur d'installation pour sodistore home",
"onSiteStep3Sub5": "Raccorder le câble d'arrivée (Grid) à sodistore home",
"onSiteStep3Sub6": "Raccorder le câble Load à sodistore home",
"onSiteStep3Subheading1": "Exigences pour l'alimentation de secours",
"onSiteStep3Sub7": "Définir les groupes alimentés en secours dans le tableau principal",
"onSiteStep3Sub8": "Attention : les groupes TNS et TNC ne doivent pas être mélangés !!!",
"onSiteStep3Sub9": "L'onduleur doit être réglé sur TNS ou TNC selon le système",
"onSiteStep3Sub10": "Protéger les groupes de secours avec un disjoncteur Type B et un DDR 300mA",
"onSiteStep3Warning": "inesco energy recommande un inverseur de réseau en mode secours. En cas de panne, l'installation peut être basculée sur le réseau et la maison reste alimentée en énergie.",
"onSiteStep4Title": "Raccordement du compteur",
"onSiteStep4Sub1": "Installer le compteur dans le tableau principal ou dans sodistore home",
"onSiteStep4Sub2": "Installer disjoncteur / 3LN pour la tension de référence du compteur",
"onSiteStep4Sub3": "Pincer les TC autour du câble d'arrivée — respecter le bon sens",
"onSiteStep4Sub4": "Installer et brancher le câble bus du compteur à l'onduleur (rallonge autorisée)",
"onSiteStep4Warning": "Les câbles des TC ne doivent pas être rallongés ni raccourcis, sinon les mesures seront fausses. Les TC doivent garder une distance minimale de 15 cm avec d'autres TC ou des signaux haute fréquence dans la zone haute tension, sinon les mesures peuvent également être erronées.",
"onSiteStep5Title": "inesco Gateway et internet",
"onSiteStep5Sub1": "Tirer un câble internet jusqu'au inesco Gateway",
"onSiteStep5Sub2": "Vérifier la connexion inesco Gateway — onduleur (USB vers BMS)",
"onSiteStep5Sub3": "Alimenter inesco Gateway",
"onSiteStep5Sub4": "Établir la connexion WiFi avec l'onduleur",
"onSiteStep5Warning": "Les onduleurs nécessitent impérativement une connexion WiFi — pas de LAN RJ45 — sinon ils ne peuvent pas communiquer.",
"onSiteStep6Title": "Connexions système",
"onSiteStep6Sub1": "Connexions de terre",
"onSiteStep6Sub2": "inesco Gateway (USB) connecté à l'onduleur (via splitter)",
"onSiteStep6Sub3": "Batteries +/- vers l'onduleur selon le schéma",
"onSiteStep6Sub4": "CAN de la batterie vers l'onduleur sur BMS (via splitter)",
"onSiteStep6Sub5": "Chaînage des batteries RS485 2 vers RS485 2",
"onSiteStep6Sub6": "Du splitter vers l'onduleur",
"onSiteStep6Sub7": "Pour les ensembles 27/36, les onduleurs doivent être reliés via PARA",
"onSiteStep6Warning1": "Les connexions système doivent être câblées selon le schéma. Chaque ensemble (9, 18, 27 et 36) a son propre schéma à respecter.",
"onSiteStep6Warning2": "Les câbles RJ45 ne doivent pas être inversés. Le câble noir est connecté au contact A et va vers le inesco Gateway en USB. Le contact B est connecté à la batterie.",
"onSiteStep6Warning3": "Les câbles ont des brochages différents et sont sertis en conséquence — ils ne doivent pas être inversés.",
"onSiteStep6Warning4": "Chaîner au maximum 2 batteries +/-.",
"onSiteStep7Title": "Raccordement de l'onduleur PV",
"onSiteStep7Sub1": "Raccorder les lignes DC à l'onduleur inesco — respecter les tensions des strings.",
"onSiteStep7Warning": "Si l'installation est couplée en AC, l'onduleur existant reste en place et l'onduleur inesco n'est pas raccordé aux lignes DC de l'installation PV.",
"onSiteStep8Title": "Mise en service",
"onSiteStep8Sub1": "Mise en service par téléphone avec inesco",
"onSiteStep8Sub2": "Inspection visuelle",
"onSiteStep8Sub3": "Mesures selon la norme — DC et AC",
"onSiteStep8Sub4": "Mettre sous tension",
"onSiteStep8Sub5": "Enclencher l'interrupteur DC sur l'onduleur",
"onSiteStep8Sub6": "Allumer les batteries — placer le bouton sur ON",
"onSiteStep8Sub7": "Placer le disjoncteur des batteries sur ON",
"onSiteStep8Sub8": "Vérifier que tous les appareils sont affichés (onduleur, batterie, inesco Gateway)",
"onSiteStep8Sub9": "Ouvrir l'installation via l'application ESS Link",
"onSiteStep8Sub10": "Configurer pour le client via l'application ESS Link",
"onSiteStep8Sub11": "Comparer le compteur de l'utilité (mêmes valeurs kW que sur le portail)",
"onSiteStep9Title": "Finalisation",
"onSiteStep9Sub1": "Signer la check-list et l'envoyer à inesco energy (kontakt@inesco.energy)"
} }

View File

@ -745,7 +745,7 @@
"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", "checklist": "Checklist di onboarding Monitor",
"checklistTitle": "Passi per collegare l'installazione a Monitor", "checklistTitle": "Passi per collegare l'installazione a Monitor",
"checklistProgress": "Avanzamento: {done}/{total} ({percent}%)", "checklistProgress": "Avanzamento: {done}/{total} ({percent}%)",
"checklistStep": "Passo", "checklistStep": "Passo",
@ -800,7 +800,7 @@
"checklistStep10Sub2": "Rapporto tempi e materiali caricato su Monitoring", "checklistStep10Sub2": "Rapporto tempi e materiali caricato su Monitoring",
"checklistStep10Sub3": "Contattare Atef se è presente un EMS esterno", "checklistStep10Sub3": "Contattare Atef se è presente un EMS esterno",
"checklistNoAttachments": "Nessun file allegato.", "checklistNoAttachments": "Nessun file allegato.",
"setupProgress": "Avanzamento installazione", "setupProgress": "Avanzamento onboarding Monitor",
"checklistPhaseEmpty": "Non avviato", "checklistPhaseEmpty": "Non avviato",
"checklistPhasePreparation": "Preparazione", "checklistPhasePreparation": "Preparazione",
"checklistPhaseOnSite": "In sito", "checklistPhaseOnSite": "In sito",
@ -810,5 +810,96 @@
"minDischargeVoltageV": "Tensione di scarica min. (V)", "minDischargeVoltageV": "Tensione di scarica min. (V)",
"maxDischargeCurrentA": "Corrente di scarica max. (A)", "maxDischargeCurrentA": "Corrente di scarica max. (A)",
"maxChargeCurrentA": "Corrente di carica max. (A)", "maxChargeCurrentA": "Corrente di carica max. (A)",
"maxChargeVoltageV": "Tensione di carica max. (V)" "maxChargeVoltageV": "Tensione di carica max. (V)",
"onSiteChecklist": "Checklist in sito",
"onSiteChecklistTitle": "Checklist di installazione in sito",
"onSiteChecklistProgress": "Avanzamento: {done}/{total} ({percent}%)",
"onSiteStepHeading": "Passaggio",
"onSiteImagesHeading": "Foto dell'installazione",
"onSiteCommentsPlaceholder": "Note, osservazioni…",
"onSiteSaveFailed": "Impossibile salvare la modifica",
"onSiteLocked": "Questa checklist è stata firmata ed è bloccata.",
"onSiteRemarksTitle": "Osservazioni dell'installatore",
"onSiteRemarksPlaceholder": "Osservazioni aggiuntive…",
"onSiteSignatureTitle": "Firma elettronica",
"onSiteFullName": "Nome completo",
"onSiteSignButton": "Firma",
"onSiteSignedBy": "Firmato da {name} il {date}",
"onSiteSignedSuccess": "Checklist firmata con successo",
"onSiteResetSignature": "Reimposta firma",
"onSiteShowMore": "Mostra di più",
"onSiteShowLess": "Mostra di meno",
"onSiteConsentShort": "Confermo che l'installazione è stata eseguita e messa in servizio secondo la checklist sopra.",
"onSiteConsentLong": "Con la firma elettronica, l'installatore esecutore conferma che l'installazione è stata eseguita e messa in servizio secondo la checklist sopra e le regole tecniche riconosciute. Le informazioni sopra sono complete e veritiere. Questa conferma fa parte della messa in servizio e viene trasmessa insieme alla checklist a inesco energy.",
"onSiteStep1Title": "Disimballaggio",
"onSiteStep1Sub1": "Disimballare sodistore home con cura",
"onSiteStep1Sub2": "Verificare le condizioni del sito (ambiente asciutto 20 a +55°C, superficie piana)",
"onSiteStep1Warning": "In caso di danni da trasporto, documentarli con foto e segnalarli a inesco.",
"onSiteStep2Title": "Installare l'inverter inesco",
"onSiteStep2Sub1": "Rimontare l'inverter sul supporto",
"onSiteStep2Sub2": "Verificare che i cavi non siano stati danneggiati durante il trasporto",
"onSiteStep2Sub3": "Ricollegare il cavo AC: GRID a GRID e BACK-UP a Load",
"onSiteStep3Title": "Lavori nel quadro principale (lato AC)",
"onSiteStep3Sub1": "Installare l'AC secondo lo schema",
"onSiteStep3Sub2": "Installare il commutatore di rete inesco (consigliato)",
"onSiteStep3Sub3": "Installare l'interruttore dell'inverter (25A o 40A a seconda del modello)",
"onSiteStep3Sub4": "Installare l'interruttore di impianto per sodistore home",
"onSiteStep3Sub5": "Collegare il cavo di alimentazione (Grid) a sodistore home",
"onSiteStep3Sub6": "Collegare il cavo Load a sodistore home",
"onSiteStep3Subheading1": "Requisiti per l'alimentazione di emergenza",
"onSiteStep3Sub7": "Definire i gruppi alimentati in emergenza nel quadro principale",
"onSiteStep3Sub8": "Attenzione: i gruppi TNS e TNC non devono essere mischiati !!!",
"onSiteStep3Sub9": "L'inverter deve essere impostato su TNS o TNC a seconda del sistema",
"onSiteStep3Sub10": "Proteggere i gruppi di emergenza con interruttore Tipo B e differenziale 300mA",
"onSiteStep3Warning": "inesco energy raccomanda un commutatore di rete in modalità di emergenza. In caso di guasto l'impianto può essere commutato sulla rete e la casa rimane comunque alimentata.",
"onSiteStep4Title": "Collegamento del contatore",
"onSiteStep4Sub1": "Installare il contatore nel quadro principale o all'interno di sodistore home",
"onSiteStep4Sub2": "Installare interruttore / 3LN per la tensione di riferimento del contatore",
"onSiteStep4Sub3": "Pinzare i TA attorno al cavo di alimentazione — rispettare il verso corretto",
"onSiteStep4Sub4": "Installare e collegare il cavo bus dal contatore all'inverter (prolunga consentita)",
"onSiteStep4Warning": "I cavi dei TA non devono essere prolungati né accorciati, altrimenti le misure risulteranno sbagliate. I TA devono mantenere una distanza minima di 15 cm da altri TA o segnali ad alta frequenza nell'area di alta tensione, altrimenti anche le misure possono risultare errate.",
"onSiteStep5Title": "inesco Gateway e internet",
"onSiteStep5Sub1": "Tirare un cavo internet fino al inesco Gateway",
"onSiteStep5Sub2": "Verificare il collegamento inesco Gateway — inverter (USB verso BMS)",
"onSiteStep5Sub3": "Alimentare inesco Gateway",
"onSiteStep5Sub4": "Stabilire la connessione WiFi con l'inverter",
"onSiteStep5Warning": "Gli inverter richiedono assolutamente una connessione WiFi — non LAN RJ45 — altrimenti non possono comunicare.",
"onSiteStep6Title": "Connessioni di sistema",
"onSiteStep6Sub1": "Collegamenti di terra",
"onSiteStep6Sub2": "inesco Gateway (USB) collegato all'inverter (tramite splitter)",
"onSiteStep6Sub3": "Batterie +/- verso l'inverter secondo lo schema",
"onSiteStep6Sub4": "CAN dalla batteria all'inverter su BMS (tramite splitter)",
"onSiteStep6Sub5": "Concatenare le batterie RS485 2 a RS485 2",
"onSiteStep6Sub6": "Dallo splitter all'inverter",
"onSiteStep6Sub7": "Per gli assemblaggi 27/36 gli inverter devono essere collegati via PARA",
"onSiteStep6Warning1": "Le connessioni di sistema devono essere cablate secondo lo schema. Ogni assemblaggio (9, 18, 27 e 36) ha il proprio schema da rispettare.",
"onSiteStep6Warning2": "I cavi RJ45 non devono essere invertiti. Il cavo nero è collegato al contatto A e va al inesco Gateway tramite USB. Il contatto B è collegato alla batteria.",
"onSiteStep6Warning3": "I cavi hanno piedinature diverse e sono crimpati di conseguenza — non devono essere invertiti.",
"onSiteStep6Warning4": "Concatenare al massimo 2 batterie +/-.",
"onSiteStep7Title": "Collegamento inverter PV",
"onSiteStep7Sub1": "Collegare le linee DC all'inverter inesco — rispettare le tensioni di stringa.",
"onSiteStep7Warning": "Se l'impianto è accoppiato in AC, l'inverter esistente rimane in funzione e l'inverter inesco non viene collegato alle linee DC dell'impianto PV.",
"onSiteStep8Title": "Messa in servizio",
"onSiteStep8Sub1": "Messa in servizio per telefono con inesco",
"onSiteStep8Sub2": "Ispezione visiva",
"onSiteStep8Sub3": "Misurazioni secondo la norma — DC e AC",
"onSiteStep8Sub4": "Mettere sotto tensione",
"onSiteStep8Sub5": "Accendere l'interruttore DC sull'inverter",
"onSiteStep8Sub6": "Accendere le batterie — portare il pulsante su ON",
"onSiteStep8Sub7": "Portare il breaker delle batterie su ON",
"onSiteStep8Sub8": "Verificare che tutti i dispositivi siano visualizzati (inverter, batteria, inesco Gateway)",
"onSiteStep8Sub9": "Aprire l'impianto tramite l'app ESS Link",
"onSiteStep8Sub10": "Configurare per il cliente tramite l'app ESS Link",
"onSiteStep8Sub11": "Confrontare il contatore del distributore (stessi valori in kW del portale)",
"onSiteStep9Title": "Completamento",
"onSiteStep9Sub1": "Firmare la checklist e inviarla a inesco energy (kontakt@inesco.energy)"
} }