Document Workflow

Learn how to implement a complete document workflow from loading to submission using the MIMS SDK.

Overview

A typical document workflow involves these stages:

  1. Load - Fetch document from API and display in viewer
  2. View - Navigate pages and zoom
  3. Edit - Fill in form fields, add signatures
  4. Validate - Check required fields are completed
  5. Submit - Send completed document to the API

Complete Implementation

1

Create Document Service

Create a service to handle document API calls:

lib/document-service.tstypescript
import type { DocumentMetadata, AnyFieldDefinition } from '@mims/sdk-react';

export interface DocumentWithFields {
  metadata: DocumentMetadata;
  documentUrl: string;
  fields: AnyFieldDefinition[];
}

export async function fetchDocument(
  documentId: string,
  apiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>
): Promise<DocumentWithFields> {
  return apiRequest<DocumentWithFields>(`/sdk/documents/${documentId}`);
}

export async function submitDocument(
  payload: DocumentSubmissionPayload,
  apiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>
): Promise<{ success: boolean; submissionId: string }> {
  return apiRequest('/sdk/documents/submit', {
    method: 'POST',
    body: JSON.stringify(payload),
  });
}
2

Create Document Container

Build a container component that manages the document lifecycle:

components/DocumentContainer.tsxtsx
'use client';

import { useEffect, useState } from 'react';
import { 
  PDFViewer, 
  useSDK, 
  useDocumentStore,
  type DocumentSubmissionPayload 
} from '@mims/sdk-react';
import { fetchDocument, submitDocument } from '@/lib/document-service';

interface DocumentContainerProps {
  documentId: string;
  onComplete?: (submissionId: string) => void;
  onError?: (error: Error) => void;
}

export function DocumentContainer({ 
  documentId, 
  onComplete, 
  onError 
}: DocumentContainerProps) {
  const { apiRequest, isInitialized } = useSDK();
  const [documentUrl, setDocumentUrl] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  
  // Store actions
  const initDocument = useDocumentStore((s) => s.initDocument);
  const addField = useDocumentStore((s) => s.addField);
  const reset = useDocumentStore((s) => s.reset);

  // Load document on mount
  useEffect(() => {
    if (!isInitialized) return;

    async function loadDocument() {
      try {
        setLoading(true);
        setError(null);
        
        const doc = await fetchDocument(documentId, apiRequest);
        
        // Initialize document store
        initDocument(documentId, doc.documentUrl);
        setDocumentUrl(doc.documentUrl);
        
        // Add predefined fields
        doc.fields.forEach((field) => addField(field));
        
      } catch (err) {
        const message = err instanceof Error ? err.message : 'Failed to load document';
        setError(message);
        onError?.(err instanceof Error ? err : new Error(message));
      } finally {
        setLoading(false);
      }
    }

    loadDocument();

    // Cleanup on unmount
    return () => reset();
  }, [documentId, isInitialized, apiRequest, initDocument, addField, reset, onError]);

  if (loading) {
    return <DocumentSkeleton />;
  }

  if (error) {
    return <DocumentError message={error} onRetry={() => window.location.reload()} />;
  }

  if (!documentUrl) {
    return null;
  }

  return (
    <div className="document-container">
      <DocumentToolbar documentId={documentId} onComplete={onComplete} />
      <PDFViewer documentUrl={documentUrl} />
    </div>
  );
}

function DocumentSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-12 bg-neutral-200 rounded mb-4" />
      <div className="aspect-[8.5/11] bg-neutral-200 rounded" />
    </div>
  );
}

function DocumentError({ message, onRetry }: { message: string; onRetry: () => void }) {
  return (
    <div className="text-center py-12">
      <p className="text-red-600 mb-4">{message}</p>
      <button onClick={onRetry} className="px-4 py-2 bg-black text-white rounded">
        Try Again
      </button>
    </div>
  );
}
3

Create Document Toolbar

Build toolbar with navigation, zoom, and submit functionality:

components/DocumentToolbar.tsxtsx
'use client';

import { useState } from 'react';
import { useSDK, useDocument, useDocumentStore } from '@mims/sdk-react';
import { submitDocument } from '@/lib/document-service';

interface DocumentToolbarProps {
  documentId: string;
  onComplete?: (submissionId: string) => void;
}

