Main: revise specs to 1.5.0 (completed)

This commit is contained in:
2025-12-01 01:28:32 +07:00
parent 241022ada6
commit 71c091055a
69 changed files with 28252 additions and 74 deletions

View File

@@ -0,0 +1,408 @@
# 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
1. Create RFA list with status filtering
2. Implement RFA creation form with multiple items
3. Build detail view showing items and approval history
4. Add approval workflow UI (Approve/Reject with comments)
5. Implement revision management
6. 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
- [TASK-BE-007: RFA Module](./TASK-BE-007-rfa-module.md)
- [ADR-013: Form Handling](../../05-decisions/ADR-013-form-handling-validation.md)
---
**Created:** 2025-12-01
**Status:** Ready