diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 000000000..1b997d844
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,8 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(git checkout feature/ticket-dashboard)",
+ "Bash(npx react-scripts build)"
+ ]
+ }
+}
diff --git a/csharp/App/Backend/DataTypes/Ticket.cs b/csharp/App/Backend/DataTypes/Ticket.cs
index 53659e3f0..ef3ca3658 100644
--- a/csharp/App/Backend/DataTypes/Ticket.cs
+++ b/csharp/App/Backend/DataTypes/Ticket.cs
@@ -4,7 +4,24 @@ namespace InnovEnergy.App.Backend.DataTypes;
public enum TicketStatus { Open = 0, InProgress = 1, Escalated = 2, Resolved = 3, Closed = 4 }
public enum TicketPriority { Critical = 0, High = 1, Medium = 2, Low = 3 }
-public enum TicketCategory { Hardware = 0, Software = 1, DataApi = 2, UserAccess = 3 }
+public enum TicketCategory { Hardware = 0, Software = 1, Network = 2, UserAccess = 3, Firmware = 4 }
+
+public enum TicketSubCategory
+{
+ General = 0,
+ Other = 99,
+ // Hardware (1xx)
+ Battery = 100, Inverter = 101, Cable = 102, Gateway = 103,
+ Metering = 104, Cooling = 105, PvSolar = 106, Safety = 107,
+ // Software (2xx)
+ Backend = 200, Frontend = 201, Database = 202, Api = 203,
+ // Network (3xx)
+ Connectivity = 300, VpnAccess = 301, S3Storage = 302,
+ // UserAccess (4xx)
+ Permissions = 400, Login = 401,
+ // Firmware (5xx)
+ BatteryFirmware = 500, InverterFirmware = 501, ControllerFirmware = 502
+}
public enum TicketSource { Manual = 0, AutoAlert = 1, Email = 2, Api = 3 }
public class Ticket
@@ -17,6 +34,7 @@ public class Ticket
[Indexed] public Int32 Status { get; set; } = (Int32)TicketStatus.Open;
public Int32 Priority { get; set; } = (Int32)TicketPriority.Medium;
public Int32 Category { get; set; } = (Int32)TicketCategory.Hardware;
+ public Int32 SubCategory { get; set; } = (Int32)TicketSubCategory.General;
public Int32 Source { get; set; } = (Int32)TicketSource.Manual;
[Indexed] public Int64 InstallationId { get; set; }
diff --git a/typescript/frontend-marios2/src/App.tsx b/typescript/frontend-marios2/src/App.tsx
index 3ff590030..19e054b97 100644
--- a/typescript/frontend-marios2/src/App.tsx
+++ b/typescript/frontend-marios2/src/App.tsx
@@ -23,6 +23,7 @@ import SalidomoInstallationTabs from './content/dashboards/SalidomoInstallations
import SodioHomeInstallationTabs from './content/dashboards/SodiohomeInstallations';
import { ProductIdContext } from './contexts/ProductIdContextProvider';
import { TourProvider } from './contexts/TourContext';
+import Tickets from './content/dashboards/Tickets';
function App() {
const context = useContext(UserContext);
@@ -236,6 +237,7 @@ function App() {
}
/>
+ } />
} />
= {
+ 1: [
+ { value: 1, label: 'Cerbo' },
+ { value: 2, label: 'Venus' }
+ ],
+ 2: [
+ { value: 3, label: 'Growatt' },
+ { value: 4, label: 'Sinexcel' }
+ ]
+};
+
+const categoryLabels: Record = {
+ [TicketCategory.Hardware]: 'Hardware',
+ [TicketCategory.Software]: 'Software',
+ [TicketCategory.Network]: 'Network',
+ [TicketCategory.UserAccess]: 'User Access',
+ [TicketCategory.Firmware]: 'Firmware'
+};
+
+interface Props {
+ open: boolean;
+ onClose: () => void;
+ onCreated: () => void;
+}
+
+function CreateTicketModal({ open, onClose, onCreated }: Props) {
+ const [subject, setSubject] = useState('');
+ const [selectedProduct, setSelectedProduct] = useState('');
+ const [selectedDevice, setSelectedDevice] = useState('');
+ const [allInstallations, setAllInstallations] = useState([]);
+ const [selectedInstallation, setSelectedInstallation] =
+ useState(null);
+ const [loadingInstallations, setLoadingInstallations] = useState(false);
+ const [priority, setPriority] = useState(TicketPriority.Medium);
+ const [category, setCategory] = useState(TicketCategory.Hardware);
+ const [subCategory, setSubCategory] = useState(
+ TicketSubCategory.General
+ );
+ const [description, setDescription] = useState('');
+ const [error, setError] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+
+ const hasDeviceOptions =
+ selectedProduct !== '' && selectedProduct in deviceOptionsByProduct;
+
+ useEffect(() => {
+ if (selectedProduct === '') {
+ setAllInstallations([]);
+ setSelectedInstallation(null);
+ setSelectedDevice('');
+ return;
+ }
+ setLoadingInstallations(true);
+ setSelectedInstallation(null);
+ setSelectedDevice('');
+ axiosConfig
+ .get('/GetAllInstallationsFromProduct', {
+ params: { product: selectedProduct }
+ })
+ .then((res) => {
+ const data = res.data;
+ if (Array.isArray(data)) {
+ setAllInstallations(
+ data.map((item: any) => ({
+ id: item.id,
+ name: item.name,
+ device: item.device
+ }))
+ );
+ }
+ })
+ .catch(() => setAllInstallations([]))
+ .finally(() => setLoadingInstallations(false));
+ }, [selectedProduct]);
+
+ useEffect(() => {
+ setSelectedInstallation(null);
+ }, [selectedDevice]);
+
+ useEffect(() => {
+ setSubCategory(TicketSubCategory.General);
+ }, [category]);
+
+ const filteredInstallations = useMemo(() => {
+ if (!hasDeviceOptions || selectedDevice === '') return allInstallations;
+ return allInstallations.filter((inst) => inst.device === selectedDevice);
+ }, [allInstallations, selectedDevice, hasDeviceOptions]);
+
+ const installationReady =
+ selectedProduct !== '' && (!hasDeviceOptions || selectedDevice !== '');
+
+ const resetForm = () => {
+ setSubject('');
+ setSelectedProduct('');
+ setSelectedDevice('');
+ setAllInstallations([]);
+ setSelectedInstallation(null);
+ setPriority(TicketPriority.Medium);
+ setCategory(TicketCategory.Hardware);
+ setSubCategory(TicketSubCategory.General);
+ setDescription('');
+ setError('');
+ };
+
+ const handleSubmit = () => {
+ if (!subject.trim() || !selectedInstallation) return;
+ setSubmitting(true);
+ setError('');
+
+ axiosConfig
+ .post('/CreateTicket', {
+ subject,
+ description,
+ installationId: selectedInstallation.id,
+ priority,
+ category,
+ subCategory
+ })
+ .then(() => {
+ resetForm();
+ onCreated();
+ onClose();
+ })
+ .catch(() => setError('Failed to create ticket.'))
+ .finally(() => setSubmitting(false));
+ };
+
+ const availableSubCategories = subCategoriesByCategory[category] ?? [0];
+
+ return (
+
+ );
+}
+
+export default CreateTicketModal;
diff --git a/typescript/frontend-marios2/src/content/dashboards/Tickets/TicketList.tsx b/typescript/frontend-marios2/src/content/dashboards/Tickets/TicketList.tsx
new file mode 100644
index 000000000..1c4a85361
--- /dev/null
+++ b/typescript/frontend-marios2/src/content/dashboards/Tickets/TicketList.tsx
@@ -0,0 +1,241 @@
+import React, { useEffect, useState } from 'react';
+import {
+ Alert,
+ Box,
+ Button,
+ Chip,
+ Container,
+ FormControl,
+ InputLabel,
+ MenuItem,
+ Select,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
+ Typography
+} from '@mui/material';
+import AddIcon from '@mui/icons-material/Add';
+import { FormattedMessage } from 'react-intl';
+import axiosConfig from 'src/Resources/axiosConfig';
+import {
+ Ticket,
+ TicketStatus,
+ TicketPriority,
+ TicketCategory,
+ TicketSubCategory,
+ subCategoryLabels
+} from 'src/interfaces/TicketTypes';
+import Footer from 'src/components/Footer';
+import CreateTicketModal from './CreateTicketModal';
+
+const statusLabels: Record = {
+ [TicketStatus.Open]: 'Open',
+ [TicketStatus.InProgress]: 'In Progress',
+ [TicketStatus.Escalated]: 'Escalated',
+ [TicketStatus.Resolved]: 'Resolved',
+ [TicketStatus.Closed]: 'Closed'
+};
+
+const statusColors: Record = {
+ [TicketStatus.Open]: 'error',
+ [TicketStatus.InProgress]: 'warning',
+ [TicketStatus.Escalated]: 'error',
+ [TicketStatus.Resolved]: 'success',
+ [TicketStatus.Closed]: 'default'
+};
+
+const priorityLabels: Record = {
+ [TicketPriority.Critical]: 'Critical',
+ [TicketPriority.High]: 'High',
+ [TicketPriority.Medium]: 'Medium',
+ [TicketPriority.Low]: 'Low'
+};
+
+const categoryLabels: Record = {
+ [TicketCategory.Hardware]: 'Hardware',
+ [TicketCategory.Software]: 'Software',
+ [TicketCategory.Network]: 'Network',
+ [TicketCategory.UserAccess]: 'User Access',
+ [TicketCategory.Firmware]: 'Firmware'
+};
+
+function TicketList() {
+ const [tickets, setTickets] = useState([]);
+ const [search, setSearch] = useState('');
+ const [statusFilter, setStatusFilter] = useState('');
+ const [createOpen, setCreateOpen] = useState(false);
+ const [error, setError] = useState('');
+
+ const fetchTickets = () => {
+ axiosConfig
+ .get('/GetAllTickets')
+ .then((res) => setTickets(res.data))
+ .catch(() => setError('Failed to load tickets'));
+ };
+
+ useEffect(() => {
+ fetchTickets();
+ }, []);
+
+ const filtered = tickets.filter((t) => {
+ const matchesSearch =
+ search === '' ||
+ t.subject.toLowerCase().includes(search.toLowerCase()) ||
+ t.description.toLowerCase().includes(search.toLowerCase());
+ const matchesStatus = statusFilter === '' || t.status === statusFilter;
+ return matchesSearch && matchesStatus;
+ });
+
+ return (
+
+
+
+
+
+
+ }
+ onClick={() => setCreateOpen(true)}
+ >
+
+
+
+
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.target.value)}
+ sx={{ minWidth: 250 }}
+ />
+
+
+
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {filtered.length === 0 && !error ? (
+
+
+
+ ) : (
+
+
+
+
+ #
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {filtered.map((ticket) => (
+
+ {ticket.id}
+ {ticket.subject}
+
+
+
+ {priorityLabels[ticket.priority] ?? '-'}
+
+ {categoryLabels[ticket.category] ?? '-'}
+ {ticket.subCategory !== TicketSubCategory.General &&
+ ` — ${subCategoryLabels[ticket.subCategory] ?? ''}`}
+
+
+ {new Date(ticket.createdAt).toLocaleDateString()}
+
+
+ ))}
+
+
+
+ )}
+
+ setCreateOpen(false)}
+ onCreated={fetchTickets}
+ />
+
+
+
+ );
+}
+
+export default TicketList;
diff --git a/typescript/frontend-marios2/src/content/dashboards/Tickets/index.tsx b/typescript/frontend-marios2/src/content/dashboards/Tickets/index.tsx
new file mode 100644
index 000000000..57d325748
--- /dev/null
+++ b/typescript/frontend-marios2/src/content/dashboards/Tickets/index.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import { Route, Routes } from 'react-router-dom';
+import TicketList from './TicketList';
+
+function Tickets() {
+ return (
+
+ } />
+
+ );
+}
+
+export default Tickets;
diff --git a/typescript/frontend-marios2/src/interfaces/TicketTypes.tsx b/typescript/frontend-marios2/src/interfaces/TicketTypes.tsx
new file mode 100644
index 000000000..8107d2ba8
--- /dev/null
+++ b/typescript/frontend-marios2/src/interfaces/TicketTypes.tsx
@@ -0,0 +1,149 @@
+export enum TicketStatus {
+ Open = 0,
+ InProgress = 1,
+ Escalated = 2,
+ Resolved = 3,
+ Closed = 4
+}
+
+export enum TicketPriority {
+ Critical = 0,
+ High = 1,
+ Medium = 2,
+ Low = 3
+}
+
+export enum TicketCategory {
+ Hardware = 0,
+ Software = 1,
+ Network = 2,
+ UserAccess = 3,
+ Firmware = 4
+}
+
+export enum TicketSubCategory {
+ General = 0,
+ Other = 99,
+ // Hardware (1xx)
+ Battery = 100, Inverter = 101, Cable = 102, Gateway = 103,
+ Metering = 104, Cooling = 105, PvSolar = 106, Safety = 107,
+ // Software (2xx)
+ Backend = 200, Frontend = 201, Database = 202, Api = 203,
+ // Network (3xx)
+ Connectivity = 300, VpnAccess = 301, S3Storage = 302,
+ // UserAccess (4xx)
+ Permissions = 400, Login = 401,
+ // Firmware (5xx)
+ BatteryFirmware = 500, InverterFirmware = 501, ControllerFirmware = 502
+}
+
+export const subCategoryLabels: Record = {
+ [TicketSubCategory.General]: 'General',
+ [TicketSubCategory.Other]: 'Other',
+ [TicketSubCategory.Battery]: 'Battery', [TicketSubCategory.Inverter]: 'Inverter',
+ [TicketSubCategory.Cable]: 'Cable', [TicketSubCategory.Gateway]: 'Gateway',
+ [TicketSubCategory.Metering]: 'Metering', [TicketSubCategory.Cooling]: 'Cooling',
+ [TicketSubCategory.PvSolar]: 'PV / Solar', [TicketSubCategory.Safety]: 'Safety',
+ [TicketSubCategory.Backend]: 'Backend', [TicketSubCategory.Frontend]: 'Frontend',
+ [TicketSubCategory.Database]: 'Database', [TicketSubCategory.Api]: 'API',
+ [TicketSubCategory.Connectivity]: 'Connectivity', [TicketSubCategory.VpnAccess]: 'VPN Access',
+ [TicketSubCategory.S3Storage]: 'S3 Storage',
+ [TicketSubCategory.Permissions]: 'Permissions', [TicketSubCategory.Login]: 'Login',
+ [TicketSubCategory.BatteryFirmware]: 'Battery Firmware',
+ [TicketSubCategory.InverterFirmware]: 'Inverter Firmware',
+ [TicketSubCategory.ControllerFirmware]: 'Controller Firmware'
+};
+
+export const subCategoriesByCategory: Record = {
+ [TicketCategory.Hardware]: [0, 100, 101, 102, 103, 104, 105, 106, 107, 99],
+ [TicketCategory.Software]: [0, 200, 201, 202, 203, 99],
+ [TicketCategory.Network]: [0, 300, 301, 302, 99],
+ [TicketCategory.UserAccess]: [0, 400, 401, 99],
+ [TicketCategory.Firmware]: [0, 500, 501, 502, 99]
+};
+
+export enum TicketSource {
+ Manual = 0,
+ AutoAlert = 1,
+ Email = 2,
+ Api = 3
+}
+
+export enum CommentAuthorType {
+ Human = 0,
+ AiAgent = 1
+}
+
+export enum DiagnosisStatus {
+ Pending = 0,
+ Analyzing = 1,
+ Completed = 2,
+ Failed = 3
+}
+
+export enum TimelineEventType {
+ Created = 0,
+ StatusChanged = 1,
+ Assigned = 2,
+ CommentAdded = 3,
+ AiDiagnosisAttached = 4,
+ Escalated = 5
+}
+
+export type Ticket = {
+ id: number;
+ subject: string;
+ description: string;
+ status: number;
+ priority: number;
+ category: number;
+ subCategory: number;
+ source: number;
+ installationId: number;
+ assigneeId: number | null;
+ createdByUserId: number;
+ tags: string;
+ createdAt: string;
+ updatedAt: string;
+ resolvedAt: string | null;
+};
+
+export type TicketComment = {
+ id: number;
+ ticketId: number;
+ authorType: number;
+ authorId: number | null;
+ body: string;
+ createdAt: string;
+};
+
+export type TicketAiDiagnosis = {
+ id: number;
+ ticketId: number;
+ status: number;
+ rootCause: string | null;
+ confidence: number | null;
+ recommendedActions: string | null;
+ similarTicketIds: string | null;
+ feedback: number | null;
+ overrideText: string | null;
+ createdAt: string;
+ completedAt: string | null;
+};
+
+export type TicketTimelineEvent = {
+ id: number;
+ ticketId: number;
+ eventType: number;
+ description: string;
+ actorType: number;
+ actorId: number | null;
+ createdAt: string;
+};
+
+export type TicketDetail = {
+ ticket: Ticket;
+ comments: TicketComment[];
+ diagnosis: TicketAiDiagnosis | null;
+ timeline: TicketTimelineEvent[];
+};
diff --git a/typescript/frontend-marios2/src/lang/de.json b/typescript/frontend-marios2/src/lang/de.json
index caaa984ce..de571333b 100644
--- a/typescript/frontend-marios2/src/lang/de.json
+++ b/typescript/frontend-marios2/src/lang/de.json
@@ -515,5 +515,14 @@
"tourConfigurationTitle": "Konfiguration",
"tourConfigurationContent": "Geräteeinstellungen für diese Installation anzeigen und ändern.",
"tourHistoryTitle": "Verlauf",
- "tourHistoryContent": "Protokoll der Aktionen an dieser Installation — wer hat was und wann geändert."
+ "tourHistoryContent": "Protokoll der Aktionen an dieser Installation — wer hat was und wann geändert.",
+ "tickets": "Tickets",
+ "createTicket": "Ticket erstellen",
+ "subject": "Betreff",
+ "description": "Beschreibung",
+ "priority": "Priorität",
+ "category": "Kategorie",
+ "allStatuses": "Alle Status",
+ "createdAt": "Erstellt",
+ "noTickets": "Keine Tickets gefunden."
}
\ No newline at end of file
diff --git a/typescript/frontend-marios2/src/lang/en.json b/typescript/frontend-marios2/src/lang/en.json
index 50d0db465..52cd08915 100644
--- a/typescript/frontend-marios2/src/lang/en.json
+++ b/typescript/frontend-marios2/src/lang/en.json
@@ -263,5 +263,14 @@
"tourConfigurationTitle": "Configuration",
"tourConfigurationContent": "View and modify device settings for this installation.",
"tourHistoryTitle": "History",
- "tourHistoryContent": "Audit trail of actions performed on this installation — who changed what and when."
+ "tourHistoryContent": "Audit trail of actions performed on this installation — who changed what and when.",
+ "tickets": "Tickets",
+ "createTicket": "Create Ticket",
+ "subject": "Subject",
+ "description": "Description",
+ "priority": "Priority",
+ "category": "Category",
+ "allStatuses": "All Statuses",
+ "createdAt": "Created",
+ "noTickets": "No tickets found."
}
diff --git a/typescript/frontend-marios2/src/lang/fr.json b/typescript/frontend-marios2/src/lang/fr.json
index 022079478..b84d6c028 100644
--- a/typescript/frontend-marios2/src/lang/fr.json
+++ b/typescript/frontend-marios2/src/lang/fr.json
@@ -515,5 +515,14 @@
"tourConfigurationTitle": "Configuration",
"tourConfigurationContent": "Afficher et modifier les paramètres de l'appareil pour cette installation.",
"tourHistoryTitle": "Historique",
- "tourHistoryContent": "Journal des actions effectuées sur cette installation — qui a changé quoi et quand."
+ "tourHistoryContent": "Journal des actions effectuées sur cette installation — qui a changé quoi et quand.",
+ "tickets": "Tickets",
+ "createTicket": "Créer un ticket",
+ "subject": "Objet",
+ "description": "Description",
+ "priority": "Priorité",
+ "category": "Catégorie",
+ "allStatuses": "Tous les statuts",
+ "createdAt": "Créé",
+ "noTickets": "Aucun ticket trouvé."
}
\ No newline at end of file
diff --git a/typescript/frontend-marios2/src/lang/it.json b/typescript/frontend-marios2/src/lang/it.json
index 9c3a47e28..d4bca6997 100644
--- a/typescript/frontend-marios2/src/lang/it.json
+++ b/typescript/frontend-marios2/src/lang/it.json
@@ -515,5 +515,14 @@
"tourConfigurationTitle": "Configurazione",
"tourConfigurationContent": "Visualizza e modifica le impostazioni del dispositivo per questa installazione.",
"tourHistoryTitle": "Cronologia",
- "tourHistoryContent": "Registro delle azioni eseguite su questa installazione — chi ha cambiato cosa e quando."
+ "tourHistoryContent": "Registro delle azioni eseguite su questa installazione — chi ha cambiato cosa e quando.",
+ "tickets": "Ticket",
+ "createTicket": "Crea ticket",
+ "subject": "Oggetto",
+ "description": "Descrizione",
+ "priority": "Priorità",
+ "category": "Categoria",
+ "allStatuses": "Tutti gli stati",
+ "createdAt": "Creato",
+ "noTickets": "Nessun ticket trovato."
}
diff --git a/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx b/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx
index fdc8d54e9..3a25cf5ef 100644
--- a/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx
+++ b/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx
@@ -13,6 +13,7 @@ import { NavLink as RouterLink } from 'react-router-dom';
import { SidebarContext } from 'src/contexts/SidebarContext';
import BrightnessLowTwoToneIcon from '@mui/icons-material/BrightnessLowTwoTone';
import TableChartTwoToneIcon from '@mui/icons-material/TableChartTwoTone';
+import ConfirmationNumberTwoToneIcon from '@mui/icons-material/ConfirmationNumberTwoTone';
import { FormattedMessage } from 'react-intl';
import { UserContext } from '../../../../contexts/userContext';
import { UserType } from '../../../../interfaces/UserTypes';
@@ -310,6 +311,19 @@ function SidebarMenu() {
+
+
+ }
+ >
+
+
+
+
)}