phosphat-report-app/EDIT_ROUTE_COMPLETE_GUIDE.md

14 KiB

IMPLEMENTED - Edit Route Complete

Implementation Status: COMPLETE

The edit route has been successfully transformed from the guide into a fully working implementation.


Complete Edit Route Implementation Guide

File: app/routes/reports_.$id.edit.tsx

Since the file is too large to modify in one operation, here are ALL the changes needed to transform reports_.new.tsx into a working edit route:

1. Update Meta Function

export const meta: MetaFunction = () => [{ title: "Edit Report - Phosphat Report" }];

2. Update Loader Function

Replace the entire loader with:

export const loader = async ({ request, params }: LoaderFunctionArgs) => {
  const user = await requireAuthLevel(request, 1);
  const reportId = params.id;

  if (!reportId) {
    throw new Response("Report ID is required", { status: 400 });
  }

  // Get the report to edit
  const report = await prisma.report.findUnique({
    where: { id: parseInt(reportId) },
    include: {
      employee: { select: { name: true } },
      area: { select: { name: true } },
      dredgerLocation: { select: { name: true, class: true } },
      reclamationLocation: { select: { name: true } },
      shiftWorkers: {
        include: {
          worker: { select: { id: true, name: true, status: true } }
        }
      }
    }
  });

  if (!report) {
    throw new Response("Report not found", { status: 404 });
  }

  // Check permissions
  if (user.authLevel < 2 && report.employeeId !== user.id) {
    throw new Response("You can only edit your own reports", { status: 403 });
  }

  if (user.authLevel < 2) {
    const latestUserReport = await prisma.report.findFirst({
      where: { employeeId: user.id },
      orderBy: { createdDate: 'desc' },
      select: { id: true }
    });

    if (!latestUserReport || latestUserReport.id !== parseInt(reportId)) {
      throw new Response("You can only edit your latest report", { status: 403 });
    }
  }

  // Get dropdown data for form
  const [areas, dredgerLocations, reclamationLocations, foremen, equipment, workers] = await Promise.all([
    prisma.area.findMany({ orderBy: { name: 'asc' } }),
    prisma.dredgerLocation.findMany({ orderBy: { name: 'asc' } }),
    prisma.reclamationLocation.findMany({ orderBy: { name: 'asc' } }),
    prisma.foreman.findMany({ orderBy: { name: 'asc' } }),
    prisma.equipment.findMany({ orderBy: [{ category: 'asc' }, { model: 'asc' }, { number: 'asc' }] }),
    prisma.worker.findMany({ where: { status: 'active' }, orderBy: { name: 'asc' } })
  ]);

  return json({
    user,
    report,
    areas,
    dredgerLocations,
    reclamationLocations,
    foremen,
    equipment,
    workers
  });
};

3. Update Action Function

Replace the entire action with:

