From c872566e83dd941792ec5683d3e4d8a797827d4e Mon Sep 17 00:00:00 2001 From: kaushik Date: Wed, 15 Jul 2026 14:24:26 +0530 Subject: [PATCH] dev tickets and issues raised --- src/api/activityFeedsService.ts | 4 +- src/api/adminService.ts | 87 ++++---- src/components/common/BulkDeleteDialog.tsx | 31 ++- src/components/common/TopFilterBar.tsx | 230 +++++++++------------ src/constants/media.ts | 14 ++ src/pages/activity-feeds/AnomalyTable.tsx | 22 +- src/pages/activity-feeds/FeedFilters.tsx | 3 +- src/pages/activity-feeds/FeedTabs.tsx | 2 +- src/pages/activity-feeds/GlobalFeed.tsx | 5 +- src/pages/admin/AdminConsole.tsx | 3 - src/pages/admin/DevTickets.tsx | 79 ++++++- src/pages/admin/RecordDetailsDialog.tsx | 212 +++++++++++++++++++ src/pages/admin/UnderReview.tsx | 209 ------------------- src/pages/audit-session/AuditSession.tsx | 4 +- src/store/feedStore.ts | 3 + 15 files changed, 489 insertions(+), 419 deletions(-) create mode 100644 src/constants/media.ts create mode 100644 src/pages/admin/RecordDetailsDialog.tsx delete mode 100644 src/pages/admin/UnderReview.tsx diff --git a/src/api/activityFeedsService.ts b/src/api/activityFeedsService.ts index 168386c..328f6a6 100644 --- a/src/api/activityFeedsService.ts +++ b/src/api/activityFeedsService.ts @@ -6,9 +6,9 @@ export const activityFeedsService = { return response; }, - getHistoryDates: async (isRectificationEnabled: boolean, siteIds?: string) => { + getHistoryDates: async (isRectificationEnabled: boolean, siteIds?: string, auditedByMe = false) => { const response = await axiosClient.get('/api/audit/anomaly/get_history_dates', { - params: { isRectificationEnabled, siteIds: siteIds || '' } + params: { isRectificationEnabled, siteIds: siteIds || '', auditedByMe } }); return response; }, diff --git a/src/api/adminService.ts b/src/api/adminService.ts index 3e7827e..e4c1f1a 100644 --- a/src/api/adminService.ts +++ b/src/api/adminService.ts @@ -126,6 +126,43 @@ export interface DevTicket { retry_count: number; last_retry_at: string | null; created_at: string; + // Which org table the record lives in; null on legacy tickets. + record_type: 'audits' | 'rectification' | null; + // Unified resolution stamp - set by the admin Resolve button or by the + // git-state cron when the Gitea issue is closed. + resolved_at: string | null; + resolved_by: string | null; + // Live record state, attached by the list endpoint. + record_status: 'under_review' | 'resolved' | 'audited' | 'deleted' | 'unknown'; +} + +// Full record row as returned by /admin/dev-tickets/record-details. +export interface TicketRecordDetails { + ticket: DevTicket; + recordType: 'audits' | 'rectification'; + record: { + id: number; + Asset: string | null; + asset_id: number | null; + Plaza: string | null; + Chainage: string | null; + Side: string | null; + Pos: string | null; + video_date: string | null; + Created_on: string | null; + Frame_Test: string | null; + Frame_Master: string | null; + Extra_Image: string | null; + video_url: string | null; + Comments: string | null; + IsAudited: number; + Audit_status: number | null; + Audit_value: string | null; + under_review: number | null; + deleted: number; + road?: string | null; + updated_user_name: string | null; + }; } export interface DevTicketsResult { @@ -193,27 +230,6 @@ export interface MissingMediaResult { summary: MissingMediaSummary; } -// A pending record quarantined by Report to Dev (under_review = 1), with -// its latest dev ticket merged in (null if the ticket predates ticket -// logging or was raised outside this org). -export interface UnderReviewRecord { - id: number; - Asset: string | null; - Plaza: string | null; - Chainage: string | null; - Side: string | null; - video_date: string | null; - updated_user_name: string | null; - Frame_Test: string | null; - ticket: { - anomaly_id: number; - dev_handle: string; - reporter: string | null; - created_at: string; - git_status: 'pending' | 'success' | 'failed'; - git_issue_url: string | null; - } | null; -} export const adminService = { getUsers: async (): Promise => { @@ -331,28 +347,19 @@ export const adminService = { return (await axiosClient.get('/api/audit/admin/media-status', { params })) as unknown as MissingMediaResult; }, - // Pending records quarantined by Report to Dev (under_review = 1) - - // hidden from auditors until released here. - getUnderReviewRecords: async (params: { - dbName: string; - type: 'audits' | 'rectification'; - limit?: number; - offset?: number; - }): Promise<{ records: UnderReviewRecord[]; total: number }> => { - return (await axiosClient.get('/api/audit/admin/under-review', { params })) as unknown as { - records: UnderReviewRecord[]; total: number; + // Unified resolve: returns the quarantined record to the auditors' feed + // and (best-effort) closes the linked Gitea issue. + resolveTicket: async (ticketId: number): Promise<{ recordReleased: boolean; issueClosed: boolean }> => { + return (await axiosClient.post('/api/audit/admin/dev-tickets/resolve', { ticketId })) as unknown as { + recordReleased: boolean; issueClosed: boolean; }; }, - // Put up to 50 quarantined records back into the auditors' pending feed. - releaseUnderReview: async (payload: { - dbName: string; - type: 'audits' | 'rectification'; - ids: number[]; - }): Promise<{ released: number }> => { - return (await axiosClient.post('/api/audit/admin/under-review/release', payload)) as unknown as { - released: number; - }; + // Complete record + ticket details for the admin popup. + getTicketRecordDetails: async (ticketId: number): Promise => { + return (await axiosClient.get('/api/audit/admin/dev-tickets/record-details', { + params: { ticketId }, + })) as unknown as TicketRecordDetails; }, // Immediate CDN re-verify for up to 50 records (also resets the cron's diff --git a/src/components/common/BulkDeleteDialog.tsx b/src/components/common/BulkDeleteDialog.tsx index 3dac128..f94ed6c 100644 --- a/src/components/common/BulkDeleteDialog.tsx +++ b/src/components/common/BulkDeleteDialog.tsx @@ -7,6 +7,7 @@ import { import CloseIcon from '@mui/icons-material/Close'; import { useFeedStore } from '../../store/feedStore'; import { useAuthStore } from '../../store/authStore'; +import { useToastStore } from '../../store/toastStore'; import { activityFeedsService } from '../../api/activityFeedsService'; interface BulkDeleteDialogProps { @@ -17,8 +18,9 @@ interface BulkDeleteDialogProps { const REASONS = ['Low Light', 'Bad Image', 'Wrong Algorithm', 'Out of Range']; export const BulkDeleteDialog: React.FC = ({ open, onClose }) => { - const { assetTypes, selectedSite, sites } = useFeedStore(); + const { assetTypes, selectedSite, sites, isRectificationEnabled, tabs, selectedTabIndex, updateTab, dashboardCache, setDashboardCache } = useFeedStore(); const { user } = useAuthStore(); + const showToast = useToastStore((s) => s.showToast); const [fromDate, setFromDate] = useState(''); const [tillDate, setTillDate] = useState(''); @@ -83,11 +85,11 @@ export const BulkDeleteDialog: React.FC = ({ open, onClos const handleApply = async () => { if (!fromDate || !tillDate) { - alert("Please select both From and Till dates."); + showToast("Please select both From and Till dates.", "warning"); return; } if (selectedAssets.size === 0) { - alert("Please select at least one asset."); + showToast("Please select at least one asset.", "warning"); return; } @@ -108,20 +110,33 @@ export const BulkDeleteDialog: React.FC = ({ open, onClos site_id: siteIds, fromDate: fromDate, toDate: tillDate, - user_id: Number(user?.user_id), audit_value: reason, isBulkDelete: true, - audit_type: auditType + audit_type: auditType, + isRectificationEnabled, }; try { setIsLoading(true); - await activityFeedsService.bulkDelete(payload); - alert("Bulk delete successful!"); + const res: any = await activityFeedsService.bulkDelete(payload); + const affected = res?.data?.affectedRows ?? res?.data?.rowCount; + // Drop the deleted records from view: clear the active tab's cache so + // GlobalFeed refetches (its guard refetches when anomalies is empty), + // and invalidate dashboard stats. + if (selectedTabIndex >= 0 && tabs[selectedTabIndex]) { + updateTab(selectedTabIndex, { anomalies: [], anomaliesCopy: [], filters: {} }); + } + setDashboardCache({ ...dashboardCache, stats: null }); + showToast( + typeof affected === 'number' + ? `Bulk delete successful — ${affected} record(s) removed.` + : 'Bulk delete successful.', + 'success' + ); onClose(); } catch (err: any) { console.error("Bulk delete failed", err); - alert("Bulk delete failed. " + (err?.response?.data?.message || err.message || "")); + showToast("Bulk delete failed. " + (err?.response?.data?.message || err.message || ""), "error"); } finally { setIsLoading(false); } diff --git a/src/components/common/TopFilterBar.tsx b/src/components/common/TopFilterBar.tsx index bbe9114..f3afa05 100644 --- a/src/components/common/TopFilterBar.tsx +++ b/src/components/common/TopFilterBar.tsx @@ -118,18 +118,21 @@ export const TopFilterBar: React.FC = () => { const [loadingHistory, setLoadingHistory] = useState(false); const [loadingRectHistory, setLoadingRectHistory] = useState(false); const [isHistoryDialogOpen, setIsHistoryDialogOpen] = useState(false); - const [selectedHistoryDate, setSelectedHistoryDate] = useState('2026-06-12'); - const [historyDropdownValue, setHistoryDropdownValue] = useState(''); + // One dialog serves both History surfaces; the mode decides which table + // (audits vs rectification) the tab it opens will query. + const [historyDialogMode, setHistoryDialogMode] = useState<'audits' | 'rect'>('rect'); + const [historyFrom, setHistoryFrom] = useState(''); + const [historyTill, setHistoryTill] = useState(''); - // Fetch normal history dates (isRectificationEnabled=false) for the History dropdown - // Rectification history (isRectificationEnabled=true) is fetched separately by the RECTIFICATION HISTORY button + // Fetch normal history dates (isRectificationEnabled=false) for the audits-mode History dialog chips + // Rectification dates (isRectificationEnabled=true) are fetched when the dialog opens in rect mode useEffect(() => { if (!selectedSite || selectedSite.site_id === 'All Sites') return; const fetchDates = async () => { setLoadingHistory(true); try { const siteIds = String(selectedSite.site_id); - const dates: any = await activityFeedsService.getHistoryDates(false, siteIds); + const dates: any = await activityFeedsService.getHistoryDates(false, siteIds, true); setHistoryDates(Array.isArray(dates) ? dates : (dates?.responseData || [])); } catch (err) { console.error("Failed to load history dates", err); @@ -140,50 +143,41 @@ export const TopFilterBar: React.FC = () => { fetchDates(); }, [selectedSite?.site_id]); // eslint-disable-line react-hooks/exhaustive-deps - const handleHistoryClick = async () => { + const openHistoryDialog = async (mode: 'audits' | 'rect') => { + setHistoryDialogMode(mode); + setHistoryFrom(''); + setHistoryTill(''); setIsHistoryDialogOpen(true); - if (!selectedSite || selectedSite.site_id === 'All Sites') return; - setLoadingRectHistory(true); - try { - const siteIds = String(selectedSite.site_id); - const dates: any = await activityFeedsService.getHistoryDates(true, siteIds); - setRectHistoryDates(Array.isArray(dates) ? dates : (dates?.responseData || [])); - } catch (err) { - console.error("Failed to load rectification history dates", err); - } finally { - setLoadingRectHistory(false); + if (mode === 'rect') { + if (!selectedSite || selectedSite.site_id === 'All Sites') return; + setLoadingRectHistory(true); + try { + const siteIds = String(selectedSite.site_id); + const dates: any = await activityFeedsService.getHistoryDates(true, siteIds, true); + setRectHistoryDates(Array.isArray(dates) ? dates : (dates?.responseData || [])); + } catch (err) { + console.error("Failed to load rectification history dates", err); + } finally { + setLoadingRectHistory(false); + } } + // 'audits' chips come from the historyDates effect (refetched per site). }; - // Used by RECTIFICATION HISTORY dialog — isRectificationTab: true → fetches with isRectificationEnabled=true - const handleSelectDate = (date: string) => { - const existingIndex = tabs.findIndex(t => t.date === date && t.isRectificationTab); + // Opens (or focuses) a History tab: records audited BY the logged-in user + // (auditedByMe) on one date or a From-Till range. The range is encoded in + // the tab's `date` as "from_till" - the backend parses it; single day = + // plain date. Audits and rectification stay separate via isRectificationTab. + const openHistoryTab = (isRect: boolean, from: string, till: string) => { + const dateKey = from === till ? from : `${from}_${till}`; + const existingIndex = tabs.findIndex(t => t.date === dateKey && !!t.isRectificationTab === isRect); if (existingIndex !== -1) { setSelectedTabIndex(existingIndex); } else { addTab({ - date: date, - isRectificationTab: true, - totalAnomalyCount: 0, - anomalies: [], - anomaliesCopy: [], - selectedAnomalyIndex: 0, - filters: {}, - page: 0, - rowsPerPage: 20 - }); - } - }; - - // 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, + date: dateKey, + isRectificationTab: isRect, + auditedByMe: true, totalAnomalyCount: 0, anomalies: [], anomaliesCopy: [], @@ -196,8 +190,12 @@ export const TopFilterBar: React.FC = () => { }; const handleViewHistory = () => { - if (!selectedHistoryDate) return; - handleSelectDate(selectedHistoryDate); + if (!historyFrom || !historyTill) return; + // Tolerate a reversed range instead of erroring. + const [from, till] = historyFrom <= historyTill + ? [historyFrom, historyTill] + : [historyTill, historyFrom]; + openHistoryTab(historyDialogMode === 'rect', from, till); setIsHistoryDialogOpen(false); }; @@ -238,14 +236,6 @@ export const TopFilterBar: React.FC = () => { > FILTERS - - {/* History dropdown — only visible when a specific site is selected */} - {localSiteId !== 'All Sites' && - - } + HISTORY + + )} { } }} > - Rectification History + + {historyDialogMode === 'rect' ? 'Rectification History' : 'Audit History'} + - Enter a date (YYYY-MM-DD) or pick one from the list to view historical rectification records. + Pick a date or a From–Till range to view the {historyDialogMode === 'rect' ? 'rectification' : 'audit'} records you audited. A quick-select chip fills both fields with one day. - History Date - setSelectedHistoryDate(e.target.value)} - fullWidth - sx={{ - mb: 3, - '& .MuiOutlinedInput-root': { - color: '#f8fafc', - '& fieldset': { borderColor: '#334155' }, - '&:hover fieldset': { borderColor: '#475569' }, - '&.Mui-focused fieldset': { borderColor: '#3b82f6' }, - }, - '& input[type="date"]::-webkit-calendar-picker-indicator': { - filter: 'invert(1)', - cursor: 'pointer', - } - }} - /> - + + {([['From', historyFrom, setHistoryFrom], ['Till', historyTill, setHistoryTill]] as const).map(([label, value, setter]) => ( + + {label} + setter(e.target.value)} + fullWidth + sx={{ + '& .MuiOutlinedInput-root': { + color: '#f8fafc', + '& fieldset': { borderColor: '#334155' }, + '&:hover fieldset': { borderColor: '#475569' }, + '&.Mui-focused fieldset': { borderColor: '#3b82f6' }, + }, + '& input[type="date"]::-webkit-calendar-picker-indicator': { + filter: 'invert(1)', + cursor: 'pointer', + } + }} + /> + + ))} + + - Quick Select (Audited Dates) + Quick Select (Dates You Audited) - {loadingRectHistory ? ( + {(historyDialogMode === 'rect' ? loadingRectHistory : loadingHistory) ? ( - ) : rectHistoryDates.length === 0 ? ( + ) : (historyDialogMode === 'rect' ? rectHistoryDates : historyDates).length === 0 ? ( No dates found ) : ( - rectHistoryDates.map((d) => { + (historyDialogMode === 'rect' ? rectHistoryDates : historyDates).map((d) => { const dateStr = d.audited_on ? d.audited_on.split(' ')[0] : ''; + const selected = historyFrom === dateStr && historyTill === dateStr; return ( setSelectedHistoryDate(dateStr || d.audited_on)} + onClick={() => { setHistoryFrom(dateStr); setHistoryTill(dateStr); }} sx={{ - bgcolor: selectedHistoryDate === (dateStr || d.audited_on) ? '#3b82f6' : '#1e293b', + bgcolor: selected ? '#3b82f6' : '#1e293b', color: '#f8fafc', '&:hover': { bgcolor: '#334155' }, borderRadius: 1, cursor: 'pointer', border: '1px solid transparent', - borderColor: selectedHistoryDate === (dateStr || d.audited_on) ? '#60a5fa' : 'transparent', + borderColor: selected ? '#60a5fa' : 'transparent', }} /> ); @@ -371,20 +343,20 @@ export const TopFilterBar: React.FC = () => { - - + ); })} @@ -229,6 +294,12 @@ export const DevTickets: React.FC = () => { /> )} + + setDetailsTicket(null)} + /> ); }; diff --git a/src/pages/admin/RecordDetailsDialog.tsx b/src/pages/admin/RecordDetailsDialog.tsx new file mode 100644 index 0000000..ef3c00a --- /dev/null +++ b/src/pages/admin/RecordDetailsDialog.tsx @@ -0,0 +1,212 @@ +import React, { useEffect, useState } from 'react'; +import { + Dialog, DialogTitle, DialogContent, DialogActions, Button, Box, + Typography, Chip, CircularProgress, Link, Divider, IconButton, +} from '@mui/material'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { adminService } from '../../api/adminService'; +import type { DevTicket, TicketRecordDetails } from '../../api/adminService'; +import { masterImageUrl, testImageUrl, TEST_IMAGE_PREFIX } from '../../constants/media'; + +interface RecordDetailsDialogProps { + ticket: DevTicket | null; // null = closed + liveIssueState?: 'open' | 'closed' | null; + onClose: () => void; +} + +const Field: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => ( + + {label} + {children ?? '—'} + +); + +const MediaImage: React.FC<{ label: string; src: string | null }> = ({ label, src }) => { + const [failed, setFailed] = useState(false); + useEffect(() => { setFailed(false); }, [src]); + return ( + + {label} + {src && !failed ? ( + setFailed(true)} + sx={{ width: '100%', maxHeight: 260, objectFit: 'contain', borderRadius: 1, bgcolor: 'rgba(0,0,0,0.2)' }} /> + ) : ( + + {src ? 'Image unavailable' : 'No image'} + + )} + + ); +}; + +interface MediaItem { + kind: 'image' | 'video'; + src: string; + label: string; +} + +// Right-hand pane: the anomaly's test-side media (test image, extra images, +// video) as a carousel. The