# RE Workflow Management System - Detailed Sprint Implementation Guide
## Comprehensive Feature Breakdown for Development Team

**Version:** 1.0  
**Date:** October 23, 2025  
**Project:** RE Workflow Management System (Non-Templatized)  
**Focus:** Core Application Features (Workflow Creation, Approvals, Work Notes, Documents)

---

## 📖 Table of Contents
1. [Sprint 0: Foundation Setup](#sprint-0-foundation-setup)
2. [Sprint 1: SSO Authentication](#sprint-1-sso-authentication)
3. [Sprint 2: Workflow Creation Wizard](#sprint-2-workflow-creation-wizard)
4. [Sprint 3: Approval Actions & TAT Tracking](#sprint-3-approval-actions--tat-tracking)
5. [Sprint 4: Documents & Work Notes](#sprint-4-documents--work-notes)
6. [Sprint 5: Dashboard & Analytics](#sprint-5-dashboard--analytics)
7. [Sprint 6: Testing & Deployment](#sprint-6-testing--deployment)

---

## Sprint 0: Foundation Setup

### Backend Foundation (3 days)

**BE-001 to BE-004:** Database, Express, Sequelize setup
- See main task document for technical setup details
- Focus: Get server running with database connection

### Frontend Foundation (4 days)

**Current Status:**
- ✅ UI Components: 80% extracted from Figma
- ❌ Redux Store: Not created (2-3 days needed)
- ❌ API Service Layer: Not configured (1 day needed)
- ❌ Route Guards: Not implemented (1 day needed)

**Critical Work This Week:**
1. Complete UI extraction (20% remaining)
2. **Setup Redux store - CANNOT SKIP**
3. **Configure Axios API layer - CANNOT SKIP**
4. Implement protected routes

---

## Sprint 1: SSO Authentication (Week 2-3)

### 🔐 SSO Integration Overview

**Important:** No login/signup/password reset screens needed!

#### Backend SSO Integration (BE-101: 1.5 days)

**SSO Flow Implementation:**

**Step 1: Configure SSO Bridge**
```typescript
// src/config/sso.ts
export const ssoConfig = {
  authorizationURL: process.env.SSO_AUTHORIZATION_URL, // RE SSO Bridge URL
  tokenURL: process.env.SSO_TOKEN_URL,
  userInfoURL: process.env.SSO_USERINFO_URL,
  clientID: process.env.SSO_CLIENT_ID,
  clientSecret: process.env.SSO_CLIENT_SECRET,
  callbackURL: process.env.SSO_CALLBACK_URL, // https://workflow.re.com/api/v1/auth/callback
  scope: ['openid', 'profile', 'email', 'employee_info']
};
```

**Step 2: Implement SSO Auth Service**
```typescript
// src/services/auth.service.ts
import passport from 'passport';
import { Strategy as OAuth2Strategy } from 'passport-oauth2';

// Configure Passport OAuth2 strategy
passport.use('re-sso', new OAuth2Strategy({
  authorizationURL: ssoConfig.authorizationURL,
  tokenURL: ssoConfig.tokenURL,
  clientID: ssoConfig.clientID,
  clientSecret: ssoConfig.clientSecret,
  callbackURL: ssoConfig.callbackURL
}, async (accessToken, refreshToken, profile, done) => {
  // Get user info from SSO
  const userInfo = await fetchUserInfoFromSSO(accessToken);
  
  // Sync user to local database
  const user = await syncUserFromAD(userInfo);
  
  return done(null, user);
}));

async function syncUserFromAD(ssoUserInfo) {
  // Check if user exists in local database
  let user = await User.findOne({ where: { employee_id: ssoUserInfo.employeeId } });
  
  if (!user) {
    // Create new user from AD info
    user = await User.create({
      employee_id: ssoUserInfo.employeeId,
      email: ssoUserInfo.email,
      first_name: ssoUserInfo.firstName,
      last_name: ssoUserInfo.lastName,
      department: ssoUserInfo.department,
      designation: ssoUserInfo.designation,
      phone: ssoUserInfo.phone,
      profile_picture_url: ssoUserInfo.profilePicture
    });
  } else {
    // Update existing user with latest AD info
    await user.update({
      email: ssoUserInfo.email,
      first_name: ssoUserInfo.firstName,
      last_name: ssoUserInfo.lastName,
      department: ssoUserInfo.department,
      designation: ssoUserInfo.designation,
      last_login: new Date()
    });
  }
  
  return user;
}
```

**Step 3: Implement Auth Controller**
```typescript
// src/controllers/auth.controller.ts
export const authController = {
  // Initiate SSO login
  login: (req, res) => {
    // Redirect to SSO Bridge
    passport.authenticate('re-sso')(req, res);
  },
  
  // Handle SSO callback
  callback: async (req, res) => {
    passport.authenticate('re-sso', async (err, user) => {
      if (err || !user) {
        return res.redirect('/auth/error?message=authentication_failed');
      }
      
      // Generate JWT token for app session
      const jwtToken = generateJWT({
        userId: user.user_id,
        email: user.email,
        employeeId: user.employee_id
      });
      
      // Redirect to frontend with token
      res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${jwtToken}`);
    })(req, res);
  },
  
  // Get current user
  me: async (req, res) => {
    const user = await User.findByPk(req.user.userId);
    res.json({ success: true, data: user });
  },
  
  // Logout
  logout: (req, res) => {
    // Clear session
    req.logout();
    
    // Redirect to SSO logout
    const ssoLogoutUrl = `${ssoConfig.logoutURL}?redirect_uri=${process.env.FRONTEND_URL}`;
    res.json({ success: true, redirectUrl: ssoLogoutUrl });
  }
};
```

#### Frontend SSO Integration (FE-101, FE-102: 1.5 days)

**Frontend Work (Minimal):**

**1. SSO Callback Handler (FE-101: 1 day)**
```typescript
// src/pages/Auth/SSOCallback.tsx
const SSOCallback = () => {
  const navigate = useNavigate();
  const dispatch = useAppDispatch();
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // Get token from URL params
    const params = new URLSearchParams(window.location.search);
    const token = params.get('token');
    const errorMsg = params.get('error');
    
    if (errorMsg) {
      setError('Authentication failed. Please try again.');
      setTimeout(() => navigate('/'), 3000);
      return;
    }
    
    if (token) {
      // Store JWT token
      localStorage.setItem('accessToken', token);
      
      // Update Redux state
      dispatch(setAuthenticated(true));
      
      // Fetch user details
      dispatch(fetchCurrentUser());
      
      // Redirect to dashboard
      navigate('/dashboard');
    }
  }, []);
  
  if (error) {
    return <ErrorMessage message={error} />;
  }
  
  return (
    <div className="sso-callback-page">
      <Spinner />
      <p>Authenticating... Please wait.</p>
    </div>
  );
};
```

**2. Protected Routes (FE-101: Included in above)**
```typescript
// src/routes/PrivateRoute.tsx
const PrivateRoute = ({ children }) => {
  const { isAuthenticated } = useAppSelector(state => state.auth);
  
  if (!isAuthenticated) {
    // Redirect to SSO login
    window.location.href = `${API_BASE_URL}/auth/login`;
    return <LoadingSpinner />;
  }
  
  return <>{children}</>;
};
```

**3. Logout Button (FE-102: 0.5 days)**
```typescript
// In Header component
const handleLogout = async () => {
  try {
    // Call logout API
    const response = await authService.logout();
    
    // Clear local state
    dispatch(clearAuth());
    localStorage.removeItem('accessToken');
    
    // Redirect to SSO logout
    window.location.href = response.data.redirectUrl;
  } catch (error) {
    console.error('Logout failed:', error);
  }
};

// Logout button in header
<Button onClick={handleLogout}>Logout</Button>
```

**4. User Profile Display (FE-103: 1 day)**
```typescript
// Fetch user on app load
useEffect(() => {
  if (isAuthenticated) {
    dispatch(fetchCurrentUser()); // GET /api/v1/auth/me
  }
}, [isAuthenticated]);

