diff --git a/src/pages/ClosedRequests/ClosedRequests.tsx b/src/pages/ClosedRequests/ClosedRequests.tsx index 20f4c20..8559fdd 100644 --- a/src/pages/ClosedRequests/ClosedRequests.tsx +++ b/src/pages/ClosedRequests/ClosedRequests.tsx @@ -1,27 +1,12 @@ -import { useState, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { - Calendar, - Filter, - Search, - FileText, - AlertCircle, - CheckCircle, - ArrowRight, - SortAsc, - SortDesc, - Flame, - Target, - RefreshCw, - Settings2, - X, - XCircle -} from 'lucide-react'; +import { Calendar, Filter, Search, FileText, AlertCircle, CheckCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, Settings2, X, XCircle } from 'lucide-react'; +import workflowApi from '@/services/workflowApi'; interface Request { id: string; @@ -29,12 +14,9 @@ interface Request { description: string; status: 'approved' | 'rejected'; priority: 'express' | 'standard'; - initiator: { - name: string; - avatar: string; - }; + initiator: { name: string; avatar: string }; createdAt: string; - dueDate: string; + dueDate?: string; reason?: string; department?: string; } @@ -43,66 +25,7 @@ interface ClosedRequestsProps { onViewRequest?: (requestId: string, requestTitle?: string) => void; } -// Static data for closed requests -const CLOSED_REQUESTS: Request[] = [ - { - id: 'RE-REQ-CM-001', - title: 'Dealer Marketing Activity Claim - Diwali Festival Campaign', - description: 'Claim request for dealer-led Diwali festival marketing campaign using Claim Management template workflow.', - status: 'approved', - priority: 'standard', - initiator: { name: 'Sneha Patil', avatar: 'SP' }, - createdAt: '2024-10-07', - dueDate: '2024-10-16', - reason: 'Budget approved with quarterly review conditions', - department: 'Marketing - West Zone' - }, - { - id: 'RE-REQ-001', - title: 'Marketing Campaign Budget Approval', - description: 'Request for Q4 marketing campaign budget allocation of $50,000 for digital advertising across social media platforms and content creation.', - status: 'approved', - priority: 'express', - initiator: { name: 'Sarah Chen', avatar: 'SC' }, - createdAt: '2024-10-07', - dueDate: '2024-10-09', - reason: 'All equipment approved and ordered through preferred vendor', - department: 'Marketing' - }, - { - id: 'RE-REQ-002', - title: 'IT Equipment Purchase', - description: 'Purchase of 10 new laptops for the development team including software licenses and accessories for enhanced productivity.', - status: 'rejected', - priority: 'standard', - initiator: { name: 'David Kumar', avatar: 'DK' }, - createdAt: '2024-10-06', - dueDate: '2024-10-12', - reason: 'Pricing not competitive, seek alternative vendors' - }, - { - id: 'RE-REQ-003', - title: 'Vendor Contract Renewal', - description: 'Annual renewal of cleaning services contract with updated terms and pricing structure for office maintenance.', - status: 'approved', - priority: 'standard', - initiator: { name: 'John Doe', avatar: 'JD' }, - createdAt: '2024-10-05', - dueDate: '2024-10-08', - reason: 'Lease terms negotiated and approved by legal team' - }, - { - id: 'RE-REQ-004', - title: 'Office Space Expansion', - description: 'Lease additional office space for growing team, 2000 sq ft in the same building with modern amenities.', - status: 'approved', - priority: 'express', - initiator: { name: 'Lisa Wong', avatar: 'LW' }, - createdAt: '2024-10-04', - dueDate: '2024-10-15', - reason: 'Program approved with budget adjustments' - } -]; +// Removed static data; will load from API // Utility functions const getPriorityConfig = (priority: string) => { @@ -158,9 +81,48 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority'>('due'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let mounted = true; + (async () => { + try { + setLoading(true); + const result = await workflowApi.listClosedByMe({ page: 1, limit: 50 }); + const data = Array.isArray((result as any)?.data) + ? (result as any).data + : Array.isArray((result as any)?.data?.data) + ? (result as any).data.data + : Array.isArray(result as any) + ? (result as any) + : []; + if (!mounted) return; + const mapped: Request[] = data + .filter((r: any) => ['APPROVED', 'REJECTED'].includes((r.status || '').toString())) + .map((r: any) => ({ + id: r.requestId || r.requestNumber, + displayId: r.requestNumber || r.requestId, + title: r.title, + description: r.description, + status: (r.status || '').toString().toLowerCase(), + priority: (r.priority || '').toString().toLowerCase(), + initiator: { name: r.initiator?.displayName || r.initiator?.email || '—', avatar: (r.initiator?.displayName || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase() }, + createdAt: r.submittedAt || '—', + dueDate: undefined, + reason: r.conclusionRemark, + department: r.department + })); + setItems(mapped); + } finally { + if (mounted) setLoading(false); + } + })(); + return () => { mounted = false; }; + }, []); const filteredAndSortedRequests = useMemo(() => { - let filtered = CLOSED_REQUESTS.filter(request => { + let filtered = items.filter(request => { const matchesSearch = request.title.toLowerCase().includes(searchTerm.toLowerCase()) || request.id.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -202,7 +164,7 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { }); return filtered; - }, [searchTerm, priorityFilter, statusFilter, sortBy, sortOrder]); + }, [items, searchTerm, priorityFilter, statusFilter, sortBy, sortOrder]); const clearFilters = () => { setSearchTerm(''); @@ -234,7 +196,7 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
- {filteredAndSortedRequests.length} closed requests + {loading ? 'Loading…' : `${filteredAndSortedRequests.length} closed requests`} - +

Supported formats: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG, PPT, PPTX

@@ -1760,7 +2027,7 @@ export function CreateRequest({ onBack, onSubmit }: CreateRequestProps) {
- {currentStep === totalSteps ? ( diff --git a/src/pages/MyRequests/MyRequests.tsx b/src/pages/MyRequests/MyRequests.tsx index bc62da3..a031a33 100644 --- a/src/pages/MyRequests/MyRequests.tsx +++ b/src/pages/MyRequests/MyRequests.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; @@ -19,86 +19,14 @@ import { Target } from 'lucide-react'; import { motion } from 'framer-motion'; +import workflowApi from '@/services/workflowApi'; interface MyRequestsProps { onViewRequest: (requestId: string, requestTitle?: string) => void; dynamicRequests?: any[]; } -// Mock data for user's requests -const MY_REQUESTS_DATA = [ - { - id: 'RE-REQ-2024-CM-001', - title: 'Dealer Marketing Activity Claim - Diwali Festival Campaign', - description: 'Claim request for dealer-led Diwali festival marketing campaign', - status: 'pending', - priority: 'standard', - department: 'Marketing - West Zone', - submittedDate: '2024-10-07', - currentApprover: 'Royal Motors Mumbai (Dealer)', - approverLevel: '1 of 8', - dueDate: '2024-10-16', - templateType: 'claim-management', - templateName: 'Claim Management', - estimatedCompletion: '2024-10-16' - }, - { - id: 'RE-REQ-001', - title: 'Marketing Campaign Budget Approval', - description: 'Request for Q4 marketing campaign budget allocation for new motorcycle launch', - status: 'pending', - priority: 'express', - department: 'Marketing', - submittedDate: '2024-10-05', - currentApprover: 'Sarah Johnson', - approverLevel: '2 of 3', - dueDate: '2024-10-12' - }, - { - id: 'RE-REQ-002', - title: 'IT Equipment Purchase Request', - description: 'New laptops and workstations for the development team', - status: 'approved', - priority: 'standard', - submittedDate: '2024-09-28', - currentApprover: 'Completed', - approverLevel: '3 of 3', - dueDate: '2024-10-01' - }, - { - id: 'RE-REQ-003', - title: 'Training Program Authorization', - description: 'Employee skill development program for technical team', - status: 'in-review', - priority: 'standard', - submittedDate: '2024-10-03', - currentApprover: 'Michael Chen', - approverLevel: '1 of 2', - estimatedCompletion: '2024-10-10' - }, - { - id: 'RE-REQ-004', - title: 'Vendor Contract Renewal', - description: 'Annual renewal for supply chain vendor contracts', - status: 'rejected', - priority: 'express', - submittedDate: '2024-09-25', - currentApprover: 'Rejected by Alex Kumar', - approverLevel: '1 of 3', - estimatedCompletion: 'N/A' - }, - { - id: 'RE-REQ-005', - title: 'Office Space Renovation', - description: 'Workspace renovation for improved employee experience', - status: 'draft', - priority: 'standard', - submittedDate: '2024-10-07', - currentApprover: 'Not submitted', - approverLevel: '0 of 2', - estimatedCompletion: 'Pending submission' - } -]; +// Removed mock data; list renders API data only const getPriorityConfig = (priority: string) => { @@ -169,25 +97,48 @@ const getStatusConfig = (status: string) => { export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsProps) { const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); + const [apiRequests, setApiRequests] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let mounted = true; + (async () => { + try { + setLoading(true); + const result = await workflowApi.listMyWorkflows({ page: 1, limit: 20 }); + const items = Array.isArray(result?.data) ? result.data : Array.isArray(result) ? result : []; + if (!mounted) return; + setApiRequests(items); + } catch (_) { + if (!mounted) return; + setApiRequests([]); + } finally { + if (mounted) setLoading(false); + } + })(); + return () => { mounted = false; }; + }, []); - // Convert dynamic requests to the format expected by this component - const convertedDynamicRequests = dynamicRequests.map(req => ({ - id: req.id, + // Convert API/dynamic requests to the format expected by this component + const sourceRequests = (apiRequests.length ? apiRequests : dynamicRequests); + const convertedDynamicRequests = sourceRequests.map((req: any) => ({ + id: req.requestId || req.id || req.request_id, + displayId: req.requestNumber || req.request_number || req.id, title: req.title, description: req.description, - status: req.status, - priority: req.priority, + status: (req.status || '').toString().toLowerCase().replace('in_progress','in-review'), + priority: (req.priority || '').toString().toLowerCase(), department: req.department, - submittedDate: new Date(req.createdAt).toISOString().split('T')[0], - currentApprover: req.approvalFlow?.[0]?.approver || 'Current User (Initiator)', - approverLevel: `${req.currentStep} of ${req.totalSteps}`, - dueDate: new Date(req.dueDate).toISOString().split('T')[0], + submittedDate: req.submittedAt || (req.createdAt ? new Date(req.createdAt).toISOString().split('T')[0] : undefined), + currentApprover: req.currentApprover?.name || req.currentApprover?.email || '—', + approverLevel: req.currentLevel && req.totalLevels ? `${req.currentLevel} of ${req.totalLevels}` : (req.currentStep && req.totalSteps ? `${req.currentStep} of ${req.totalSteps}` : '—'), + dueDate: req.dueDate ? new Date(req.dueDate).toISOString().split('T')[0] : undefined, templateType: req.templateType, templateName: req.templateName })); - // Merge static mock data with dynamic requests (dynamic requests first) - const allRequests = [...convertedDynamicRequests, ...MY_REQUESTS_DATA]; + // Use only API/dynamic requests + const allRequests = convertedDynamicRequests; const [priorityFilter, setPriorityFilter] = useState('all'); // Filter requests based on search and filters @@ -337,7 +288,11 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr {/* Requests List */}
- {filteredRequests.length === 0 ? ( + {loading ? ( + + Loading your requests… + + ) : filteredRequests.length === 0 ? ( @@ -397,7 +352,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr {request.description}

- ID: {request.id} + ID: {(request as any).displayId || request.id} Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'}
diff --git a/src/pages/OpenRequests/OpenRequests.tsx b/src/pages/OpenRequests/OpenRequests.tsx index 7ab6ef6..25859c2 100644 --- a/src/pages/OpenRequests/OpenRequests.tsx +++ b/src/pages/OpenRequests/OpenRequests.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -6,23 +6,8 @@ import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Progress } from '@/components/ui/progress'; -import { - Calendar, - Clock, - Filter, - Search, - FileText, - AlertCircle, - ArrowRight, - SortAsc, - SortDesc, - Flame, - Target, - Eye, - RefreshCw, - Settings2, - X -} from 'lucide-react'; +import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, Eye, RefreshCw, Settings2, X } from 'lucide-react'; +import workflowApi from '@/services/workflowApi'; interface Request { id: string; @@ -30,19 +15,13 @@ interface Request { description: string; status: 'pending' | 'in-review'; priority: 'express' | 'standard'; - initiator: { - name: string; - avatar: string; - }; - currentApprover?: { - name: string; - avatar: string; - }; + initiator: { name: string; avatar: string }; + currentApprover?: { name: string; avatar: string }; slaProgress: number; slaRemaining: string; createdAt: string; - dueDate: string; - approvalStep: string; + dueDate?: string; + approvalStep?: string; department?: string; } @@ -50,95 +29,7 @@ interface OpenRequestsProps { onViewRequest?: (requestId: string, requestTitle?: string) => void; } -// Static data for open requests -const OPEN_REQUESTS: Request[] = [ - { - id: 'RE-REQ-CM-001', - title: 'Dealer Marketing Activity Claim - Diwali Festival Campaign', - description: 'Claim request for dealer-led Diwali festival marketing campaign using Claim Management template workflow.', - status: 'pending', - priority: 'standard', - initiator: { name: 'Sneha Patil', avatar: 'SP' }, - currentApprover: { name: 'Sneha Patil (Initiator)', avatar: 'SP' }, - slaProgress: 35, - slaRemaining: '4 days 12 hours', - createdAt: '2024-10-07', - dueDate: '2024-10-16', - approvalStep: 'Initiator Review & Confirmation', - department: 'Marketing - West Zone' - }, - { - id: 'RE-REQ-001', - title: 'Marketing Campaign Budget Approval', - description: 'Request for Q4 marketing campaign budget allocation of $50,000 for digital advertising across social media platforms and content creation.', - status: 'pending', - priority: 'express', - initiator: { name: 'Sarah Chen', avatar: 'SC' }, - currentApprover: { name: 'Mike Johnson', avatar: 'MJ' }, - slaProgress: 85, - slaRemaining: '2 hours', - createdAt: '2024-10-07', - dueDate: '2024-10-09', - approvalStep: 'Awaiting Finance Approval', - department: 'Marketing' - }, - { - id: 'RE-REQ-002', - title: 'IT Equipment Purchase', - description: 'Purchase of 10 new laptops for the development team including software licenses and accessories for enhanced productivity.', - status: 'in-review', - priority: 'standard', - initiator: { name: 'David Kumar', avatar: 'DK' }, - currentApprover: { name: 'Lisa Wong', avatar: 'LW' }, - slaProgress: 45, - slaRemaining: '1 day', - createdAt: '2024-10-06', - dueDate: '2024-10-12', - approvalStep: 'IT Department Review' - }, - { - id: 'RE-REQ-003', - title: 'Vendor Contract Renewal', - description: 'Annual renewal of cleaning services contract with updated terms and pricing structure for office maintenance.', - status: 'pending', - priority: 'standard', - initiator: { name: 'John Doe', avatar: 'JD' }, - currentApprover: { name: 'Anna Smith', avatar: 'AS' }, - slaProgress: 90, - slaRemaining: '30 minutes', - createdAt: '2024-10-05', - dueDate: '2024-10-08', - approvalStep: 'Final Management Approval' - }, - { - id: 'RE-REQ-004', - title: 'Office Space Expansion', - description: 'Lease additional office space for growing team, 2000 sq ft in the same building with modern amenities.', - status: 'in-review', - priority: 'express', - initiator: { name: 'Lisa Wong', avatar: 'LW' }, - currentApprover: { name: 'David Kumar', avatar: 'DK' }, - slaProgress: 30, - slaRemaining: '3 days', - createdAt: '2024-10-04', - dueDate: '2024-10-15', - approvalStep: 'Legal Review' - }, - { - id: 'RE-REQ-005', - title: 'Employee Training Program', - description: 'Approval for new employee onboarding and skill development training program with external consultants.', - status: 'pending', - priority: 'standard', - initiator: { name: 'Anna Smith', avatar: 'AS' }, - currentApprover: { name: 'Sarah Chen', avatar: 'SC' }, - slaProgress: 60, - slaRemaining: '12 hours', - createdAt: '2024-10-03', - dueDate: '2024-10-11', - approvalStep: 'HR Approval' - } -]; +// Removed static data; will load from API // Utility functions const getPriorityConfig = (priority: string) => { @@ -197,12 +88,53 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { const [searchTerm, setSearchTerm] = useState(''); const [priorityFilter, setPriorityFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); - const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>('due'); + const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>('created'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + let mounted = true; + (async () => { + try { + setLoading(true); + const result = await workflowApi.listOpenForMe({ page: 1, limit: 50 }); + const data = Array.isArray((result as any)?.data) + ? (result as any).data + : Array.isArray((result as any)?.data?.data) + ? (result as any).data.data + : Array.isArray(result as any) + ? (result as any) + : []; + if (!mounted) return; + const mapped: Request[] = data.map((r: any) => ({ + id: r.requestId || r.requestNumber, + // keep a display id for UI + displayId: r.requestNumber || r.requestId, + title: r.title, + description: r.description, + status: ((r.status || '').toString().toUpperCase() === 'IN_PROGRESS') ? 'in-review' : 'pending', + priority: (r.priority || '').toString().toLowerCase(), + initiator: { name: (r.initiator?.displayName || r.initiator?.email || '—'), avatar: ((r.initiator?.displayName || r.initiator?.email || 'NA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()) }, + currentApprover: r.currentApprover ? { name: (r.currentApprover.name || r.currentApprover.email || '—'), avatar: ((r.currentApprover.name || r.currentApprover.email || 'CA').split(' ').map((s: string) => s[0]).join('').slice(0,2).toUpperCase()) } : undefined, + slaProgress: Number(r.sla?.percent || 0), + slaRemaining: r.sla?.remainingText || '—', + createdAt: r.submittedAt || '—', + dueDate: undefined, + approvalStep: undefined, + department: r.department + })); + setItems(mapped); + } finally { + if (mounted) setLoading(false); + } + })(); + return () => { mounted = false; }; + }, []); const filteredAndSortedRequests = useMemo(() => { - let filtered = OPEN_REQUESTS.filter(request => { + let filtered = items.filter(request => { const matchesSearch = request.title.toLowerCase().includes(searchTerm.toLowerCase()) || request.id.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -224,8 +156,8 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { bValue = new Date(b.createdAt); break; case 'due': - aValue = new Date(a.dueDate); - bValue = new Date(b.dueDate); + aValue = a.dueDate ? new Date(a.dueDate).getTime() : Number.MAX_SAFE_INTEGER; + bValue = b.dueDate ? new Date(b.dueDate).getTime() : Number.MAX_SAFE_INTEGER; break; case 'priority': const priorityOrder = { express: 2, standard: 1 }; @@ -248,7 +180,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { }); return filtered; - }, [searchTerm, priorityFilter, statusFilter, sortBy, sortOrder]); + }, [items, searchTerm, priorityFilter, statusFilter, sortBy, sortOrder]); const clearFilters = () => { setSearchTerm(''); @@ -262,7 +194,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { statusFilter !== 'all' ? statusFilter : null ].filter(Boolean).length; - return ( + return (
{/* Enhanced Header */}
@@ -280,7 +212,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
- {filteredAndSortedRequests.length} open requests + {loading ? 'Loading…' : `${filteredAndSortedRequests.length} open requests`} - - + {!isSpectator && ( + + )} + {!isSpectator && ( + + )}
- - + {!isSpectator && ( + <> + + + + )}
diff --git a/src/services/documentApi.ts b/src/services/documentApi.ts new file mode 100644 index 0000000..35c5504 --- /dev/null +++ b/src/services/documentApi.ts @@ -0,0 +1,45 @@ +import apiClient from './authApi'; + +export type DocumentCategory = 'SUPPORTING' | 'APPROVAL' | 'REFERENCE' | 'FINAL' | 'OTHER'; + +export interface UploadResponse { + documentId: string; + storageUrl?: string; + fileName: string; + originalFileName: string; +} + +export async function uploadDocument( + file: File, + requestId: string, + category: DocumentCategory = 'SUPPORTING' +): Promise { + const formData = new FormData(); + formData.append('file', file); + formData.append('requestId', requestId); + formData.append('category', category); + + const res = await apiClient.post('/documents', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + const data = res.data?.data || res.data; + return { + documentId: data?.documentId || data?.document_id || '', + storageUrl: data?.storageUrl || data?.storage_url, + fileName: data?.fileName || data?.file_name || file.name, + originalFileName: data?.originalFileName || data?.original_file_name || file.name, + }; +} + +export async function uploadMany( + files: File[], + requestId: string, + category: DocumentCategory = 'SUPPORTING' +): Promise { + const tasks = files.map(f => uploadDocument(f, requestId, category)); + return Promise.all(tasks); +} + +export default { uploadDocument, uploadMany }; + + diff --git a/src/services/userApi.ts b/src/services/userApi.ts new file mode 100644 index 0000000..c44fb71 --- /dev/null +++ b/src/services/userApi.ts @@ -0,0 +1,22 @@ +import apiClient from './authApi'; + +export interface UserSummary { + userId: string; + email: string; + displayName?: string; + firstName?: string; + lastName?: string; + department?: string; + designation?: string; + isActive?: boolean; +} + +export async function searchUsers(query: string, limit: number = 10): Promise { + const res = await apiClient.get('/users/search', { params: { q: query, limit } }); + const data = (res.data?.data || res.data) as any[]; + return data as UserSummary[]; +} + +export default { searchUsers }; + + diff --git a/src/services/workflowApi.ts b/src/services/workflowApi.ts new file mode 100644 index 0000000..4173fc1 --- /dev/null +++ b/src/services/workflowApi.ts @@ -0,0 +1,204 @@ +import apiClient from './authApi'; + +export type PriorityUi = 'standard' | 'express'; + +export interface ApproverFormItem { + email: string; + name?: string; + tat?: number | ''; + tatType?: 'hours' | 'days'; +} + +export interface ParticipantItem { + id?: string; + name: string; + email: string; +} + +export interface CreateWorkflowFromFormPayload { + templateId?: string | null; + templateType: 'CUSTOM' | 'TEMPLATE'; + title: string; + description: string; + priorityUi: PriorityUi; + approverCount: number; + approvers: ApproverFormItem[]; + spectators?: ParticipantItem[]; + ccList?: ParticipantItem[]; +} + +// Utility to generate a RFC4122 v4 UUID (fallback if crypto.randomUUID not available) +function generateUuid(): string { + if (typeof crypto !== 'undefined' && (crypto as any).randomUUID) { + return (crypto as any).randomUUID(); + } + // Fallback + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +export interface CreateWorkflowResponse { + id: string; +} + +export async function createWorkflowFromForm(form: CreateWorkflowFromFormPayload): Promise { + // Map UI priority to API enum + const priority = form.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD'; + + // Build approval levels to match backend schema + const approvalLevels = Array.from({ length: form.approverCount || 1 }, (_, i) => { + const idx = i; + const a = form.approvers[idx] || {} as ApproverFormItem; + const levelNumber = idx + 1; + const tatRaw = a.tat ?? ''; + let tatHours = 0; + if (typeof tatRaw === 'number') { + tatHours = a.tatType === 'days' ? tatRaw * 24 : tatRaw; + } + const approverEmail = a.email || ''; + const approverName = (a.name && a.name.trim()) || approverEmail.split('@')[0] || `Approver ${levelNumber}`; + return { + levelNumber, + levelName: `Level ${levelNumber}`, + approverId: generateUuid(), + approverEmail, + approverName, + tatHours: tatHours > 0 ? tatHours : 24, + isFinalApprover: levelNumber === (form.approverCount || 1), + }; + }); + + // Participants -> spectators and ccList + const participants = [ + ...(form.spectators || []).map(p => ({ + userId: generateUuid(), + userEmail: p.email, + userName: p.name || p.email.split('@')[0] || 'Spectator', + participantType: 'SPECTATOR' as const, + canComment: true, + canViewDocuments: true, + canDownloadDocuments: false, + notificationEnabled: true, + })), + ...(form.ccList || []).map(p => ({ + userId: generateUuid(), + userEmail: p.email, + userName: p.name || p.email.split('@')[0] || 'CC', + participantType: 'CONSULTATION' as const, + canComment: false, + canViewDocuments: true, + canDownloadDocuments: false, + notificationEnabled: true, + })), + ]; + + const payload = { + templateType: form.templateType, + title: form.title, + description: form.description, + priority, // STANDARD | EXPRESS + approvalLevels, + participants: participants.length ? participants : undefined, + }; + + const res = await apiClient.post('/workflows', payload); + const data = (res.data?.data || res.data) as any; + return { id: data.id || data.workflowId || '' }; +} + +export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayload, files: File[], category: 'SUPPORTING' | 'APPROVAL' | 'REFERENCE' | 'FINAL' | 'OTHER' = 'SUPPORTING') { + const isUuid = (v: any) => typeof v === 'string' && /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(v.trim()); + + const payload: any = { + templateType: form.templateType, + title: form.title, + description: form.description, + priority: form.priorityUi.toUpperCase() === 'EXPRESS' ? 'EXPRESS' : 'STANDARD', + approvalLevels: Array.from({ length: form.approverCount || 1 }, (_, i) => { + const a = form.approvers[i] || ({} as any); + const tat = typeof a.tat === 'number' ? a.tat : 0; + const approverId = (a.userId || '').trim(); + if (!isUuid(approverId)) { + throw new Error(`Invalid approverId for level ${i + 1}. Please pick an approver via @ search.`); + } + return { + levelNumber: i + 1, + levelName: `Level ${i + 1}`, + approverId, + approverEmail: a.email || '', + approverName: a.name || (a.email ? a.email.split('@')[0] : `Approver ${i + 1}`), + tatHours: a.tatType === 'days' ? tat * 24 : tat || 24, + isFinalApprover: i + 1 === (form.approverCount || 1), + }; + }), + }; + // Pass participants if provided by caller (CreateRequest builds this) + const incomingParticipants = (form as any).participants; + if (Array.isArray(incomingParticipants) && incomingParticipants.length) { + payload.participants = incomingParticipants; + } + + const formData = new FormData(); + formData.append('payload', JSON.stringify(payload)); + formData.append('category', category); + files.forEach(f => formData.append('files', f)); + + const res = await apiClient.post('/workflows/multipart', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + const data = res.data?.data || res.data; + return { id: data?.requestId } as any; +} + +export async function listWorkflows(params: { page?: number; limit?: number } = {}) { + const { page = 1, limit = 20 } = params; + const res = await apiClient.get('/workflows', { params: { page, limit } }); + return res.data?.data || res.data; +} + +export async function listMyWorkflows(params: { page?: number; limit?: number } = {}) { + const { page = 1, limit = 20 } = params; + const res = await apiClient.get('/workflows/my', { params: { page, limit } }); + return res.data?.data || res.data; +} + +export async function listOpenForMe(params: { page?: number; limit?: number } = {}) { + const { page = 1, limit = 20 } = params; + const res = await apiClient.get('/workflows/open-for-me', { params: { page, limit } }); + return res.data?.data || res.data; +} + +export async function listClosedByMe(params: { page?: number; limit?: number } = {}) { + const { page = 1, limit = 20 } = params; + const res = await apiClient.get('/workflows/closed-by-me', { params: { page, limit } }); + return res.data?.data || res.data; +} + +export async function getWorkflowDetails(requestId: string) { + const res = await apiClient.get(`/workflows/${requestId}/details`); + return res.data?.data || res.data; +} + +export default { + createWorkflowFromForm, + createWorkflowMultipart, + listWorkflows, + listMyWorkflows, + listOpenForMe, + listClosedByMe, + submitWorkflow, + getWorkflowDetails, +}; + +export async function submitWorkflow(requestId: string) { + const res = await apiClient.patch(`/workflows/${requestId}/submit`); + return res.data?.data || res.data; +} + +// Also export in default for convenience +// Note: keeping separate named export above for tree-shaking + +