ui made stable for the non templatized and changed to support postman request submit

This commit is contained in:
laxmanhalaki 2026-02-05 21:06:00 +05:30
parent 6b4b80c0d4
commit 3bab9c0481
12 changed files with 1423 additions and 1352 deletions

View File

@ -12,12 +12,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Switch } from '../ui/switch'; import { Switch } from '../ui/switch';
import { Calendar } from '../ui/calendar'; import { Calendar } from '../ui/calendar';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { import {
ArrowLeft, ArrowLeft,
ArrowRight, ArrowRight,
Calendar as CalendarIcon, Calendar as CalendarIcon,
Upload, Upload,
X, X,
FileText, FileText,
Check, Check,
Users Users
@ -42,6 +42,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
spectators: [] as any[], spectators: [] as any[],
documents: [] as File[] documents: [] as File[]
}); });
const [isDragging, setIsDragging] = useState(false);
const totalSteps = 5; const totalSteps = 5;
@ -78,9 +79,36 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
updateFormData('spectators', formData.spectators.filter(s => s.id !== userId)); updateFormData('spectators', formData.spectators.filter(s => s.id !== userId));
}; };
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement> | React.DragEvent) => {
const files = Array.from(event.target.files || []); let files: File[] = [];
updateFormData('documents', [...formData.documents, ...files]); if ('target' in event && event.target instanceof HTMLInputElement && event.target.files) {
files = Array.from(event.target.files);
} else if ('dataTransfer' in event && event.dataTransfer.files) {
files = Array.from(event.dataTransfer.files);
}
if (files.length > 0) {
updateFormData('documents', [...formData.documents, ...files]);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
handleFileUpload(e);
}; };
const removeDocument = (index: number) => { const removeDocument = (index: number) => {
@ -150,7 +178,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
onChange={(e) => updateFormData('title', e.target.value)} onChange={(e) => updateFormData('title', e.target.value)}
/> />
</div> </div>
<div> <div>
<Label htmlFor="description">Description *</Label> <Label htmlFor="description">Description *</Label>
<Textarea <Textarea
@ -215,9 +243,9 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
<Label htmlFor="workflow-sequential" className="text-sm">Parallel</Label> <Label htmlFor="workflow-sequential" className="text-sm">Parallel</Label>
</div> </div>
</div> </div>
<div className="p-3 bg-muted/50 rounded-lg text-sm text-muted-foreground"> <div className="p-3 bg-muted/50 rounded-lg text-sm text-muted-foreground">
{formData.workflowType === 'sequential' {formData.workflowType === 'sequential'
? 'Approvers will review the request one after another in the order you specify.' ? 'Approvers will review the request one after another in the order you specify.'
: 'All approvers will review the request simultaneously.' : 'All approvers will review the request simultaneously.'
} }
@ -311,7 +339,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{availableUsers {availableUsers
.filter(user => .filter(user =>
!formData.spectators.find(s => s.id === user.id) && !formData.spectators.find(s => s.id === user.id) &&
!formData.approvers.find(a => a.id === user.id) !formData.approvers.find(a => a.id === user.id)
) )
@ -375,8 +403,14 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
<p className="text-sm text-muted-foreground mb-2"> <p className="text-sm text-muted-foreground mb-2">
Attach supporting documents for your request. Maximum 10MB per file. Attach supporting documents for your request. Maximum 10MB per file.
</p> </p>
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center"> <div
<Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" /> className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${isDragging ? 'border-re-green bg-re-green/5' : 'border-border'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className={`h-8 w-8 mx-auto mb-2 ${isDragging ? 'text-re-green' : 'text-muted-foreground'}`} />
<p className="text-sm text-muted-foreground mb-2"> <p className="text-sm text-muted-foreground mb-2">
Drag and drop files here, or click to browse Drag and drop files here, or click to browse
</p> </p>

View File

@ -1,3 +1,4 @@
import { useState, ChangeEvent, DragEvent, RefObject } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -20,7 +21,7 @@ interface DocumentsStepProps {
onDocumentsToDeleteChange: (ids: string[]) => void; onDocumentsToDeleteChange: (ids: string[]) => void;
onPreviewDocument: (doc: any, isExisting: boolean) => void; onPreviewDocument: (doc: any, isExisting: boolean) => void;
onDocumentErrors?: (errors: Array<{ fileName: string; reason: string }>) => void; onDocumentErrors?: (errors: Array<{ fileName: string; reason: string }>) => void;
fileInputRef: React.RefObject<HTMLInputElement>; fileInputRef: RefObject<HTMLInputElement>;
} }
/** /**
@ -47,8 +48,9 @@ export function DocumentsStep({
onDocumentErrors, onDocumentErrors,
fileInputRef fileInputRef
}: DocumentsStepProps) { }: DocumentsStepProps) {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const [isDragging, setIsDragging] = useState(false);
const files = Array.from(event.target.files || []);
const processFiles = (files: File[]) => {
if (files.length === 0) return; if (files.length === 0) return;
// Validate files // Validate files
@ -69,7 +71,7 @@ export function DocumentsStep({
// Check file extension // Check file extension
const fileName = file.name.toLowerCase(); const fileName = file.name.toLowerCase();
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
if (!documentPolicy.allowedFileTypes.includes(fileExtension)) { if (!documentPolicy.allowedFileTypes.includes(fileExtension)) {
validationErrors.push({ validationErrors.push({
fileName: file.name, fileName: file.name,
@ -90,6 +92,11 @@ export function DocumentsStep({
if (validationErrors.length > 0 && onDocumentErrors) { if (validationErrors.length > 0 && onDocumentErrors) {
onDocumentErrors(validationErrors); onDocumentErrors(validationErrors);
} }
};
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
processFiles(files);
// Reset file input // Reset file input
if (event.target) { if (event.target) {
@ -97,6 +104,27 @@ export function DocumentsStep({
} }
}; };
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragLeave = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files);
processFiles(files);
};
const handleRemove = (index: number) => { const handleRemove = (index: number) => {
const newDocs = documents.filter((_, i) => i !== index); const newDocs = documents.filter((_, i) => i !== index);
onDocumentsChange(newDocs); onDocumentsChange(newDocs);
@ -111,16 +139,16 @@ export function DocumentsStep({
const type = (doc.fileType || doc.file_type || '').toLowerCase(); const type = (doc.fileType || doc.file_type || '').toLowerCase();
const name = (doc.originalFileName || doc.fileName || '').toLowerCase(); const name = (doc.originalFileName || doc.fileName || '').toLowerCase();
return type.includes('image') || type.includes('pdf') || return type.includes('image') || type.includes('pdf') ||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.jpg') || name.endsWith('.jpeg') ||
name.endsWith('.png') || name.endsWith('.gif') || name.endsWith('.png') || name.endsWith('.gif') ||
name.endsWith('.pdf'); name.endsWith('.pdf');
} else { } else {
const type = (doc.type || '').toLowerCase(); const type = (doc.type || '').toLowerCase();
const name = (doc.name || '').toLowerCase(); const name = (doc.name || '').toLowerCase();
return type.includes('image') || type.includes('pdf') || return type.includes('image') || type.includes('pdf') ||
name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.jpg') || name.endsWith('.jpeg') ||
name.endsWith('.png') || name.endsWith('.gif') || name.endsWith('.png') || name.endsWith('.gif') ||
name.endsWith('.pdf'); name.endsWith('.pdf');
} }
}; };
@ -156,8 +184,15 @@ export function DocumentsStep({
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors" data-testid="documents-upload-area"> <div
<Upload className="h-12 w-12 mx-auto mb-4 text-gray-400" /> className={`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${isDragging ? 'border-re-green bg-re-green/5' : 'border-gray-300 hover:border-gray-400'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
data-testid="documents-upload-area"
>
<Upload className={`h-12 w-12 mx-auto mb-4 ${isDragging ? 'text-re-green' : 'text-gray-400'}`} />
<h3 className="text-lg font-semibold text-gray-900 mb-2">Upload Files</h3> <h3 className="text-lg font-semibold text-gray-900 mb-2">Upload Files</h3>
<p className="text-gray-600 mb-4"> <p className="text-gray-600 mb-4">
Drag and drop files here, or click to browse Drag and drop files here, or click to browse
@ -172,10 +207,10 @@ export function DocumentsStep({
ref={fileInputRef} ref={fileInputRef}
data-testid="documents-file-input" data-testid="documents-file-input"
/> />
<Button <Button
variant="outline" variant="outline"
size="lg" size="lg"
type="button" type="button"
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
data-testid="documents-browse-button" data-testid="documents-browse-button"
> >
@ -206,7 +241,7 @@ export function DocumentsStep({
const docId = doc.documentId || doc.document_id || ''; const docId = doc.documentId || doc.document_id || '';
const isDeleted = documentsToDelete.includes(docId); const isDeleted = documentsToDelete.includes(docId);
if (isDeleted) return null; if (isDeleted) return null;
return ( return (
<div key={docId} className="flex items-center justify-between p-4 rounded-lg border bg-gray-50" data-testid={`documents-existing-${docId}`}> <div key={docId} className="flex items-center justify-between p-4 rounded-lg border bg-gray-50" data-testid={`documents-existing-${docId}`}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -222,9 +257,9 @@ export function DocumentsStep({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{canPreview(doc, true) && ( {canPreview(doc, true) && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onPreviewDocument(doc, true)} onClick={() => onPreviewDocument(doc, true)}
data-testid={`documents-existing-${docId}-preview`} data-testid={`documents-existing-${docId}-preview`}
> >
@ -276,9 +311,9 @@ export function DocumentsStep({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{canPreview(file, false) && ( {canPreview(file, false) && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onPreviewDocument(file, false)} onClick={() => onPreviewDocument(file, false)}
data-testid={`documents-new-${index}-preview`} data-testid={`documents-new-${index}-preview`}
> >

View File

@ -255,7 +255,7 @@ export function StandardUserAllRequestsFilters({
) : ( ) : (
<> <>
<Input <Input
placeholder="Search initiator..." placeholder="Use @ to search initiator..."
value={initiatorSearch.searchQuery} value={initiatorSearch.searchQuery}
onChange={(e) => initiatorSearch.handleSearch(e.target.value)} onChange={(e) => initiatorSearch.handleSearch(e.target.value)}
onFocus={() => { onFocus={() => {
@ -325,7 +325,7 @@ export function StandardUserAllRequestsFilters({
) : ( ) : (
<> <>
<Input <Input
placeholder="Search approver..." placeholder="Use @ to search approver..."
value={approverSearch.searchQuery} value={approverSearch.searchQuery}
onChange={(e) => approverSearch.handleSearch(e.target.value)} onChange={(e) => approverSearch.handleSearch(e.target.value)}
onFocus={() => { onFocus={() => {

View File

@ -34,7 +34,7 @@ interface DealerUserAllRequestsFiltersProps {
customStartDate?: Date; customStartDate?: Date;
customEndDate?: Date; customEndDate?: Date;
showCustomDatePicker: boolean; showCustomDatePicker: boolean;
// State for user search // State for user search
initiatorSearch: { initiatorSearch: {
selectedUser: { userId: string; email: string; displayName?: string } | null; selectedUser: { userId: string; email: string; displayName?: string } | null;
@ -46,7 +46,7 @@ interface DealerUserAllRequestsFiltersProps {
handleClear: () => void; handleClear: () => void;
setShowResults: (show: boolean) => void; setShowResults: (show: boolean) => void;
}; };
approverSearch: { approverSearch: {
selectedUser: { userId: string; email: string; displayName?: string } | null; selectedUser: { userId: string; email: string; displayName?: string } | null;
searchQuery: string; searchQuery: string;
@ -57,7 +57,7 @@ interface DealerUserAllRequestsFiltersProps {
handleClear: () => void; handleClear: () => void;
setShowResults: (show: boolean) => void; setShowResults: (show: boolean) => void;
}; };
// Actions // Actions
onSearchChange: (value: string) => void; onSearchChange: (value: string) => void;
onStatusChange: (value: string) => void; onStatusChange: (value: string) => void;
@ -70,7 +70,7 @@ interface DealerUserAllRequestsFiltersProps {
onShowCustomDatePickerChange?: (show: boolean) => void; onShowCustomDatePickerChange?: (show: boolean) => void;
onApplyCustomDate?: () => void; onApplyCustomDate?: () => void;
onClearFilters: () => void; onClearFilters: () => void;
// Computed // Computed
hasActiveFilters: boolean; hasActiveFilters: boolean;
} }
@ -172,7 +172,7 @@ export function DealerUserAllRequestsFilters({
) : ( ) : (
<> <>
<Input <Input
placeholder="Search initiator..." placeholder="Use @ to search initiator..."
value={initiatorSearch.searchQuery} value={initiatorSearch.searchQuery}
onChange={(e) => initiatorSearch.handleSearch(e.target.value)} onChange={(e) => initiatorSearch.handleSearch(e.target.value)}
onFocus={() => { onFocus={() => {
@ -242,7 +242,7 @@ export function DealerUserAllRequestsFilters({
) : ( ) : (
<> <>
<Input <Input
placeholder="Search approver..." placeholder="Use @ to search approver..."
value={approverSearch.searchQuery} value={approverSearch.searchQuery}
onChange={(e) => approverSearch.handleSearch(e.target.value)} onChange={(e) => approverSearch.handleSearch(e.target.value)}
onFocus={() => { onFocus={() => {

File diff suppressed because it is too large Load Diff

View File

@ -139,6 +139,7 @@ export function useCreateRequestSubmission({
user, user,
documentsToDelete documentsToDelete
); );
(updatePayload as any).isDraft = true;
await updateWorkflowRequest( await updateWorkflowRequest(
editRequestId, editRequestId,
@ -164,6 +165,7 @@ export function useCreateRequestSubmission({
selectedTemplate, selectedTemplate,
user user
); );
(createPayload as any).isDraft = true;
const result = await createWorkflow(createPayload, documents); const result = await createWorkflow(createPayload, documents);

View File

@ -59,28 +59,22 @@ export async function submitWorkflowRequest(requestId: string): Promise<void> {
await submitWorkflow(requestId); await submitWorkflow(requestId);
} }
/**
* Create and submit a workflow in one operation
*/
export async function createAndSubmitWorkflow( export async function createAndSubmitWorkflow(
payload: CreateWorkflowPayload, payload: CreateWorkflowPayload,
documents: File[] documents: File[]
): Promise<{ id: string }> { ): Promise<{ id: string }> {
const result = await createWorkflow(payload, documents); // Pass isDraft: false (or omit) to trigger backend auto-submit
await submitWorkflowRequest(result.id); const res = await createWorkflow({ ...payload, isDraft: false }, documents);
return result; return res;
} }
/**
* Update and submit a workflow in one operation
*/
export async function updateAndSubmitWorkflow( export async function updateAndSubmitWorkflow(
requestId: string, requestId: string,
payload: UpdateWorkflowPayload, payload: UpdateWorkflowPayload,
documents: File[], documents: File[],
documentsToDelete: string[] documentsToDelete: string[]
): Promise<void> { ): Promise<void> {
await updateWorkflowRequest(requestId, payload, documents, documentsToDelete); // Pass isDraft: false (or omit) to trigger backend auto-submit
await submitWorkflowRequest(requestId); await updateWorkflowRequest(requestId, { ...payload, isDraft: false }, documents, documentsToDelete);
} }

View File

@ -67,6 +67,7 @@ export interface CreateWorkflowPayload {
email: string; email: string;
}>; }>;
participants: Participant[]; participants: Participant[];
isDraft?: boolean;
} }
export interface UpdateWorkflowPayload { export interface UpdateWorkflowPayload {
@ -76,6 +77,7 @@ export interface UpdateWorkflowPayload {
approvalLevels: ApprovalLevel[]; approvalLevels: ApprovalLevel[];
participants: Participant[]; participants: Participant[];
deleteDocumentIds?: string[]; deleteDocumentIds?: string[];
isDraft?: boolean;
} }
export interface ValidationModalState { export interface ValidationModalState {

View File

@ -665,7 +665,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
) : ( ) : (
<> <>
<Input <Input
placeholder="Search initiator..." placeholder="Use @ to search initiator..."
value={initiatorSearch.searchQuery} value={initiatorSearch.searchQuery}
onChange={(e) => initiatorSearch.handleSearch(e.target.value)} onChange={(e) => initiatorSearch.handleSearch(e.target.value)}
onFocus={() => { onFocus={() => {
@ -735,7 +735,7 @@ export function Requests({ onViewRequest }: RequestsProps) {
) : ( ) : (
<> <>
<Input <Input
placeholder="Search approver..." placeholder="Use @ to search approver..."
value={approverSearch.searchQuery} value={approverSearch.searchQuery}
onChange={(e) => approverSearch.handleSearch(e.target.value)} onChange={(e) => approverSearch.handleSearch(e.target.value)}
onFocus={() => { onFocus={() => {

View File

@ -45,14 +45,14 @@ export function useUserSearch({ allUsers, filterValue, onFilterChange }: UseUser
clearTimeout(searchTimer.current); clearTimeout(searchTimer.current);
} }
if (!query || query.trim().length < 2) { if (!query || !query.startsWith('@') || query.trim().length < 2) {
setSearchResults([]); setSearchResults([]);
setShowResults(false); setShowResults(false);
return; return;
} }
searchTimer.current = setTimeout(() => { searchTimer.current = setTimeout(() => {
const searchLower = query.toLowerCase().trim(); const searchLower = query.slice(1).toLowerCase().trim();
const filtered = allUsers.filter((user) => { const filtered = allUsers.filter((user) => {
const email = (user.email || '').toLowerCase(); const email = (user.email || '').toLowerCase();
const displayName = (user.displayName || '').toLowerCase(); const displayName = (user.displayName || '').toLowerCase();

View File

@ -102,6 +102,7 @@ export async function createWorkflowFromForm(form: CreateWorkflowFromFormPayload
priority, // STANDARD | EXPRESS priority, // STANDARD | EXPRESS
approvalLevels, approvalLevels,
participants: participants.length ? participants : undefined, participants: participants.length ? participants : undefined,
isDraft: (form as any).isDraft,
}; };
const res = await apiClient.post('/workflows', payload); const res = await apiClient.post('/workflows', payload);
@ -131,6 +132,7 @@ export async function createWorkflowMultipart(form: CreateWorkflowFromFormPayloa
tatType: a.tatType || 'hours', tatType: a.tatType || 'hours',
}; };
}), }),
isDraft: (form as any).isDraft,
}; };
// Add spectators if any (simplified - only email required) // Add spectators if any (simplified - only email required)

View File

@ -66,7 +66,9 @@ export const getPriorityConfig = (priority: string) => {
* @returns Configuration object with Tailwind CSS classes * @returns Configuration object with Tailwind CSS classes
*/ */
export const getStatusConfig = (status: string) => { export const getStatusConfig = (status: string) => {
switch (status) { switch (status?.toLowerCase()) {
case 'in-review':
case 'in_progress':
case 'pending': case 'pending':
return { return {
color: 'bg-yellow-100 text-yellow-800 border-yellow-200', color: 'bg-yellow-100 text-yellow-800 border-yellow-200',
@ -77,11 +79,6 @@ export const getStatusConfig = (status: string) => {
color: 'bg-gray-400 text-gray-100 border-gray-500', color: 'bg-gray-400 text-gray-100 border-gray-500',
label: 'paused' label: 'paused'
}; };
case 'in-review':
return {
color: 'bg-blue-100 text-blue-800 border-blue-200',
label: 'in-review'
};
case 'approved': case 'approved':
return { return {
color: 'bg-green-100 text-green-800 border-green-200', color: 'bg-green-100 text-green-800 border-green-200',