// Display in header dropdown
<UserDropdown>
  <Avatar src={user.profile_picture_url} />
  <UserInfo>
    <Name>{user.display_name}</Name>
    <Email>{user.email}</Email>
    <Department>{user.department}</Department>
    <EmployeeID>ID: {user.employee_id}</EmployeeID>
  </UserInfo>
  <Divider />
  <MenuItem onClick={openEditProfile}>Edit Profile</MenuItem>
  <MenuItem onClick={handleLogout}>Logout</MenuItem>
</UserDropdown>
```

**Sprint 1 Summary:**
- ✅ SSO handles authentication (no login screens needed)
- ✅ Frontend just handles callback and redirects
- ✅ Users auto-synced from Active Directory
- ✅ Simple logout button
- **Total Frontend Work:** 1.5-2 days (not 4-5 days!)

---

## Sprint 2: Workflow Creation Wizard (Weeks 3-4)

### 🎯 Focus: Enable users to create workflow requests

This is the **CORE FEATURE** of the application!

---

### Step 1: Template Selection (FE-202: 0.5 days)

**UI Status:** ✅ Already from Figma (80%)

**What to Build:**
```typescript
// src/components/workflow/TemplateSelector.tsx
const TemplateSelector = () => {
  const [selectedTemplate, setSelectedTemplate] = useState('CUSTOM');
  const dispatch = useAppDispatch();
  
  const handleSelect = (type) => {
    setSelectedTemplate(type);
    // Save to Redux
    dispatch(setWorkflowTemplateType(type));
  };
  
  return (
    <div className="template-selection">
      <Card 
        className={selectedTemplate === 'CUSTOM' ? 'selected' : ''}
        onClick={() => handleSelect('CUSTOM')}
      >
        <Icon>📋</Icon>
        <Title>Custom Request (Non-Templatized)</Title>
        <Description>
          Create a flexible workflow tailored to your specific needs.
          Define approval levels, TAT, and participants dynamically.
        </Description>
        <Badge>Available</Badge>
      </Card>
      
      <Card className="disabled">
        <Icon>📄</Icon>
        <Title>Template Request</Title>
        <Description>
          Use predefined workflow templates for common processes.
        </Description>
        <Badge>Coming Soon</Badge>
      </Card>
    </div>
  );
};
```

**Deliverable:** User can select "Custom Request"

---

### Step 2: Basic Information (FE-203: 1 day)

**UI Status:** ✅ Already from Figma

**What to Build:**

**1. Request Title Field:**
```typescript
const [title, setTitle] = useState('');
const [titleError, setTitleError] = useState('');

const validateTitle = (value) => {
  if (!value || value.trim() === '') {
    setTitleError('Request title is required');
    return false;
  }
  if (value.length > 500) {
    setTitleError('Title must be less than 500 characters');
    return false;
  }
  setTitleError('');
  return true;
};

<TextField
  label="Request Title"
  value={title}
  onChange={(e) => {
    setTitle(e.target.value);
    validateTitle(e.target.value);
  }}
  error={!!titleError}
  helperText={titleError || `${title.length}/500 characters`}
  placeholder="E.g., Approval for new office location"
  required
  fullWidth
/>
```

**2. Rich Text Description:**
```typescript
// Use React Quill or similar
import ReactQuill from 'react-quill';

const [description, setDescription] = useState('');

const quillModules = {
  toolbar: [
    ['bold', 'italic', 'underline'],
    [{ 'list': 'ordered'}, { 'list': 'bullet' }],
    ['link'],
    [{ 'table': 'insert-table' }],
  ]
};

<ReactQuill
  theme="snow"
  value={description}
  onChange={setDescription}
  modules={quillModules}
  placeholder="Provide detailed description of your request..."
/>
<CharacterCount>{description.length} / 5000</CharacterCount>
```

**3. Priority Selection:**
```typescript
const [priority, setPriority] = useState('STANDARD');

<RadioGroup value={priority} onChange={(e) => setPriority(e.target.value)}>
  <FormControlLabel 
    value="STANDARD" 
    control={<Radio />} 
    label={
      <div>
        <strong>Standard</strong>
        <Typography variant="caption">
          TAT calculated using working days (Mon-Fri, excluding weekends)
        </Typography>
      </div>
    }
  />
  <FormControlLabel 
    value="EXPRESS" 
    control={<Radio />} 
    label={
      <div>
        <strong>Express (Urgent)</strong>
        <Typography variant="caption">
          TAT calculated using calendar days (includes weekends)
        </Typography>
      </div>
    }
  />
</RadioGroup>
```

**4. Save to Redux:**
```typescript
const handleNext = () => {
  if (!validateTitle(title) || !description) {
    toast.error('Please fill all required fields');
    return;
  }
  
  // Save to Redux
  dispatch(updateWorkflowBasicInfo({
    title,
    description,
    priority
  }));
  
  // Go to next step
  setActiveStep(2); // Approval Workflow step
};
```

**Deliverable:** Basic information captured and validated

---

### Step 3: Approval Workflow Builder (FE-204: 2-3 days)

**UI Status:** ✅ Already from Figma  
**Complexity:** ⭐⭐⭐⭐⭐ (MOST COMPLEX SCREEN)

**What to Build:**

**1. Dynamic Approval Levels (Core Logic)**

```typescript
// State management for approval levels
const [approvalLevels, setApprovalLevels] = useState([
  {
    levelNumber: 1,
    levelName: '',
    approverId: null,
    approverDetails: null,
    tatValue: '',
    tatUnit: 'hours' // or 'days'
  }
]);

// Add new level
const handleAddLevel = () => {
  if (approvalLevels.length >= 10) {
    toast.error('Maximum 10 approval levels allowed');
    return;
  }
  
  setApprovalLevels([
    ...approvalLevels,
    {
      levelNumber: approvalLevels.length + 1,
      levelName: '',
      approverId: null,
      approverDetails: null,
      tatValue: '',
      tatUnit: 'hours'
    }
  ]);
};