export function DocumentToolbar({ documentId, onComplete }: DocumentToolbarProps) {
  const { apiRequest } = useSDK();
  const { currentPage, totalPages, isDirty } = useDocument();
  const goToPage = useDocumentStore((s) => s.goToPage);
  const validateFields = useDocumentStore((s) => s.validateFields);
  const getSubmissionPayload = useDocumentStore((s) => s.getSubmissionPayload);
  const renderContext = useDocumentStore((s) => s.renderContext);
  const setRenderContext = useDocumentStore((s) => s.setRenderContext);
  
  const [submitting, setSubmitting] = useState(false);

  const handleZoom = (delta: number) => {
    const newScale = Math.max(0.5, Math.min(3, renderContext.scale + delta));
    setRenderContext({ scale: newScale });
  };

  const handleSubmit = async () => {
    // Validate all fields
    if (!validateFields()) {
      alert('Please complete all required fields');
      return;
    }

    const payload = getSubmissionPayload();
    if (!payload) return;

    setSubmitting(true);
    
    try {
      const result = await submitDocument(payload, apiRequest);
      onComplete?.(result.submissionId);
    } catch (error) {
      console.error('Submit failed:', error);
      alert('Failed to submit document. Please try again.');
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <div className="flex items-center justify-between p-4 border-b">
      {/* Navigation */}
      <div className="flex items-center gap-2">
        <button 
          onClick={() => goToPage(currentPage - 1)}
          disabled={currentPage <= 1}
          className="p-2 disabled:opacity-50"
        >
          ← Prev
        </button>
        <span>{currentPage} / {totalPages}</span>
        <button 
          onClick={() => goToPage(currentPage + 1)}
          disabled={currentPage >= totalPages}
          className="p-2 disabled:opacity-50"
        >
          Next →
        </button>
      </div>

      {/* Zoom */}
      <div className="flex items-center gap-2">
        <button onClick={() => handleZoom(-0.25)} className="p-2">−</button>
        <span>{Math.round(renderContext.scale * 100)}%</span>
        <button onClick={() => handleZoom(0.25)} className="p-2">+</button>
      </div>

      {/* Submit */}
      <button
        onClick={handleSubmit}
        disabled={submitting || !isDirty}
        className="px-4 py-2 bg-emerald-500 text-white rounded disabled:opacity-50"
      >
        {submitting ? 'Submitting...' : 'Submit Document'}
      </button>
    </div>
  );
}
4

Create Page Component

Use the container in your page:

app/documents/[id]/page.tsxtsx
import { MIMSProvider } from '@mims/sdk-react';
import { DocumentContainer } from '@/components/DocumentContainer';
import { redirect } from 'next/navigation';

interface Props {
  params: { id: string };
}

export default function DocumentPage({ params }: Props) {
  const handleComplete = (submissionId: string) => {
    // Redirect to success page
    redirect(`/documents/${params.id}/complete?submission=${submissionId}`);
  };

  return (
    <MIMSProvider 
      apiKey={process.env.NEXT_PUBLIC_MIMS_API_KEY!}
      onError={(error) => console.error('SDK Error:', error)}
    >
      <div className="container mx-auto py-8">
        <DocumentContainer 
          documentId={params.id} 
          onComplete={handleComplete}
        />
      </div>
    </MIMSProvider>
  );
}

Workflow States

Track document workflow states for better UX:

Workflow State Machinetsx
type WorkflowState = 
  | 'loading'
  | 'viewing'
  | 'editing'
  | 'validating'
  | 'submitting'
  | 'complete'
  | 'error';

function useWorkflowState() {
  const [state, setState] = useState<WorkflowState>('loading');
  const { isLoading, error, isDirty } = useDocument();
  const selectedField = useSelectedField();

  useEffect(() => {
    if (isLoading) {
      setState('loading');
    } else if (error) {
      setState('error');
    } else if (selectedField) {
      setState('editing');
    } else if (isDirty) {
      setState('viewing');
    }
  }, [isLoading, error, isDirty, selectedField]);

  return { state, setState };
}

Progress Tracking

Show users their progress through required fields:

Progress Indicatortsx
function DocumentProgress() {
  const fields = useDocumentStore((s) => s.fields);
  
  const requiredFields = Array.from(fields.values())
    .filter(f => f.definition.required);
  
  const completedFields = requiredFields
    .filter(f => f.value !== null);
  
  const progress = requiredFields.length > 0
    ? (completedFields.length / requiredFields.length) * 100
    : 100;

  return (
    <div className="progress-container">
      <div className="flex justify-between mb-2">
        <span>Progress</span>
        <span>{completedFields.length} / {requiredFields.length} fields</span>
      </div>
      <div className="h-2 bg-neutral-200 rounded-full overflow-hidden">
        <div 
          className="h-full bg-emerald-500 transition-all"
          style={{ width: `${progress}%` }}
        />
      </div>
    </div>
  );
}

Best Practice

Always validate fields before submission and provide clear feedback about which fields need attention.

Error Recovery

Implement error recovery for failed submissions:

Error Recoverytsx
function useSubmitWithRetry() {
  const { apiRequest } = useSDK();
  const getSubmissionPayload = useDocumentStore((s) => s.getSubmissionPayload);
  const [attempts, setAttempts] = useState(0);
  const maxAttempts = 3;

  const submit = async () => {
    const payload = getSubmissionPayload();
    if (!payload) throw new Error('No document loaded');

    for (let i = 0; i < maxAttempts; i++) {
      try {
        setAttempts(i + 1);
        const result = await apiRequest('/sdk/documents/submit', {
          method: 'POST',
          body: JSON.stringify(payload),
        });
        return result;
      } catch (error) {
        if (i === maxAttempts - 1) throw error;
        // Wait before retry (exponential backoff)
        await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
      }
    }
  };

  return { submit, attempts, maxAttempts };
}

Related Guides