11 KiB
11 KiB
TASK-FE-006: RFA Management UI
ID: TASK-FE-006 Title: RFA List, Create, View & Workflow UI Category: Business Modules Priority: P1 (High) Effort: 5-7 days Dependencies: TASK-FE-003, TASK-FE-005, TASK-BE-007 Assigned To: Frontend Developer
📋 Overview
Build comprehensive UI for Request for Approval (RFA) management including list with filters, create/edit forms with items, detail view, and approval workflow.
🎯 Objectives
- Create RFA list with status filtering
- Implement RFA creation form with multiple items
- Build detail view showing items and approval history
- Add approval workflow UI (Approve/Reject with comments)
- Implement revision management
- Add response tracking
✅ Acceptance Criteria
- List displays RFAs with pagination and filters
- Create form allows adding multiple RFA items
- Detail view shows items, attachments, and workflow history
- Approve/Reject dialog with comments functional
- Revision history visible
- Response tracking works (Approved/Rejected/Approved with Comments)
🔧 Implementation Steps
Step 1: RFA List Page
// File: src/app/(dashboard)/rfas/page.tsx
import { RFAList } from '@/components/rfas/list';
import { RFAFilters } from '@/components/rfas/filters';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { Plus } from 'lucide-react';
export default async function RFAsPage({
searchParams,
}: {
searchParams: { page?: string; status?: string };
}) {
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">RFAs (Request for Approval)</h1>
<p className="text-gray-600 mt-1">
Manage approval requests and submissions
</p>
</div>
<Link href="/rfas/new">
<Button>
<Plus className="mr-2 h-4 w-4" />
New RFA
</Button>
</Link>
</div>
<RFAFilters />
<RFAList />
</div>
);
}
Step 2: RFA Form with Items
// File: src/components/rfas/form.tsx
'use client';
import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card';
import { Plus, Trash2 } from 'lucide-react';
const rfaItemSchema = z.object({
item_no: z.string(),
description: z.string().min(5),
quantity: z.number().min(0),
unit: z.string(),
drawing_reference: z.string().optional(),
});
const rfaSchema = z.object({
subject: z.string().min(5),
description: z.string().optional(),
contract_id: z.number(),
discipline_id: z.number(),
items: z.array(rfaItemSchema).min(1, 'At least one item required'),
});
type RFAFormData = z.infer<typeof rfaSchema>;
export function RFAForm() {
const {
register,
control,
handleSubmit,
formState: { errors },
} = useForm<RFAFormData>({
resolver: zodResolver(rfaSchema),
defaultValues: {
items: [{ item_no: '1', description: '', quantity: 0, unit: '' }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
const onSubmit = async (data: RFAFormData) => {
console.log(data);
// Submit to API
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-4xl space-y-6">
{/* Basic Info */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">RFA Information</h3>
<div className="space-y-4">
<div>
<Label>Subject *</Label>
<Input {...register('subject')} />
{errors.subject && (
<p className="text-sm text-red-600 mt-1">
{errors.subject.message}
</p>
)}
</div>
<div>
<Label>Description</Label>
<Input {...register('description')} />
</div>
</div>
</Card>
{/* RFA Items */}
<Card className="p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">RFA Items</h3>
<Button
type="button"
variant="outline"
size="sm"
onClick={() =>
append({
item_no: (fields.length + 1).toString(),
description: '',
quantity: 0,
unit: '',
})
}
>
<Plus className="mr-2 h-4 w-4" />
Add Item
</Button>
</div>
<div className="space-y-4">
{fields.map((field, index) => (
<Card key={field.id} className="p-4 bg-gray-50">
<div className="flex justify-between items-start mb-3">
<h4 className="font-medium">Item #{index + 1}</h4>
{fields.length > 1 && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => remove(index)}
>
<Trash2 className="h-4 w-4 text-red-600" />
</Button>
)}
</div>
<div className="grid grid-cols-4 gap-3">
<div>
<Label>Item No.</Label>
<Input {...register(`items.${index}.item_no`)} />
</div>
<div className="col-span-2">
<Label>Description *</Label>
<Input {...register(`items.${index}.description`)} />
</div>
<div>
<Label>Quantity</Label>
<Input
type="number"
{...register(`items.${index}.quantity`, {
valueAsNumber: true,
})}
/>
</div>
</div>
</Card>
))}
</div>
{errors.items?.root && (
<p className="text-sm text-red-600 mt-2">
{errors.items.root.message}
</p>
)}
</Card>
{/* Actions */}
<div className="flex justify-end gap-3">
<Button type="button" variant="outline">
Cancel
</Button>
<Button type="submit">Create RFA</Button>
</div>
</form>
);
}
Step 3: RFA Detail with Approval Actions
// File: src/components/rfas/detail.tsx
'use client';
import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { CheckCircle, XCircle } from 'lucide-react';
export function RFADetail({ data }: { data: any }) {
const [approvalDialog, setApprovalDialog] = useState<
'approve' | 'reject' | null
>(null);
const [comments, setComments] = useState('');
const handleApproval = async (action: 'approve' | 'reject') => {
// Call API
console.log({ action, comments });
setApprovalDialog(null);
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold">{data.subject}</h1>
<div className="flex gap-3 mt-2">
<Badge>{data.status}</Badge>
<span className="text-gray-600">RFA No: {data.rfa_number}</span>
</div>
</div>
{data.status === 'PENDING' && (
<div className="flex gap-2">
<Button
variant="outline"
className="text-green-600"
onClick={() => setApprovalDialog('approve')}
>
<CheckCircle className="mr-2 h-4 w-4" />
Approve
</Button>
<Button
variant="outline"
className="text-red-600"
onClick={() => setApprovalDialog('reject')}
>
<XCircle className="mr-2 h-4 w-4" />
Reject
</Button>
</div>
)}
</div>
{/* RFA Items */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4">RFA Items</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left">Item No.</th>
<th className="px-4 py-2 text-left">Description</th>
<th className="px-4 py-2 text-right">Quantity</th>
<th className="px-4 py-2 text-left">Unit</th>
<th className="px-4 py-2 text-left">Status</th>
</tr>
</thead>
<tbody>
{data.items?.map((item: any) => (
<tr key={item.rfa_item_id} className="border-t">
<td className="px-4 py-3">{item.item_no}</td>
<td className="px-4 py-3">{item.description}</td>
<td className="px-4 py-3 text-right">{item.quantity}</td>
<td className="px-4 py-3">{item.unit}</td>
<td className="px-4 py-3">
<Badge
variant={
item.status === 'APPROVED' ? 'success' : 'default'
}
>
{item.status}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
{/* Approval Dialog */}
<Dialog
open={approvalDialog !== null}
onOpenChange={() => setApprovalDialog(null)}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
{approvalDialog === 'approve' ? 'Approve RFA' : 'Reject RFA'}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Comments</Label>
<Textarea
value={comments}
onChange={(e) => setComments(e.target.value)}
rows={4}
placeholder="Enter your comments..."
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setApprovalDialog(null)}>
Cancel
</Button>
<Button
onClick={() => handleApproval(approvalDialog!)}
variant={
approvalDialog === 'approve' ? 'default' : 'destructive'
}
>
{approvalDialog === 'approve' ? 'Approve' : 'Reject'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}
📦 Deliverables
- RFA list page with filters
- Create/Edit form with dynamic items
- Detail view with items table
- Approval workflow UI (Approve/Reject)
- Revision management
- Response tracking
🔗 Related Documents
Created: 2025-12-01 Status: Ready