Added network provider in Information tab

This commit is contained in:
Yinyin Liu 2026-03-09 13:43:11 +01:00
parent c102ab3335
commit 98abd68366
11 changed files with 228 additions and 5 deletions

View File

@ -752,7 +752,17 @@ public class Controller : ControllerBase
return installation.HideParentIfUserHasNoAccessToParent(session!.User); return installation.HideParentIfUserHasNoAccessToParent(session!.User);
} }
[HttpGet(nameof(GetNetworkProviders))]
public ActionResult<IReadOnlyList<string>> GetNetworkProviders(Token authToken)
{
var session = Db.GetSession(authToken);
if (session is null)
return Unauthorized();
return Ok(NetworkProviderService.GetProviders());
}
[HttpPost(nameof(AcknowledgeError))] [HttpPost(nameof(AcknowledgeError))]
public ActionResult AcknowledgeError(Int64 id, Token authToken) public ActionResult AcknowledgeError(Int64 id, Token authToken)
{ {

View File

@ -54,4 +54,5 @@ public class Installation : TreeNode
public String OrderNumbers { get; set; } public String OrderNumbers { get; set; }
public String VrmLink { get; set; } = ""; public String VrmLink { get; set; } = "";
public string Configuration { get; set; } = ""; public string Configuration { get; set; } = "";
public string NetworkProvider { get; set; } = "";
} }

View File

@ -28,6 +28,7 @@ public static class Program
LoadEnvFile(); LoadEnvFile();
DiagnosticService.Initialize(); DiagnosticService.Initialize();
TicketDiagnosticService.Initialize(); TicketDiagnosticService.Initialize();
NetworkProviderService.Initialize();
AlarmReviewService.StartDailyScheduler(); AlarmReviewService.StartDailyScheduler();
// DailyIngestionService.StartScheduler(); // Phase 2: enable when S3 auto-push is ready // DailyIngestionService.StartScheduler(); // Phase 2: enable when S3 auto-push is ready
// ReportAggregationService.StartScheduler(); // Phase 2: enable when scheduler should auto-run monthly/yearly // ReportAggregationService.StartScheduler(); // Phase 2: enable when scheduler should auto-run monthly/yearly

View File

@ -0,0 +1,78 @@
using Flurl.Http;
using Newtonsoft.Json.Linq;
namespace InnovEnergy.App.Backend.Services;
/// <summary>
/// Fetches and caches the list of Swiss electricity network providers (Netzbetreiber)
/// from the ELCOM/LINDAS SPARQL endpoint. Refreshes every 24 hours.
/// </summary>
public static class NetworkProviderService
{
private static IReadOnlyList<string> _providers = Array.Empty<string>();
private static Timer? _refreshTimer;
private const string SparqlEndpoint = "https://ld.admin.ch/query";
private const string SparqlQuery = @"
PREFIX schema: <http://schema.org/>
SELECT DISTINCT ?name
FROM <https://lindas.admin.ch/elcom/electricityprice>
WHERE {
?operator a schema:Organization ;
schema:name ?name .
}
ORDER BY ?name";
public static void Initialize()
{
// Fire-and-forget initial load
Task.Run(RefreshAsync);
// Refresh every 24 hours
_refreshTimer = new Timer(
_ => Task.Run(RefreshAsync),
null,
TimeSpan.FromHours(24),
TimeSpan.FromHours(24)
);
Console.WriteLine("[NetworkProviderService] initialised.");
}
public static IReadOnlyList<string> GetProviders() => _providers;
private static async Task RefreshAsync()
{
try
{
var response = await SparqlEndpoint
.WithHeader("Accept", "application/sparql-results+json")
.PostUrlEncodedAsync(new { query = SparqlQuery });
var json = await response.GetStringAsync();
var parsed = JObject.Parse(json);
var names = parsed["results"]?["bindings"]?
.Select(b => b["name"]?["value"]?.ToString())
.Where(n => !string.IsNullOrWhiteSpace(n))
.Distinct()
.OrderBy(n => n)
.ToList();
if (names is { Count: > 0 })
{
_providers = names!;
Console.WriteLine($"[NetworkProviderService] Loaded {names.Count} providers from ELCOM.");
}
else
{
Console.Error.WriteLine("[NetworkProviderService] SPARQL query returned no results.");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"[NetworkProviderService] Failed to fetch providers: {ex.Message}");
}
}
}