// Remove level
const handleRemoveLevel = (levelNumber) => {
  if (approvalLevels.length === 1) {
    toast.error('At least one approval level is required');
    return;
  }
  
  setApprovalLevels(
    approvalLevels
      .filter(level => level.levelNumber !== levelNumber)
      .map((level, index) => ({ ...level, levelNumber: index + 1 })) // Renumber
  );
};
```

**2. User Search for Approver (@mention)**

```typescript
// User search component
const ApproverSearch = ({ levelIndex, onSelectUser }) => {
  const [searchQuery, setSearchQuery] = useState('');
  const [searchResults, setSearchResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  // Debounced search
  useEffect(() => {
    const timer = setTimeout(async () => {
      if (searchQuery.length >= 2) {
        setLoading(true);
        try {
          // Call user search API
          const response = await userService.searchUsers(searchQuery);
          setSearchResults(response.data);
        } catch (error) {
          console.error('User search failed:', error);
        } finally {
          setLoading(false);
        }
      } else {
        setSearchResults([]);
      }
    }, 300);
    
    return () => clearTimeout(timer);
  }, [searchQuery]);
  
  return (
    <div>
      <TextField
        placeholder="Type @ or search user name/email..."
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        InputProps={{
          startAdornment: <SearchIcon />,
          endAdornment: loading && <CircularProgress size={20} />
        }}
      />
      
      {searchResults.length > 0 && (
        <UserDropdown>
          {searchResults.map(user => (
            <UserItem 
              key={user.user_id}
              onClick={() => {
                onSelectUser(user);
                setSearchQuery('');
                setSearchResults([]);
              }}
            >
              <Avatar src={user.profile_picture_url} />
              <UserDetails>
                <Name>{user.display_name}</Name>
                <Email>{user.email}</Email>
                <Department>{user.department} • {user.designation}</Department>
              </UserDetails>
            </UserItem>
          ))}
        </UserDropdown>
      )}
    </div>
  );
};
```

**3. TAT Input and Real-Time Calculation**

```typescript
// TAT calculation logic
const calculateTotalTAT = () => {
  let totalHours = 0;
  
  approvalLevels.forEach(level => {
    const tatValue = parseFloat(level.tatValue) || 0;
    
    if (level.tatUnit === 'days') {
      totalHours += tatValue * 24;
    } else {
      totalHours += tatValue;
    }
  });
  
  return {
    totalHours,
    totalDays: Math.ceil(totalHours / 24),
    displayText: `${totalHours} hours (${Math.ceil(totalHours / 24)} days)`
  };
};

// Update total TAT when levels change
const totalTAT = useMemo(() => calculateTotalTAT(), [approvalLevels]);

// TAT display
<TATSummaryCard>
  <Title>Total TAT</Title>
  <TATValue>{totalTAT.totalHours} hours</TATValue>
  <TATSubtext>{totalTAT.totalDays} days</TATSubtext>
  <EstimatedCompletion>
    Est. Completion: {calculateExpectedDate(totalTAT.totalHours, priority)}
  </EstimatedCompletion>
</TATSummaryCard>
```

**4. Approval Level UI (For Each Level)**

```typescript
{approvalLevels.map((level, index) => (
  <ApprovalLevelCard key={level.levelNumber}>
    <LevelHeader>
      <LevelBadge>Level {level.levelNumber}</LevelBadge>
      {approvalLevels.length > 1 && (
        <IconButton onClick={() => handleRemoveLevel(level.levelNumber)}>
          <DeleteIcon />
        </IconButton>
      )}
    </LevelHeader>
    
    {/* Level Name (Optional) */}
    <TextField
      label="Level Name (Optional)"
      value={level.levelName}
      onChange={(e) => updateLevel(index, 'levelName', e.target.value)}
      placeholder="E.g., Department Head Approval"
      fullWidth
    />
    
    {/* Approver Selection */}
    <ApproverSearch
      levelIndex={index}
      onSelectUser={(user) => updateLevel(index, 'approver', user)}
    />
    
    {level.approverDetails && (
      <SelectedApprover>
        <Avatar src={level.approverDetails.profile_picture_url} />
        <ApproverInfo>
          <Name>{level.approverDetails.display_name}</Name>
          <Department>{level.approverDetails.department}</Department>
        </ApproverInfo>
        <IconButton onClick={() => updateLevel(index, 'approver', null)}>
          <ClearIcon />
        </IconButton>
      </SelectedApprover>
    )}
    
    {/* TAT Input */}
    <TATInput>
      <TextField
        label="TAT"
        type="number"
        value={level.tatValue}
        onChange={(e) => updateLevel(index, 'tatValue', e.target.value)}
        placeholder="E.g., 48"
        required
      />
      <Select
        value={level.tatUnit}
        onChange={(e) => updateLevel(index, 'tatUnit', e.target.value)}
      >
        <MenuItem value="hours">Hours</MenuItem>
        <MenuItem value="days">Days</MenuItem>
      </Select>
    </TATInput>
  </ApprovalLevelCard>
))}

<Button onClick={handleAddLevel} disabled={approvalLevels.length >= 10}>
  + Add Approval Level
</Button>
```

**5. Approval Flow Summary (Live Preview)**

```typescript
<ApprovalFlowSummary>
  <Title>Approval Flow Summary</Title>
  <Timeline>
    {approvalLevels.map((level, index) => (
      <TimelineItem key={level.levelNumber}>
        <TimelineMarker>{level.levelNumber}</TimelineMarker>
        <TimelineContent>
          <LevelName>
            {level.levelName || `Level ${level.levelNumber}`}
          </LevelName>
          <Approver>
            {level.approverDetails?.display_name || 'Not selected'}
          </Approver>
          <TAT>TAT: {level.tatValue} {level.tatUnit}</TAT>
        </TimelineContent>
        {index < approvalLevels.length - 1 && <Connector>↓</Connector>}
      </TimelineItem>
    ))}
  </Timeline>
  <TotalTAT>
    Total TAT: {totalTAT.displayText}
  </TotalTAT>
</ApprovalFlowSummary>
```

**6. Validation Before Next Step**

```typescript
const validateApprovalWorkflow = () => {
  // Check at least 1 level
  if (approvalLevels.length === 0) {
    toast.error('At least one approval level is required');
    return false;
  }
  
  // Check each level
  for (let i = 0; i < approvalLevels.length; i++) {
    const level = approvalLevels[i];
    
    // Check approver selected
    if (!level.approverId) {
      toast.error(`Please select an approver for Level ${level.levelNumber}`);
      return false;
    }
    
    // Check TAT value
    if (!level.tatValue || parseFloat(level.tatValue) <= 0) {
      toast.error(`Please enter valid TAT for Level ${level.levelNumber}`);
      return false;
    }
  }
  
  // Check for duplicate approvers
  const approverIds = approvalLevels.map(l => l.approverId);
  const uniqueApproverIds = new Set(approverIds);
  if (approverIds.length !== uniqueApproverIds.size) {
    toast.error('Same person cannot be approver at multiple levels');
    return false;
  }
  
  return true;
};

const handleNext = () => {
  if (!validateApprovalWorkflow()) {
    return;
  }
  
  // Save to Redux
  dispatch(setApprovalLevels(approvalLevels));
  
  // Calculate and save total TAT
  dispatch(setTotalTAT(calculateTotalTAT()));
  
  // Go to next step
  setActiveStep(3); // Participants step
};
```

**Backend Integration (BE-201, BE-202: 2.5 days):**
- POST /api/v1/workflows - Create draft workflow
- POST /api/v1/workflows/:id/approvals - Add approval levels
- GET /api/v1/users/search - User search for @mention

**Deliverable:** Approval hierarchy builder fully functional

---

### Step 4: Participants & Access (FE-205: 1 day)

**UI Status:** ✅ Already from Figma

**What to Build:**

**Spectator Management:**
```typescript
const [spectators, setSpectators] = useState([]);

// Add spectator (reuse ApproverSearch component)
const handleAddSpectator = (user) => {
  // Check if already added
  if (spectators.find(s => s.user_id === user.user_id)) {
    toast.warning('User already added as spectator');
    return;
  }
  
  // Check if user is already an approver
  const isApprover = approvalLevels.some(l => l.approverId === user.user_id);
  if (isApprover) {
    toast.warning('User is already an approver');
    return;
  }
  
  setSpectators([...spectators, user]);
  toast.success(`${user.display_name} added as spectator`);
};

// Remove spectator
const handleRemoveSpectator = (userId) => {
  setSpectators(spectators.filter(s => s.user_id !== userId));
};

// Display
<SpectatorList>
  {spectators.map(spectator => (
    <SpectatorChip 
      key={spectator.user_id}
      avatar={<Avatar src={spectator.profile_picture_url} />}
      label={spectator.display_name}
      onDelete={() => handleRemoveSpectator(spectator.user_id)}
    />
  ))}
</SpectatorList>

// Save to Redux
dispatch(setSpectators(spectators));
```

**Backend Integration (BE-203: 1 day):**
- POST /api/v1/workflows/:id/participants - Add spectators

**Deliverable:** Spectator management functional

---

### Step 5: Documents & Attachments (FE-206: 1 day)

**UI Status:** ✅ Already from Figma

**What to Build:**

**File Upload with react-dropzone:**
```typescript
import { useDropzone } from 'react-dropzone';

const [files, setFiles] = useState([]);
const [uploadProgress, setUploadProgress] = useState({});

const onDrop = useCallback((acceptedFiles, rejectedFiles) => {
  // Handle rejected files
  rejectedFiles.forEach(({ file, errors }) => {
    errors.forEach(error => {
      if (error.code === 'file-too-large') {
        toast.error(`${file.name} is too large. Max size: 10MB`);
      } else if (error.code === 'file-invalid-type') {
        toast.error(`${file.name} has invalid type`);
      }
    });
  });
  
  // Add accepted files
  setFiles(prevFiles => [...prevFiles, ...acceptedFiles]);
}, []);

const { getRootProps, getInputProps, isDragActive } = useDropzone({
  onDrop,
  accept: {
    'application/pdf': ['.pdf'],
    'application/msword': ['.doc'],
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
    'application/vnd.ms-excel': ['.xls'],
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
    'application/vnd.ms-powerpoint': ['.ppt'],
    'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
    'image/jpeg': ['.jpg', '.jpeg'],
    'image/png': ['.png'],
    'image/gif': ['.gif']
  },
  maxSize: 10 * 1024 * 1024, // 10MB
  multiple: true
});

<DropZone {...getRootProps()}>
  <input {...getInputProps()} />
  {isDragActive ? (
    <p>Drop files here...</p>
  ) : (
    <>
      <UploadIcon />
      <p>Drag files here or click to browse</p>
      <small>Supported: PDF, Word, Excel, PPT, Images (Max 10MB each)</small>
    </>
  )}
</DropZone>
```

**File List Display:**
```typescript
<FileList>
  {files.map((file, index) => (
    <FileItem key={index}>
      <FileIcon type={file.type} />
      <FileName>{file.name}</FileName>
      <FileSize>{(file.size / 1024 / 1024).toFixed(2)} MB</FileSize>
      <DeleteButton onClick={() => handleRemoveFile(index)}>
        <DeleteIcon />
      </DeleteButton>
    </FileItem>
  ))}
</FileList>
```

**Google Docs/Sheets Links:**
```typescript
const [googleDocsLink, setGoogleDocsLink] = useState('');
const [googleSheetsLink, setGoogleSheetsLink] = useState('');

const validateGoogleLink = (url, type) => {
  if (!url) return true;
  
  const pattern = type === 'docs' 
    ? /^https:\/\/docs\.google\.com\/document\// 
    : /^https:\/\/docs\.google\.com\/spreadsheets\//;
    
  return pattern.test(url);
};

<TextField
  label="Google Docs Link (Optional)"
  value={googleDocsLink}
  onChange={(e) => setGoogleDocsLink(e.target.value)}
  error={!validateGoogleLink(googleDocsLink, 'docs')}
  helperText={!validateGoogleLink(googleDocsLink, 'docs') && 'Invalid Google Docs URL'}
  placeholder="https://docs.google.com/document/d/..."
  fullWidth
/>
```

**Backend Integration (BE-401: 2 days):**
- POST /api/v1/workflows/:id/documents - Upload files
- File upload with Multer
- Cloud storage integration (GCP or local)

**Deliverable:** Document upload functional

---

### Step 6: Review & Submit (FE-207, FE-208: 2 days)

**UI Status:** ✅ Already from Figma

**What to Build:**

**1. Complete Summary Display:**
```typescript
const ReviewStep = () => {
  // Get all data from Redux
  const workflowData = useAppSelector(state => state.workflow.currentDraft);
  const { templateType, title, description, priority } = workflowData.basicInfo;
  const { levels } = workflowData.approvals;
  const { spectators } = workflowData.participants;
  const { files, googleDocsLink, googleSheetsLink } = workflowData.documents;
  
  return (
    <ReviewContainer>
      {/* Request Overview */}
      <Section>
        <SectionTitle>Request Overview</SectionTitle>
        <InfoGrid>
          <InfoItem>
            <Label>Request Type:</Label>
            <Value>{templateType}</Value>
          </InfoItem>
          <InfoItem>
            <Label>Priority:</Label>
            <Value><PriorityBadge priority={priority} /></Value>
          </InfoItem>
        </InfoGrid>
      </Section>
      
      {/* Basic Information */}
      <Section>
        <SectionTitle>
          Basic Information
          <EditButton onClick={() => setActiveStep(1)}>Edit</EditButton>
        </SectionTitle>
        <Title>{title}</Title>
        <Description dangerouslySetInnerHTML={{ __html: description }} />
      </Section>
      
      {/* Approval Workflow */}
      <Section>
        <SectionTitle>
          Approval Workflow
          <EditButton onClick={() => setActiveStep(2)}>Edit</EditButton>
        </SectionTitle>
        <ApprovalTimeline>
          {levels.map((level, index) => (
            <TimelineItem key={index}>
              <LevelNumber>{level.levelNumber}</LevelNumber>
              <ApproverName>{level.approverDetails.display_name}</ApproverName>
              <TAT>{level.tatValue} {level.tatUnit}</TAT>
              {index < levels.length - 1 && <Arrow>→</Arrow>}
            </TimelineItem>
          ))}
        </ApprovalTimeline>
        <TotalTAT>Total TAT: {calculateTotalTAT().displayText}</TotalTAT>
      </Section>
      
      {/* Participants */}
      <Section>
        <SectionTitle>
          Participants & Access
          <EditButton onClick={() => setActiveStep(3)}>Edit</EditButton>
        </SectionTitle>
        <SpectatorCount>{spectators.length} spectators</SpectatorCount>
        <SpectatorList>
          {spectators.map(s => (
            <Chip key={s.user_id} label={s.display_name} />
          ))}
        </SpectatorList>
      </Section>
      
      {/* Documents */}
      <Section>
        <SectionTitle>
          Documents & Attachments
          <EditButton onClick={() => setActiveStep(4)}>Edit</EditButton>
        </SectionTitle>
        <DocumentCount>{files.length} files uploaded</DocumentCount>
        <FileList>
          {files.map((file, index) => (
            <FileItem key={index}>
              <FileIcon type={file.type} />
              <FileName>{file.name}</FileName>
              <FileSize>{(file.size / 1024 / 1024).toFixed(2)} MB</FileSize>
            </FileItem>
          ))}
        </FileList>
        {googleDocsLink && <GoogleLink href={googleDocsLink}>Google Doc</GoogleLink>}
        {googleSheetsLink && <GoogleLink href={googleSheetsLink}>Google Sheet</GoogleLink>}
      </Section>
    </ReviewContainer>
  );
};
```

**2. Submit Workflow (Complex Multi-Step API)**

```typescript
const handleSubmitWorkflow = async () => {
  setSubmitting(true);
  
  try {
    // Step 1: Create workflow draft
    const workflowResponse = await workflowService.createWorkflow({
      title: workflowData.basicInfo.title,
      description: workflowData.basicInfo.description,
      priority: workflowData.basicInfo.priority,
      total_levels: workflowData.approvals.levels.length,
      total_tat_hours: calculateTotalTAT().totalHours,
      is_draft: false
    });
    
    const workflowId = workflowResponse.data.request_id;
    
    // Step 2: Upload documents
    if (files.length > 0) {
      for (const file of files) {
        const formData = new FormData();
        formData.append('file', file);
        
        await documentService.uploadDocument(workflowId, formData, {
          onUploadProgress: (progressEvent) => {
            const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
            setUploadProgress(prev => ({ ...prev, [file.name]: progress }));
          }
        });
      }
    }
    
    // Step 3: Create approval levels
    for (const level of workflowData.approvals.levels) {
      await approvalService.createApprovalLevel(workflowId, {
        level_number: level.levelNumber,
        level_name: level.levelName,
        approver_id: level.approverId,
        tat_hours: level.tatUnit === 'days' ? level.tatValue * 24 : level.tatValue
      });
    }
    
    // Step 4: Add spectators
    if (spectators.length > 0) {
      for (const spectator of spectators) {
        await participantService.addSpectator(workflowId, {
          user_id: spectator.user_id,
          participant_type: 'SPECTATOR'
        });
      }
    }
    
    // Step 5: Submit workflow for approval
    await workflowService.submitWorkflow(workflowId);
    
    // Success!
    toast.success('Workflow submitted successfully!');
    
    // Clear wizard state
    dispatch(clearWorkflowDraft());
    
    // Navigate to request detail
    navigate(`/workflows/${workflowId}`);
    
  } catch (error) {
    console.error('Workflow submission failed:', error);
    toast.error(error.response?.data?.message || 'Failed to submit workflow');
  } finally {
    setSubmitting(false);
  }
};
```

**3. Save as Draft:**
```typescript
const handleSaveDraft = async () => {
  setSavingDraft(true);
  
  try {
    const response = await workflowService.createWorkflow({
      ...workflowData,
      is_draft: true
    });
    
    toast.success('Draft saved successfully!');
    navigate('/workflows/my-requests');
  } catch (error) {
    toast.error('Failed to save draft');
  } finally {
    setSavingDraft(false);
  }
};
```

**Deliverable:** Complete workflow submission with error handling

---

## Sprint 3: Approval Actions & TAT Tracking (Weeks 5-6)

### 🎯 Focus: Enable approvers to approve/reject workflows

---

### Request Detail Page Structure (FE-303: 2 days)

**UI Status:** ✅ Already from Figma

**What to Build:**

**1. Fetch Workflow Data on Load:**
```typescript
const RequestDetail = () => {
  const { id } = useParams();
  const dispatch = useAppDispatch();
  const { currentWorkflow, loading, error } = useAppSelector(state => state.workflow);
  
  useEffect(() => {
    // Fetch workflow details
    dispatch(fetchWorkflowById(id));
    
    // Fetch approvals
    dispatch(fetchApprovals(id));
    
    // Fetch documents
    dispatch(fetchDocuments(id));
    
    // Fetch activities
    dispatch(fetchActivities(id));
    
    // Fetch TAT status
    dispatch(fetchTATStatus(id));
  }, [id]);
  
  if (loading) return <RequestDetailSkeleton />;
  if (error) return <ErrorState message={error} />;
  if (!currentWorkflow) return <NotFound />;
  
  return (
    <DetailPageLayout>
      <RequestHeader workflow={currentWorkflow} />
      <TATProgressBar workflow={currentWorkflow} />
      <TabsContainer>
        <Tab label="Overview" />
        <Tab label="Workflow" />
        <Tab label="Documents" />
        <Tab label="Activity" />
      </TabsContainer>
      <TabPanels>
        {/* Tab content */}
      </TabPanels>
      <Sidebar>
        <SpectatorsList />
        <QuickActions />
      </Sidebar>
    </DetailPageLayout>
  );
};
```

**2. TAT Progress Bar (Key Feature):**
```typescript
const TATProgressBar = ({ workflow }) => {
  // Calculate TAT status
  const tatStatus = useMemo(() => {
    if (!workflow.tat_start_date) return null;
    
    const startTime = new Date(workflow.tat_start_date);
    const currentTime = new Date();
    const totalTAT = workflow.total_tat_hours;
    
    const elapsedHours = (currentTime - startTime) / (1000 * 60 * 60);
    const percentageUsed = (elapsedHours / totalTAT) * 100;
    const remainingHours = totalTAT - elapsedHours;
    
    let status = 'ON_TRACK';
    let color = 'success';
    
    if (percentageUsed >= 100) {
      status = 'BREACHED';
      color = 'error';
    } else if (percentageUsed >= 80) {
      status = 'APPROACHING';
      color = 'warning';
    }
    
    return {
      elapsedHours: Math.floor(elapsedHours),
      remainingHours: Math.max(0, Math.floor(remainingHours)),
      percentageUsed: Math.min(100, percentageUsed),
      status,
      color
    };
  }, [workflow]);
  
  if (!tatStatus) return null;
  
  return (
    <TATCard>
      <TATLabel>TAT Progress</TATLabel>
      <LinearProgress 
        variant="determinate" 
        value={tatStatus.percentageUsed}
        color={tatStatus.color}
      />
      <TATStats>
        <Stat>
          <Label>Elapsed:</Label>
          <Value>{tatStatus.elapsedHours} hours</Value>
        </Stat>
        <Stat>
          <Label>Remaining:</Label>
          <Value>
            {tatStatus.remainingHours > 0 
              ? `${tatStatus.remainingHours} hours`
              : `${Math.abs(tatStatus.remainingHours)} hours overdue`
            }
          </Value>
        </Stat>
        <Stat>
          <Label>Status:</Label>
          <StatusBadge status={tatStatus.status}>
            {tatStatus.status === 'ON_TRACK' && '✓ On Track'}
            {tatStatus.status === 'APPROACHING' && '⚠ Approaching Deadline'}
            {tatStatus.status === 'BREACHED' && '🚨 TAT Breached'}
          </StatusBadge>
        </Stat>
      </TATStats>
    </TATCard>
  );
};
```

---

### Approve/Reject Modals (FE-307: 1.5 days)

**UI Status:** ✅ Already from Figma

**What to Build:**

**Approve Request Modal:**
```typescript
const ApproveModal = ({ open, onClose, workflow, currentLevel }) => {
  const [comments, setComments] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const dispatch = useAppDispatch();
  
  const handleApprove = async () => {
    // Validate comments
    if (!comments || comments.trim() === '') {
      toast.error('Comments are required for approval');
      return;
    }
    
    if (comments.length > 500) {
      toast.error('Comments must be less than 500 characters');
      return;
    }
    
    setSubmitting(true);
    
    try {
      // Call approve API
      await approvalService.approveRequest(workflow.request_id, currentLevel.level_id, {
        comments: comments.trim()
      });
      
      // Refresh workflow data
      dispatch(fetchWorkflowById(workflow.request_id));
      
      // Success notification
      toast.success('Request approved successfully!');
      
      // Close modal
      onClose();
      
    } catch (error) {
      toast.error(error.response?.data?.message || 'Failed to approve request');
    } finally {
      setSubmitting(false);
    }
  };
  
  return (
    <Dialog open={open} onClose={onClose} maxWidth="md">
      <DialogTitle>
        Approve Request
        <Chip label={workflow.request_number} size="small" />
      </DialogTitle>
      
      <DialogContent>
        <RequestInfo>
          <p><strong>Title:</strong> {workflow.title}</p>
          <p><strong>Initiator:</strong> {workflow.initiator_name}</p>
          <p><strong>Current Level:</strong> Level {currentLevel.level_number}</p>
        </RequestInfo>
        
        <Divider />
        
        <ContextualMessage>
          {currentLevel.is_final_approver 
            ? "⚠️ As final approver, your approval will close this request"
            : "Your approval will move this request to the next level"
          }
        </ContextualMessage>
        
        <TextField
          label="Comments & Remarks *"
          placeholder="Add your approval comments..."
          value={comments}
          onChange={(e) => setComments(e.target.value)}
          multiline
          rows={4}
          fullWidth
          required
          error={comments.length > 500}
          helperText={`${comments.length}/500 characters`}
        />
      </DialogContent>
      
      <DialogActions>
        <Button onClick={onClose} disabled={submitting}>
          Cancel
        </Button>
        <Button 
          onClick={handleApprove} 
          variant="contained" 
          color="success"
          disabled={submitting || !comments.trim()}
        >
          {submitting ? <CircularProgress size={20} /> : 'Approve Request'}
        </Button>
      </DialogActions>
    </Dialog>
  );
};
```

**Reject Request Modal (Similar Structure):**
```typescript
// Same structure as Approve, but:
// - Red/danger styling
// - Warning message about closure
// - "Rejection Reason" instead of "Comments"
// - Confirmation prompt: "Are you sure?"
```

**Backend Integration (BE-301: 2 days):**
- PATCH /api/v1/workflows/:id/approvals/:levelId/approve
- PATCH /api/v1/workflows/:id/approvals/:levelId/reject
- Complex approval state machine logic
- TAT tracking updates
- Notifications to next approver/initiator

**Deliverable:** Approval/rejection functionality working

---

### TAT Monitoring Cron Job (BE-302: 2 days)

**Backend Implementation:**
```typescript
// src/jobs/tatMonitor.job.ts
import cron from 'node-cron';

// Run every 30 minutes
cron.schedule('*/30 * * * *', async () => {
  console.log('Running TAT monitoring job...');
  
  // Get all active workflows
  const activeWorkflows = await WorkflowRequest.findAll({
    where: {
      status: ['PENDING', 'IN_PROGRESS']
    },
    include: [
      {
        model: ApprovalLevel,
        where: { status: 'IN_PROGRESS' },
        required: true
      }
    ]
  });
  
  for (const workflow of activeWorkflows) {
    const currentLevel = workflow.approval_levels[0];
    
    // Calculate elapsed time
    const elapsedHours = calculateElapsedHours(
      currentLevel.level_start_time,
      new Date(),
      workflow.priority // STANDARD or EXPRESS
    );
    
    const percentageUsed = (elapsedHours / currentLevel.tat_hours) * 100;
    
    // Determine TAT status
    let tatStatus = 'ON_TRACK';
    if (percentageUsed >= 100) {
      tatStatus = 'BREACHED';
    } else if (percentageUsed >= 80) {
      tatStatus = 'APPROACHING';
    }
    
    // Update TAT tracking
    await TATTracking.upsert({
      request_id: workflow.request_id,
      level_id: currentLevel.level_id,
      tat_status: tatStatus,
      percentage_used: percentageUsed,
      remaining_hours: currentLevel.tat_hours - elapsedHours,
      checked_at: new Date()
    });
    
    // Send notifications based on status
    if (percentageUsed >= 100 && !currentLevel.breach_alert_sent) {
      // TAT Breached - send urgent notification
      await notificationService.create({
        user_id: currentLevel.approver_id,
        request_id: workflow.request_id,
        notification_type: 'TAT_BREACH',
        title: 'TAT BREACHED!',
        message: `Request ${workflow.request_number} has exceeded TAT deadline`,
        priority: 'URGENT'
      });
      
      // Mark alert sent
      await currentLevel.update({ breach_alert_sent: true });
      
    } else if (percentageUsed >= 80 && !currentLevel.warning_alert_sent) {
      // TAT Warning - send high priority notification
      await notificationService.create({
        user_id: currentLevel.approver_id,
        request_id: workflow.request_id,
        notification_type: 'TAT_WARNING',
        title: 'TAT Warning: Action Required',
        message: `Request ${workflow.request_number} has used 80% of TAT`,
        priority: 'HIGH'
      });
      
      // Mark alert sent
      await currentLevel.update({ warning_alert_sent: true });
    }
  }
  
  console.log(`TAT monitoring completed. Checked ${activeWorkflows.length} workflows.`);
});
```

**Deliverable:** Automated TAT monitoring and alerts

---

## Sprint 4: Documents & Work Notes (Week 7)

### 📎 Focus: Document management and collaboration

---

### Document Management (BE-401, FE-305: 2 days total)

**Backend Document Upload (BE-401: 1.5 days):**

```typescript
// src/services/storage.service.ts
import { Storage } from '@google-cloud/storage';
import multer from 'multer';

const storage = new Storage({
  projectId: process.env.GCP_PROJECT_ID,
  keyFilename: process.env.GCP_KEY_FILE
});

const bucket = storage.bucket(process.env.GCP_BUCKET_NAME);

export const uploadToGCP = async (file) => {
  const fileName = `${Date.now()}-${file.originalname}`;
  const blob = bucket.file(fileName);
  
  const blobStream = blob.createWriteStream({
    metadata: {
      contentType: file.mimetype
    }
  });
  
  return new Promise((resolve, reject) => {
    blobStream.on('error', reject);
    blobStream.on('finish', async () => {
      const publicUrl = `https://storage.googleapis.com/${bucket.name}/${blob.name}`;
      resolve({ fileName, publicUrl });
    });
    blobStream.end(file.buffer);
  });
};

// src/controllers/document.controller.ts
export const documentController = {
  upload: async (req, res) => {
    const { id: requestId } = req.params;
    const file = req.file;
    
    // Upload to GCP
    const { fileName, publicUrl } = await uploadToGCP(file);
    
    // Save metadata to database
    const document = await Document.create({
      request_id: requestId,
      uploaded_by: req.user.userId,
      file_name: file.originalname,
      file_type: file.mimetype,
      file_size: file.size,
      file_path: fileName,
      storage_url: publicUrl
    });
    
    // Create activity log
    await Activity.create({
      request_id: requestId,
      user_id: req.user.userId,
      activity_type: 'DOCUMENT_UPLOADED',
      activity_description: `Uploaded document: ${file.originalname}`
    });
    
    res.json({ success: true, data: document });
  }
};
```

**Frontend Document Display (FE-305: 0.5 days):**

```typescript
const DocumentsTab = ({ workflowId }) => {
  const dispatch = useAppDispatch();
  const { documents, loading } = useAppSelector(state => state.document);
  const [previewModal, setPreviewModal] = useState({ open: false, document: null });
  
  useEffect(() => {
    dispatch(fetchDocuments(workflowId));
  }, [workflowId]);
  
  const handlePreview = (document) => {
    if (document.file_type.includes('pdf') || document.file_type.includes('image')) {
      setPreviewModal({ open: true, document });
    } else {
      toast.info('Preview not available. Please download to view.');
    }
  };
  
  const handleDownload = async (documentId) => {
    try {
      const response = await documentService.getDownloadUrl(documentId);
      window.open(response.data.downloadUrl, '_blank');
    } catch (error) {
      toast.error('Failed to download document');
    }
  };
  
  return (
    <TabPanel>
      {loading && <LoadingSkeleton />}
      
      {!loading && documents.length === 0 && (
        <EmptyState>
          <DocumentIcon />
          <p>No documents uploaded yet</p>
        </EmptyState>
      )}
      
      <DocumentGrid>
        {documents.map(doc => (
          <DocumentCard key={doc.document_id}>
            <FileTypeIcon type={doc.file_type} size="large" />
            <FileName>{doc.file_name}</FileName>
            <FileSize>{(doc.file_size / 1024 / 1024).toFixed(2)} MB</FileSize>
            <UploadInfo>
              Uploaded by {doc.uploader_name}
              <br />
              {formatDate(doc.uploaded_at)}
            </UploadInfo>
            <Actions>
              {(doc.file_type.includes('pdf') || doc.file_type.includes('image')) && (
                <IconButton onClick={() => handlePreview(doc)}>
                  <PreviewIcon />
                </IconButton>
              )}
              <IconButton onClick={() => handleDownload(doc.document_id)}>
                <DownloadIcon />
              </IconButton>
            </Actions>
          </DocumentCard>
        ))}
      </DocumentGrid>
      
      <PreviewModal 
        open={previewModal.open}
        document={previewModal.document}
        onClose={() => setPreviewModal({ open: false, document: null })}
      />
    </TabPanel>
  );
};
```

---

### Work Notes / Chat (BE-402, FE-308: 2 days total)

**UI Status:** ✅ Already from Figma + Partial functionality  
**Current:** Document selection and emoji working  
**Needs:** Backend API integration, @mentions, real data

**Backend Work Notes API (BE-402: 1.5 days):**

```typescript
// src/controllers/workNote.controller.ts
export const workNoteController = {
  getAll: async (req, res) => {
    const { id: requestId } = req.params;
    
    const workNotes = await WorkNote.findAll({
      where: { request_id: requestId },
      include: [{
        model: User,
        attributes: ['user_id', 'display_name', 'profile_picture_url']
      }],
      order: [['created_at', 'ASC']]
    });
    
    res.json({ success: true, data: workNotes });
  },
  
  create: async (req, res) => {
    const { id: requestId } = req.params;
    const { message } = req.body;
    
    // Parse @mentions from message
    const mentionedUserIds = parseMentions(message);
    
    // Create work note
    const workNote = await WorkNote.create({
      request_id: requestId,
      user_id: req.user.userId,
      message,
      mentioned_users: mentionedUserIds,
      has_attachment: false
    });
    
    // Send notifications to mentioned users
    for (const mentionedUserId of mentionedUserIds) {
      await Notification.create({
        user_id: mentionedUserId,
        request_id: requestId,
        notification_type: 'WORK_NOTE_MENTION',
        title: `${req.user.displayName} mentioned you`,
        message: `"${message.substring(0, 100)}..."`,
        priority: 'NORMAL',
        action_url: `/workflows/${requestId}#work-notes`
      });
    }
    
    // Create activity
    await Activity.create({
      request_id: requestId,
      user_id: req.user.userId,
      activity_type: 'WORK_NOTE_ADDED',
      activity_description: `Added a work note`
    });
    
    res.json({ success: true, data: workNote });
  },
  
  addReaction: async (req, res) => {
    const { noteId } = req.params;
    const { emoji } = req.body;
    
    const workNote = await WorkNote.findByPk(noteId);
    
    // Update reactions (JSONB column)
    const reactions = workNote.reactions || {};
    if (!reactions[emoji]) {
      reactions[emoji] = [];
    }
    
    // Toggle reaction
    const userIdIndex = reactions[emoji].indexOf(req.user.userId);
    if (userIdIndex > -1) {
      reactions[emoji].splice(userIdIndex, 1); // Remove
    } else {
      reactions[emoji].push(req.user.userId); // Add
    }
    
    await workNote.update({ reactions });
    
    res.json({ success: true, data: workNote });
  }
};

// Helper function to parse @mentions
function parseMentions(message) {
  const mentionPattern = /@(\w+)|@"([^"]+)"/g;
  const mentions = [];
  let match;
  
  while ((match = mentionPattern.exec(message)) !== null) {
    const username = match[1] || match[2];
    // Look up user in database
    const user = await User.findOne({ 
      where: { 
        [Op.or]: [
          { email: { [Op.like]: `%${username}%` } },
          { display_name: { [Op.like]: `%${username}%` } }
        ]
      }
    });
    
    if (user) {
      mentions.push(user.user_id);
    }
  }
  
  return [...new Set(mentions)]; // Remove duplicates
}
```

**Frontend Work Notes Integration (FE-308: 0.5 days):**

Since UI and emoji/doc selection already work, just connect to backend:

```typescript
const WorkNotes = ({ workflowId }) => {
  const dispatch = useAppDispatch();
  const { notes, loading } = useAppSelector(state => state.workNote);
  const [message, setMessage] = useState('');
  const [sending, setSending] = useState(false);
  const messagesEndRef = useRef(null);
  
  // Fetch notes on load
  useEffect(() => {
    dispatch(fetchWorkNotes(workflowId));
    
    // Poll for new messages every 10 seconds
    const interval = setInterval(() => {
      dispatch(fetchWorkNotes(workflowId));
    }, 10000);
    
    return () => clearInterval(interval);
  }, [workflowId]);
  
  // Auto-scroll to bottom
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [notes]);
  
  const handleSendMessage = async () => {
    if (!message.trim()) return;
    
    setSending(true);
    
    try {
      await dispatch(createWorkNote({
        workflowId,
        message: message.trim()
      }));
      
      setMessage(''); // Clear input
      
    } catch (error) {
      toast.error('Failed to send message');
    } finally {
      setSending(false);
    }
  };
  
  const handleAddReaction = async (noteId, emoji) => {
    try {
      await dispatch(addReactionToNote({ noteId, emoji }));
    } catch (error) {
      toast.error('Failed to add reaction');
    }
  };
  
  return (
    <ChatContainer>
      <MessageList>
        {notes.map(note => (
          <MessageItem key={note.note_id}>
            <Avatar src={note.user.profile_picture_url} />
            <MessageContent>
              <MessageHeader>
                <UserName>{note.user.display_name}</UserName>
                <RoleBadge>{getUserRole(note.user_id)}</RoleBadge>
                <Timestamp>{formatRelativeTime(note.created_at)}</Timestamp>
              </MessageHeader>
              <MessageText>{note.message}</MessageText>
              {note.has_attachment && <AttachmentDisplay />}
              <ReactionsBar>
                {Object.entries(note.reactions || {}).map(([emoji, userIds]) => (
                  <ReactionChip 
                    key={emoji}
                    emoji={emoji}
                    count={userIds.length}
                    active={userIds.includes(currentUserId)}
                    onClick={() => handleAddReaction(note.note_id, emoji)}
                  />
                ))}
                <AddReactionButton onClick={() => showEmojiPicker(note.note_id)} />
              </ReactionsBar>
            </MessageContent>
          </MessageItem>
        ))}
        <div ref={messagesEndRef} />
      </MessageList>
      
      <MessageComposer>
        <TextArea
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type your message... Use @ to mention users"
          onKeyPress={(e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
              e.preventDefault();
              handleSendMessage();
            }
          }}
        />
        <Actions>
          <IconButton><EmojiIcon /></IconButton>
          <IconButton><AttachIcon /></IconButton>
          <SendButton 
            onClick={handleSendMessage}
            disabled={!message.trim() || sending}
          >
            {sending ? <CircularProgress size={20} /> : <SendIcon />}
          </SendButton>
        </Actions>
        <CharacterCount>{message.length} / 2000</CharacterCount>
      </MessageComposer>
    </ChatContainer>
  );
};
```

**Deliverable:** Fully functional work notes with @mentions, emojis, real-time updates

---

## Sprint 5: Dashboard & Analytics (Week 8)

### 📊 Focus: Statistics, charts, AI conclusions

---

### Dashboard Statistics (BE-501, FE-402: 2 days total)

**Backend Dashboard API (BE-501: 1 day):**

```typescript
// src/services/dashboard.service.ts
export const dashboardService = {
  getStats: async (userId) => {
    // Total requests by user
    const total_requests = await WorkflowRequest.count({
      where: { initiator_id: userId }
    });
    
    // Pending approvals (where user is current approver)
    const pending_approvals = await WorkflowRequest.count({
      include: [{
        model: ApprovalLevel,
        where: {
          approver_id: userId,
          status: 'IN_PROGRESS'
        },
        required: true
      }]
    });
    
    // Approved/Rejected counts
    const approved_requests = await WorkflowRequest.count({
      where: { initiator_id: userId, status: 'APPROVED' }
    });
    
    const rejected_requests = await WorkflowRequest.count({
      where: { initiator_id: userId, status: 'REJECTED' }
    });
    
    // TAT breached
    const tat_breached = await TATTracking.count({
      where: { tat_status: 'BREACHED' }
    });
    
    // Requests by status
    const requests_by_status = await WorkflowRequest.findAll({
      attributes: [
        'status',
        [sequelize.fn('COUNT', sequelize.col('request_id')), 'count']
      ],
      where: { initiator_id: userId },
      group: ['status']
    });
    
    // Approval trend (last 30 days)
    const approval_trend = await WorkflowRequest.findAll({
      attributes: [
        [sequelize.fn('DATE', sequelize.col('submission_date')), 'date'],
        [sequelize.fn('COUNT', sequelize.col('request_id')), 'count']
      ],
      where: {
        submission_date: {
          [Op.gte]: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
        }
      },
      group: [sequelize.fn('DATE', sequelize.col('submission_date'))]
    });
    
    return {
      total_requests,
      pending_approvals,
      approved_requests,
      rejected_requests,
      tat_breached,
      requests_by_status,
      approval_trend
    };
  }
};
```

**Frontend Dashboard (FE-402: 1 day):**

```typescript
import { PieChart, BarChart, LineChart } from 'recharts';

