Compare commits

..

No commits in common. "b0e3e4755385e3f3b87d6c643b0a070aa70cd09c" and "ffc5b124100186c239ba08b3cdd1af47f5f815c2" have entirely different histories.

16 changed files with 21 additions and 544 deletions

View File

@ -891,59 +891,13 @@ public class Controller : ControllerBase
[HttpPut(nameof(UpdateUser))]
public async Task<ActionResult<User>> UpdateUser([FromBody] User updatedUser, Token authToken)
public ActionResult<User> UpdateUser([FromBody] User updatedUser, Token authToken)
{
var session = Db.GetSession(authToken);
var sessionUser = session?.User;
var session = Db.GetSession(authToken);
// Normalize: trim surrounding whitespace so a stray space can't cause a login mismatch.
updatedUser.Email = updatedUser.Email?.Trim();
// Capture the original row BEFORE the update — needed to detect an email change
// and to roll back if the set-password email can't be delivered.
var original = Db.GetUserById(updatedUser.Id);
var emailChanged = original is not null
&& !String.Equals(original.Email, updatedUser.Email, StringComparison.OrdinalIgnoreCase);
// An email change re-registers the account (resets the password + sends mail), so it is
// ADMIN-ONLY. Checked BEFORE session.Update so a non-admin's email change is never written
// (partners may still edit name/role/information of their own users — that path is unchanged).
if (emailChanged && sessionUser?.UserType != 2) // 2 == admin
if (!session.Update(updatedUser))
return Unauthorized();
// Reject a malformed new email before anything is written (defense alongside the frontend check).
if (emailChanged &&
!System.Text.RegularExpressions.Regex.IsMatch(updatedUser.Email ?? "", @"^[^\s@]+@[^\s@]+\.[^\s@]+$"))
return BadRequest("Invalid email format");
if (!session.Update(updatedUser)) // enforces permissions + email uniqueness; preserves parent/access
return Unauthorized();
if (emailChanged)
{
// Re-register: force the new address to set its own password via the welcome email.
// ACCEPTED LIMITATION: this is two DB writes (email via session.Update, then password
// here) plus an external email send, with no enclosing transaction (the codebase uses a
// single static connection and no transactions). A crash between the password clear and a
// successful send leaves the new email + an empty password (MustResetPassword=true). That
// state is recoverable by the admin re-applying the change, and the empty-password +
// MustResetPassword combination is the same posture as a freshly CreateUser'd account.
// We clear the password BEFORE sending (favouring the new owner's access) rather than
// after — sending first would, on a crash, lock the new owner out behind the old password.
Db.DeleteUserPassword(updatedUser); // Password="", MustResetPassword=true
var mailSuccess = await Db.SendNewUserEmail(updatedUser); // set-password mail -> NEW email
if (!mailSuccess)
{
// Full revert of the entire row to its pre-edit state — this also discards any
// Name/Role/Information edits made in the same Apply, by design (all-or-nothing).
Db.Update(original!);
return StatusCode(500, "Set-password email failed to send; email change reverted");
}
await Db.SendEmailChangedReminder(updatedUser, original!.Email); // reminder -> OLD email, best-effort
}
return updatedUser.HidePassword();
}

View File

@ -61,8 +61,8 @@ public class Installation : TreeNode
public string PvStringsPerInverter { get; set; } = "";
public string InstallationModel { get; set; } = "";
public string ExternalEms { get; set; } = "No";
public string? CouplingType { get; set; }
public string? BackupLoad { get; set; }
public string CouplingType { get; set; } = "DC";
public string BackupLoad { get; set; } = "None";
[Ignore]
public String OrderNumbers { get; set; }

View File

