feat: implement activity feed module with debounced filtering, history tracking, and axios service integration
This commit is contained in:
@@ -1,2 +1,4 @@
|
|||||||
VITE_DASHBOARD_API_URL=https://python.seekright.com
|
# VITE_DASHBOARD_API_URL=https://python.seekright.com
|
||||||
VITE_AUDIT_API_URL=https://node-audit.seekright.com
|
# VITE_AUDIT_API_URL=https://node-audit.seekright.com
|
||||||
|
VITE_DASHBOARD_API_URL=https://sr-backend-api.takeleap.in
|
||||||
|
VITE_AUDIT_API_URL=https://sr-audit.takeleap.in
|
||||||
|
|||||||
@@ -43,11 +43,13 @@ import { Login } from './pages/account/Login';
|
|||||||
import { Dashboard } from './pages/dashboard/Dashboard';
|
import { Dashboard } from './pages/dashboard/Dashboard';
|
||||||
import { GlobalFeed } from './pages/activity-feeds/GlobalFeed';
|
import { GlobalFeed } from './pages/activity-feeds/GlobalFeed';
|
||||||
import { AuditSession } from './pages/audit-session/AuditSession';
|
import { AuditSession } from './pages/audit-session/AuditSession';
|
||||||
|
import { ToastProvider } from './components/common/ToastProvider';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
<ToastProvider />
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Public Routes */}
|
{/* Public Routes */}
|
||||||
|
|||||||
@@ -62,8 +62,15 @@ export const activityFeedsService = {
|
|||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Called after normal (non-rectification) true anomaly audit
|
||||||
updateAnomalyInDashboard: async (payload: { site_id: string, anomaly_id: number[] }) => {
|
updateAnomalyInDashboard: async (payload: { site_id: string, anomaly_id: number[] }) => {
|
||||||
const response = await axiosClient.post(`/api/audit/auditor/anomaly/update`, payload);
|
const response = await axiosClient.post('/api/audit/auditor/anomaly/update', payload);
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Called after rectification true anomaly audit
|
||||||
|
markAnomalyCompleted: async (payload: { anomaly_id: number[], site_id: number }) => {
|
||||||
|
const response = await axiosClient.post('/api/audit/auditor/anomaly/completed', payload);
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
import { useToastStore } from '../store/toastStore';
|
||||||
|
|
||||||
// You can move this to an environment variable later
|
// You can move this to an environment variable later
|
||||||
// Base URL is empty because we use specific proxy prefixes in the service calls
|
// Base URL is empty because we use specific proxy prefixes in the service calls
|
||||||
@@ -15,6 +16,18 @@ export const axiosClient = axios.create({
|
|||||||
// Request Interceptor: Attach token if available
|
// Request Interceptor: Attach token if available
|
||||||
axiosClient.interceptors.request.use(
|
axiosClient.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
|
// Rocket.Chat webhook: skip auth headers, skip dbName, let Vite proxy handle in dev
|
||||||
|
if (config.url?.startsWith('/api/rocketchat')) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// Leave URL as-is — Vite proxy rewrites /api/rocketchat → https://text.seekright.com
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
// Production: rewrite URL directly (no Vite proxy)
|
||||||
|
const rcUrl = import.meta.env.VITE_ROCKETCHAT_URL || 'https://text.seekright.com';
|
||||||
|
config.url = config.url.replace('/api/rocketchat', rcUrl);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
const user = useAuthStore.getState().user;
|
const user = useAuthStore.getState().user;
|
||||||
if (user?.access_token) {
|
if (user?.access_token) {
|
||||||
config.headers['x-access-token'] = user.access_token;
|
config.headers['x-access-token'] = user.access_token;
|
||||||
@@ -23,8 +36,6 @@ axiosClient.interceptors.request.use(
|
|||||||
config.params = { ...config.params, dbName: user.db_name };
|
config.params = { ...config.params, dbName: user.db_name };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically rewrite local proxy paths to the real URLs from environment variables
|
|
||||||
// This allows us to remove the Vite/Nginx proxy completely!
|
|
||||||
if (config.url?.startsWith('/api/dashboard')) {
|
if (config.url?.startsWith('/api/dashboard')) {
|
||||||
const dashboardUrl = import.meta.env.VITE_DASHBOARD_API_URL || 'https://sr-backend-api.takeleap.in';
|
const dashboardUrl = import.meta.env.VITE_DASHBOARD_API_URL || 'https://sr-backend-api.takeleap.in';
|
||||||
config.url = config.url.replace('/api/dashboard', dashboardUrl);
|
config.url = config.url.replace('/api/dashboard', dashboardUrl);
|
||||||
@@ -43,22 +54,21 @@ axiosClient.interceptors.response.use(
|
|||||||
(response) => response.data,
|
(response) => response.data,
|
||||||
(error) => {
|
(error) => {
|
||||||
if (!error.response) {
|
if (!error.response) {
|
||||||
// Network offline or CORS error (status 0 in Angular)
|
useToastStore.getState().showToast('Network error — one of the services appears to be offline.', 'error');
|
||||||
alert('Seems one of the sub-application is offline. Please restart the application again.');
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { status, data } = error.response;
|
const { status, data } = error.response;
|
||||||
|
|
||||||
if (status === 400) {
|
if (status === 400) {
|
||||||
const message = data?.responseStatus?.responseMessage || data?.errorMessage || 'OOPS! Something wrong has happened. Please refresh the browser and try again.';
|
const message = data?.responseStatus?.responseMessage || data?.errorMessage || 'Something went wrong. Please try again.';
|
||||||
alert(message);
|
useToastStore.getState().showToast(message, 'error');
|
||||||
} else if (status === 401) {
|
} else if (status === 401) {
|
||||||
console.error('Login issue');
|
console.error('Login issue');
|
||||||
useAuthStore.getState().logout();
|
useAuthStore.getState().logout();
|
||||||
window.location.href = '/login'; // Redirect to login
|
window.location.href = '/login';
|
||||||
} else {
|
} else {
|
||||||
alert('OOPS! Something wrong has happened. Please refresh the browser and try again.');
|
useToastStore.getState().showToast('Something went wrong. Please refresh and try again.', 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|||||||
39
src/components/common/ToastProvider.tsx
Normal file
39
src/components/common/ToastProvider.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Alert, Stack } from '@mui/material';
|
||||||
|
import { useToastStore } from '../../store/toastStore';
|
||||||
|
|
||||||
|
export const ToastProvider: React.FC = () => {
|
||||||
|
const { toasts, removeToast } = useToastStore();
|
||||||
|
|
||||||
|
if (toasts.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
spacing={1}
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: 24,
|
||||||
|
right: 24,
|
||||||
|
zIndex: 99999,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{toasts.map((toast) => (
|
||||||
|
<Alert
|
||||||
|
key={toast.id}
|
||||||
|
severity={toast.severity}
|
||||||
|
variant="filled"
|
||||||
|
onClose={() => removeToast(toast.id)}
|
||||||
|
sx={{
|
||||||
|
minWidth: 280,
|
||||||
|
maxWidth: 420,
|
||||||
|
boxShadow: '0 4px 16px rgba(0,0,0,0.5)',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{toast.message}
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -155,6 +155,7 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Used by RECTIFICATION HISTORY dialog — isRectificationTab: true → fetches with isRectificationEnabled=true
|
||||||
const handleSelectDate = (date: string) => {
|
const handleSelectDate = (date: string) => {
|
||||||
const existingIndex = tabs.findIndex(t => t.date === date && t.isRectificationTab);
|
const existingIndex = tabs.findIndex(t => t.date === date && t.isRectificationTab);
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
@@ -174,6 +175,26 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Used by History dropdown — isRectificationTab: false → fetches with isRectificationEnabled=false
|
||||||
|
const handleHistoryDateSelect = (date: string) => {
|
||||||
|
const existingIndex = tabs.findIndex(t => t.date === date && !t.isRectificationTab);
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
setSelectedTabIndex(existingIndex);
|
||||||
|
} else {
|
||||||
|
addTab({
|
||||||
|
date: date,
|
||||||
|
isRectificationTab: false,
|
||||||
|
totalAnomalyCount: 0,
|
||||||
|
anomalies: [],
|
||||||
|
anomaliesCopy: [],
|
||||||
|
selectedAnomalyIndex: 0,
|
||||||
|
filters: {},
|
||||||
|
page: 0,
|
||||||
|
rowsPerPage: 20
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleViewHistory = () => {
|
const handleViewHistory = () => {
|
||||||
if (!selectedHistoryDate) return;
|
if (!selectedHistoryDate) return;
|
||||||
handleSelectDate(selectedHistoryDate);
|
handleSelectDate(selectedHistoryDate);
|
||||||
@@ -241,7 +262,7 @@ export const TopFilterBar: React.FC = () => {
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const date = e.target.value as string;
|
const date = e.target.value as string;
|
||||||
if (date) {
|
if (date) {
|
||||||
handleSelectDate(date);
|
handleHistoryDateSelect(date);
|
||||||
setHistoryDropdownValue('');
|
setHistoryDropdownValue('');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export const MainLayout: React.FC = () => {
|
|||||||
|
|
||||||
{/* Child routes get injected here */}
|
{/* Child routes get injected here */}
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Slider, Checkbox } from '@mui/material';
|
import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Slider, Checkbox, Skeleton } from '@mui/material';
|
||||||
import { useFeedStore } from '../../store/feedStore';
|
import { useFeedStore } from '../../store/feedStore';
|
||||||
|
|
||||||
|
const SKELETON_COLS = 13;
|
||||||
|
|
||||||
export const AnomalyTable: React.FC = () => {
|
export const AnomalyTable: React.FC = () => {
|
||||||
const { tabs, selectedTabIndex, updateTab, filterPlaza, isRectificationEnabled } = useFeedStore();
|
const { tabs, selectedTabIndex, updateTab, filterPlaza, isRectificationEnabled, isFeedLoading } = useFeedStore();
|
||||||
|
|
||||||
if (tabs.length === 0 || selectedTabIndex < 0) {
|
if (tabs.length === 0 || selectedTabIndex < 0) {
|
||||||
return <Typography sx={{ p: 2 }}>Loading feeds...</Typography>;
|
return <Typography sx={{ p: 2 }}>Loading feeds...</Typography>;
|
||||||
@@ -64,7 +66,16 @@ export const AnomalyTable: React.FC = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{anomalies.map((anomaly: any, _filteredIndex: number) => {
|
{isFeedLoading && Array.from({ length: 8 }).map((_, i) => (
|
||||||
|
<TableRow key={`skel-${i}`}>
|
||||||
|
{Array.from({ length: SKELETON_COLS }).map((__, j) => (
|
||||||
|
<TableCell key={j} sx={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||||
|
<Skeleton variant="text" sx={{ bgcolor: 'rgba(255,255,255,0.06)', borderRadius: 1 }} />
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{!isFeedLoading && anomalies.map((anomaly: any, _filteredIndex: number) => {
|
||||||
const realIndex = rawAnomalies.indexOf(anomaly);
|
const realIndex = rawAnomalies.indexOf(anomaly);
|
||||||
const isSelected = activeTab.selectedAnomalyIndex === realIndex;
|
const isSelected = activeTab.selectedAnomalyIndex === realIndex;
|
||||||
const assetName = anomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset';
|
const assetName = anomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset';
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
isRectificationEnabled,
|
isRectificationEnabled,
|
||||||
filterFromDate, filterTillDate,
|
filterFromDate, filterTillDate,
|
||||||
filterMinChainage, filterMaxChainage,
|
filterMinChainage, filterMaxChainage,
|
||||||
selectedAssetIds
|
selectedAssetIds,
|
||||||
|
setIsFeedLoading
|
||||||
} = useFeedStore();
|
} = useFeedStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -70,6 +71,7 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
const siteIdParam = (!selectedSite || selectedSite?.site_id === 'All Sites')
|
const siteIdParam = (!selectedSite || selectedSite?.site_id === 'All Sites')
|
||||||
? '' : String(selectedSite.site_id || '');
|
? '' : String(selectedSite.site_id || '');
|
||||||
|
|
||||||
|
setIsFeedLoading(true);
|
||||||
try {
|
try {
|
||||||
const historyRes: any = await activityFeedsService.getHistory({
|
const historyRes: any = await activityFeedsService.getHistory({
|
||||||
date: activeTab.date || '',
|
date: activeTab.date || '',
|
||||||
@@ -106,6 +108,8 @@ export const GlobalFeed: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[GlobalFeed] Failed to load history for active tab/filters change', err);
|
console.error('[GlobalFeed] Failed to load history for active tab/filters change', err);
|
||||||
|
} finally {
|
||||||
|
setIsFeedLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions, IconButton } from '@mui/material';
|
import { Box, Typography, Button, Chip, Switch, Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Select, MenuItem, TextField, CircularProgress } from '@mui/material';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
||||||
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
|
import ZoomOutIcon from '@mui/icons-material/ZoomOut';
|
||||||
import { activityFeedsService } from '../../api/activityFeedsService';
|
import { activityFeedsService } from '../../api/activityFeedsService';
|
||||||
|
import { axiosClient } from '../../api/axiosClient';
|
||||||
|
import { useToastStore } from '../../store/toastStore';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
import { useFeedStore } from '../../store/feedStore';
|
import { useFeedStore } from '../../store/feedStore';
|
||||||
|
|
||||||
@@ -59,6 +61,68 @@ export const AuditSession: React.FC = () => {
|
|||||||
const [isNotesFocused, setIsNotesFocused] = useState(false);
|
const [isNotesFocused, setIsNotesFocused] = useState(false);
|
||||||
const [showCompletionModal, setShowCompletionModal] = useState(false);
|
const [showCompletionModal, setShowCompletionModal] = useState(false);
|
||||||
const [isShortcutsDialogOpen, setIsShortcutsDialogOpen] = useState(false);
|
const [isShortcutsDialogOpen, setIsShortcutsDialogOpen] = useState(false);
|
||||||
|
const [isReportDialogOpen, setIsReportDialogOpen] = useState(false);
|
||||||
|
const [reportDev, setReportDev] = useState('');
|
||||||
|
const [reportMessage, setReportMessage] = useState('');
|
||||||
|
const [reportSending, setReportSending] = useState(false);
|
||||||
|
|
||||||
|
const DEVS = ['aromal', 'divya', 'Poonam', 'ravi', 'Kaushik', 'Saleth', 'Jagan', 'Yash', 'faraz_siddiqui', 'brinda.n', 'SasiVardhan', 'Shubham'];
|
||||||
|
const ROCKETCHAT_WEBHOOK_PATH = '/api/rocketchat/hooks/6a22ad73a88367bc6e1095a2/xcHkbDSce5rCwofdBZF3ri2rb6ntxT2mahqT6Gg7iKdectYk';
|
||||||
|
|
||||||
|
const handleSendReport = async () => {
|
||||||
|
if (!reportDev || !reportMessage.trim()) return;
|
||||||
|
setReportSending(true);
|
||||||
|
try {
|
||||||
|
const a = currentAnomaly;
|
||||||
|
const masterPrefix = isRectActive
|
||||||
|
? 'https://sr-img.seekright.com/'
|
||||||
|
: 'https://auditor-master-images.seekright.com/SeekRight/';
|
||||||
|
const testPrefix = 'https://dashboard-images.seekright.com/SeekRight/';
|
||||||
|
|
||||||
|
const masterImageUrl = a?.Frame_Master ? `${masterPrefix}${a.Frame_Master}` : null;
|
||||||
|
const anomalyImageUrl = a?.Frame_Test ? `${testPrefix}${a.Frame_Test}` : null;
|
||||||
|
const videoUrl = a?.video_url ? `${testPrefix}${a.video_url}` : null;
|
||||||
|
const extraImages: string[] = a?.Extra_Image
|
||||||
|
? a.Extra_Image.split(',').map((src: string) => `${testPrefix}${src.trim()}`)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const context = [
|
||||||
|
`*Anomaly Report*`,
|
||||||
|
`• *ID:* #${a?.id}`,
|
||||||
|
`• *Asset:* ${a?.Asset?.replace(/_/gi, ' ') || 'N/A'}`,
|
||||||
|
`• *Plaza:* ${a?.Plaza || 'N/A'}`,
|
||||||
|
`• *Chainage:* ${a?.Chainage || 'N/A'}`,
|
||||||
|
`• *Side:* ${a?.Side || 'N/A'}`,
|
||||||
|
`• *Position:* ${a?.Pos || 'N/A'}`,
|
||||||
|
`• *Road:* ${a?.road || 'N/A'}`,
|
||||||
|
`• *Algorithm:* ${a?.Algorithm || 'N/A'}`,
|
||||||
|
`• *Created:* ${a?.Created_on ? new Date(a.Created_on).toLocaleString() : 'N/A'}`,
|
||||||
|
``,
|
||||||
|
`*Media:*`,
|
||||||
|
masterImageUrl ? `• *Master Image:* ${masterImageUrl}` : `• *Master Image:* Not available`,
|
||||||
|
anomalyImageUrl ? `• *Anomaly Image:* ${anomalyImageUrl}` : `• *Anomaly Image:* Not available`,
|
||||||
|
videoUrl ? `• *Video:* ${videoUrl}` : `• *Video:* Not available`,
|
||||||
|
...(extraImages.length > 1
|
||||||
|
? [`• *Extra Images:*`, ...extraImages.map((url, i) => ` ${i + 1}. ${url}`)]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
``,
|
||||||
|
`*Message from ${user?.username || user?.email}:*`,
|
||||||
|
reportMessage.trim(),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
await axiosClient.post(ROCKETCHAT_WEBHOOK_PATH, { channel: `@${reportDev}`, text: context });
|
||||||
|
setIsReportDialogOpen(false);
|
||||||
|
setReportDev('');
|
||||||
|
setReportMessage('');
|
||||||
|
useToastStore.getState().showToast(`Message sent to @${reportDev}`, 'success');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to send report:', err);
|
||||||
|
useToastStore.getState().showToast('Failed to send message. Please try again.', 'error');
|
||||||
|
} finally {
|
||||||
|
setReportSending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const {
|
const {
|
||||||
@@ -80,7 +144,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
};
|
};
|
||||||
const [sessionPageNo, setSessionPageNo] = useState(() => location.state?.startPage || 0);
|
const [sessionPageNo, setSessionPageNo] = useState(() => location.state?.startPage || 0);
|
||||||
const hasInitializedSession = useRef(false);
|
const hasInitializedSession = useRef(false);
|
||||||
|
|
||||||
const [masterZoom, setMasterZoom] = useState(1);
|
const [masterZoom, setMasterZoom] = useState(1);
|
||||||
const [anomalyZoom, setAnomalyZoom] = useState(1);
|
const [anomalyZoom, setAnomalyZoom] = useState(1);
|
||||||
|
|
||||||
@@ -124,7 +188,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
setNotAnAnomalySubCategory(null);
|
setNotAnAnomalySubCategory(null);
|
||||||
setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
|
setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
|
||||||
} else {
|
} else {
|
||||||
alert("No more anomalies left in the feed.");
|
useToastStore.getState().showToast('No more anomalies left in the feed.', 'info');
|
||||||
setShowCompletionModal(false);
|
setShowCompletionModal(false);
|
||||||
navigate('/activity-feeds');
|
navigate('/activity-feeds');
|
||||||
}
|
}
|
||||||
@@ -163,8 +227,8 @@ export const AuditSession: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.anomalies && res.anomalies.length > 0) {
|
if (res && res.anomalies && res.anomalies.length > 0) {
|
||||||
updateTab(selectedTabIndex, {
|
updateTab(selectedTabIndex, {
|
||||||
anomalies: res.anomalies,
|
anomalies: res.anomalies,
|
||||||
anomaliesCopy: res.anomalies,
|
anomaliesCopy: res.anomalies,
|
||||||
page: nextPage
|
page: nextPage
|
||||||
});
|
});
|
||||||
@@ -180,7 +244,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
setAnomalyZoom(1);
|
setAnomalyZoom(1);
|
||||||
setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
|
setItemAuditStates(res.anomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
|
||||||
} else {
|
} else {
|
||||||
alert("No more anomalies left in the feed.");
|
useToastStore.getState().showToast('No more anomalies left in the feed.', 'info');
|
||||||
navigate('/activity-feeds');
|
navigate('/activity-feeds');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -204,8 +268,8 @@ export const AuditSession: React.FC = () => {
|
|||||||
setAnomalies(filteredAnomalies);
|
setAnomalies(filteredAnomalies);
|
||||||
// Find correct index in filtered list
|
// Find correct index in filtered list
|
||||||
const origSelectedAnomaly = activeTab.anomalies[navState.startIndex || 0];
|
const origSelectedAnomaly = activeTab.anomalies[navState.startIndex || 0];
|
||||||
const newIdx = origSelectedAnomaly
|
const newIdx = origSelectedAnomaly
|
||||||
? filteredAnomalies.findIndex((a: any) => (a.id === origSelectedAnomaly.id || a._id === origSelectedAnomaly._id))
|
? filteredAnomalies.findIndex((a: any) => (a.id === origSelectedAnomaly.id || a._id === origSelectedAnomaly._id))
|
||||||
: 0;
|
: 0;
|
||||||
setCurrentIndex(newIdx !== -1 ? newIdx : 0);
|
setCurrentIndex(newIdx !== -1 ? newIdx : 0);
|
||||||
setItemAuditStates(filteredAnomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
|
setItemAuditStates(filteredAnomalies.map((a: any) => a.IsAudited === 1 ? 'audited' : 'pending'));
|
||||||
@@ -236,7 +300,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [location.state, tabs, selectedTabIndex, loadSessionData, filterPlaza]);
|
}, [location.state, tabs, selectedTabIndex, loadSessionData, filterPlaza]);
|
||||||
|
|
||||||
const IMAGES_AWS_PREFIX = isRectActive
|
const IMAGES_AWS_PREFIX = isRectActive
|
||||||
? "https://sr-img.seekright.com/"
|
? "https://sr-img.seekright.com/"
|
||||||
: "https://auditor-master-images.seekright.com/SeekRight/";
|
: "https://auditor-master-images.seekright.com/SeekRight/";
|
||||||
const IMAGES_TEST_AUDIT_PREFIX = "https://dashboard-images.seekright.com/SeekRight/";
|
const IMAGES_TEST_AUDIT_PREFIX = "https://dashboard-images.seekright.com/SeekRight/";
|
||||||
@@ -257,7 +321,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
let masterImageSrc = '';
|
let masterImageSrc = '';
|
||||||
let latStr = 'N/A';
|
let latStr = 'N/A';
|
||||||
let longStr = 'E/A';
|
let longStr = 'E/A';
|
||||||
|
|
||||||
if (currentAnomaly) {
|
if (currentAnomaly) {
|
||||||
if (currentAnomaly.Frame_Master) {
|
if (currentAnomaly.Frame_Master) {
|
||||||
masterImageSrc = IMAGES_AWS_PREFIX + currentAnomaly.Frame_Master;
|
masterImageSrc = IMAGES_AWS_PREFIX + currentAnomaly.Frame_Master;
|
||||||
@@ -287,10 +351,10 @@ export const AuditSession: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaItems: { type: 'image'|'video', url: string }[] = React.useMemo(() => {
|
const mediaItems: { type: 'image' | 'video', url: string }[] = React.useMemo(() => {
|
||||||
if (!currentAnomaly) return [];
|
if (!currentAnomaly) return [];
|
||||||
const items: { type: 'image'|'video', url: string }[] = [];
|
const items: { type: 'image' | 'video', url: string }[] = [];
|
||||||
|
|
||||||
if (currentAnomaly.images && currentAnomaly.images.length > 0) {
|
if (currentAnomaly.images && currentAnomaly.images.length > 0) {
|
||||||
currentAnomaly.images.forEach((img: any) => {
|
currentAnomaly.images.forEach((img: any) => {
|
||||||
items.push({ type: 'image', url: IMAGES_TEST_AUDIT_PREFIX + img.src });
|
items.push({ type: 'image', url: IMAGES_TEST_AUDIT_PREFIX + img.src });
|
||||||
@@ -302,7 +366,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
if (currentAnomaly.video_url) {
|
if (currentAnomaly.video_url) {
|
||||||
items.push({ type: 'video', url: IMAGES_TEST_AUDIT_PREFIX + currentAnomaly.video_url });
|
items.push({ type: 'video', url: IMAGES_TEST_AUDIT_PREFIX + currentAnomaly.video_url });
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [currentAnomaly]);
|
}, [currentAnomaly]);
|
||||||
|
|
||||||
@@ -341,20 +405,19 @@ export const AuditSession: React.FC = () => {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
handleNext();
|
handleNext();
|
||||||
}; const saveAuditState = useCallback(async (
|
}; const saveAuditState = useCallback(async (
|
||||||
anomalyVal: boolean | null,
|
anomalyVal: boolean | null,
|
||||||
catVal: number | null,
|
catVal: number | null,
|
||||||
notesVal: string,
|
notesVal: string,
|
||||||
plantSub?: string | null,
|
plantSub?: string | null,
|
||||||
notAnAnomalySub?: string | null
|
notAnAnomalySub?: string | null
|
||||||
) => {
|
) => {
|
||||||
if (!currentAnomaly || !user) return;
|
if (!currentAnomaly || !user) return;
|
||||||
try {
|
try {
|
||||||
const isAnomalyBool = anomalyVal ?? true;
|
const isAnomalyBool = anomalyVal ?? true;
|
||||||
const auditStatusVal = isAnomalyBool ? 1 : 0;
|
|
||||||
const categories = isAnomalyBool ? ANOMALY_CATEGORIES : SAFE_CATEGORIES;
|
const categories = isAnomalyBool ? ANOMALY_CATEGORIES : SAFE_CATEGORIES;
|
||||||
const matchedCategory = categories.find(c => c.id === catVal);
|
const matchedCategory = categories.find(c => c.id === catVal);
|
||||||
|
|
||||||
let auditValueVal = matchedCategory ? matchedCategory.value : '';
|
let auditValueVal = matchedCategory ? matchedCategory.value : '';
|
||||||
if (isAnomalyBool && catVal === 1 && plantSub) {
|
if (isAnomalyBool && catVal === 1 && plantSub) {
|
||||||
auditValueVal = plantSub;
|
auditValueVal = plantSub;
|
||||||
@@ -362,49 +425,55 @@ export const AuditSession: React.FC = () => {
|
|||||||
auditValueVal = notAnAnomalySub;
|
auditValueVal = notAnAnomalySub;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct properties matching Angular properties list:
|
// Body matches exactly what the API expects (dbName + isRectificationEnabled go in URL params via axiosClient)
|
||||||
// ["dbName", "isRectificationEnabled", "Audit_value", "Comments", "Audit_status", "Frame_Test", "Frame_Master", "Chainage", "Pos", "id", "road", "user_id", "Algorithm"]
|
|
||||||
const auditPayload = {
|
const auditPayload = {
|
||||||
dbName: user.db_name || '',
|
|
||||||
isRectificationEnabled: isRectActive,
|
|
||||||
Audit_value: auditValueVal,
|
Audit_value: auditValueVal,
|
||||||
Comments: notesVal || '',
|
Comments: notesVal || '',
|
||||||
Audit_status: auditStatusVal,
|
Audit_status: isAnomalyBool, // boolean true/false, not 1/0
|
||||||
Frame_Test: currentAnomaly.Frame_Test || '',
|
Frame_Test: currentAnomaly.Frame_Test || '',
|
||||||
Frame_Master: currentAnomaly.Frame_Master || '',
|
Frame_Master: currentAnomaly.Frame_Master || '',
|
||||||
Chainage: currentAnomaly.Chainage || '',
|
Chainage: currentAnomaly.Chainage || '',
|
||||||
Pos: currentAnomaly.Pos || '',
|
Pos: currentAnomaly.Pos || '',
|
||||||
id: currentAnomaly.id,
|
id: currentAnomaly.id,
|
||||||
road: currentAnomaly.road || 'mcw',
|
road: currentAnomaly.road || 'mcw',
|
||||||
|
Algorithm: currentAnomaly.Algorithm || '',
|
||||||
user_id: user.user_id || 0,
|
user_id: user.user_id || 0,
|
||||||
Algorithm: currentAnomaly.Algorithm || ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call API 1: Update anomaly
|
// Call API 1: Save audit result
|
||||||
await activityFeedsService.updateAnomaly([auditPayload], isRectActive);
|
await activityFeedsService.updateAnomaly([auditPayload], isRectActive);
|
||||||
|
|
||||||
// Call API 2: If true anomaly, update anomaly status in dashboard to notify client portal
|
// Call API 2: Notify dashboard — only for true anomaly, endpoint differs by mode
|
||||||
if (auditStatusVal === 1) {
|
if (isAnomalyBool) {
|
||||||
await activityFeedsService.updateAnomalyInDashboard({
|
if (isRectActive) {
|
||||||
site_id: String(currentAnomaly.site_id),
|
// Rectification mode: /auditor/anomaly/completed, site_id as number
|
||||||
anomaly_id: [currentAnomaly.id]
|
await activityFeedsService.markAnomalyCompleted({
|
||||||
});
|
anomaly_id: [currentAnomaly.id],
|
||||||
|
site_id: currentAnomaly.site_id
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Normal audit mode: /auditor/anomaly/update, site_id as string
|
||||||
|
await activityFeedsService.updateAnomalyInDashboard({
|
||||||
|
site_id: String(currentAnomaly.site_id),
|
||||||
|
anomaly_id: [currentAnomaly.id]
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to submit audit state:', err);
|
console.error('Failed to submit audit state:', err);
|
||||||
}
|
}
|
||||||
}, [currentAnomaly, user]);
|
}, [currentAnomaly, user, isRectActive]);
|
||||||
|
|
||||||
const handleSaveAndNext = useCallback(async () => {
|
const handleSaveAndNext = useCallback(async () => {
|
||||||
if (!currentAnomaly) return;
|
if (!currentAnomaly) return;
|
||||||
|
|
||||||
if (selectedCategory === 1) {
|
if (selectedCategory === 1) {
|
||||||
if ((isAnomaly === null || isAnomaly === true) && !plantSubCategory) {
|
if ((isAnomaly === null || isAnomaly === true) && !plantSubCategory) {
|
||||||
alert("Please select feature before submit.");
|
useToastStore.getState().showToast('Please select a feature before submitting.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isAnomaly === false && !notAnAnomalySubCategory) {
|
if (isAnomaly === false && !notAnAnomalySubCategory) {
|
||||||
alert("Please select feature before submit.");
|
useToastStore.getState().showToast('Please select a feature before submitting.', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,17 +483,17 @@ export const AuditSession: React.FC = () => {
|
|||||||
next[currentIndex] = 'audited';
|
next[currentIndex] = 'audited';
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call the API
|
// Call the API
|
||||||
await saveAuditState(isAnomaly, selectedCategory, notes, plantSubCategory, notAnAnomalySubCategory);
|
await saveAuditState(isAnomaly, selectedCategory, notes, plantSubCategory, notAnAnomalySubCategory);
|
||||||
|
|
||||||
handleNext();
|
handleNext();
|
||||||
}, [currentIndex, currentAnomaly, isAnomaly, selectedCategory, notes, plantSubCategory, notAnAnomalySubCategory, saveAuditState, handleNext]);
|
}, [currentIndex, currentAnomaly, isAnomaly, selectedCategory, notes, plantSubCategory, notAnAnomalySubCategory, saveAuditState, handleNext]);
|
||||||
|
|
||||||
// Quick Auto Advance save trigger
|
// Quick Auto Advance save trigger
|
||||||
const handleQuickSaveAndNext = useCallback(async (
|
const handleQuickSaveAndNext = useCallback(async (
|
||||||
anomalyVal: boolean,
|
anomalyVal: boolean,
|
||||||
catVal: number,
|
catVal: number,
|
||||||
plantSub?: string | null,
|
plantSub?: string | null,
|
||||||
notAnAnomalySub?: string | null
|
notAnAnomalySub?: string | null
|
||||||
) => {
|
) => {
|
||||||
@@ -437,7 +506,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
|
|
||||||
// Call the API
|
// Call the API
|
||||||
await saveAuditState(anomalyVal, catVal, notes, plantSub, notAnAnomalySub);
|
await saveAuditState(anomalyVal, catVal, notes, plantSub, notAnAnomalySub);
|
||||||
|
|
||||||
if (currentIndex < anomalies.length - 1) {
|
if (currentIndex < anomalies.length - 1) {
|
||||||
setCurrentIndex(prev => prev + 1);
|
setCurrentIndex(prev => prev + 1);
|
||||||
setSelectedCategory(null);
|
setSelectedCategory(null);
|
||||||
@@ -451,10 +520,10 @@ export const AuditSession: React.FC = () => {
|
|||||||
}, [currentIndex, anomalies.length, currentAnomaly, notes, saveAuditState]);
|
}, [currentIndex, anomalies.length, currentAnomaly, notes, saveAuditState]);
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
if (isNotesFocused) return;
|
if (isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen) return;
|
||||||
|
|
||||||
const key = e.key.toLowerCase();
|
const key = e.key.toLowerCase();
|
||||||
|
|
||||||
// Register active flash pulse
|
// Register active flash pulse
|
||||||
setActiveKeyFlash(key);
|
setActiveKeyFlash(key);
|
||||||
setTimeout(() => setActiveKeyFlash(null), 150);
|
setTimeout(() => setActiveKeyFlash(null), 150);
|
||||||
@@ -558,7 +627,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
handleSkip();
|
handleSkip();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [currentIndex, anomalies, isAnomaly, selectedCategory, notes, plantSubCategory, isQuickAudit, isNotesFocused, handleQuickSaveAndNext, handleSaveAndNext, handleSkip]);
|
}, [currentIndex, anomalies, isAnomaly, selectedCategory, notes, plantSubCategory, isQuickAudit, isNotesFocused, isReportDialogOpen, isShortcutsDialogOpen, handleQuickSaveAndNext, handleSaveAndNext, handleSkip]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
@@ -596,25 +665,25 @@ export const AuditSession: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
bgcolor: '#0b1121',
|
bgcolor: '#0b1121',
|
||||||
color: '#f8fafc',
|
color: '#f8fafc',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
zIndex: 9999
|
zIndex: 9999
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', p: 1, px: 2, borderBottom: '1px solid #1e293b', bgcolor: '#0f172a' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', p: 1, px: 2, borderBottom: '1px solid #1e293b', bgcolor: '#0f172a' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<ArrowBackIcon fontSize="small" />}
|
startIcon={<ArrowBackIcon fontSize="small" />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const navState = location.state as any;
|
const navState = location.state as any;
|
||||||
if (!navState?.useExistingFeed) invalidateDashboardCache();
|
if (!navState?.useExistingFeed) invalidateDashboardCache();
|
||||||
@@ -647,15 +716,15 @@ export const AuditSession: React.FC = () => {
|
|||||||
color = '#eab308'; // skipped (orange)
|
color = '#eab308'; // skipped (orange)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={index}
|
key={index}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
bgcolor: color,
|
bgcolor: color,
|
||||||
mr: index === itemAuditStates.length - 1 ? 0 : '1px',
|
mr: index === itemAuditStates.length - 1 ? 0 : '1px',
|
||||||
transition: 'background-color 0.2s ease'
|
transition: 'background-color 0.2s ease'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -663,10 +732,10 @@ export const AuditSession: React.FC = () => {
|
|||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden', p: 2, gap: 2 }}>
|
<Box sx={{ display: 'flex', flex: 1, overflow: 'hidden', p: 2, gap: 2 }}>
|
||||||
|
|
||||||
{/* Left Side: Images & Overlay */}
|
{/* Left Side: Images & Overlay */}
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, gap: 2, overflow: 'hidden' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, gap: 2, overflow: 'hidden' }}>
|
||||||
|
|
||||||
{/* Images Row */}
|
{/* Images Row */}
|
||||||
<Box sx={{ display: 'flex', flex: 1, gap: 2, overflow: 'hidden' }}>
|
<Box sx={{ display: 'flex', flex: 1, gap: 2, overflow: 'hidden' }}>
|
||||||
{/* Previous Image Pane */}
|
{/* Previous Image Pane */}
|
||||||
@@ -684,16 +753,16 @@ export const AuditSession: React.FC = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
{masterImageSrc ? (
|
{masterImageSrc ? (
|
||||||
<img
|
<img
|
||||||
src={masterImageSrc}
|
src={masterImageSrc}
|
||||||
alt="Master"
|
alt="Master"
|
||||||
style={{
|
style={{
|
||||||
width: masterZoom === 1 ? '100%' : `${masterZoom * 100}%`,
|
width: masterZoom === 1 ? '100%' : `${masterZoom * 100}%`,
|
||||||
height: masterZoom === 1 ? '100%' : 'auto',
|
height: masterZoom === 1 ? '100%' : 'auto',
|
||||||
objectFit: masterZoom === 1 ? 'contain' : 'cover',
|
objectFit: masterZoom === 1 ? 'contain' : 'cover',
|
||||||
transition: 'width 0.2s ease, height 0.2s ease',
|
transition: 'width 0.2s ease, height 0.2s ease',
|
||||||
transformOrigin: 'top left'
|
transformOrigin: 'top left'
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.currentTarget.onerror = null;
|
e.currentTarget.onerror = null;
|
||||||
e.currentTarget.src = '/assets/images/master_image_not_available.png';
|
e.currentTarget.src = '/assets/images/master_image_not_available.png';
|
||||||
@@ -723,30 +792,30 @@ export const AuditSession: React.FC = () => {
|
|||||||
{mediaItems.length > 0 ? (
|
{mediaItems.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{mediaItems[carouselIndex]?.type === 'video' ? (
|
{mediaItems[carouselIndex]?.type === 'video' ? (
|
||||||
<video src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{
|
<video src={mediaItems[carouselIndex]?.url} controls autoPlay loop style={{
|
||||||
width: anomalyZoom === 1 ? '100%' : `${anomalyZoom * 100}%`,
|
width: anomalyZoom === 1 ? '100%' : `${anomalyZoom * 100}%`,
|
||||||
height: anomalyZoom === 1 ? '100%' : 'auto',
|
height: anomalyZoom === 1 ? '100%' : 'auto',
|
||||||
objectFit: anomalyZoom === 1 ? 'contain' : 'cover',
|
objectFit: anomalyZoom === 1 ? 'contain' : 'cover',
|
||||||
transition: 'width 0.2s ease, height 0.2s ease'
|
transition: 'width 0.2s ease, height 0.2s ease'
|
||||||
}} />
|
}} />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={mediaItems[carouselIndex]?.url}
|
src={mediaItems[carouselIndex]?.url}
|
||||||
alt="Anomaly"
|
alt="Anomaly"
|
||||||
style={{
|
style={{
|
||||||
width: anomalyZoom === 1 ? '100%' : `${anomalyZoom * 100}%`,
|
width: anomalyZoom === 1 ? '100%' : `${anomalyZoom * 100}%`,
|
||||||
height: anomalyZoom === 1 ? '100%' : 'auto',
|
height: anomalyZoom === 1 ? '100%' : 'auto',
|
||||||
objectFit: anomalyZoom === 1 ? 'contain' : 'cover',
|
objectFit: anomalyZoom === 1 ? 'contain' : 'cover',
|
||||||
transition: 'width 0.2s ease, height 0.2s ease',
|
transition: 'width 0.2s ease, height 0.2s ease',
|
||||||
transformOrigin: 'top left'
|
transformOrigin: 'top left'
|
||||||
}}
|
}}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
e.currentTarget.onerror = null;
|
e.currentTarget.onerror = null;
|
||||||
e.currentTarget.src = '/assets/images/anomaly-placeholder.png';
|
e.currentTarget.src = '/assets/images/anomaly-placeholder.png';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mediaItems.length > 1 && (
|
{mediaItems.length > 1 && (
|
||||||
<Box sx={{ position: 'absolute', bottom: 16, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 1, bgcolor: 'rgba(0,0,0,0.6)', p: 0.5, borderRadius: 2 }}>
|
<Box sx={{ position: 'absolute', bottom: 16, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 1, bgcolor: 'rgba(0,0,0,0.6)', p: 0.5, borderRadius: 2 }}>
|
||||||
<Button size="small" variant="text" sx={{ color: 'white', minWidth: 'auto', px: 2, '&:hover': { bgcolor: 'rgba(255,255,255,0.1)' } }} onClick={() => setCarouselIndex(prev => prev > 0 ? prev - 1 : mediaItems.length - 1)}>
|
<Button size="small" variant="text" sx={{ color: 'white', minWidth: 'auto', px: 2, '&:hover': { bgcolor: 'rgba(255,255,255,0.1)' } }} onClick={() => setCarouselIndex(prev => prev > 0 ? prev - 1 : mediaItems.length - 1)}>
|
||||||
@@ -777,13 +846,13 @@ export const AuditSession: React.FC = () => {
|
|||||||
{currentAnomaly?.Plaza || 'Unknown Location'}
|
{currentAnomaly?.Plaza || 'Unknown Location'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Chip
|
<Chip
|
||||||
label={`ID #${currentAnomaly?.id || currentAnomaly?.anomaly_id || 'N/A'}`}
|
label={`ID #${currentAnomaly?.id || currentAnomaly?.anomaly_id || 'N/A'}`}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ bgcolor: 'rgba(59, 130, 246, 0.15)', color: '#60a5fa', fontWeight: 'bold', border: '1px solid rgba(59, 130, 246, 0.3)' }}
|
sx={{ bgcolor: 'rgba(59, 130, 246, 0.15)', color: '#60a5fa', fontWeight: 'bold', border: '1px solid rgba(59, 130, 246, 0.3)' }}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||||
<Chip size="small" label={currentAnomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset'} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5, fontWeight: 'medium' }} />
|
<Chip size="small" label={currentAnomaly?.Asset?.replace(/_/gi, ' ') || 'Unknown Asset'} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5, fontWeight: 'medium' }} />
|
||||||
<Chip size="small" label={currentAnomaly?.date_of_audit || currentAnomaly?.Created_on ? new Date(currentAnomaly?.date_of_audit || currentAnomaly?.Created_on).toLocaleString() : 'No Date'} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
|
<Chip size="small" label={currentAnomaly?.date_of_audit || currentAnomaly?.Created_on ? new Date(currentAnomaly?.date_of_audit || currentAnomaly?.Created_on).toLocaleString() : 'No Date'} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
|
||||||
@@ -791,19 +860,29 @@ export const AuditSession: React.FC = () => {
|
|||||||
<Chip size="small" label={`Side: ${currentAnomaly?.Side || 'N/A'}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
|
<Chip size="small" label={`Side: ${currentAnomaly?.Side || 'N/A'}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
|
||||||
<Chip size="small" label={`${latStr}${longStr ? `, ${longStr}` : ''}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
|
<Chip size="small" label={`${latStr}${longStr ? `, ${longStr}` : ''}`} sx={{ bgcolor: '#1e293b', color: '#e2e8f0', borderRadius: 1.5 }} />
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setIsReportDialogOpen(true)}
|
||||||
|
sx={{ borderColor: '#f59e0b', color: '#f59e0b', textTransform: 'none', fontSize: '0.75rem', borderRadius: 1.5, '&:hover': { bgcolor: 'rgba(245,158,11,0.1)', borderColor: '#f59e0b' } }}
|
||||||
|
>
|
||||||
|
Report to Dev
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Right Side: Sidebar */}
|
{/* Right Side: Sidebar */}
|
||||||
<Box sx={{ width: 320, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
<Box sx={{ width: 320, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
|
||||||
{/* Quick-Audit Auto-Advance Toggle */}
|
{/* Quick-Audit Auto-Advance Toggle */}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: '#0f172a', p: 1.5, borderRadius: 2, border: '1px solid #1e293b' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: '#0f172a', p: 1.5, borderRadius: 2, border: '1px solid #1e293b' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography sx={{ fontSize: '0.8rem', fontWeight: 'bold', color: '#f8fafc' }}>Quick Audit</Typography>
|
<Typography sx={{ fontSize: '0.8rem', fontWeight: 'bold', color: '#f8fafc' }}>Quick Audit</Typography>
|
||||||
<Typography sx={{ fontSize: '0.65rem', color: '#64748b' }}>Auto-advances on keypress</Typography>
|
<Typography sx={{ fontSize: '0.65rem', color: '#64748b' }}>Auto-advances on keypress</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={isQuickAudit}
|
checked={isQuickAudit}
|
||||||
onChange={(e) => handleToggleQuickAudit(e.target.checked)}
|
onChange={(e) => handleToggleQuickAudit(e.target.checked)}
|
||||||
@@ -821,15 +900,15 @@ export const AuditSession: React.FC = () => {
|
|||||||
<Typography sx={{ fontSize: '0.75rem', color: '#64748b' }}>A / S</Typography>
|
<Typography sx={{ fontSize: '0.75rem', color: '#64748b' }}>A / S</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant={isAnomaly === true ? "contained" : "outlined"}
|
variant={isAnomaly === true ? "contained" : "outlined"}
|
||||||
color={isAnomaly === true ? "error" : "inherit"}
|
color={isAnomaly === true ? "error" : "inherit"}
|
||||||
onClick={() => setIsAnomaly(true)}
|
onClick={() => setIsAnomaly(true)}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
borderColor: '#334155',
|
borderColor: '#334155',
|
||||||
color: isAnomaly === true ? 'white' : '#cbd5e1',
|
color: isAnomaly === true ? 'white' : '#cbd5e1',
|
||||||
transition: 'all 0.15s ease',
|
transition: 'all 0.15s ease',
|
||||||
border: activeKeyFlash === 'a' ? '2px solid #ef4444' : '1px solid #334155',
|
border: activeKeyFlash === 'a' ? '2px solid #ef4444' : '1px solid #334155',
|
||||||
@@ -838,8 +917,8 @@ export const AuditSession: React.FC = () => {
|
|||||||
>
|
>
|
||||||
⚠️ Anomaly
|
⚠️ Anomaly
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant={isAnomaly === false ? "contained" : "outlined"}
|
variant={isAnomaly === false ? "contained" : "outlined"}
|
||||||
color={isAnomaly === false ? "success" : "inherit"}
|
color={isAnomaly === false ? "success" : "inherit"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -847,10 +926,10 @@ export const AuditSession: React.FC = () => {
|
|||||||
setSelectedCategory(null);
|
setSelectedCategory(null);
|
||||||
setPlantSubCategory(null);
|
setPlantSubCategory(null);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
borderColor: '#334155',
|
borderColor: '#334155',
|
||||||
color: isAnomaly === false ? 'white' : '#cbd5e1',
|
color: isAnomaly === false ? 'white' : '#cbd5e1',
|
||||||
transition: 'all 0.15s ease',
|
transition: 'all 0.15s ease',
|
||||||
border: activeKeyFlash === 's' ? '2px solid #22c55e' : '1px solid #334155',
|
border: activeKeyFlash === 's' ? '2px solid #22c55e' : '1px solid #334155',
|
||||||
@@ -874,7 +953,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
if (isAnomaly === null) {
|
if (isAnomaly === null) {
|
||||||
setIsAnomaly(true);
|
setIsAnomaly(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If selecting Plant (Anomaly 1) or Not an Anomaly (Safe 1), do not auto-advance; wait for nested sub-options
|
// If selecting Plant (Anomaly 1) or Not an Anomaly (Safe 1), do not auto-advance; wait for nested sub-options
|
||||||
if (cat.id === 1) {
|
if (cat.id === 1) {
|
||||||
return;
|
return;
|
||||||
@@ -884,9 +963,9 @@ export const AuditSession: React.FC = () => {
|
|||||||
setTimeout(() => handleQuickSaveAndNext(currentIsAnom, cat.id), 120);
|
setTimeout(() => handleQuickSaveAndNext(currentIsAnom, cat.id), 120);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
color: selectedCategory === cat.id ? 'white' : '#cbd5e1',
|
color: selectedCategory === cat.id ? 'white' : '#cbd5e1',
|
||||||
bgcolor: selectedCategory === cat.id ? 'rgba(255,255,255,0.05)' : 'transparent',
|
bgcolor: selectedCategory === cat.id ? 'rgba(255,255,255,0.05)' : 'transparent',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
@@ -982,20 +1061,20 @@ export const AuditSession: React.FC = () => {
|
|||||||
{/* Notes */}
|
{/* Notes */}
|
||||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||||
<Typography sx={{ fontSize: '0.75rem', fontWeight: 'bold', color: '#94a3b8', letterSpacing: 1, mb: 1 }}>NOTES</Typography>
|
<Typography sx={{ fontSize: '0.75rem', fontWeight: 'bold', color: '#94a3b8', letterSpacing: 1, mb: 1 }}>NOTES</Typography>
|
||||||
<Box
|
<Box
|
||||||
component="textarea"
|
component="textarea"
|
||||||
value={notes}
|
value={notes}
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
onFocus={() => setIsNotesFocused(true)}
|
onFocus={() => setIsNotesFocused(true)}
|
||||||
onBlur={() => setIsNotesFocused(false)}
|
onBlur={() => setIsNotesFocused(false)}
|
||||||
placeholder="Optional observations..."
|
placeholder="Optional observations..."
|
||||||
sx={{
|
sx={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 60,
|
minHeight: 60,
|
||||||
bgcolor: '#0f172a',
|
bgcolor: '#0f172a',
|
||||||
border: '1px solid #1e293b',
|
border: '1px solid #1e293b',
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
color: 'white',
|
color: 'white',
|
||||||
p: 2,
|
p: 2,
|
||||||
fontFamily: 'inherit',
|
fontFamily: 'inherit',
|
||||||
@@ -1007,17 +1086,17 @@ export const AuditSession: React.FC = () => {
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color={selectedCategory !== null ? "primary" : "inherit"}
|
color={selectedCategory !== null ? "primary" : "inherit"}
|
||||||
onClick={selectedCategory !== null ? handleSaveAndNext : handleSkip}
|
onClick={selectedCategory !== null ? handleSaveAndNext : handleSkip}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
py: 1.5,
|
py: 1.5,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
px: 3,
|
px: 3,
|
||||||
transition: 'all 0.15s ease',
|
transition: 'all 0.15s ease',
|
||||||
bgcolor: selectedCategory !== null ? '' : '#1e293b',
|
bgcolor: selectedCategory !== null ? '' : '#1e293b',
|
||||||
@@ -1029,35 +1108,35 @@ export const AuditSession: React.FC = () => {
|
|||||||
{selectedCategory !== null ? "Save & Next" : "Next (Skip)"} <Typography component="span" sx={{ fontSize: '0.75rem', opacity: 0.7 }}>Enter</Typography>
|
{selectedCategory !== null ? "Save & Next" : "Next (Skip)"} <Typography component="span" sx={{ fontSize: '0.75rem', opacity: 0.7 }}>Enter</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handlePrev}
|
onClick={handlePrev}
|
||||||
disabled={currentIndex === 0}
|
disabled={currentIndex === 0}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
borderColor: '#1e293b',
|
borderColor: '#1e293b',
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
transition: 'all 0.15s ease',
|
transition: 'all 0.15s ease',
|
||||||
border: activeKeyFlash === 'arrowleft' ? '2px solid #64748b' : '1px solid #1e293b',
|
border: activeKeyFlash === 'arrowleft' ? '2px solid #64748b' : '1px solid #1e293b',
|
||||||
'&:hover': { borderColor: '#334155' }
|
'&:hover': { borderColor: '#334155' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
← Prev
|
← Prev
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={handleSkip}
|
onClick={handleSkip}
|
||||||
sx={{
|
sx={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
borderColor: '#1e293b',
|
borderColor: '#1e293b',
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
transition: 'all 0.15s ease',
|
transition: 'all 0.15s ease',
|
||||||
border: activeKeyFlash === 'tab' ? '2px solid #64748b' : '1px solid #1e293b',
|
border: activeKeyFlash === 'tab' ? '2px solid #64748b' : '1px solid #1e293b',
|
||||||
'&:hover': { borderColor: '#334155' }
|
'&:hover': { borderColor: '#334155' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Skip <span style={{ fontSize: '0.75rem', opacity: 0.7, marginLeft: 8 }}>Tab</span>
|
Skip <span style={{ fontSize: '0.75rem', opacity: 0.7, marginLeft: 8 }}>Tab</span>
|
||||||
@@ -1070,9 +1149,9 @@ export const AuditSession: React.FC = () => {
|
|||||||
{/* Footer Shortcuts */}
|
{/* Footer Shortcuts */}
|
||||||
<Box sx={{ borderTop: '1px solid #1e293b', p: 1, px: 2, display: 'flex', gap: 3, alignItems: 'center', overflowX: 'auto', '& > div': { display: 'flex', alignItems: 'center', gap: 1, whiteSpace: 'nowrap' } }}>
|
<Box sx={{ borderTop: '1px solid #1e293b', p: 1, px: 2, display: 'flex', gap: 3, alignItems: 'center', overflowX: 'auto', '& > div': { display: 'flex', alignItems: 'center', gap: 1, whiteSpace: 'nowrap' } }}>
|
||||||
<Typography sx={{ fontSize: '0.75rem', color: '#64748b' }}>
|
<Typography sx={{ fontSize: '0.75rem', color: '#64748b' }}>
|
||||||
{isNotesFocused ? "Shortcuts Paused (Typing notes)" : "Keys:"}
|
{(isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen) ? "Shortcuts Paused" : "Keys:"}
|
||||||
</Typography>
|
</Typography>
|
||||||
{!isNotesFocused && (
|
{!(isNotesFocused || isReportDialogOpen || isShortcutsDialogOpen) && (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
<Chip label="A" size="small" sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#1e293b', color: '#cbd5e1' }} />
|
<Chip label="A" size="small" sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#1e293b', color: '#cbd5e1' }} />
|
||||||
@@ -1106,6 +1185,77 @@ export const AuditSession: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* Report to Dev Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={isReportDialogOpen}
|
||||||
|
onClose={() => { setIsReportDialogOpen(false); setReportDev(''); setReportMessage(''); }}
|
||||||
|
sx={{ zIndex: 10000 }}
|
||||||
|
slotProps={{ paper: { sx: { bgcolor: '#0f172a', color: '#f8fafc', border: '1px solid #1e293b', minWidth: 400 } } }}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontWeight: 'bold', pb: 1, borderBottom: '1px solid #1e293b' }}>
|
||||||
|
Report to Dev
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent sx={{ pt: 2, display: 'flex', flexDirection: 'column', gap: 2.5 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5 }}>Anomaly</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: '#f8fafc' }}>
|
||||||
|
ID #{currentAnomaly?.id} — {currentAnomaly?.Asset?.replace(/_/gi, ' ')} @ {currentAnomaly?.Plaza} (CH: {currentAnomaly?.Chainage})
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5 }}>Select Developer</Typography>
|
||||||
|
<Select
|
||||||
|
value={reportDev}
|
||||||
|
onChange={(e) => setReportDev(e.target.value)}
|
||||||
|
displayEmpty
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
sx={{ color: '#f8fafc', '& fieldset': { borderColor: '#334155' }, '& .MuiSvgIcon-root': { color: '#94a3b8' } }}
|
||||||
|
slotProps={{ input: { sx: { color: '#f8fafc' } } }}
|
||||||
|
MenuProps={{ sx: { zIndex: 10001 }, slotProps: { paper: { sx: { bgcolor: '#0f172a', border: '1px solid #1e293b' } } } }}
|
||||||
|
>
|
||||||
|
<MenuItem disabled value=""><em style={{ color: '#64748b' }}>Choose a developer</em></MenuItem>
|
||||||
|
{DEVS.map(dev => (
|
||||||
|
<MenuItem key={dev} value={dev} sx={{ color: '#f8fafc', bgcolor: '#0f172a', '&:hover': { bgcolor: '#1e293b' }, '&.Mui-selected': { bgcolor: '#1e3a5f' }, '&.Mui-selected:hover': { bgcolor: '#1e3a5f' } }}>@{dev}</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mb: 0.5 }}>Message</Typography>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
fullWidth
|
||||||
|
placeholder="Describe the issue..."
|
||||||
|
value={reportMessage}
|
||||||
|
onChange={(e) => setReportMessage(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
color: '#f8fafc',
|
||||||
|
'& fieldset': { borderColor: '#334155' },
|
||||||
|
'&:hover fieldset': { borderColor: '#475569' },
|
||||||
|
'&.Mui-focused fieldset': { borderColor: '#3b82f6' },
|
||||||
|
},
|
||||||
|
'& .MuiInputBase-input::placeholder': { color: '#64748b', opacity: 1 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ borderTop: '1px solid #1e293b', px: 3, py: 1.5, gap: 1 }}>
|
||||||
|
<Button onClick={() => { setIsReportDialogOpen(false); setReportDev(''); setReportMessage(''); }} sx={{ color: '#94a3b8', textTransform: 'none' }}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSendReport}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!reportDev || !reportMessage.trim() || reportSending}
|
||||||
|
sx={{ bgcolor: '#f59e0b', color: '#000', fontWeight: 'bold', textTransform: 'none', '&:hover': { bgcolor: '#d97706' }, '&.Mui-disabled': { bgcolor: 'rgba(245,158,11,0.3)', color: 'rgba(0,0,0,0.4)' } }}
|
||||||
|
>
|
||||||
|
{reportSending ? <CircularProgress size={18} sx={{ color: '#000' }} /> : 'Send'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Keyboard Shortcuts Dialog */}
|
{/* Keyboard Shortcuts Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isShortcutsDialogOpen}
|
open={isShortcutsDialogOpen}
|
||||||
@@ -1182,7 +1332,7 @@ export const AuditSession: React.FC = () => {
|
|||||||
<Typography variant="body1" sx={{ color: '#cbd5e1', mb: 2 }}>
|
<Typography variant="body1" sx={{ color: '#cbd5e1', mb: 2 }}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const navState = location.state as any;
|
const navState = location.state as any;
|
||||||
return navState?.useExistingFeed
|
return navState?.useExistingFeed
|
||||||
? `You have successfully completed this feed session.`
|
? `You have successfully completed this feed session.`
|
||||||
: `You have successfully audited this session of 50 items.`;
|
: `You have successfully audited this session of 50 items.`;
|
||||||
})()}
|
})()}
|
||||||
@@ -1204,9 +1354,9 @@ export const AuditSession: React.FC = () => {
|
|||||||
const feedPageSize = activeTab?.rowsPerPage || 20;
|
const feedPageSize = activeTab?.rowsPerPage || 20;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={isFeedSession ? handleLoadNextFeedPage : async () => {
|
onClick={isFeedSession ? handleLoadNextFeedPage : async () => {
|
||||||
setShowCompletionModal(false);
|
setShowCompletionModal(false);
|
||||||
const nextPage = sessionPageNo + 1;
|
const nextPage = sessionPageNo + 1;
|
||||||
@@ -1217,9 +1367,9 @@ export const AuditSession: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{isFeedSession ? `Audit Next ${feedPageSize} Anomalies` : 'Audit Next 50 Items'}
|
{isFeedSession ? `Audit Next ${feedPageSize} Anomalies` : 'Audit Next 50 Items'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowCompletionModal(false);
|
setShowCompletionModal(false);
|
||||||
if (!isFeedSession) invalidateDashboardCache();
|
if (!isFeedSession) invalidateDashboardCache();
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ interface FeedState {
|
|||||||
dashboardCache: DashboardCache;
|
dashboardCache: DashboardCache;
|
||||||
searchAnomalyId: string;
|
searchAnomalyId: string;
|
||||||
isRectificationEnabled: boolean;
|
isRectificationEnabled: boolean;
|
||||||
|
isFeedLoading: boolean;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
setTabs: (tabs: FeedTab[]) => void;
|
setTabs: (tabs: FeedTab[]) => void;
|
||||||
@@ -82,6 +83,7 @@ interface FeedState {
|
|||||||
setFilterPlaza: (val: string) => void;
|
setFilterPlaza: (val: string) => void;
|
||||||
setSearchAnomalyId: (id: string) => void;
|
setSearchAnomalyId: (id: string) => void;
|
||||||
setIsRectificationEnabled: (val: boolean) => void;
|
setIsRectificationEnabled: (val: boolean) => void;
|
||||||
|
setIsFeedLoading: (val: boolean) => void;
|
||||||
resetStore: () => void;
|
resetStore: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +122,7 @@ const initialFeedState = {
|
|||||||
dashboardCache: EMPTY_DASHBOARD_CACHE,
|
dashboardCache: EMPTY_DASHBOARD_CACHE,
|
||||||
searchAnomalyId: '',
|
searchAnomalyId: '',
|
||||||
isRectificationEnabled: false,
|
isRectificationEnabled: false,
|
||||||
|
isFeedLoading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFeedStore = create<FeedState>()(
|
export const useFeedStore = create<FeedState>()(
|
||||||
@@ -148,6 +151,7 @@ export const useFeedStore = create<FeedState>()(
|
|||||||
setFilterPlaza: (filterPlaza) => set({ filterPlaza }),
|
setFilterPlaza: (filterPlaza) => set({ filterPlaza }),
|
||||||
setSearchAnomalyId: (searchAnomalyId) => set({ searchAnomalyId }),
|
setSearchAnomalyId: (searchAnomalyId) => set({ searchAnomalyId }),
|
||||||
setIsRectificationEnabled: (isRectificationEnabled) => set({ isRectificationEnabled }),
|
setIsRectificationEnabled: (isRectificationEnabled) => set({ isRectificationEnabled }),
|
||||||
|
setIsFeedLoading: (isFeedLoading) => set({ isFeedLoading }),
|
||||||
addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })),
|
addTab: (tab) => set((state) => ({ tabs: [...state.tabs, tab], selectedTabIndex: state.tabs.length })),
|
||||||
closeTab: (index) => set((state) => {
|
closeTab: (index) => set((state) => {
|
||||||
const newTabs = [...state.tabs];
|
const newTabs = [...state.tabs];
|
||||||
|
|||||||
30
src/store/toastStore.ts
Normal file
30
src/store/toastStore.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export type ToastSeverity = 'success' | 'error' | 'warning' | 'info';
|
||||||
|
|
||||||
|
interface Toast {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
severity: ToastSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToastState {
|
||||||
|
toasts: Toast[];
|
||||||
|
showToast: (message: string, severity?: ToastSeverity) => void;
|
||||||
|
removeToast: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useToastStore = create<ToastState>((set) => ({
|
||||||
|
toasts: [],
|
||||||
|
|
||||||
|
showToast: (message, severity = 'info') => {
|
||||||
|
const id = `${Date.now()}-${Math.random()}`;
|
||||||
|
set((state) => ({ toasts: [...state.toasts, { id, message, severity }] }));
|
||||||
|
setTimeout(() => {
|
||||||
|
useToastStore.getState().removeToast(id);
|
||||||
|
}, 4000);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeToast: (id) =>
|
||||||
|
set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) })),
|
||||||
|
}));
|
||||||
@@ -6,7 +6,13 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
// Reverse proxy is no longer needed since endpoints allow CORS natively
|
proxy: {
|
||||||
|
'/api/rocketchat': {
|
||||||
|
target: 'https://text.seekright.com',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api\/rocketchat/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user