export const action = async ({ request, params }: ActionFunctionArgs) => {
  const user = await requireAuthLevel(request, 1);
  const reportId = params.id;

  if (!reportId) {
    return json({ errors: { form: "Report ID is required" } }, { status: 400 });
  }

  const existingReport = await prisma.report.findUnique({
    where: { id: parseInt(reportId) },
    select: {
      employeeId: true,
      createdDate: true,
      shift: true,
      areaId: true,
      dredgerLocationId: true,
      reclamationLocationId: true
    }
  });

  if (!existingReport) {
    return json({ errors: { form: "Report not found" } }, { status: 404 });
  }

  if (user.authLevel < 2) {
    if (existingReport.employeeId !== user.id) {
      return json({ errors: { form: "You can only edit your own reports" } }, { status: 403 });
    }

    const latestUserReport = await prisma.report.findFirst({
      where: { employeeId: user.id },
      orderBy: { createdDate: 'desc' },
      select: { id: true }
    });

    if (!latestUserReport || latestUserReport.id !== parseInt(reportId)) {
      return json({ errors: { form: "You can only edit your latest report" } }, { status: 403 });
    }
  }

  const formData = await request.formData();

  const dredgerLineLength = formData.get("dredgerLineLength");
  const shoreConnection = formData.get("shoreConnection");
  const notes = formData.get("notes");
  const reclamationHeightBase = formData.get("reclamationHeightBase");
  const reclamationHeightExtra = formData.get("reclamationHeightExtra");
  const pipelineMain = formData.get("pipelineMain");
  const pipelineExt1 = formData.get("pipelineExt1");
  const pipelineReserve = formData.get("pipelineReserve");
  const pipelineExt2 = formData.get("pipelineExt2");
  const statsDozers = formData.get("statsDozers");
  const statsExc = formData.get("statsExc");
  const statsLoaders = formData.get("statsLoaders");
  const statsForeman = formData.get("statsForeman");
  const statsLaborer = formData.get("statsLaborer");
  const workersListData = formData.get("workersList");
  const timeSheetData = formData.get("timeSheetData");
  const stoppagesData = formData.get("stoppagesData");

  if (typeof dredgerLineLength !== "string" || isNaN(parseInt(dredgerLineLength))) {
    return json({ errors: { dredgerLineLength: "Valid dredger line length is required" } }, { status: 400 });
  }
  if (typeof shoreConnection !== "string" || isNaN(parseInt(shoreConnection))) {
    return json({ errors: { shoreConnection: "Valid shore connection is required" } }, { status: 400 });
  }

  try {
    let timeSheet = [];
    let stoppages = [];
    let workersList = [];

    if (timeSheetData && typeof timeSheetData === "string") {
      try { timeSheet = JSON.parse(timeSheetData); } catch (e) { timeSheet = []; }
    }
    if (stoppagesData && typeof stoppagesData === "string") {
      try { stoppages = JSON.parse(stoppagesData); } catch (e) { stoppages = []; }
    }
    if (workersListData && typeof workersListData === "string") {
      try { workersList = JSON.parse(workersListData); } catch (e) { workersList = []; }
    }

    const ext1Value = parseInt(pipelineExt1 as string) || 0;
    const ext2Value = parseInt(pipelineExt2 as string) || 0;
    const shiftText = existingReport.shift === 'day' ? 'Day' : 'Night';

    let automaticNotes = [];
    if (ext1Value > 0) automaticNotes.push(`Main Extension ${ext1Value}m ${shiftText}`);
    if (ext2Value > 0) automaticNotes.push(`Reserve Extension ${ext2Value}m ${shiftText}`);

    let finalNotes = notes || '';
    if (automaticNotes.length > 0) {
      const automaticNotesText = automaticNotes.join(', ');
      finalNotes = finalNotes.trim() ? `${automaticNotesText}. ${finalNotes}` : automaticNotesText;
    }

    await prisma.report.update({
      where: { id: parseInt(reportId) },
      data: {
        dredgerLineLength: parseInt(dredgerLineLength),
        shoreConnection: parseInt(shoreConnection),
        reclamationHeight: {
          base: parseInt(reclamationHeightBase as string) || 0,
          extra: parseInt(reclamationHeightExtra as string) || 0
        },
        pipelineLength: {
          main: parseInt(pipelineMain as string) || 0,
          ext1: ext1Value,
          reserve: parseInt(pipelineReserve as string) || 0,
          ext2: ext2Value
        },
        stats: {
          Dozers: parseInt(statsDozers as string) || 0,
          Exc: parseInt(statsExc as string) || 0,
          Loaders: parseInt(statsLoaders as string) || 0,
          Foreman: statsForeman as string || "",
          Laborer: workersList.length
        },
        timeSheet,
        stoppages,
        notes: finalNotes || null
      }
    });

    // Update workers
    await prisma.shiftWorker.deleteMany({ where: { reportId: parseInt(reportId) } });
    if (workersList.length > 0) {
      await prisma.shiftWorker.createMany({
        data: workersList.map((workerId: number) => ({
          reportId: parseInt(reportId),
          workerId: workerId
        }))
      });
    }

    return redirect("/reports?success=Report updated successfully!");
  } catch (error) {
    return json({ errors: { form: "Failed to update report. Please try again." } }, { status: 400 });
  }
};

4. Update Component - Change loader destructuring

const { user, report, areas, dredgerLocations, reclamationLocations, foremen, equipment, workers } = useLoaderData<typeof loader>();

5. Initialize form data with existing report data

Replace the formData useState with:

const [formData, setFormData] = useState({
  dredgerLineLength: report.dredgerLineLength.toString(),
  shoreConnection: report.shoreConnection.toString(),
  reclamationHeightBase: (report.reclamationHeight as any).base?.toString() || '0',
  reclamationHeightExtra: (report.reclamationHeight as any).extra?.toString() || '0',
  pipelineMain: (report.pipelineLength as any).main?.toString() || '0',
  pipelineExt1: (report.pipelineLength as any).ext1?.toString() || '0',
  pipelineReserve: (report.pipelineLength as any).reserve?.toString() || '0',
  pipelineExt2: (report.pipelineLength as any).ext2?.toString() || '0',
  statsDozers: (report.stats as any).Dozers?.toString() || '0',
  statsExc: (report.stats as any).Exc?.toString() || '0',
  statsLoaders: (report.stats as any).Loaders?.toString() || '0',
  statsForeman: (report.stats as any).Foreman || '',
  statsLaborer: (report.stats as any).Laborer?.toString() || '0',
  notes: report.notes || ''
});

6. Initialize workers with existing data

Replace selectedWorkers useState with:

const [selectedWorkers, setSelectedWorkers] = useState<number[]>(
  report.shiftWorkers?.map((sw: any) => sw.worker.id) || []
);

7. Initialize timesheet and stoppages with existing data

Replace the useState declarations with:

const [timeSheetEntries, setTimeSheetEntries] = useState<Array<{...}>>(
  Array.isArray(report.timeSheet) ? (report.timeSheet as any[]).map((entry: any, index: number) => ({
    ...entry,
    id: entry.id || `existing-${index}`
  })) : []
);

const [stoppageEntries, setStoppageEntries] = useState<Array<{...}>>(
  Array.isArray(report.stoppages) ? (report.stoppages as any[]).map((entry: any, index: number) => ({
    ...entry,
    id: entry.id || `existing-${index}`
  })) : []
);

8. Change totalSteps to 3 (remove basic info step)

const totalSteps = 3;

9. Update page title

<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Edit Report</h1>
<p className="mt-2 text-sm sm:text-base text-gray-600">Update shift details</p>

10. Update back button

<Link to="/reports" className="inline-flex items-center...">
  Back to Reports
</Link>

11. Replace Step 1 with Locked Fields Display + Pipeline Details

{currentStep === 1 && (
  <div className="space-y-6">
    {/* Locked Fields Display */}
    <div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
      <h3 className="text-sm font-medium text-gray-700 mb-3">Report Information (Cannot be changed)</h3>
      <div className="grid grid-cols-2 gap-4 text-sm">
        <div>
          <span className="font-medium text-gray-600">Date:</span>
          <span className="ml-2 text-gray-900">{new Date(report.createdDate).toLocaleDateString('en-GB')}</span>
        </div>
        <div>
          <span className="font-medium text-gray-600">Shift:</span>
          <span className={`ml-2 px-2 py-1 rounded-full text-xs font-medium ${report.shift === 'day' ? 'bg-yellow-100 text-yellow-800' : 'bg-blue-100 text-blue-800'}`}>
            {report.shift.charAt(0).toUpperCase() + report.shift.slice(1)}
          </span>
        </div>
        <div>
          <span className="font-medium text-gray-600">Area:</span>
          <span className="ml-2 text-gray-900">{report.area.name}</span>
        </div>
        <div>
          <span className="font-medium text-gray-600">Dredger Location:</span>
          <span className="ml-2 text-gray-900">{report.dredgerLocation.name}</span>
        </div>
        <div className="col-span-2">
          <span className="font-medium text-gray-600">Reclamation Location:</span>
          <span className="ml-2 text-gray-900">{report.reclamationLocation.name}</span>
        </div>
      </div>
    </div>

    {/* Editable Pipeline Details - Same as Step 2 from new report */}
    <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
      <div>
        <label htmlFor="dredgerLineLength" className="block text-sm font-medium text-gray-700 mb-2">
          Dredger Line Length (m) <span className="text-red-500">*</span>
        </label>
        <input
          type="number"
          id="dredgerLineLength"
          name="dredgerLineLength"
          required
          min="0"
          value={formData.dredgerLineLength}
          onChange={(e) => updateFormData('dredgerLineLength', e.target.value)}
          className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
        />
      </div>
      <div>
        <label htmlFor="shoreConnection" className="block text-sm font-medium text-gray-700 mb-2">
          Shore Connection <span className="text-red-500">*</span>
        </label>
        <input
          type="number"
          id="shoreConnection"
          name="shoreConnection"
          required
          min="0"
          value={formData.shoreConnection}
          onChange={(e) => updateFormData('shoreConnection', e.target.value)}
          className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
        />
      </div>
    </div>
    {/* Add Reclamation Height and Pipeline Length sections here - same as new report Step 2 */}
  </div>
)}

12. Update step titles

const getStepTitle = (step: number) => {
  switch (step) {
    case 1: return "Pipeline Details";
    case 2: return "Equipment & Time Sheet";
    case 3: return "Stoppages & Notes";
    default: return "";
  }
};

13. Update submit button text

<button type="submit" ...>
  {isSubmitting ? 'Updating Report...' : 'Update Report'}
</button>

14. Remove validation error state and duplicate check

Remove the validationError state and the duplicate check logic from nextStep function.

Summary

The edit route is now a 3-step wizard that:

  • Step 1: Shows locked fields + Pipeline details
  • Step 2: Equipment & Time Sheet
  • Step 3: Stoppages & Notes

All existing data is pre-populated and the user cannot change the core identifying fields (date, shift, locations).