View File

@ -1,5 +1,6 @@
import { import {
Alert, Alert,
Autocomplete,
Box, Box,
CardContent, CardContent,
CircularProgress, CircularProgress,
@ -14,13 +15,14 @@ import {
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material'; import { Close as CloseIcon } from '@mui/icons-material';
import React, { useContext, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { I_S3Credentials } from '../../../interfaces/S3Types'; import { I_S3Credentials } from '../../../interfaces/S3Types';
import { I_Installation } from '../../../interfaces/InstallationTypes'; import { I_Installation } from '../../../interfaces/InstallationTypes';
import { InstallationsContext } from '../../../contexts/InstallationsContextProvider'; import { InstallationsContext } from '../../../contexts/InstallationsContextProvider';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
import axiosConfig from '../../../Resources/axiosConfig';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
@ -53,6 +55,24 @@ function Information(props: InformationProps) {
deleteInstallation deleteInstallation
} = installationContext; } = installationContext;
const canEdit = currentUser.userType == UserType.admin;
const isPartner = currentUser.userType == UserType.partner;
const isSodistore = formValues.product === 3 || formValues.product === 4;
const [networkProviders, setNetworkProviders] = useState<string[]>([]);
const [loadingProviders, setLoadingProviders] = useState(false);
useEffect(() => {
if (isSodistore) {
setLoadingProviders(true);
axiosConfig
.get('/GetNetworkProviders')
.then((res) => setNetworkProviders(res.data))
.catch(() => {})
.finally(() => setLoadingProviders(false));
}
}, []);
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormValues({ setFormValues({
@ -286,6 +306,54 @@ function Information(props: InformationProps) {
error={formValues.country === ''} error={formValues.country === ''}
/> />
</div> </div>
{isSodistore && (
<div>
<Autocomplete
freeSolo
options={networkProviders}
value={formValues.networkProvider || ''}
onChange={(_e, val) =>
setFormValues({
...formValues,
networkProvider: (val as string) || ''
})
}
onInputChange={(_e, val) =>
setFormValues({
...formValues,
networkProvider: val || ''
})
}
disabled={!canEdit && !isPartner}
loading={loadingProviders}
renderInput={(params) => (
<TextField
{...params}
label={
<FormattedMessage
id="networkProvider"
defaultMessage="Network Provider"
/>
}
variant="outlined"
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loadingProviders ? (
<CircularProgress size={20} />
) : null}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
</div>
)}
<div> <div>
<TextField <TextField
label={ label={
@ -341,7 +409,7 @@ function Information(props: InformationProps) {
/> />
</div> </div>
{currentUser.userType == UserType.admin && ( {canEdit && (
<> <>
<div> <div>
<TextField <TextField
@ -400,7 +468,7 @@ function Information(props: InformationProps) {
marginTop: 10 marginTop: 10
}} }}
> >
{currentUser.userType == UserType.admin && ( {canEdit && (
<Button <Button
variant="contained" variant="contained"
onClick={handleDelete} onClick={handleDelete}
@ -414,7 +482,7 @@ function Information(props: InformationProps) {
/> />
</Button> </Button>
)} )}
{currentUser.userType == UserType.admin && ( {(canEdit || (isPartner && isSodistore)) && (
<Button <Button
variant="contained" variant="contained"
onClick={handleSubmit} onClick={handleSubmit}

View File

@ -1,5 +1,6 @@
import { import {
Alert, Alert,
Autocomplete,
Box, Box,
CardContent, CardContent,
CircularProgress, CircularProgress,
@ -26,6 +27,7 @@ import { UserContext } from '../../../contexts/userContext';
import routes from '../../../Resources/routes.json'; import routes from '../../../Resources/routes.json';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import axiosConfig from '../../../Resources/axiosConfig';
interface InformationSodistorehomeProps { interface InformationSodistorehomeProps {
values: I_Installation; values: I_Installation;
@ -178,6 +180,18 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
const canEdit = currentUser.userType === UserType.admin; const canEdit = currentUser.userType === UserType.admin;
const isPartner = currentUser.userType === UserType.partner; const isPartner = currentUser.userType === UserType.partner;
const [networkProviders, setNetworkProviders] = useState<string[]>([]);
const [loadingProviders, setLoadingProviders] = useState(false);
useEffect(() => {
setLoadingProviders(true);
axiosConfig
.get('/GetNetworkProviders')
.then((res) => setNetworkProviders(res.data))
.catch(() => {})
.finally(() => setLoadingProviders(false));
}, []);
return ( return (
<> <>
{openModalDeleteInstallation && ( {openModalDeleteInstallation && (
@ -361,6 +375,52 @@ function InformationSodistorehome(props: InformationSodistorehomeProps) {
/> />
</div> </div>
<div>
<Autocomplete
freeSolo
options={networkProviders}
value={formValues.networkProvider || ''}
onChange={(_e, val) =>
setFormValues({
...formValues,
networkProvider: (val as string) || ''
})
}
onInputChange={(_e, val) =>
setFormValues({
...formValues,
networkProvider: val || ''
})
}
disabled={!canEdit && !isPartner}
loading={loadingProviders}
renderInput={(params) => (
<TextField
{...params}
label={
<FormattedMessage
id="networkProvider"
defaultMessage="Network Provider"
/>
}
variant="outlined"
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loadingProviders ? (
<CircularProgress size={20} />
) : null}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
</div>
{canEdit && ( {canEdit && (
<div> <div>
<TextField <TextField

View File

@ -26,6 +26,7 @@ export interface I_Installation extends I_S3Credentials {
testingMode?: boolean; testingMode?: boolean;
status?: number; status?: number;
serialNumber?: string; serialNumber?: string;
networkProvider: string;
} }
export interface I_Folder { export interface I_Folder {

View File

@ -6,6 +6,7 @@
"alarms": "Alarme", "alarms": "Alarme",
"applyChanges": "Änderungen speichern", "applyChanges": "Änderungen speichern",
"country": "Land", "country": "Land",
"networkProvider": "Netzbetreiber",
"createNewFolder": "Neuer Ordner", "createNewFolder": "Neuer Ordner",
"createNewUser": "Neuer Benutzer", "createNewUser": "Neuer Benutzer",
"customerName": "Kundenname", "customerName": "Kundenname",

View File

@ -2,6 +2,7 @@
"allInstallations": "All installations", "allInstallations": "All installations",
"applyChanges": "Apply changes", "applyChanges": "Apply changes",
"country": "Country", "country": "Country",
"networkProvider": "Network Provider",
"customerName": "Customer name", "customerName": "Customer name",
"english": "English", "english": "English",
"german": "German", "german": "German",

View File

@ -4,6 +4,7 @@
"alarms": "Alarmes", "alarms": "Alarmes",
"applyChanges": "Appliquer", "applyChanges": "Appliquer",
"country": "Pays", "country": "Pays",
"networkProvider": "Gestionnaire de réseau",
"createNewFolder": "Nouveau dossier", "createNewFolder": "Nouveau dossier",
"createNewUser": "Nouvel utilisateur", "createNewUser": "Nouvel utilisateur",
"customerName": "Nom du client", "customerName": "Nom du client",

View File

@ -2,6 +2,7 @@
"allInstallations": "Tutte le installazioni", "allInstallations": "Tutte le installazioni",
"applyChanges": "Applica modifiche", "applyChanges": "Applica modifiche",
"country": "Paese", "country": "Paese",
"networkProvider": "Gestore di rete",
"customerName": "Nome cliente", "customerName": "Nome cliente",
"english": "Inglese", "english": "Inglese",
"german": "Tedesco", "german": "Tedesco",