ticket dashboard beta version
This commit is contained in:
parent
88173303d9
commit
bf47a82b25
|
|
@ -2004,10 +2004,21 @@ public class Controller : ControllerBase
|
|||
var existing = Db.GetTicketById(ticket.Id);
|
||||
if (existing is null) return NotFound();
|
||||
|
||||
// Enforce resolution when resolving
|
||||
if (ticket.Status == (Int32)TicketStatus.Resolved
|
||||
&& (String.IsNullOrWhiteSpace(ticket.RootCause) || String.IsNullOrWhiteSpace(ticket.Solution)))
|
||||
{
|
||||
return BadRequest("Root Cause and Solution are required to resolve a ticket.");
|
||||
}
|
||||
|
||||
ticket.CreatedAt = existing.CreatedAt;
|
||||
ticket.CreatedByUserId = existing.CreatedByUserId;
|
||||
ticket.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Track resolution added
|
||||
var resolutionAdded = String.IsNullOrWhiteSpace(existing.RootCause)
|
||||
&& !String.IsNullOrWhiteSpace(ticket.RootCause);
|
||||
|
||||
if (ticket.Status != existing.Status)
|
||||
{
|
||||
if (ticket.Status == (Int32)TicketStatus.Resolved)
|
||||
|
|
@ -2024,6 +2035,19 @@ public class Controller : ControllerBase
|
|||
});
|
||||
}
|
||||
|
||||
if (resolutionAdded)
|
||||
{
|
||||
Db.Create(new TicketTimelineEvent
|
||||
{
|
||||
TicketId = ticket.Id,
|
||||
EventType = (Int32)TimelineEventType.ResolutionAdded,
|
||||
Description = $"Resolution added by {user.Name}.",
|
||||
ActorType = (Int32)TimelineActorType.Human,
|
||||
ActorId = user.Id,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
return Db.Update(ticket) ? ticket : StatusCode(500, "Update failed.");
|
||||
}
|
||||
|
||||
|
|
@ -2149,7 +2173,34 @@ public class Controller : ControllerBase
|
|||
diagnosis.Feedback = feedback;
|
||||
diagnosis.OverrideText = overrideText;
|
||||
|
||||
return Db.Update(diagnosis) ? Ok() : StatusCode(500, "Failed to save feedback.");
|
||||
if (!Db.Update(diagnosis)) return StatusCode(500, "Failed to save feedback.");
|
||||
|
||||
// On Accept: pre-fill ticket resolution from AI (only if not already filled)
|
||||
if (feedback == (Int32)DiagnosisFeedback.Accepted)
|
||||
{
|
||||
var ticket = Db.GetTicketById(ticketId);
|
||||
if (ticket is not null && String.IsNullOrWhiteSpace(ticket.RootCause))
|
||||
{
|
||||
ticket.RootCause = diagnosis.RootCause ?? "";
|
||||
|
||||
// RecommendedActions is stored as JSON array — parse to readable text
|
||||
try
|
||||
{
|
||||
var actions = JsonConvert.DeserializeObject<string[]>(diagnosis.RecommendedActions ?? "[]");
|
||||
ticket.Solution = actions is not null ? String.Join("\n", actions) : "";
|
||||
}
|
||||
catch
|
||||
{
|
||||
ticket.Solution = diagnosis.RecommendedActions ?? "";
|
||||
}
|
||||
|
||||
ticket.PreFilledFromAi = true;
|
||||
ticket.UpdatedAt = DateTime.UtcNow;
|
||||
Db.Update(ticket);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,4 +45,8 @@ public class Ticket
|
|||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ResolvedAt { get; set; }
|
||||
|
||||
public String? RootCause { get; set; }
|
||||
public String? Solution { get; set; }
|
||||
public Boolean PreFilledFromAi { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ namespace InnovEnergy.App.Backend.DataTypes;
|
|||
public enum TimelineEventType
|
||||
{
|
||||
Created = 0, StatusChanged = 1, Assigned = 2,
|
||||
CommentAdded = 3, AiDiagnosisAttached = 4, Escalated = 5
|
||||
CommentAdded = 3, AiDiagnosisAttached = 4, Escalated = 5,
|
||||
ResolutionAdded = 6
|
||||
}
|
||||
|
||||
public enum TimelineActorType { System = 0, AiAgent = 1, Human = 2 }
|
||||
|
|
|
|||
|
|
@ -14,18 +14,20 @@ import PersonIcon from '@mui/icons-material/Person';
|
|||
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import { CommentAuthorType, TicketComment } from 'src/interfaces/TicketTypes';
|
||||
import { AdminUser, CommentAuthorType, TicketComment } from 'src/interfaces/TicketTypes';
|
||||
|
||||
interface CommentThreadProps {
|
||||
ticketId: number;
|
||||
comments: TicketComment[];
|
||||
onCommentAdded: () => void;
|
||||
adminUsers?: AdminUser[];
|
||||
}
|
||||
|
||||
function CommentThread({
|
||||
ticketId,
|
||||
comments,
|
||||
onCommentAdded
|
||||
onCommentAdded,
|
||||
adminUsers = []
|
||||
}: CommentThreadProps) {
|
||||
const [body, setBody] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
|
@ -89,7 +91,7 @@ function CommentThread({
|
|||
sx={{ display: 'flex', alignItems: 'baseline', gap: 1 }}
|
||||
>
|
||||
<Typography variant="subtitle2">
|
||||
{isAi ? 'AI' : `User #${comment.authorId ?? '?'}`}
|
||||
{isAi ? 'AI' : (adminUsers.find(u => u.id === comment.authorId)?.name ?? `User #${comment.authorId ?? '?'}`)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.disabled">
|
||||
{new Date(comment.createdAt).toLocaleString()}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@ import {
|
|||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import {
|
||||
TicketDetail as TicketDetailType,
|
||||
|
|
@ -33,7 +35,9 @@ import {
|
|||
TicketCategory,
|
||||
TicketSubCategory,
|
||||
AdminUser,
|
||||
subCategoryLabels
|
||||
subCategoryLabels,
|
||||
subCategoryKeys,
|
||||
subCategoriesByCategory
|
||||
} from 'src/interfaces/TicketTypes';
|
||||
import Footer from 'src/components/Footer';
|
||||
import StatusChip from './StatusChip';
|
||||
|
|
@ -41,32 +45,33 @@ import AiDiagnosisPanel from './AiDiagnosisPanel';
|
|||
import CommentThread from './CommentThread';
|
||||
import TimelinePanel from './TimelinePanel';
|
||||
|
||||
const priorityLabels: Record<number, string> = {
|
||||
[TicketPriority.Critical]: 'Critical',
|
||||
[TicketPriority.High]: 'High',
|
||||
[TicketPriority.Medium]: 'Medium',
|
||||
[TicketPriority.Low]: 'Low'
|
||||
const priorityKeys: Record<number, { id: string; defaultMessage: string }> = {
|
||||
[TicketPriority.Critical]: { id: 'priorityCritical', defaultMessage: 'Critical' },
|
||||
[TicketPriority.High]: { id: 'priorityHigh', defaultMessage: 'High' },
|
||||
[TicketPriority.Medium]: { id: 'priorityMedium', defaultMessage: 'Medium' },
|
||||
[TicketPriority.Low]: { id: 'priorityLow', defaultMessage: 'Low' }
|
||||
};
|
||||
|
||||
const categoryLabels: Record<number, string> = {
|
||||
[TicketCategory.Hardware]: 'Hardware',
|
||||
[TicketCategory.Software]: 'Software',
|
||||
[TicketCategory.Network]: 'Network',
|
||||
[TicketCategory.UserAccess]: 'User Access',
|
||||
[TicketCategory.Firmware]: 'Firmware'
|
||||
const categoryKeys: Record<number, { id: string; defaultMessage: string }> = {
|
||||
[TicketCategory.Hardware]: { id: 'categoryHardware', defaultMessage: 'Hardware' },
|
||||
[TicketCategory.Software]: { id: 'categorySoftware', defaultMessage: 'Software' },
|
||||
[TicketCategory.Network]: { id: 'categoryNetwork', defaultMessage: 'Network' },
|
||||
[TicketCategory.UserAccess]: { id: 'categoryUserAccess', defaultMessage: 'User Access' },
|
||||
[TicketCategory.Firmware]: { id: 'categoryFirmware', defaultMessage: 'Firmware' }
|
||||
};
|
||||
|
||||
const statusOptions = [
|
||||
{ value: TicketStatus.Open, label: 'Open' },
|
||||
{ value: TicketStatus.InProgress, label: 'In Progress' },
|
||||
{ value: TicketStatus.Escalated, label: 'Escalated' },
|
||||
{ value: TicketStatus.Resolved, label: 'Resolved' },
|
||||
{ value: TicketStatus.Closed, label: 'Closed' }
|
||||
const statusKeys: { value: number; id: string; defaultMessage: string }[] = [
|
||||
{ value: TicketStatus.Open, id: 'statusOpen', defaultMessage: 'Open' },
|
||||
{ value: TicketStatus.InProgress, id: 'statusInProgress', defaultMessage: 'In Progress' },
|
||||
{ value: TicketStatus.Escalated, id: 'statusEscalated', defaultMessage: 'Escalated' },
|
||||
{ value: TicketStatus.Resolved, id: 'statusResolved', defaultMessage: 'Resolved' },
|
||||
{ value: TicketStatus.Closed, id: 'statusClosed', defaultMessage: 'Closed' }
|
||||
];
|
||||
|
||||
function TicketDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const intl = useIntl();
|
||||
const [detail, setDetail] = useState<TicketDetailType | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
|
@ -75,6 +80,15 @@ function TicketDetailPage() {
|
|||
const [updatingAssignee, setUpdatingAssignee] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [rootCause, setRootCause] = useState('');
|
||||
const [solution, setSolution] = useState('');
|
||||
const [savingResolution, setSavingResolution] = useState(false);
|
||||
const [resolutionError, setResolutionError] = useState('');
|
||||
const [resolutionSaved, setResolutionSaved] = useState(false);
|
||||
const [description, setDescription] = useState('');
|
||||
const [editingDescription, setEditingDescription] = useState(false);
|
||||
const [savingDescription, setSavingDescription] = useState(false);
|
||||
const [descriptionSaved, setDescriptionSaved] = useState(false);
|
||||
|
||||
const fetchDetail = useCallback(() => {
|
||||
if (!id) return;
|
||||
|
|
@ -82,6 +96,9 @@ function TicketDetailPage() {
|
|||
.get('/GetTicketDetail', { params: { id: Number(id) } })
|
||||
.then((res) => {
|
||||
setDetail(res.data);
|
||||
setRootCause(res.data.ticket.rootCause ?? '');
|
||||
setSolution(res.data.ticket.solution ?? '');
|
||||
setDescription(res.data.ticket.description ?? '');
|
||||
setError('');
|
||||
})
|
||||
.catch(() => setError('Failed to load ticket details.'))
|
||||
|
|
@ -98,9 +115,24 @@ function TicketDetailPage() {
|
|||
|
||||
const handleStatusChange = (newStatus: number) => {
|
||||
if (!detail) return;
|
||||
if (
|
||||
newStatus === TicketStatus.Resolved &&
|
||||
(!rootCause.trim() || !solution.trim())
|
||||
) {
|
||||
setResolutionError(
|
||||
'Root Cause and Solution are required to resolve a ticket.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
setResolutionError('');
|
||||
setUpdatingStatus(true);
|
||||
axiosConfig
|
||||
.put('/UpdateTicket', { ...detail.ticket, status: newStatus })
|
||||
.put('/UpdateTicket', {
|
||||
...detail.ticket,
|
||||
status: newStatus,
|
||||
rootCause,
|
||||
solution
|
||||
})
|
||||
.then(() => fetchDetail())
|
||||
.catch(() => setError('Failed to update status.'))
|
||||
.finally(() => setUpdatingStatus(false));
|
||||
|
|
@ -119,6 +151,14 @@ function TicketDetailPage() {
|
|||
.finally(() => setUpdatingAssignee(false));
|
||||
};
|
||||
|
||||
const handleTicketFieldChange = (fields: Partial<typeof detail.ticket>) => {
|
||||
if (!detail) return;
|
||||
axiosConfig
|
||||
.put('/UpdateTicket', { ...detail.ticket, ...fields })
|
||||
.then(() => fetchDetail())
|
||||
.catch(() => setError('Failed to update ticket.'));
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!id) return;
|
||||
setDeleting(true);
|
||||
|
|
@ -132,6 +172,36 @@ function TicketDetailPage() {
|
|||
});
|
||||
};
|
||||
|
||||
const handleSaveDescription = () => {
|
||||
if (!detail) return;
|
||||
setSavingDescription(true);
|
||||
setDescriptionSaved(false);
|
||||
axiosConfig
|
||||
.put('/UpdateTicket', { ...detail.ticket, description })
|
||||
.then(() => {
|
||||
fetchDetail();
|
||||
setDescriptionSaved(true);
|
||||
setEditingDescription(false);
|
||||
})
|
||||
.catch(() => setError('Failed to save description.'))
|
||||
.finally(() => setSavingDescription(false));
|
||||
};
|
||||
|
||||
const handleSaveResolution = () => {
|
||||
if (!detail) return;
|
||||
setSavingResolution(true);
|
||||
setResolutionError('');
|
||||
setResolutionSaved(false);
|
||||
axiosConfig
|
||||
.put('/UpdateTicket', { ...detail.ticket, rootCause, solution })
|
||||
.then(() => {
|
||||
fetchDetail();
|
||||
setResolutionSaved(true);
|
||||
})
|
||||
.catch(() => setResolutionError('Failed to save resolution.'))
|
||||
.finally(() => setSavingResolution(false));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ mt: 4, textAlign: 'center' }}>
|
||||
|
|
@ -183,10 +253,10 @@ function TicketDetailPage() {
|
|||
<Box display="flex" gap={1} alignItems="center">
|
||||
<StatusChip status={ticket.status} size="medium" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{priorityLabels[ticket.priority] ?? '-'} ·{' '}
|
||||
{categoryLabels[ticket.category] ?? '-'}
|
||||
{intl.formatMessage(priorityKeys[ticket.priority] ?? { id: 'unknown', defaultMessage: '-' })} ·{' '}
|
||||
{intl.formatMessage(categoryKeys[ticket.category] ?? { id: 'unknown', defaultMessage: '-' })}
|
||||
{ticket.subCategory !== TicketSubCategory.General &&
|
||||
` · ${subCategoryLabels[ticket.subCategory] ?? ''}`}
|
||||
` · ${subCategoryKeys[ticket.subCategory] ? intl.formatMessage(subCategoryKeys[ticket.subCategory]) : subCategoryLabels[ticket.subCategory] ?? ''}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -202,26 +272,76 @@ function TicketDetailPage() {
|
|||
defaultMessage="Description"
|
||||
/>
|
||||
}
|
||||
action={
|
||||
!editingDescription && (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<EditIcon />}
|
||||
onClick={() => setEditingDescription(true)}
|
||||
>
|
||||
<FormattedMessage id="edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ whiteSpace: 'pre-wrap' }}
|
||||
>
|
||||
{ticket.description || (
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="noDescription"
|
||||
defaultMessage="No description provided."
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
{editingDescription ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
<TextField
|
||||
multiline
|
||||
minRows={3}
|
||||
fullWidth
|
||||
value={description}
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value);
|
||||
setDescriptionSaved(false);
|
||||
}}
|
||||
/>
|
||||
<Box display="flex" justifyContent="flex-end" alignItems="center" gap={1}>
|
||||
{descriptionSaved && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
<FormattedMessage id="descriptionSaved" defaultMessage="Description saved." />
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setEditingDescription(false);
|
||||
setDescription(ticket.description ?? '');
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="cancel" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleSaveDescription}
|
||||
disabled={savingDescription}
|
||||
>
|
||||
<FormattedMessage id="save" defaultMessage="Save" />
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ whiteSpace: 'pre-wrap' }}
|
||||
>
|
||||
{ticket.description || (
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="noDescription"
|
||||
defaultMessage="No description provided."
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -232,10 +352,128 @@ function TicketDetailPage() {
|
|||
/>
|
||||
</Box>
|
||||
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardHeader
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="resolution"
|
||||
defaultMessage="Resolution"
|
||||
/>
|
||||
}
|
||||
subheader={
|
||||
detail.ticket.preFilledFromAi ? (
|
||||
<Typography variant="caption" color="info.main">
|
||||
<FormattedMessage
|
||||
id="preFilledFromAi"
|
||||
defaultMessage="Pre-filled from AI diagnosis"
|
||||
/>
|
||||
</Typography>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<CardContent
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
|
||||
>
|
||||
{ticket.status === TicketStatus.Resolved ||
|
||||
ticket.status === TicketStatus.Closed ? (
|
||||
<>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
<FormattedMessage
|
||||
id="rootCauseLabel"
|
||||
defaultMessage="Root Cause"
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{ticket.rootCause || '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
<FormattedMessage
|
||||
id="solutionLabel"
|
||||
defaultMessage="Solution"
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{ticket.solution || '-'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="rootCauseLabel"
|
||||
defaultMessage="Root Cause"
|
||||
/>
|
||||
}
|
||||
multiline
|
||||
minRows={2}
|
||||
fullWidth
|
||||
value={rootCause}
|
||||
onChange={(e) => setRootCause(e.target.value)}
|
||||
error={
|
||||
!!resolutionError && !rootCause.trim()
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="solutionLabel"
|
||||
defaultMessage="Solution"
|
||||
/>
|
||||
}
|
||||
multiline
|
||||
minRows={2}
|
||||
fullWidth
|
||||
value={solution}
|
||||
onChange={(e) => setSolution(e.target.value)}
|
||||
error={
|
||||
!!resolutionError && !solution.trim()
|
||||
}
|
||||
/>
|
||||
<Box display="flex" justifyContent="flex-end" alignItems="center" gap={1}>
|
||||
{resolutionSaved && (
|
||||
<Typography variant="body2" color="success.main">
|
||||
<FormattedMessage
|
||||
id="resolutionSaved"
|
||||
defaultMessage="Resolution saved successfully."
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
{resolutionError && (
|
||||
<Typography variant="body2" color="error.main">
|
||||
{resolutionError}
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleSaveResolution}
|
||||
disabled={
|
||||
savingResolution ||
|
||||
(!rootCause.trim() && !solution.trim())
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="saveResolution"
|
||||
defaultMessage="Save Resolution"
|
||||
/>
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<CommentThread
|
||||
ticketId={ticket.id}
|
||||
comments={comments}
|
||||
onCommentAdded={fetchDetail}
|
||||
adminUsers={adminUsers}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
|
|
@ -266,9 +504,9 @@ function TicketDetailPage() {
|
|||
}
|
||||
disabled={updatingStatus}
|
||||
>
|
||||
{statusOptions.map((opt) => (
|
||||
{statusKeys.map((opt) => (
|
||||
<MenuItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
{intl.formatMessage({ id: opt.id, defaultMessage: opt.defaultMessage })}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
|
@ -303,6 +541,67 @@ function TicketDetailPage() {
|
|||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>
|
||||
<FormattedMessage id="priority" defaultMessage="Priority" />
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={ticket.priority}
|
||||
label="Priority"
|
||||
onChange={(e) =>
|
||||
handleTicketFieldChange({ priority: Number(e.target.value) })
|
||||
}
|
||||
>
|
||||
{Object.entries(priorityKeys).map(([value, msg]) => (
|
||||
<MenuItem key={value} value={Number(value)}>
|
||||
{intl.formatMessage(msg)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>
|
||||
<FormattedMessage id="category" defaultMessage="Category" />
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={ticket.category}
|
||||
label="Category"
|
||||
onChange={(e) => {
|
||||
const newCat = Number(e.target.value);
|
||||
handleTicketFieldChange({
|
||||
category: newCat,
|
||||
subCategory: TicketSubCategory.General
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Object.entries(categoryKeys).map(([value, msg]) => (
|
||||
<MenuItem key={value} value={Number(value)}>
|
||||
{intl.formatMessage(msg)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>
|
||||
<FormattedMessage id="subCategory" defaultMessage="Sub-Category" />
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={ticket.subCategory}
|
||||
label="Sub-Category"
|
||||
onChange={(e) =>
|
||||
handleTicketFieldChange({ subCategory: Number(e.target.value) })
|
||||
}
|
||||
>
|
||||
{(subCategoriesByCategory[ticket.category] ?? [0]).map((sc) => (
|
||||
<MenuItem key={sc} value={sc}>
|
||||
{subCategoryKeys[sc] ? intl.formatMessage(subCategoryKeys[sc]) : subCategoryLabels[sc] ?? 'Unknown'}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -423,6 +722,7 @@ function TicketDetailPage() {
|
|||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
</Container>
|
||||
<Footer />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
Typography
|
||||
} from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import axiosConfig from 'src/Resources/axiosConfig';
|
||||
import {
|
||||
TicketSummary,
|
||||
|
|
@ -27,37 +27,39 @@ import {
|
|||
TicketPriority,
|
||||
TicketCategory,
|
||||
TicketSubCategory,
|
||||
subCategoryLabels
|
||||
subCategoryLabels,
|
||||
subCategoryKeys
|
||||
} from 'src/interfaces/TicketTypes';
|
||||
import Footer from 'src/components/Footer';
|
||||
import CreateTicketModal from './CreateTicketModal';
|
||||
import StatusChip from './StatusChip';
|
||||
|
||||
const statusLabels: Record<number, string> = {
|
||||
[TicketStatus.Open]: 'Open',
|
||||
[TicketStatus.InProgress]: 'In Progress',
|
||||
[TicketStatus.Escalated]: 'Escalated',
|
||||
[TicketStatus.Resolved]: 'Resolved',
|
||||
[TicketStatus.Closed]: 'Closed'
|
||||
const statusKeys: Record<number, { id: string; defaultMessage: string }> = {
|
||||
[TicketStatus.Open]: { id: 'statusOpen', defaultMessage: 'Open' },
|
||||
[TicketStatus.InProgress]: { id: 'statusInProgress', defaultMessage: 'In Progress' },
|
||||
[TicketStatus.Escalated]: { id: 'statusEscalated', defaultMessage: 'Escalated' },
|
||||
[TicketStatus.Resolved]: { id: 'statusResolved', defaultMessage: 'Resolved' },
|
||||
[TicketStatus.Closed]: { id: 'statusClosed', defaultMessage: 'Closed' }
|
||||
};
|
||||
|
||||
const priorityLabels: Record<number, string> = {
|
||||
[TicketPriority.Critical]: 'Critical',
|
||||
[TicketPriority.High]: 'High',
|
||||
[TicketPriority.Medium]: 'Medium',
|
||||
[TicketPriority.Low]: 'Low'
|
||||
const priorityKeys: Record<number, { id: string; defaultMessage: string }> = {
|
||||
[TicketPriority.Critical]: { id: 'priorityCritical', defaultMessage: 'Critical' },
|
||||
[TicketPriority.High]: { id: 'priorityHigh', defaultMessage: 'High' },
|
||||
[TicketPriority.Medium]: { id: 'priorityMedium', defaultMessage: 'Medium' },
|
||||
[TicketPriority.Low]: { id: 'priorityLow', defaultMessage: 'Low' }
|
||||
};
|
||||
|
||||
const categoryLabels: Record<number, string> = {
|
||||
[TicketCategory.Hardware]: 'Hardware',
|
||||
[TicketCategory.Software]: 'Software',
|
||||
[TicketCategory.Network]: 'Network',
|
||||
[TicketCategory.UserAccess]: 'User Access',
|
||||
[TicketCategory.Firmware]: 'Firmware'
|
||||
const categoryKeys: Record<number, { id: string; defaultMessage: string }> = {
|
||||
[TicketCategory.Hardware]: { id: 'categoryHardware', defaultMessage: 'Hardware' },
|
||||
[TicketCategory.Software]: { id: 'categorySoftware', defaultMessage: 'Software' },
|
||||
[TicketCategory.Network]: { id: 'categoryNetwork', defaultMessage: 'Network' },
|
||||
[TicketCategory.UserAccess]: { id: 'categoryUserAccess', defaultMessage: 'User Access' },
|
||||
[TicketCategory.Firmware]: { id: 'categoryFirmware', defaultMessage: 'Firmware' }
|
||||
};
|
||||
|
||||
function TicketList() {
|
||||
const navigate = useNavigate();
|
||||
const intl = useIntl();
|
||||
const [tickets, setTickets] = useState<TicketSummary[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<number | ''>('');
|
||||
|
|
@ -75,14 +77,16 @@ function TicketList() {
|
|||
fetchTickets();
|
||||
}, []);
|
||||
|
||||
const filtered = tickets.filter((t) => {
|
||||
const matchesSearch =
|
||||
search === '' ||
|
||||
t.subject.toLowerCase().includes(search.toLowerCase()) ||
|
||||
t.installationName.toLowerCase().includes(search.toLowerCase());
|
||||
const matchesStatus = statusFilter === '' || t.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
});
|
||||
const filtered = tickets
|
||||
.filter((t) => {
|
||||
const matchesSearch =
|
||||
search === '' ||
|
||||
t.subject.toLowerCase().includes(search.toLowerCase()) ||
|
||||
t.installationName.toLowerCase().includes(search.toLowerCase());
|
||||
const matchesStatus = statusFilter === '' || t.status === statusFilter;
|
||||
return matchesSearch && matchesStatus;
|
||||
})
|
||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
return (
|
||||
<div style={{ userSelect: 'none' }}>
|
||||
|
|
@ -133,9 +137,9 @@ function TicketList() {
|
|||
defaultMessage="All Statuses"
|
||||
/>
|
||||
</MenuItem>
|
||||
{Object.entries(statusLabels).map(([val, label]) => (
|
||||
{Object.entries(statusKeys).map(([val, msg]) => (
|
||||
<MenuItem key={val} value={Number(val)}>
|
||||
{label}
|
||||
{intl.formatMessage(msg)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
|
@ -207,11 +211,11 @@ function TicketList() {
|
|||
<TableCell>
|
||||
<StatusChip status={ticket.status} />
|
||||
</TableCell>
|
||||
<TableCell>{priorityLabels[ticket.priority] ?? '-'}</TableCell>
|
||||
<TableCell>{intl.formatMessage(priorityKeys[ticket.priority] ?? { id: 'unknown', defaultMessage: '-' })}</TableCell>
|
||||
<TableCell>
|
||||
{categoryLabels[ticket.category] ?? '-'}
|
||||
{intl.formatMessage(categoryKeys[ticket.category] ?? { id: 'unknown', defaultMessage: '-' })}
|
||||
{ticket.subCategory !== TicketSubCategory.General &&
|
||||
` — ${subCategoryLabels[ticket.subCategory] ?? ''}`}
|
||||
` — ${subCategoryKeys[ticket.subCategory] ? intl.formatMessage(subCategoryKeys[ticket.subCategory]) : subCategoryLabels[ticket.subCategory] ?? ''}`}
|
||||
</TableCell>
|
||||
<TableCell>{ticket.installationName}</TableCell>
|
||||
<TableCell>
|
||||
|
|
|
|||
|
|
@ -7,19 +7,20 @@ import {
|
|||
Divider,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import {
|
||||
TicketTimelineEvent,
|
||||
TimelineEventType
|
||||
} from 'src/interfaces/TicketTypes';
|
||||
|
||||
const eventTypeLabels: Record<number, string> = {
|
||||
[TimelineEventType.Created]: 'Created',
|
||||
[TimelineEventType.StatusChanged]: 'Status Changed',
|
||||
[TimelineEventType.Assigned]: 'Assigned',
|
||||
[TimelineEventType.CommentAdded]: 'Comment Added',
|
||||
[TimelineEventType.AiDiagnosisAttached]: 'AI Diagnosis',
|
||||
[TimelineEventType.Escalated]: 'Escalated'
|
||||
const eventTypeKeys: Record<number, { id: string; defaultMessage: string }> = {
|
||||
[TimelineEventType.Created]: { id: 'timelineCreated', defaultMessage: 'Created' },
|
||||
[TimelineEventType.StatusChanged]: { id: 'timelineStatusChanged', defaultMessage: 'Status Changed' },
|
||||
[TimelineEventType.Assigned]: { id: 'timelineAssigned', defaultMessage: 'Assigned' },
|
||||
[TimelineEventType.CommentAdded]: { id: 'timelineCommentAdded', defaultMessage: 'Comment Added' },
|
||||
[TimelineEventType.AiDiagnosisAttached]: { id: 'timelineAiDiagnosis', defaultMessage: 'AI Diagnosis' },
|
||||
[TimelineEventType.Escalated]: { id: 'timelineEscalated', defaultMessage: 'Escalated' },
|
||||
[TimelineEventType.ResolutionAdded]: { id: 'timelineResolutionAdded', defaultMessage: 'Resolution Added' }
|
||||
};
|
||||
|
||||
const eventTypeColors: Record<number, string> = {
|
||||
|
|
@ -28,7 +29,8 @@ const eventTypeColors: Record<number, string> = {
|
|||
[TimelineEventType.Assigned]: '#9c27b0',
|
||||
[TimelineEventType.CommentAdded]: '#2e7d32',
|
||||
[TimelineEventType.AiDiagnosisAttached]: '#0288d1',
|
||||
[TimelineEventType.Escalated]: '#d32f2f'
|
||||
[TimelineEventType.Escalated]: '#d32f2f',
|
||||
[TimelineEventType.ResolutionAdded]: '#4caf50'
|
||||
};
|
||||
|
||||
interface TimelinePanelProps {
|
||||
|
|
@ -36,6 +38,7 @@ interface TimelinePanelProps {
|
|||
}
|
||||
|
||||
function TimelinePanel({ events }: TimelinePanelProps) {
|
||||
const intl = useIntl();
|
||||
const sorted = [...events].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
|
@ -80,7 +83,9 @@ function TimelinePanel({ events }: TimelinePanelProps) {
|
|||
/>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{eventTypeLabels[event.eventType] ?? 'Event'}
|
||||
{eventTypeKeys[event.eventType]
|
||||
? intl.formatMessage(eventTypeKeys[event.eventType])
|
||||
: 'Event'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{event.description}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,31 @@ export const subCategoryLabels: Record<number, string> = {
|
|||
[TicketSubCategory.ControllerFirmware]: 'Controller Firmware'
|
||||
};
|
||||
|
||||
export const subCategoryKeys: Record<number, { id: string; defaultMessage: string }> = {
|
||||
[TicketSubCategory.General]: { id: 'subCatGeneral', defaultMessage: 'General' },
|
||||
[TicketSubCategory.Other]: { id: 'subCatOther', defaultMessage: 'Other' },
|
||||
[TicketSubCategory.Battery]: { id: 'subCatBattery', defaultMessage: 'Battery' },
|
||||
[TicketSubCategory.Inverter]: { id: 'subCatInverter', defaultMessage: 'Inverter' },
|
||||
[TicketSubCategory.Cable]: { id: 'subCatCable', defaultMessage: 'Cable' },
|
||||
[TicketSubCategory.Gateway]: { id: 'subCatGateway', defaultMessage: 'Gateway' },
|
||||
[TicketSubCategory.Metering]: { id: 'subCatMetering', defaultMessage: 'Metering' },
|
||||
[TicketSubCategory.Cooling]: { id: 'subCatCooling', defaultMessage: 'Cooling' },
|
||||
[TicketSubCategory.PvSolar]: { id: 'subCatPvSolar', defaultMessage: 'PV / Solar' },
|
||||
[TicketSubCategory.Safety]: { id: 'subCatSafety', defaultMessage: 'Safety' },
|
||||
[TicketSubCategory.Backend]: { id: 'subCatBackend', defaultMessage: 'Backend' },
|
||||
[TicketSubCategory.Frontend]: { id: 'subCatFrontend', defaultMessage: 'Frontend' },
|
||||
[TicketSubCategory.Database]: { id: 'subCatDatabase', defaultMessage: 'Database' },
|
||||
[TicketSubCategory.Api]: { id: 'subCatApi', defaultMessage: 'API' },
|
||||
[TicketSubCategory.Connectivity]: { id: 'subCatConnectivity', defaultMessage: 'Connectivity' },
|
||||
[TicketSubCategory.VpnAccess]: { id: 'subCatVpnAccess', defaultMessage: 'VPN Access' },
|
||||
[TicketSubCategory.S3Storage]: { id: 'subCatS3Storage', defaultMessage: 'S3 Storage' },
|
||||
[TicketSubCategory.Permissions]: { id: 'subCatPermissions', defaultMessage: 'Permissions' },
|
||||
[TicketSubCategory.Login]: { id: 'subCatLogin', defaultMessage: 'Login' },
|
||||
[TicketSubCategory.BatteryFirmware]: { id: 'subCatBatteryFirmware', defaultMessage: 'Battery Firmware' },
|
||||
[TicketSubCategory.InverterFirmware]: { id: 'subCatInverterFirmware', defaultMessage: 'Inverter Firmware' },
|
||||
[TicketSubCategory.ControllerFirmware]: { id: 'subCatControllerFirmware', defaultMessage: '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],
|
||||
|
|
@ -87,7 +112,8 @@ export enum TimelineEventType {
|
|||
Assigned = 2,
|
||||
CommentAdded = 3,
|
||||
AiDiagnosisAttached = 4,
|
||||
Escalated = 5
|
||||
Escalated = 5,
|
||||
ResolutionAdded = 6
|
||||
}
|
||||
|
||||
export type Ticket = {
|
||||
|
|
@ -106,6 +132,9 @@ export type Ticket = {
|
|||
createdAt: string;
|
||||
updatedAt: string;
|
||||
resolvedAt: string | null;
|
||||
rootCause: string | null;
|
||||
solution: string | null;
|
||||
preFilledFromAi: boolean;
|
||||
};
|
||||
|
||||
export type TicketComment = {
|
||||
|
|
|
|||
|
|
@ -552,5 +552,67 @@
|
|||
"diagnosisFeedbackLabel": "War das hilfreich?",
|
||||
"feedbackSubmitted": "Feedback: {feedback}",
|
||||
"accept": "Akzeptieren",
|
||||
"reject": "Ablehnen"
|
||||
"reject": "Ablehnen",
|
||||
"resolution": "Lösung",
|
||||
"rootCauseLabel": "Ursache",
|
||||
"solutionLabel": "Lösung",
|
||||
"saveResolution": "Lösung speichern",
|
||||
"preFilledFromAi": "Vorausgefüllt durch KI-Diagnose",
|
||||
"resolutionRequired": "Ursache und Lösung sind erforderlich, um ein Ticket zu lösen.",
|
||||
"resolutionSaved": "Lösung erfolgreich gespeichert.",
|
||||
"statusOpen": "Offen",
|
||||
"statusInProgress": "In Bearbeitung",
|
||||
"statusEscalated": "Eskaliert",
|
||||
"statusResolved": "Gelöst",
|
||||
"statusClosed": "Geschlossen",
|
||||
"priorityCritical": "Kritisch",
|
||||
"priorityHigh": "Hoch",
|
||||
"priorityMedium": "Mittel",
|
||||
"priorityLow": "Niedrig",
|
||||
"categoryHardware": "Hardware",
|
||||
"categorySoftware": "Software",
|
||||
"categoryNetwork": "Netzwerk",
|
||||
"categoryUserAccess": "Benutzerzugang",
|
||||
"categoryFirmware": "Firmware",
|
||||
"subCategory": "Unterkategorie",
|
||||
"edit": "Bearbeiten",
|
||||
"save": "Speichern",
|
||||
"descriptionSaved": "Beschreibung gespeichert.",
|
||||
"subCatGeneral": "Allgemein",
|
||||
"subCatOther": "Sonstiges",
|
||||
"subCatBattery": "Batterie",
|
||||
"subCatInverter": "Wechselrichter",
|
||||
"subCatCable": "Kabel",
|
||||
"subCatGateway": "Gateway",
|
||||
"subCatMetering": "Messung",
|
||||
"subCatCooling": "Kühlung",
|
||||
"subCatPvSolar": "PV / Solar",
|
||||
"subCatSafety": "Sicherheit",
|
||||
"subCatBackend": "Backend",
|
||||
"subCatFrontend": "Frontend",
|
||||
"subCatDatabase": "Datenbank",
|
||||
"subCatApi": "API",
|
||||
"subCatConnectivity": "Konnektivität",
|
||||
"subCatVpnAccess": "VPN-Zugang",
|
||||
"subCatS3Storage": "S3-Speicher",
|
||||
"subCatPermissions": "Berechtigungen",
|
||||
"subCatLogin": "Anmeldung",
|
||||
"subCatBatteryFirmware": "Batterie-Firmware",
|
||||
"subCatInverterFirmware": "Wechselrichter-Firmware",
|
||||
"subCatControllerFirmware": "Controller-Firmware",
|
||||
"timelineCreated": "Erstellt",
|
||||
"timelineStatusChanged": "Status geändert",
|
||||
"timelineAssigned": "Zugewiesen",
|
||||
"timelineCommentAdded": "Kommentar hinzugefügt",
|
||||
"timelineAiDiagnosis": "KI-Diagnose",
|
||||
"timelineEscalated": "Eskaliert",
|
||||
"timelineResolutionAdded": "Lösung hinzugefügt",
|
||||
"timelineCreatedDesc": "Ticket erstellt von {name}.",
|
||||
"timelineStatusChangedDesc": "Status geändert auf {status}.",
|
||||
"timelineAssignedDesc": "Zugewiesen an {name}.",
|
||||
"timelineCommentAddedDesc": "Kommentar hinzugefügt von {name}.",
|
||||
"timelineAiDiagnosisCompletedDesc": "KI-Diagnose abgeschlossen.",
|
||||
"timelineAiDiagnosisFailedDesc": "KI-Diagnose fehlgeschlagen.",
|
||||
"timelineEscalatedDesc": "Ticket eskaliert.",
|
||||
"timelineResolutionAddedDesc": "Lösung hinzugefügt von {name}."
|
||||
}
|
||||
|
|
@ -300,5 +300,67 @@
|
|||
"diagnosisFeedbackLabel": "Was this helpful?",
|
||||
"feedbackSubmitted": "Feedback: {feedback}",
|
||||
"accept": "Accept",
|
||||
"reject": "Reject"
|
||||
"reject": "Reject",
|
||||
"resolution": "Resolution",
|
||||
"rootCauseLabel": "Root Cause",
|
||||
"solutionLabel": "Solution",
|
||||
"saveResolution": "Save Resolution",
|
||||
"preFilledFromAi": "Pre-filled from AI diagnosis",
|
||||
"resolutionRequired": "Root Cause and Solution are required to resolve a ticket.",
|
||||
"resolutionSaved": "Resolution saved successfully.",
|
||||
"statusOpen": "Open",
|
||||
"statusInProgress": "In Progress",
|
||||
"statusEscalated": "Escalated",
|
||||
"statusResolved": "Resolved",
|
||||
"statusClosed": "Closed",
|
||||
"priorityCritical": "Critical",
|
||||
"priorityHigh": "High",
|
||||
"priorityMedium": "Medium",
|
||||
"priorityLow": "Low",
|
||||
"categoryHardware": "Hardware",
|
||||
"categorySoftware": "Software",
|
||||
"categoryNetwork": "Network",
|
||||
"categoryUserAccess": "User Access",
|
||||
"categoryFirmware": "Firmware",
|
||||
"subCategory": "Sub-Category",
|
||||
"edit": "Edit",
|
||||
"save": "Save",
|
||||
"descriptionSaved": "Description saved.",
|
||||
"subCatGeneral": "General",
|
||||
"subCatOther": "Other",
|
||||
"subCatBattery": "Battery",
|
||||
"subCatInverter": "Inverter",
|
||||
"subCatCable": "Cable",
|
||||
"subCatGateway": "Gateway",
|
||||
"subCatMetering": "Metering",
|
||||
"subCatCooling": "Cooling",
|
||||
"subCatPvSolar": "PV / Solar",
|
||||
"subCatSafety": "Safety",
|
||||
"subCatBackend": "Backend",
|
||||
"subCatFrontend": "Frontend",
|
||||
"subCatDatabase": "Database",
|
||||
"subCatApi": "API",
|
||||
"subCatConnectivity": "Connectivity",
|
||||
"subCatVpnAccess": "VPN Access",
|
||||
"subCatS3Storage": "S3 Storage",
|
||||
"subCatPermissions": "Permissions",
|
||||
"subCatLogin": "Login",
|
||||
"subCatBatteryFirmware": "Battery Firmware",
|
||||
"subCatInverterFirmware": "Inverter Firmware",
|
||||
"subCatControllerFirmware": "Controller Firmware",
|
||||
"timelineCreated": "Created",
|
||||
"timelineStatusChanged": "Status Changed",
|
||||
"timelineAssigned": "Assigned",
|
||||
"timelineCommentAdded": "Comment Added",
|
||||
"timelineAiDiagnosis": "AI Diagnosis",
|
||||
"timelineEscalated": "Escalated",
|
||||
"timelineResolutionAdded": "Resolution Added",
|
||||
"timelineCreatedDesc": "Ticket created by {name}.",
|
||||
"timelineStatusChangedDesc": "Status changed to {status}.",
|
||||
"timelineAssignedDesc": "Assigned to {name}.",
|
||||
"timelineCommentAddedDesc": "Comment added by {name}.",
|
||||
"timelineAiDiagnosisCompletedDesc": "AI diagnosis completed.",
|
||||
"timelineAiDiagnosisFailedDesc": "AI diagnosis failed.",
|
||||
"timelineEscalatedDesc": "Ticket escalated.",
|
||||
"timelineResolutionAddedDesc": "Resolution added by {name}."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -552,5 +552,67 @@
|
|||
"diagnosisFeedbackLabel": "Était-ce utile ?",
|
||||
"feedbackSubmitted": "Retour : {feedback}",
|
||||
"accept": "Accepter",
|
||||
"reject": "Rejeter"
|
||||
"reject": "Rejeter",
|
||||
"resolution": "Résolution",
|
||||
"rootCauseLabel": "Cause principale",
|
||||
"solutionLabel": "Solution",
|
||||
"saveResolution": "Enregistrer la résolution",
|
||||
"preFilledFromAi": "Pré-rempli par le diagnostic IA",
|
||||
"resolutionRequired": "La cause principale et la solution sont requises pour résoudre un ticket.",
|
||||
"resolutionSaved": "Résolution enregistrée avec succès.",
|
||||
"statusOpen": "Ouvert",
|
||||
"statusInProgress": "En cours",
|
||||
"statusEscalated": "Escaladé",
|
||||
"statusResolved": "Résolu",
|
||||
"statusClosed": "Fermé",
|
||||
"priorityCritical": "Critique",
|
||||
"priorityHigh": "Élevée",
|
||||
"priorityMedium": "Moyenne",
|
||||
"priorityLow": "Faible",
|
||||
"categoryHardware": "Matériel",
|
||||
"categorySoftware": "Logiciel",
|
||||
"categoryNetwork": "Réseau",
|
||||
"categoryUserAccess": "Accès utilisateur",
|
||||
"categoryFirmware": "Firmware",
|
||||
"subCategory": "Sous-catégorie",
|
||||
"edit": "Modifier",
|
||||
"save": "Enregistrer",
|
||||
"descriptionSaved": "Description enregistrée.",
|
||||
"subCatGeneral": "Général",
|
||||
"subCatOther": "Autre",
|
||||
"subCatBattery": "Batterie",
|
||||
"subCatInverter": "Onduleur",
|
||||
"subCatCable": "Câble",
|
||||
"subCatGateway": "Passerelle",
|
||||
"subCatMetering": "Comptage",
|
||||
"subCatCooling": "Refroidissement",
|
||||
"subCatPvSolar": "PV / Solaire",
|
||||
"subCatSafety": "Sécurité",
|
||||
"subCatBackend": "Backend",
|
||||
"subCatFrontend": "Frontend",
|
||||
"subCatDatabase": "Base de données",
|
||||
"subCatApi": "API",
|
||||
"subCatConnectivity": "Connectivité",
|
||||
"subCatVpnAccess": "Accès VPN",
|
||||
"subCatS3Storage": "Stockage S3",
|
||||
"subCatPermissions": "Autorisations",
|
||||
"subCatLogin": "Connexion",
|
||||
"subCatBatteryFirmware": "Firmware batterie",
|
||||
"subCatInverterFirmware": "Firmware onduleur",
|
||||
"subCatControllerFirmware": "Firmware contrôleur",
|
||||
"timelineCreated": "Créé",
|
||||
"timelineStatusChanged": "Statut modifié",
|
||||
"timelineAssigned": "Assigné",
|
||||
"timelineCommentAdded": "Commentaire ajouté",
|
||||
"timelineAiDiagnosis": "Diagnostic IA",
|
||||
"timelineEscalated": "Escaladé",
|
||||
"timelineResolutionAdded": "Résolution ajoutée",
|
||||
"timelineCreatedDesc": "Ticket créé par {name}.",
|
||||
"timelineStatusChangedDesc": "Statut modifié en {status}.",
|
||||
"timelineAssignedDesc": "Assigné à {name}.",
|
||||
"timelineCommentAddedDesc": "Commentaire ajouté par {name}.",
|
||||
"timelineAiDiagnosisCompletedDesc": "Diagnostic IA terminé.",
|
||||
"timelineAiDiagnosisFailedDesc": "Diagnostic IA échoué.",
|
||||
"timelineEscalatedDesc": "Ticket escaladé.",
|
||||
"timelineResolutionAddedDesc": "Résolution ajoutée par {name}."
|
||||
}
|
||||
|
|
@ -552,5 +552,67 @@
|
|||
"diagnosisFeedbackLabel": "È stato utile?",
|
||||
"feedbackSubmitted": "Feedback: {feedback}",
|
||||
"accept": "Accetta",
|
||||
"reject": "Rifiuta"
|
||||
"reject": "Rifiuta",
|
||||
"resolution": "Risoluzione",
|
||||
"rootCauseLabel": "Causa principale",
|
||||
"solutionLabel": "Soluzione",
|
||||
"saveResolution": "Salva risoluzione",
|
||||
"preFilledFromAi": "Precompilato dalla diagnosi IA",
|
||||
"resolutionRequired": "Causa principale e soluzione sono necessarie per risolvere un ticket.",
|
||||
"resolutionSaved": "Risoluzione salvata con successo.",
|
||||
"statusOpen": "Aperto",
|
||||
"statusInProgress": "In corso",
|
||||
"statusEscalated": "Escalato",
|
||||
"statusResolved": "Risolto",
|
||||
"statusClosed": "Chiuso",
|
||||
"priorityCritical": "Critica",
|
||||
"priorityHigh": "Alta",
|
||||
"priorityMedium": "Media",
|
||||
"priorityLow": "Bassa",
|
||||
"categoryHardware": "Hardware",
|
||||
"categorySoftware": "Software",
|
||||
"categoryNetwork": "Rete",
|
||||
"categoryUserAccess": "Accesso utente",
|
||||
"categoryFirmware": "Firmware",
|
||||
"subCategory": "Sottocategoria",
|
||||
"edit": "Modifica",
|
||||
"save": "Salva",
|
||||
"descriptionSaved": "Descrizione salvata.",
|
||||
"subCatGeneral": "Generale",
|
||||
"subCatOther": "Altro",
|
||||
"subCatBattery": "Batteria",
|
||||
"subCatInverter": "Inverter",
|
||||
"subCatCable": "Cavo",
|
||||
"subCatGateway": "Gateway",
|
||||
"subCatMetering": "Misurazione",
|
||||
"subCatCooling": "Raffreddamento",
|
||||
"subCatPvSolar": "PV / Solare",
|
||||
"subCatSafety": "Sicurezza",
|
||||
"subCatBackend": "Backend",
|
||||
"subCatFrontend": "Frontend",
|
||||
"subCatDatabase": "Database",
|
||||
"subCatApi": "API",
|
||||
"subCatConnectivity": "Connettività",
|
||||
"subCatVpnAccess": "Accesso VPN",
|
||||
"subCatS3Storage": "Storage S3",
|
||||
"subCatPermissions": "Permessi",
|
||||
"subCatLogin": "Accesso",
|
||||
"subCatBatteryFirmware": "Firmware batteria",
|
||||
"subCatInverterFirmware": "Firmware inverter",
|
||||
"subCatControllerFirmware": "Firmware controller",
|
||||
"timelineCreated": "Creato",
|
||||
"timelineStatusChanged": "Stato modificato",
|
||||
"timelineAssigned": "Assegnato",
|
||||
"timelineCommentAdded": "Commento aggiunto",
|
||||
"timelineAiDiagnosis": "Diagnosi IA",
|
||||
"timelineEscalated": "Escalato",
|
||||
"timelineResolutionAdded": "Risoluzione aggiunta",
|
||||
"timelineCreatedDesc": "Ticket creato da {name}.",
|
||||
"timelineStatusChangedDesc": "Stato modificato in {status}.",
|
||||
"timelineAssignedDesc": "Assegnato a {name}.",
|
||||
"timelineCommentAddedDesc": "Commento aggiunto da {name}.",
|
||||
"timelineAiDiagnosisCompletedDesc": "Diagnosi IA completata.",
|
||||
"timelineAiDiagnosisFailedDesc": "Diagnosi IA fallita.",
|
||||
"timelineEscalatedDesc": "Ticket escalato.",
|
||||
"timelineResolutionAddedDesc": "Risoluzione aggiunta da {name}."
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue