added Tickets tab to each installation and allowed to create ticket from there with pre-filled installation information and connected between them

This commit is contained in:
Yinyin Liu 2026-03-16 12:51:35 +01:00
parent bf47a82b25
commit ac21c46c0e
9 changed files with 283 additions and 15 deletions

View File

@ -23,5 +23,6 @@
"mainstats": "mainstats",
"detailed_view": "detailed_view/",
"report": "report",
"installationTickets": "installationTickets",
"tickets": "/tickets/"
}

View File

@ -28,6 +28,7 @@ import Topology from '../Topology/Topology';
import BatteryView from '../BatteryView/BatteryView';
import Configuration from '../Configuration/Configuration';
import PvView from '../PvView/PvView';
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
interface singleInstallationProps {
current_installation?: I_Installation;
@ -381,7 +382,8 @@ function Installation(props: singleInstallationProps) {
currentTab != 'information' &&
currentTab != 'history' &&
currentTab != 'manage' &&
currentTab != 'log' && (
currentTab != 'log' &&
currentTab != 'installationTickets' && (
<Container
maxWidth="xl"
sx={{
@ -550,6 +552,17 @@ function Installation(props: singleInstallationProps) {
/>
)}
{currentUser.userType == UserType.admin && (
<Route
path={routes.installationTickets}
element={
<InstallationTicketsTab
installationId={props.current_installation.id}
/>
}
/>
)}
<Route
path={'*'}
element={<Navigate to={routes.live}></Navigate>}

View File

@ -32,7 +32,8 @@ function InstallationTabs(props: InstallationTabsProps) {
'information',
'configuration',
'history',
'pvview'
'pvview',
'installationTickets'
];
const [currentTab, setCurrentTab] = useState<string>(undefined);
@ -165,6 +166,10 @@ function InstallationTabs(props: InstallationTabsProps) {
{
value: 'pvview',
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
},
{
value: 'installationTickets',
label: <FormattedMessage id="tickets" defaultMessage="Tickets" />
}
]
: currentUser.userType == UserType.partner
@ -294,6 +299,10 @@ function InstallationTabs(props: InstallationTabsProps) {
defaultMessage="History Of Actions"
/>
)
},
{
value: 'installationTickets',
label: <FormattedMessage id="tickets" defaultMessage="Tickets" />
}
]
: currentUser.userType == UserType.partner

View File

@ -23,6 +23,7 @@ import SalidomoOverview from '../Overview/salidomoOverview';
import { UserType } from '../../../interfaces/UserTypes';
import HistoryOfActions from '../History/History';
import BuildIcon from '@mui/icons-material/Build';
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
import AccessContextProvider from '../../../contexts/AccessContextProvider';
import Access from '../ManageAccess/Access';
@ -324,7 +325,8 @@ function SalidomoInstallation(props: singleInstallationProps) {
currentTab != 'information' &&
currentTab != 'manage' &&
currentTab != 'history' &&
currentTab != 'log' && (
currentTab != 'log' &&
currentTab != 'installationTickets' && (
<Container
maxWidth="xl"
sx={{
@ -428,6 +430,17 @@ function SalidomoInstallation(props: singleInstallationProps) {
/>
)}
{currentUser.userType == UserType.admin && (
<Route
path={routes.installationTickets}
element={
<InstallationTicketsTab
installationId={props.current_installation.id}
/>
}
/>
)}
<Route
path={'*'}
element={<Navigate to={routes.batteryview}></Navigate>}

View File

@ -28,7 +28,8 @@ function SalidomoInstallationTabs(props: InstallationTabsProps) {
'manage',
'overview',
'log',
'history'
'history',
'installationTickets'
];
const [currentTab, setCurrentTab] = useState<string>(undefined);
@ -136,6 +137,10 @@ function SalidomoInstallationTabs(props: InstallationTabsProps) {
defaultMessage="History Of Actions"
/>
)
},
{
value: 'installationTickets',
label: <FormattedMessage id="tickets" defaultMessage="Tickets" />
}
]
: [
@ -217,6 +222,10 @@ function SalidomoInstallationTabs(props: InstallationTabsProps) {
defaultMessage="History Of Actions"
/>
)
},
{
value: 'installationTickets',
label: <FormattedMessage id="tickets" defaultMessage="Tickets" />
}
]
: currentTab != 'list' &&

View File

@ -28,6 +28,7 @@ import SodistoreHomeConfiguration from './SodistoreHomeConfiguration';
import TopologySodistoreHome from '../Topology/TopologySodistoreHome';
import Overview from '../Overview/overview';
import WeeklyReport from './WeeklyReport';
import InstallationTicketsTab from '../Tickets/InstallationTicketsTab';
interface singleInstallationProps {
current_installation?: I_Installation;
@ -473,7 +474,8 @@ function SodioHomeInstallation(props: singleInstallationProps) {
currentTab != 'manage' &&
currentTab != 'history' &&
currentTab != 'log' &&
currentTab != 'report' && (
currentTab != 'report' &&
currentTab != 'installationTickets' && (
<Container
maxWidth="xl"
sx={{
@ -618,6 +620,17 @@ function SodioHomeInstallation(props: singleInstallationProps) {
/>
)}
{currentUser.userType == UserType.admin && (
<Route
path={routes.installationTickets}
element={
<InstallationTicketsTab
installationId={props.current_installation.id}
/>
}
/>
)}
<Route
path={'*'}
element={<Navigate to={routes.live}></Navigate>}

View File

@ -31,7 +31,8 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
'log',
'history',
'configuration',
'report'
'report',
'installationTickets'
];
const [currentTab, setCurrentTab] = useState<string>(undefined);
@ -159,6 +160,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
defaultMessage="Report"
/>
)
},
{
value: 'installationTickets',
label: <FormattedMessage id="tickets" defaultMessage="Tickets" />
}
]
: currentUser.userType == UserType.partner
@ -310,6 +315,10 @@ function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) {
defaultMessage="Report"
/>
)
},
{
value: 'installationTickets',
label: <FormattedMessage id="tickets" defaultMessage="Tickets" />
}
]
: inInstallationView && currentUser.userType == UserType.partner

View File

@ -61,9 +61,10 @@ interface Props {
open: boolean;
onClose: () => void;
onCreated: () => void;
defaultInstallationId?: number;
}
function CreateTicketModal({ open, onClose, onCreated }: Props) {
function CreateTicketModal({ open, onClose, onCreated, defaultInstallationId }: Props) {
const [subject, setSubject] = useState('');
const [selectedProduct, setSelectedProduct] = useState<number | ''>('');
const [selectedDevice, setSelectedDevice] = useState<number | ''>('');
@ -100,13 +101,19 @@ function CreateTicketModal({ open, onClose, onCreated }: Props) {
.then((res) => {
const data = res.data;
if (Array.isArray(data)) {
setAllInstallations(
data.map((item: any) => ({
id: item.id,
name: item.name,
device: item.device
}))
);
const mapped = data.map((item: any) => ({
id: item.id,
name: item.name,
device: item.device
}));
setAllInstallations(mapped);
if (defaultInstallationId != null) {
const match = mapped.find((inst: Installation) => inst.id === defaultInstallationId);
if (match) {
setSelectedInstallation(match);
setSelectedDevice(match.device ?? '');
}
}
}
})
.catch(() => setAllInstallations([]))
@ -114,7 +121,20 @@ function CreateTicketModal({ open, onClose, onCreated }: Props) {
}, [selectedProduct]);
useEffect(() => {
setSelectedInstallation(null);
if (defaultInstallationId == null || !open) return;
axiosConfig
.get('/GetInstallationById', { params: { id: defaultInstallationId } })
.then((res) => {
const inst = res.data;
if (inst) {
setSelectedProduct(inst.product);
}
})
.catch(() => {});
}, [defaultInstallationId, open]);
useEffect(() => {
if (defaultInstallationId == null) setSelectedInstallation(null);
}, [selectedDevice]);
useEffect(() => {

View File

@ -0,0 +1,181 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Alert,
Box,
Button,
Chip,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import { FormattedMessage, useIntl } from 'react-intl';
import axiosConfig from 'src/Resources/axiosConfig';
import { Ticket, TicketStatus } from 'src/interfaces/TicketTypes';
import StatusChip from './StatusChip';
import CreateTicketModal from './CreateTicketModal';
interface Props {
installationId: number;
}
const statusCountKeys: {
status: number;
id: string;
defaultMessage: string;
color: string;
}[] = [
{ status: TicketStatus.Open, id: 'statusOpen', defaultMessage: 'Open', color: '#d32f2f' },
{ status: TicketStatus.InProgress, id: 'statusInProgress', defaultMessage: 'In Progress', color: '#ed6c02' },
{ status: TicketStatus.Escalated, id: 'statusEscalated', defaultMessage: 'Escalated', color: '#9c27b0' },
{ status: TicketStatus.Resolved, id: 'statusResolved', defaultMessage: 'Resolved', color: '#2e7d32' },
{ status: TicketStatus.Closed, id: 'statusClosed', defaultMessage: 'Closed', color: '#757575' }
];
function InstallationTicketsTab({ installationId }: Props) {
const navigate = useNavigate();
const intl = useIntl();
const [tickets, setTickets] = useState<Ticket[]>([]);
const [error, setError] = useState('');
const [loading, setLoading] = useState(true);
const [createOpen, setCreateOpen] = useState(false);
const fetchTickets = () => {
axiosConfig
.get('/GetTicketsForInstallation', { params: { installationId } })
.then((res) => {
setTickets(res.data);
setError('');
})
.catch(() => setError('Failed to load tickets.'))
.finally(() => setLoading(false));
};
useEffect(() => {
fetchTickets();
}, [installationId]);
const statusCounts = statusCountKeys.map((s) => ({
...s,
count: tickets.filter((t) => t.status === s.status).length
}));
if (loading) return null;
return (
<Box sx={{ p: 2 }}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h4">
<FormattedMessage id="tickets" defaultMessage="Tickets" />
</Typography>
<Button
variant="contained"
size="small"
startIcon={<AddIcon />}
onClick={() => setCreateOpen(true)}
>
<FormattedMessage id="createTicket" defaultMessage="Create Ticket" />
</Button>
</Box>
<Box display="flex" gap={1.5} mb={2} flexWrap="wrap">
{statusCounts.map((s) => (
<Chip
key={s.status}
label={`${intl.formatMessage({ id: s.id, defaultMessage: s.defaultMessage })}: ${s.count}`}
size="small"
sx={{
backgroundColor: s.count > 0 ? s.color : '#e0e0e0',
color: s.count > 0 ? '#fff' : '#757575',
fontWeight: 600
}}
/>
))}
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{tickets.length === 0 && !error ? (
<Typography variant="body2" color="text.secondary">
<FormattedMessage id="noTickets" defaultMessage="No tickets found." />
</Typography>
) : (
<TableContainer>
<Table size="small">
<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="createdAt" defaultMessage="Created" />
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tickets
.sort(
(a, b) =>
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime()
)
.map((ticket) => (
<TableRow
key={ticket.id}
hover
sx={{ cursor: 'pointer' }}
onClick={() => navigate(`/tickets/${ticket.id}`)}
>
<TableCell>{ticket.id}</TableCell>
<TableCell>{ticket.subject}</TableCell>
<TableCell>
<StatusChip status={ticket.status} />
</TableCell>
<TableCell>
{intl.formatMessage({
id: `priority${['Critical', 'High', 'Medium', 'Low'][ticket.priority]}`,
defaultMessage: ['Critical', 'High', 'Medium', 'Low'][
ticket.priority
]
})}
</TableCell>
<TableCell>
{new Date(ticket.createdAt).toLocaleDateString()}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
<CreateTicketModal
open={createOpen}
onClose={() => setCreateOpen(false)}
onCreated={() => {
setCreateOpen(false);
fetchTickets();
}}
defaultInstallationId={installationId}
/>
</Box>
);
}
export default InstallationTicketsTab;