const Dashboard = () => {
  const dispatch = useAppDispatch();
  const stats = useAppSelector(state => state.dashboard.stats);
  
  useEffect(() => {
    dispatch(fetchDashboardStats());
  }, []);
  
  return (
    <DashboardLayout>
      {/* Metric Cards */}
      <MetricsRow>
        <MetricCard color="primary">
          <MetricValue>{stats.total_requests}</MetricValue>
          <MetricLabel>Total Requests</MetricLabel>
          <MetricIcon><ChartIcon /></MetricIcon>
        </MetricCard>
        
        <MetricCard color="warning">
          <MetricValue>{stats.pending_approvals}</MetricValue>
          <MetricLabel>Pending Approvals</MetricLabel>
          <MetricIcon><ClockIcon /></MetricIcon>
        </MetricCard>
        
        <MetricCard color="success">
          <MetricValue>{stats.approved_requests}</MetricValue>
          <MetricLabel>Approved</MetricLabel>
          <MetricIcon><CheckIcon /></MetricIcon>
        </MetricCard>
        
        <MetricCard color="error">
          <MetricValue>{stats.tat_breached}</MetricValue>
          <MetricLabel>TAT Breached</MetricLabel>
          <MetricIcon><AlertIcon /></MetricIcon>
        </MetricCard>
      </MetricsRow>
      
      {/* Charts */}
      <ChartsRow>
        <Card>
          <CardHeader title="Requests by Status" />
          <PieChart width={400} height={300} data={stats.requests_by_status}>
            <Pie dataKey="count" nameKey="status" />
            <Tooltip />
            <Legend />
          </PieChart>
        </Card>
        
        <Card>
          <CardHeader title="Approval Trend (30 Days)" />
          <LineChart width={600} height={300} data={stats.approval_trend}>
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Line type="monotone" dataKey="count" stroke="#8884d8" />
          </LineChart>
        </Card>
      </ChartsRow>
    </DashboardLayout>
  );
};
```

**Deliverable:** Dashboard with real-time statistics and charts

---

### AI Conclusion Generation (BE-502, FE-403: 2 days total)

**Backend AI Service (BE-502: 1 day):**

```typescript
// src/services/ai.service.ts
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

