added ticket dashboard frontend: List & Create

This commit is contained in:
Yinyin Liu 2026-03-06 10:43:31 +01:00
parent d54fc1c2ab
commit a40c168f1a
13 changed files with 836 additions and 6 deletions

8
.claude/settings.json Normal file
View File

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git checkout feature/ticket-dashboard)",
"Bash(npx react-scripts build)"
]
}
}

View File

@ -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; }

View File

@ -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() {
}
/>
<Route path={routes.tickets + '*'} element={<Tickets />} />
<Route path={routes.users + '*'} element={<Users />} />
<Route
path={'*'}

View File

@ -22,5 +22,6 @@
"history": "history",
"mainstats": "mainstats",
"detailed_view": "detailed_view/",
"report": "report"
"report": "report",
"tickets": "/tickets/"
}

View File

@ -0,0 +1,348 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Alert,
Autocomplete,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControl,
InputLabel,
MenuItem,
Select,
TextField
} from '@mui/material';
import { FormattedMessage } from 'react-intl';
import axiosConfig from 'src/Resources/axiosConfig';
import {
TicketPriority,
TicketCategory,
TicketSubCategory,
subCategoryLabels,
subCategoriesByCategory
} from 'src/interfaces/TicketTypes';
type Installation = {
id: number;
name: string;
device: number;
};
const productOptions = [
{ value: 0, label: 'Salimax' },
{ value: 1, label: 'Salidomo' },
{ value: 2, label: 'Sodistore Home' },
{ value: 3, label: 'Sodistore Max' },
{ value: 4, label: 'Sodistore Grid' }
];
const deviceOptionsByProduct: Record<number, { value: number; label: string }[]> = {
1: [
{ value: 1, label: 'Cerbo' },
{ value: 2, label: 'Venus' }
],
2: [
{ value: 3, label: 'Growatt' },
{ value: 4, label: 'Sinexcel' }
]
};
const categoryLabels: Record<number, string> = {
[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<number | ''>('');
const [selectedDevice, setSelectedDevice] = useState<number | ''>('');
const [allInstallations, setAllInstallations] = useState<Installation[]>([]);
const [selectedInstallation, setSelectedInstallation] =
useState<Installation | null>(null);
const [loadingInstallations, setLoadingInstallations] = useState(false);
const [priority, setPriority] = useState<number>(TicketPriority.Medium);
const [category, setCategory] = useState<number>(TicketCategory.Hardware);
const [subCategory, setSubCategory] = useState<number>(
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 (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>
<FormattedMessage id="createTicket" defaultMessage="Create Ticket" />
</DialogTitle>
<DialogContent
sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}
>
{error && <Alert severity="error">{error}</Alert>}
<TextField
label={<FormattedMessage id="subject" defaultMessage="Subject" />}
value={subject}
onChange={(e) => setSubject(e.target.value)}
required
fullWidth
margin="dense"
/>
<FormControl fullWidth margin="dense">
<InputLabel>
<FormattedMessage id="product" defaultMessage="Product" />
</InputLabel>
<Select
value={selectedProduct}
label="Product"
onChange={(e) =>
setSelectedProduct(
e.target.value === '' ? '' : Number(e.target.value)
)
}
>
{productOptions.map((p) => (
<MenuItem key={p.value} value={p.value}>
{p.label}
</MenuItem>
))}
</Select>
</FormControl>
{hasDeviceOptions && (
<FormControl fullWidth margin="dense">
<InputLabel>
<FormattedMessage id="device" defaultMessage="Device" />
</InputLabel>
<Select
value={selectedDevice}
label="Device"
onChange={(e) =>
setSelectedDevice(
e.target.value === '' ? '' : Number(e.target.value)
)
}
>
{deviceOptionsByProduct[selectedProduct as number].map((d) => (
<MenuItem key={d.value} value={d.value}>
{d.label}
</MenuItem>
))}
</Select>
</FormControl>
)}
<Autocomplete<Installation, false, false, false>
options={filteredInstallations}
getOptionLabel={(opt) => opt.name}
value={selectedInstallation}
onChange={(_e, val) => setSelectedInstallation(val)}
disabled={!installationReady}
loading={loadingInstallations}
renderInput={(params) => (
<TextField
{...params}
label={
<FormattedMessage
id="installation"
defaultMessage="Installation"
/>
}
margin="dense"
InputProps={{
...params.InputProps,
endAdornment: (
<>
{loadingInstallations ? (
<CircularProgress size={20} />
) : null}
{params.InputProps.endAdornment}
</>
)
}}
/>
)}
/>
<FormControl fullWidth margin="dense">
<InputLabel>
<FormattedMessage id="priority" defaultMessage="Priority" />
</InputLabel>
<Select
value={priority}
label="Priority"
onChange={(e) => setPriority(Number(e.target.value))}
>
<MenuItem value={TicketPriority.Critical}>Critical</MenuItem>
<MenuItem value={TicketPriority.High}>High</MenuItem>
<MenuItem value={TicketPriority.Medium}>Medium</MenuItem>
<MenuItem value={TicketPriority.Low}>Low</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth margin="dense">
<InputLabel>
<FormattedMessage id="category" defaultMessage="Category" />
</InputLabel>
<Select
value={category}
label="Category"
onChange={(e) => setCategory(Number(e.target.value))}
>
{Object.entries(categoryLabels).map(([val, label]) => (
<MenuItem key={val} value={Number(val)}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth margin="dense">
<InputLabel>
<FormattedMessage
id="subCategory"
defaultMessage="Sub-Category"
/>
</InputLabel>
<Select
value={subCategory}
label="Sub-Category"
onChange={(e) => setSubCategory(Number(e.target.value))}
>
{availableSubCategories.map((val) => (
<MenuItem key={val} value={val}>
{subCategoryLabels[val] ?? 'Unknown'}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
label={
<FormattedMessage id="description" defaultMessage="Description" />
}
value={description}
onChange={(e) => setDescription(e.target.value)}
multiline
rows={4}
fullWidth
margin="dense"
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
<FormattedMessage id="cancel" defaultMessage="Cancel" />
</Button>
<Button
variant="contained"
onClick={handleSubmit}
disabled={
submitting || !subject.trim() || !selectedInstallation
}
>
<FormattedMessage id="submit" defaultMessage="Submit" />
</Button>
</DialogActions>
</Dialog>
);
}
export default CreateTicketModal;

View File

@ -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<number, string> = {
[TicketStatus.Open]: 'Open',
[TicketStatus.InProgress]: 'In Progress',
[TicketStatus.Escalated]: 'Escalated',
[TicketStatus.Resolved]: 'Resolved',
[TicketStatus.Closed]: 'Closed'
};
const statusColors: Record<number, 'error' | 'warning' | 'info' | 'success' | 'default'> = {
[TicketStatus.Open]: 'error',
[TicketStatus.InProgress]: 'warning',
[TicketStatus.Escalated]: 'error',
[TicketStatus.Resolved]: 'success',
[TicketStatus.Closed]: 'default'
};
const priorityLabels: Record<number, string> = {
[TicketPriority.Critical]: 'Critical',
[TicketPriority.High]: 'High',
[TicketPriority.Medium]: 'Medium',
[TicketPriority.Low]: 'Low'
};
const categoryLabels: Record<number, string> = {
[TicketCategory.Hardware]: 'Hardware',
[TicketCategory.Software]: 'Software',
[TicketCategory.Network]: 'Network',
[TicketCategory.UserAccess]: 'User Access',
[TicketCategory.Firmware]: 'Firmware'
};
function TicketList() {
const [tickets, setTickets] = useState<Ticket[]>([]);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<number | ''>('');
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 (
<div style={{ userSelect: 'none' }}>
<Container maxWidth="xl" sx={{ marginTop: '20px' }}>
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
mb={2}
>
<Typography variant="h3">
<FormattedMessage id="tickets" defaultMessage="Tickets" />
</Typography>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => setCreateOpen(true)}
>
<FormattedMessage
id="createTicket"
defaultMessage="Create Ticket"
/>
</Button>
</Box>
<Box display="flex" gap={2} mb={2}>
<TextField
size="small"
label={<FormattedMessage id="search" defaultMessage="Search" />}
value={search}
onChange={(e) => setSearch(e.target.value)}
sx={{ minWidth: 250 }}
/>
<FormControl size="small" sx={{ minWidth: 160 }}>
<InputLabel>
<FormattedMessage id="status" defaultMessage="Status" />
</InputLabel>
<Select
value={statusFilter}
label="Status"
onChange={(e) =>
setStatusFilter(e.target.value === '' ? '' : Number(e.target.value))
}
>
<MenuItem value="">
<FormattedMessage
id="allStatuses"
defaultMessage="All Statuses"
/>
</MenuItem>
{Object.entries(statusLabels).map(([val, label]) => (
<MenuItem key={val} value={Number(val)}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{filtered.length === 0 && !error ? (
<Alert severity="info">
<FormattedMessage
id="noTickets"
defaultMessage="No tickets found."
/>
</Alert>
) : (
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>#</TableCell>
<TableCell>
<FormattedMessage
id="subject"
defaultMessage="Subject"
/>
</TableCell>
<TableCell>
<FormattedMessage
id="status"
defaultMessage="Status"
/>
</TableCell>
<TableCell>
<FormattedMessage
id="priority"
defaultMessage="Priority"
/>
</TableCell>
<TableCell>
<FormattedMessage
id="category"
defaultMessage="Category"
/>
</TableCell>
<TableCell>
<FormattedMessage
id="createdAt"
defaultMessage="Created"
/>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filtered.map((ticket) => (
<TableRow key={ticket.id} hover sx={{ cursor: 'pointer' }}>
<TableCell>{ticket.id}</TableCell>
<TableCell>{ticket.subject}</TableCell>
<TableCell>
<Chip
label={statusLabels[ticket.status] ?? 'Unknown'}
color={statusColors[ticket.status] ?? 'default'}
size="small"
/>
</TableCell>
<TableCell>{priorityLabels[ticket.priority] ?? '-'}</TableCell>
<TableCell>
{categoryLabels[ticket.category] ?? '-'}
{ticket.subCategory !== TicketSubCategory.General &&
`${subCategoryLabels[ticket.subCategory] ?? ''}`}
</TableCell>
<TableCell>
{new Date(ticket.createdAt).toLocaleDateString()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
<CreateTicketModal
open={createOpen}
onClose={() => setCreateOpen(false)}
onCreated={fetchTickets}
/>
</Container>
<Footer />
</div>
);
}
export default TicketList;

View File

@ -0,0 +1,13 @@
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import TicketList from './TicketList';
function Tickets() {
return (
<Routes>
<Route index element={<TicketList />} />
</Routes>
);
}
export default Tickets;

View File

@ -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<number, string> = {
[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<number, number[]> = {
[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[];
};

View File

@ -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."
}

View File

@ -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."
}

View File

@ -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é."
}

View File

@ -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."
}

View File

@ -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() {
</Button>
</ListItem>
</List>
<List component="div">
<ListItem component="div">
<Button
disableRipple
component={RouterLink}
onClick={closeSidebar}
to="/tickets"
startIcon={<ConfirmationNumberTwoToneIcon />}
>
<FormattedMessage id="tickets" defaultMessage="Tickets" />
</Button>
</ListItem>
</List>
</SubMenuWrapper>
</List>
)}