Files
lcbp3/specs/09-history/TASK-FE-006-rfa-ui.md
admin c8a0f281ef
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled
251210:1709 Frontend: reeactor organization and run build
2025-12-10 17:09:11 +07:00

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

  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

// 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


Created: 2025-12-01 Status: Ready