export const generateConclusion = async (workflowId) => {
  // Get workflow data
  const workflow = await WorkflowRequest.findByPk(workflowId, {
    include: [
      { model: ApprovalLevel, include: [User] },
      { model: WorkNote, include: [User] }
    ]
  });
  
  // Compile approval comments
  const approvalComments = workflow.approval_levels
    .filter(l => l.status === 'APPROVED' && l.comments)
    .map(l => `Level ${l.level_number} (${l.approver.display_name}): ${l.comments}`)
    .join('\n');
  
  // Compile work notes
  const discussionPoints = workflow.work_notes
    .slice(0, 10) // Last 10 notes
    .map(n => `${n.user.display_name}: ${n.message}`)
    .join('\n');
  
  // Create AI prompt
  const prompt = `
Generate a professional conclusion for this workflow request:

Title: ${workflow.title}
Description: ${workflow.description}

Approval Journey:
${approvalComments}

Key Discussion Points:
${discussionPoints}

Please generate a concise conclusion (max 300 words) that summarizes:
1. What was requested
2. Key decision points
3. Final outcome
4. Next steps (if applicable)
  `;
  
  // Call OpenAI API
  const completion = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 500,
    temperature: 0.7
  });
  
  const conclusion = completion.choices[0].message.content;
  
  // Save to database
  await ConclusionRemark.create({
    request_id: workflowId,
    ai_generated_remark: conclusion,
    final_remark: conclusion,
    is_edited: false
  });
  
  return conclusion;
};
```

**Frontend Conclusion Display (FE-403: 1 day):**

```typescript
const ConclusionSection = ({ workflowId, isInitiator }) => {
  const [conclusion, setConclusion] = useState(null);
  const [editing, setEditing] = useState(false);
  const [editedText, setEditedText] = useState('');
  
  useEffect(() => {
    fetchConclusion();
  }, [workflowId]);
  
  const fetchConclusion = async () => {
    try {
      const response = await conclusionService.getConclusion(workflowId);
      setConclusion(response.data);
      setEditedText(response.data.final_remark);
    } catch (error) {
      console.error('Failed to fetch conclusion');
    }
  };
  
  const handleSaveEdit = async () => {
    try {
      await conclusionService.updateConclusion(workflowId, {
        final_remark: editedText
      });
      
      toast.success('Conclusion updated successfully');
      setEditing(false);
      fetchConclusion();
    } catch (error) {
      toast.error('Failed to update conclusion');
    }
  };
  
  if (!conclusion) {
    return (
      <EmptyState>
        <p>Conclusion will be generated after final approval</p>
      </EmptyState>
    );
  }
  
  return (
    <ConclusionCard>
      <CardHeader>
        <Title>Conclusion Remark</Title>
        {isInitiator && !editing && (
          <IconButton onClick={() => setEditing(true)}>
            <EditIcon />
          </IconButton>
        )}
      </CardHeader>
      
      {editing ? (
        <>
          <TextField
            value={editedText}
            onChange={(e) => setEditedText(e.target.value)}
            multiline
            rows={8}
            fullWidth
          />
          <Actions>
            <Button onClick={() => setEditing(false)}>Cancel</Button>
            <Button variant="contained" onClick={handleSaveEdit}>Save</Button>
          </Actions>
        </>
      ) : (
        <>
          <ConclusionText>{conclusion.final_remark}</ConclusionText>
          {conclusion.is_edited && (
            <EditedBadge>Edited by {conclusion.edited_by_name}</EditedBadge>
          )}
        </>
      )}
    </ConclusionCard>
  );
};
```

**Deliverable:** AI-powered conclusion generation with edit capability

---

## 🎯 Key Integration Points

### Frontend-Backend Integration Checklist:

**For Each Feature:**
1. ✅ Backend API ready and documented
2. ✅ Postman collection updated
3. ✅ Frontend Redux slice created
4. ✅ Frontend API service method created
5. ✅ Connect UI to Redux action
6. ✅ Handle loading state
7. ✅ Handle success state
8. ✅ Handle error state
9. ✅ Add validation
10. ✅ Test end-to-end

---

## 💡 Development Best Practices

### For Frontend:
1. **Always use Redux for shared state** (don't use local state for API data)
2. **Always show loading states** (users need feedback)
3. **Always handle errors gracefully** (show user-friendly messages)
4. **Always validate inputs** (client-side validation first)
5. **Always test on mobile** (responsive from start)

### For Backend:
1. **Always validate inputs** (use Zod schemas)
2. **Always log activities** (for audit trail)
3. **Always send notifications** (keep users informed)
4. **Always handle errors** (return consistent error format)
5. **Always document APIs** (Swagger documentation)

---

## 📈 Effort Estimation per Feature

| Feature | UI (Figma) | Redux | API | Logic | Testing | Total |
|---------|------------|-------|-----|-------|---------|-------|
| **Workflow Creation Wizard** | ✅ Done | 1 day | 2 days | 2 days | 1 day | 6 days |
| **Request Detail Page** | ✅ Done | 0.5 day | 1.5 days | 1 day | 0.5 day | 3.5 days |
| **Approval Actions** | ✅ Done | 0.5 day | 1 day | 1 day | 0.5 day | 3 days |
| **Work Notes** | ✅ Done | 0.5 day | 0.5 day | 0.5 day | 0.5 day | 2 days |
| **Documents** | ✅ Done | 0.5 day | 1 day | 0.5 day | 0.5 day | 2.5 days |
| **Dashboard** | ✅ Done | 0.5 day | 1 day | 1 day | 0.5 day | 3 days |
| **Notifications** | ✅ Done | 0.5 day | 1 day | 0.5 day | 0.5 day | 2.5 days |

**Total Frontend Integration Work:** ~23 days (not including Sprint 0 foundation)

---

**Document Focus:** Core application features (workflow creation, approvals, work notes, documents, dashboard)  
**Authentication:** SSO-based (no login/signup/password screens)  
**Prepared By:** .NET Expert Team  
**Last Updated:** October 23, 2025