@ -298,41 +298,6 @@ public static class UserMethods
return user.SendEmail(subject, body);
}
public static Task SendEmailChangedNotice(this User user, String oldEmail)
{
var newEmail = user.Email;
var (subject, body) = (user.Language ?? "en") switch
{
"de" => (
"Die E-Mail-Adresse Ihres inesco energy Kontos wurde geändert",
$"Sehr geehrte/r {user.Name}\n" +
$"Die E-Mail-Adresse Ihres inesco energy Kontos wurde auf {newEmail} geändert. " +
$"Falls Sie diese Änderung nicht veranlasst haben, wenden Sie sich bitte an das inesco energy Support-Team."
),
"fr" => (
"L'adresse e-mail de votre compte inesco energy a été modifiée",
$"Cher/Chère {user.Name}\n" +
$"L'adresse e-mail de votre compte inesco energy a été remplacée par {newEmail}. " +
$"Si vous n'êtes pas à l'origine de cette modification, veuillez contacter l'équipe d'assistance inesco energy."
),
"it" => (
"L'indirizzo email del tuo account inesco energy è stato modificato",
$"Gentile {user.Name}\n" +
$"L'indirizzo email del tuo account inesco energy è stato cambiato in {newEmail}. " +
$"Se non hai richiesto questa modifica, contatta il team di supporto inesco energy."
),
_ => (
"Your inesco energy account email was changed",
$"Dear {user.Name}\n" +
$"The email address for your inesco energy account was changed to {newEmail}. " +
$"If you did not request this change, please contact the inesco energy Support Team."
)
};
return Mailer.Send(user.Name, oldEmail, subject, body);
}
public static Task SendTicketAssignedEmail(this User user, Ticket ticket)
{
var ticketLink = $"https://monitor.inesco.energy/tickets/{ticket.Id}";

View File

@ -294,6 +294,7 @@ public static partial class Db
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 PvStringsPerInverter = '' WHERE PvStringsPerInverter IS NULL");
fileConnection.Execute("UPDATE Installation SET BackupLoad = 'None' WHERE BackupLoad IS NULL");
return fileConnection;
//return CopyDbToMemory(fileConnection);
@ -516,21 +517,6 @@ public static partial class Db
}
}
public static async Task<Boolean> SendEmailChangedReminder(User user, String oldEmail)
{
try
{
await user.SendEmailChangedNotice(oldEmail);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Email-changed reminder failed for {oldEmail}");
Console.WriteLine(ex.ToString());
return false;
}
}
public static Boolean DeleteUserPassword(User user)
{
user.Password = "";

View File

@ -376,14 +376,7 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
})
.catch(() => {});
}
updateInstallation(
{
...formValues,
couplingType: formValues.couplingType || null,
backupLoad: formValues.backupLoad || null
},
props.type
);
updateInstallation(formValues, props.type);
};
const handleDelete = () => {
@ -919,15 +912,12 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
</InputLabel>
<Select
name="couplingType"
value={formValues.couplingType || ''}
value={formValues.couplingType || 'DC'}
onChange={handleChange}
inputProps={{ readOnly: !canEdit }}
displayEmpty
notched
>
<MenuItem value="">
<em><FormattedMessage id="notSet" defaultMessage="Not set" /></em>
</MenuItem>
<MenuItem value="AC">
<FormattedMessage id="couplingAC" defaultMessage="AC-coupled" />
</MenuItem>
@ -951,15 +941,12 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
</InputLabel>
<Select
name="backupLoad"
value={formValues.backupLoad || ''}
value={formValues.backupLoad || 'None'}
onChange={handleChange}
inputProps={{ readOnly: !canEdit }}
displayEmpty
notched
>
<MenuItem value="">
<em><FormattedMessage id="notSet" defaultMessage="Not set" /></em>
</MenuItem>
<MenuItem value="Whole">
<FormattedMessage id="backupWhole" defaultMessage="Whole house" />
</MenuItem>

View File

@ -82,13 +82,6 @@ function UserAccess(props: UserAccessProps) {
const [openFolder, setOpenFolder] = useState(false);
const [openModal, setOpenModal] = useState(false);
// Pending access revoke awaiting confirmation (folder or direct installation)
const [revokeTarget, setRevokeTarget] = useState<{
type: 'folder' | 'installation';
id: number;
name: string;
} | null>(null);
const [selectedFolderNames, setSelectedFolderNames] = useState<string[]>([]);
const [selectedInstallations, setSelectedInstallations] = useState<I_Installation[]>([]);
@ -255,16 +248,6 @@ function UserAccess(props: UserAccessProps) {
});
};
const confirmRevoke = () => {
if (!revokeTarget) return;
if (revokeTarget.type === 'folder') {
handleRevokeFolder(revokeTarget.id, revokeTarget.name);
} else {
handleRevokeInstallation(revokeTarget.id);
}
setRevokeTarget(null);
};
const handleSubmit = async () => {
for (const folderName of selectedFolderNames) {
const folder = availableFolders.find((f) => f.name === folderName);
@ -422,66 +405,6 @@ function UserAccess(props: UserAccessProps) {
</Box>
</Modal>
{/* Revoke Access Confirmation Modal */}
<Modal
open={revokeTarget !== null}
onClose={() => setRevokeTarget(null)}
aria-labelledby="revoke-confirm-modal"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 350,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography variant="body1" gutterBottom sx={{ fontWeight: 'bold', textAlign: 'center' }}>
<FormattedMessage
id="confirmRevokeAccess"
defaultMessage='Do you want to remove access to "{name}"?'
values={{ name: revokeTarget?.name ?? '' }}
/>
</Typography>
<div style={{ display: 'flex', alignItems: 'center', marginTop: 10 }}>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={confirmRevoke}
>
<FormattedMessage id="delete" defaultMessage="Delete" />
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={() => setRevokeTarget(null)}
>
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
</div>
</Box>
</Modal>
<Button
variant="contained"
onClick={handleGrantAccess}
@ -514,12 +437,7 @@ function UserAccess(props: UserAccessProps) {
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
{currentUser.userType === UserType.admin && (
<IconButton
onClick={() =>
setRevokeTarget({ type: 'folder', id: folder.id, name: folder.name })
}
edge="end"
>
<IconButton onClick={() => handleRevokeFolder(folder.id, folder.name)} edge="end">
<PersonRemoveIcon />
</IconButton>
)}
@ -611,16 +529,7 @@ function UserAccess(props: UserAccessProps) {
sx={{ mb: isLast ? 4 : 0 }}
secondaryAction={
currentUser.userType === UserType.admin && (
<IconButton
onClick={() =>
setRevokeTarget({
type: 'installation',
id: installation.id,
name: installation.name
})
}
edge="end"
>
<IconButton onClick={() => handleRevokeInstallation(installation.id)} edge="end">
<PersonRemoveIcon />
</IconButton>
)

View File

@ -501,7 +501,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
currentTab != 'installationTickets' &&
currentTab != 'documents' &&
currentTab != 'checklist' &&
currentTab != 'onsiteChecklist' && (
currentTab != 'onSiteChecklist' && (
<Container
maxWidth="xl"
sx={{

View File

@ -535,43 +535,6 @@ function SodistoreHomeConfigurationV2(props: SodistoreHomeConfigurationProps) {
/>
</div>
{device === 4 && (product === 2 || product === 5) && (
<>
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
<FormattedMessage id="digitalInputs" defaultMessage="Digital Inputs" />
</Typography>
<Divider sx={{ mb: 2 }} />
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3, mb: 1, ml: 1 }}>
{[1, 2, 3, 4].map((n) => {
const raw = (props.values.Config as any)[`DigitalInput${n}`];
// No datapath (field absent/null) → grey; otherwise green/red
const indicator =
raw == null
? '⚪'
: String(raw).toLowerCase() === 'true'
? '🟢'
: '🔴';
return (
<Box
key={`digitalInput${n}`}
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
>
<span style={{ fontSize: '1.1rem', lineHeight: 1 }}>
{indicator}
</span>
<Typography component="span">
<FormattedMessage
id={`digitalInput${n}`}
defaultMessage={`Digital Input ${n}`}
/>
</Typography>
</Box>
);
})}
</Box>
</>
)}
{device === 4 && (
<>
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>

View File

@ -14,17 +14,9 @@ import {
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
import { useSearchParams } from 'react-router-dom';
import { InnovEnergyUser, UserType } from 'src/interfaces/UserTypes';
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import User from './User';
// Translation keys for each UserType, indexed by the enum value
// (client=0, partner=1, admin=2).
export const USER_ROLE_LABEL_IDS: Record<UserType, string> = {
[UserType.client]: 'roleClient',
[UserType.partner]: 'rolePartner',
[UserType.admin]: 'roleAdmin'
};
interface FlatUsersViewProps {
users: InnovEnergyUser[];
fetchDataAgain: () => void;
@ -87,7 +79,6 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
<TableRow>
<TableCell padding="checkbox"></TableCell>
<TableCell><FormattedMessage id="username" defaultMessage="Username" /></TableCell>
<TableCell><FormattedMessage id="role" defaultMessage="Role" /></TableCell>
<TableCell><FormattedMessage id="email" defaultMessage="Email" /></TableCell>
</TableRow>
</TableHead>
@ -124,21 +115,6 @@ const FlatUsersView = (props: FlatUsersViewProps) => {
{user.name}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
<FormattedMessage
id={
USER_ROLE_LABEL_IDS[user.userType] ?? 'roleClient'
}
/>
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"

View File

@ -42,12 +42,6 @@ function User(props: singleUserProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [updated, setUpdated] = useState(false);
// Tracks whether the last successful save changed the email (re-registration banner).
const [emailChanged, setEmailChanged] = useState(false);
// Specific error message key for the email-change failure cases; null => generic error.
const [errorMsgId, setErrorMsgId] = useState<string | null>(null);
// Confirmation dialog shown before an email change (resets password, emails both addresses).
const [openModalEmailChange, setOpenModalEmailChange] = useState(false);
// Active tab is kept in the URL (?tab=) alongside ?userId= so Back restores it.
const [searchParams, setSearchParams] = useSearchParams();
const [currentTab, setCurrentTab] = useState<string>(
@ -117,13 +111,7 @@ function User(props: singleUserProps) {
});
};
// Basic email-format guard (frontend half of the validation; backend enforces too).
const emailIsValid = (email: string) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((email ?? '').trim());
// Performs the actual update request. Called directly for non-email edits, or from
// the confirmation dialog when the email changed.
const doUpdate = async (didEmailChange: boolean) => {
const handleSubmit = async (e) => {
setLoading(true);
setError(false);
@ -131,18 +119,6 @@ function User(props: singleUserProps) {
.put(`/UpdateUser`, formValues)
.catch((err) => {
if (err.response) {
if (didEmailChange && err.response.status === 500) {
// Backend reverted the change because the set-password email failed.
setErrorMsgId('userEmailChangeFailedReverted');
} else if (didEmailChange && err.response.status === 401) {
// Email already in use by another user, or not permitted (admins only).
setErrorMsgId('userEmailChangeNotAllowedOrTaken');
} else if (didEmailChange && err.response.status === 400) {
// Backend rejected a malformed email (frontend normally catches this first).
setErrorMsgId('invalidEmailFormat');
} else {
setErrorMsgId(null);
}
setError(true);
setLoading(false);
}
@ -151,7 +127,6 @@ function User(props: singleUserProps) {
if (res) {
props.fetchDataAgain();
setLoading(false);
setEmailChanged(didEmailChange);
setUpdated(true);
setTimeout(() => {
@ -160,37 +135,6 @@ function User(props: singleUserProps) {
}
};
const handleSubmit = async (e) => {
setError(false);
// Compare against the originally-loaded email to detect a re-registration.
const didEmailChange = formValues.email !== props.current_user.email;
// Reject a malformed new email before anything is sent (defense alongside backend guard).
if (didEmailChange && !emailIsValid(formValues.email)) {
setErrorMsgId('invalidEmailFormat');
setError(true);
return;
}
// An email change resets the password and emails both addresses — confirm first.
if (didEmailChange) {
setOpenModalEmailChange(true);
return;
}
await doUpdate(false);
};
const emailChangeModalConfirm = async () => {
setOpenModalEmailChange(false);
await doUpdate(true);
};
const emailChangeModalCancel = () => {
setOpenModalEmailChange(false);
};
const handleDelete = (e) => {
setLoading(true);
setError(false);
@ -310,94 +254,6 @@ function User(props: singleUserProps) {
</Modal>
)}
{openModalEmailChange && (
<Modal
open={openModalEmailChange}
onClose={emailChangeModalCancel}
aria-labelledby="email-change-modal"
aria-describedby="email-change-modal-description"
>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
borderRadius: 4,
boxShadow: 24,
p: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
<Typography
variant="body1"
gutterBottom
sx={{ fontWeight: 'bold' }}
>
<FormattedMessage
id="emailChangeConfirmTitle"
defaultMessage="Change this user's email?"
/>
</Typography>
<Typography
variant="body2"
sx={{ mt: 1, textAlign: 'center', wordBreak: 'break-all' }}
>
{props.current_user.email} {formValues.email}
</Typography>
<Typography
variant="body2"
sx={{ mt: 2, textAlign: 'center' }}
>
<FormattedMessage
id="emailChangeConfirmWarning"
defaultMessage="This resets the user's password and emails both the new and the previous address. The user must set a new password via the link sent to the new address."
/>
</Typography>
<div
style={{
display: 'flex',
alignItems: 'center',
marginTop: 10
}}
>
<Button
sx={{
marginTop: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={emailChangeModalConfirm}
>
<FormattedMessage id="confirm" defaultMessage="Confirm" />
</Button>
<Button
sx={{
marginTop: 2,
marginLeft: 2,
textTransform: 'none',
bgcolor: '#ffc04d',
color: '#111111',
'&:hover': { bgcolor: '#f7b34d' }
}}
onClick={emailChangeModalCancel}
>
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
</div>
</Box>
</Modal>
)}
<Grid item xs={12} md={12}>
<TabsContainerWrapper>
<Tabs
@ -549,11 +405,7 @@ function User(props: singleUserProps) {
alignItems: 'center'
}}
>
{errorMsgId ? (
<FormattedMessage id={errorMsgId} />
) : (
<FormattedMessage id="errorOccured" />
)}
An error has occurred
<IconButton
color="inherit"
size="small"
@ -573,11 +425,7 @@ function User(props: singleUserProps) {
alignItems: 'center'
}}
>
{emailChanged ? (
<FormattedMessage id="userEmailChangedSetPasswordSent" />
) : (
<FormattedMessage id="successfullyUpdated" />
)}
Successfully updated
<IconButton
color="inherit"
size="small"

View File

@ -5,7 +5,6 @@ import {
Grid,
IconButton,
InputAdornment,
MenuItem,
TextField
} from '@mui/material';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
@ -21,8 +20,6 @@ import { UserType } from '../../../interfaces/UserTypes';
function UsersSearch() {
const intl = useIntl();
const [searchTerm, setSearchTerm] = useState('');
// 'all' = no role filter; otherwise the selected UserType enum value.
const [roleFilter, setRoleFilter] = useState<UserType | 'all'>('all');
const { allUsers, fetchAllUsers } = useContext(AccessContext);
const [filteredData, setFilteredData] = useState(allUsers);
const [openModal, setOpenModal] = useState(false);
@ -40,13 +37,11 @@ function UsersSearch() {
};
useEffect(() => {
const filtered = allUsers.filter(
(item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
(roleFilter === 'all' || item.userType === roleFilter)
const filtered = allUsers.filter((item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredData(filtered);
}, [searchTerm, roleFilter, allUsers]);
}, [searchTerm, allUsers]);
const handleSubmit = () => {
setOpenModal(true);
@ -167,36 +162,6 @@ function UsersSearch() {
/>
</FormControl>
</Grid>
<Grid item xs={12} md={isMobile ? 5 : 3}>
<FormControl variant="outlined" fullWidth>
<TextField
select
label={intl.formatMessage({ id: 'role' })}
value={roleFilter}
onChange={(e) =>
setRoleFilter(
e.target.value === 'all'
? 'all'
: (Number(e.target.value) as UserType)
)
}
fullWidth
>
<MenuItem value="all">
{intl.formatMessage({ id: 'allRoles' })}
</MenuItem>
<MenuItem value={UserType.client}>
{intl.formatMessage({ id: 'roleClient' })}
</MenuItem>
<MenuItem value={UserType.partner}>
{intl.formatMessage({ id: 'rolePartner' })}
</MenuItem>
<MenuItem value={UserType.admin}>
{intl.formatMessage({ id: 'roleAdmin' })}
</MenuItem>
</TextField>
</FormControl>
</Grid>
</Grid>
<FlatUsersView users={filteredData} fetchDataAgain={fetchDataAgain} />
</>

View File

@ -28,8 +28,8 @@ export interface I_Installation extends I_S3Credentials {
pvStringsPerInverter: string;
installationModel: string;
externalEms: string;
couplingType: string | null;
backupLoad: string | null;
couplingType: string;
backupLoad: string;
parentId: number;
s3WriteKey: string;

View File

@ -139,7 +139,6 @@
"backupWhole": "Komplettes Haus",
"backupPartial": "Teilnetz",
"backupNone": "nicht verwendet",
"notSet": "Nicht festgelegt",
"selectModel": "Modell auswählen...",
"inverterN": "Wechselrichter {n}",
"clusterN": "Cluster {n}",
@ -182,7 +181,6 @@
"unableToLoadData": "Daten können nicht geladen werden",
"unableToRevokeAccess": "Der Zugriff konnte nicht widerrufen werden",
"revokedAccessFromUser": "Zugriff vom Benutzer widerrufen",
"confirmRevokeAccess": "Möchten Sie den Zugriff auf \"{name}\" entfernen?",
"Show Errors": "Fehler anzeigen",
"Show Warnings": "Warnungen anzeigen",
"lastSeen": "Zuletzt gesehen",
@ -532,11 +530,6 @@
"stopTimeMustBeLater": "Die Stoppzeit muss nach der Startzeit liegen",
"signIn": "Anmelden",
"username": "Benutzername",
"role": "Rolle",
"allRoles": "Alle Rollen",
"roleClient": "Kunde",
"rolePartner": "Partner",
"roleAdmin": "Admin",
"password": "Passwort",
"rememberMe": "Angemeldet bleiben",
"login": "Anmelden",
@ -544,13 +537,6 @@
"forgotPasswordLink": "Passwort vergessen?",
"provideYourUsername": "Geben Sie Ihren Benutzernamen ein",
"userName": "Benutzername",
"userEmailChangedSetPasswordSent": "E-Mail aktualisiert. Eine E-Mail zum Festlegen des Passworts wurde an die neue Adresse gesendet, die bisherige Adresse wurde benachrichtigt.",
"userEmailChangeFailedReverted": "Die E-Mail zum Festlegen des Passworts konnte nicht gesendet werden — die Änderung wurde rückgängig gemacht.",
"userEmailChangeNotAllowedOrTaken": "Die E-Mail konnte nicht geändert werden — sie wird bereits von einem anderen Benutzer verwendet oder ist nicht zulässig.",
"invalidEmailFormat": "Bitte geben Sie eine gültige E-Mail-Adresse ein.",
"emailChangeConfirmTitle": "E-Mail-Adresse dieses Benutzers ändern?",
"emailChangeConfirmWarning": "Dadurch wird das Passwort des Benutzers zurückgesetzt und sowohl die neue als auch die bisherige Adresse benachrichtigt. Der Benutzer muss über den an die neue Adresse gesendeten Link ein neues Passwort festlegen.",
"confirm": "Bestätigen",
"resetPassword": "Passwort zurücksetzen",
"setNewPassword": "Neues Passwort setzen",
"verifyPassword": "Passwort bestätigen",
@ -603,11 +589,6 @@
"relay2": "Relais 2",
"relay3": "Relais 3",
"relay4": "Relais 4",
"digitalInputs": "Digitaleingänge",
"digitalInput1": "Digitaleingang 1",
"digitalInput2": "Digitaleingang 2",
"digitalInput3": "Digitaleingang 3",
"digitalInput4": "Digitaleingang 4",
"systemSettings": "Systemeinstellungen",
"pvPerInverter": "PV pro Wechselrichter",
"pvInInverter": "PV in Wechselrichter {number}",

View File

@ -121,7 +121,6 @@
"backupWhole": "Whole house",
"backupPartial": "Partial",
"backupNone": "Not used",
"notSet": "Not set",
"selectModel": "Select model...",
"inverterN": "Inverter {n}",
"clusterN": "Cluster {n}",
@ -164,7 +163,6 @@
"unableToLoadData": "Unable to load data",
"unableToRevokeAccess": "Unable to revoke access",
"revokedAccessFromUser": "Revoked access from user: ",
"confirmRevokeAccess": "Do you want to remove access to \"{name}\"?",
"Show Errors": "Show Errors",
"Show Warnings": "Show Warnings",
"lastSeen": "Last seen",
@ -280,11 +278,6 @@
"stopTimeMustBeLater": "Stop time must be later than start time",
"signIn": "Sign in",
"username": "Username",
"role": "Role",
"allRoles": "All roles",
"roleClient": "Client",
"rolePartner": "Partner",
"roleAdmin": "Admin",
"password": "Password",
"rememberMe": "Remember me",
"login": "Login",
@ -292,13 +285,6 @@
"forgotPasswordLink": "Forgot password?",
"provideYourUsername": "Provide your username",
"userName": "User Name",
"userEmailChangedSetPasswordSent": "Email updated. A set-password email was sent to the new address, and the previous address was notified.",
"userEmailChangeFailedReverted": "Couldn't send the set-password email — the email change was reverted.",
"userEmailChangeNotAllowedOrTaken": "Couldn't change the email — it is already in use by another user or not permitted.",
"invalidEmailFormat": "Please enter a valid email address.",
"emailChangeConfirmTitle": "Change this user's email?",
"emailChangeConfirmWarning": "This resets the user's password and emails both the new and the previous address. The user must set a new password via the link sent to the new address.",
"confirm": "Confirm",
"resetPassword": "Reset Password",
"setNewPassword": "Set New Password",
"verifyPassword": "Verify Password",
@ -351,11 +337,6 @@
"relay2": "Relay 2",
"relay3": "Relay 3",
"relay4": "Relay 4",
"digitalInputs": "Digital Inputs",
"digitalInput1": "Digital Input 1",
"digitalInput2": "Digital Input 2",
"digitalInput3": "Digital Input 3",
"digitalInput4": "Digital Input 4",
"systemSettings": "System Settings",
"pvPerInverter": "PV per Inverter",
"pvInInverter": "PV in Inverter {number}",

View File

@ -133,7 +133,6 @@
"backupWhole": "Maison entière",
"backupPartial": "Partiel",
"backupNone": "Non utilisé",
"notSet": "Non défini",
"selectModel": "Sélectionner le modèle...",
"inverterN": "Onduleur {n}",
"clusterN": "Cluster {n}",
@ -176,7 +175,6 @@
"unableToLoadData": "Impossible de charger les données",
"unableToRevokeAccess": "Impossible de révoquer l'accès",
"revokedAccessFromUser": "Accès révoqué de l'utilisateur",
"confirmRevokeAccess": "Voulez-vous retirer l'accès à « {name} » ?",
"Show Errors": "Afficher les erreurs",
"Show Warnings": "Afficher les avertissements",
"lastSeen": "Dernière connexion",
@ -532,11 +530,6 @@
"stopTimeMustBeLater": "L'heure d'arrêt doit être postérieure à l'heure de début",
"signIn": "Se connecter",
"username": "Nom d'utilisateur",
"role": "Rôle",
"allRoles": "Tous les rôles",
"roleClient": "Client",
"rolePartner": "Partenaire",
"roleAdmin": "Admin",
"password": "Mot de passe",
"rememberMe": "Se souvenir de moi",
"login": "Connexion",
@ -544,13 +537,6 @@
"forgotPasswordLink": "Mot de passe oublié ?",
"provideYourUsername": "Entrez votre nom d'utilisateur",
"userName": "Nom d'utilisateur",
"userEmailChangedSetPasswordSent": "E-mail mis à jour. Un e-mail de définition du mot de passe a été envoyé à la nouvelle adresse, et l'ancienne adresse a été avertie.",
"userEmailChangeFailedReverted": "Impossible d'envoyer l'e-mail de définition du mot de passe — la modification a été annulée.",
"userEmailChangeNotAllowedOrTaken": "Impossible de modifier l'e-mail — il est déjà utilisé par un autre utilisateur ou n'est pas autorisé.",
"invalidEmailFormat": "Veuillez saisir une adresse e-mail valide.",
"emailChangeConfirmTitle": "Modifier l'e-mail de cet utilisateur ?",
"emailChangeConfirmWarning": "Cela réinitialise le mot de passe de l'utilisateur et avertit à la fois la nouvelle et l'ancienne adresse. L'utilisateur doit définir un nouveau mot de passe via le lien envoyé à la nouvelle adresse.",
"confirm": "Confirmer",
"resetPassword": "Réinitialiser le mot de passe",
"setNewPassword": "Définir un nouveau mot de passe",
"verifyPassword": "Vérifier le mot de passe",
@ -603,11 +589,6 @@
"relay2": "Relais 2",
"relay3": "Relais 3",
"relay4": "Relais 4",
"digitalInputs": "Entrées numériques",
"digitalInput1": "Entrée numérique 1",
"digitalInput2": "Entrée numérique 2",
"digitalInput3": "Entrée numérique 3",
"digitalInput4": "Entrée numérique 4",
"systemSettings": "Paramètres système",
"pvPerInverter": "PV par onduleur",
"pvInInverter": "PV dans l'onduleur {number}",

View File

@ -121,7 +121,6 @@
"backupWhole": "Intera casa",
"backupPartial": "Parziale",
"backupNone": "Non utilizzato",
"notSet": "Non impostato",
"selectModel": "Seleziona modello...",
"inverterN": "Inverter {n}",
"clusterN": "Cluster {n}",
@ -164,7 +163,6 @@
"unableToLoadData": "Impossibile caricare i dati",
"unableToRevokeAccess": "Impossibile revocare l'accesso",
"revokedAccessFromUser": "Accesso revocato all'utente: ",
"confirmRevokeAccess": "Vuoi rimuovere l'accesso a \"{name}\"?",
"alarms": "Allarmi",
"overview": "Panoramica",
"manage": "Gestione accessi",
@ -532,11 +530,6 @@
"stopTimeMustBeLater": "L'ora di fine deve essere successiva all'ora di inizio",
"signIn": "Accedi",
"username": "Nome utente",
"role": "Ruolo",
"allRoles": "Tutti i ruoli",
"roleClient": "Cliente",
"rolePartner": "Partner",
"roleAdmin": "Admin",
"password": "Password",
"rememberMe": "Ricordami",
"login": "Accedi",
@ -544,13 +537,6 @@
"forgotPasswordLink": "Password dimenticata?",
"provideYourUsername": "Inserisci il tuo nome utente",
"userName": "Nome utente",
"userEmailChangedSetPasswordSent": "Email aggiornata. Un'email per impostare la password è stata inviata al nuovo indirizzo e il precedente è stato avvisato.",
"userEmailChangeFailedReverted": "Impossibile inviare l'email per impostare la password — la modifica è stata annullata.",
"userEmailChangeNotAllowedOrTaken": "Impossibile modificare l'email — è già utilizzata da un altro utente o non è consentita.",
"invalidEmailFormat": "Inserisci un indirizzo email valido.",
"emailChangeConfirmTitle": "Modificare l'email di questo utente?",
"emailChangeConfirmWarning": "Questo reimposta la password dell'utente e avvisa sia il nuovo indirizzo sia quello precedente. L'utente deve impostare una nuova password tramite il link inviato al nuovo indirizzo.",
"confirm": "Conferma",
"resetPassword": "Reimposta password",
"setNewPassword": "Imposta nuova password",
"verifyPassword": "Verifica password",
@ -603,11 +589,6 @@
"relay2": "Relè 2",
"relay3": "Relè 3",
"relay4": "Relè 4",
"digitalInputs": "Ingressi digitali",
"digitalInput1": "Ingresso digitale 1",
"digitalInput2": "Ingresso digitale 2",
"digitalInput3": "Ingresso digitale 3",
"digitalInput4": "Ingresso digitale 4",
"systemSettings": "Impostazioni di sistema",
"pvPerInverter": "PV per inverter",
"pvInInverter": "PV nell'inverter {number}",