location-bulk-edit-page.tsx 10 KB
import React, { useEffect, useState } from "react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Switch } from "../ui/switch";
import { toast } from "sonner";
import { ApiError } from "../../lib/apiClient";
import { updateLocationsBulk, type LocationBulkUpdateItemVo } from "../../services/locationService";
import type { LocationDto } from "../../types/location";

const ZERO = "00000000-0000-0000-0000-000000000000";

function isValidBulkId(id: string): boolean {
  const s = (id ?? "").trim();
  if (!s) return false;
  return s.toLowerCase() !== ZERO;
}

export type LocationBulkEditPageProps = {
  seed: LocationDto[];
  onBack: () => void;
  onSaved: () => void;
};

type RowState = LocationBulkUpdateItemVo & { locationCodeReadonly: string };

function locToRow(loc: LocationDto): RowState {
  return {
    id: loc.id,
    locationCodeReadonly: (loc.locationCode ?? loc.id ?? "").trim(),
    partner: loc.partner ?? "",
    groupName: loc.groupName ?? "",
    locationName: (loc.locationName ?? "").trim() || "",
    street: loc.street ?? "",
    city: loc.city ?? "",
    stateCode: loc.stateCode ?? "",
    country: loc.country ?? "",
    zipCode: loc.zipCode ?? "",
    phone: loc.phone ?? "",
    email: loc.email ?? "",
    latitude: loc.latitude ?? null,
    longitude: loc.longitude ?? null,
    state: loc.state !== false,
  };
}

export function LocationBulkEditPage({ seed, onBack, onSaved }: LocationBulkEditPageProps) {
  const [rows, setRows] = useState<RowState[]>([]);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    setRows(seed.map(locToRow));
  }, [seed]);

  const updateRow = (idx: number, patch: Partial<RowState>) => {
    setRows((prev) => {
      const next = [...prev];
      next[idx] = { ...next[idx], ...patch };
      return next;
    });
  };

  const handleSave = async () => {
    const items: LocationBulkUpdateItemVo[] = rows
      .filter((r) => isValidBulkId(r.id))
      .map((r) => ({
        id: r.id.trim(),
        partner: r.partner?.trim() || null,
        groupName: r.groupName?.trim() || null,
        locationName: r.locationName.trim(),
        street: r.street?.trim() || null,
        city: r.city?.trim() || null,
        stateCode: r.stateCode?.trim() || null,
        country: r.country?.trim() || null,
        zipCode: r.zipCode?.trim() || null,
        phone: r.phone?.trim() || null,
        email: r.email?.trim() || null,
        latitude: r.latitude,
        longitude: r.longitude,
        state: r.state !== false,
      }));

    if (items.length === 0) {
      toast.error("No valid rows", { description: "Select locations in the list first, then open Bulk Edit." });
      return;
    }

    setSaving(true);
    try {
      const res = await updateLocationsBulk({ items });
      toast.success("Bulk update finished", {
        description: `Success: ${res.successCount}, failed: ${res.failCount}`,
      });
      if (res.errors?.length) {
        const preview = res.errors
          .slice(0, 5)
          .map((e) => `Row ${e.rowNumber ?? "?"}: ${e.message ?? ""}`)
          .join("\n");
        toast.message("Errors (first 5)", { description: preview });
      }
      onSaved();
      onBack();
    } catch (e) {
      const msg = e instanceof ApiError ? e.message : e instanceof Error ? e.message : "Save failed.";
      toast.error("Bulk save failed", { description: msg });
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="flex flex-col h-full min-h-0 bg-white">
      <div className="flex items-center justify-between gap-3 px-4 py-3 border-b border-gray-200 shrink-0">
        <Button type="button" variant="outline" onClick={onBack}>
          Back
        </Button>
        <h1 className="text-base font-semibold text-gray-900 flex-1 text-center truncate px-2">Location bulk edit</h1>
        <Button
          type="button"
          className="bg-green-600 hover:bg-green-700 text-white shrink-0"
          disabled={saving}
          onClick={() => void handleSave()}
        >
          {saving ? "Saving…" : "Save All"}
        </Button>
      </div>
      <div className="overflow-auto flex-1 min-h-0 px-2 py-3">
        <table className="w-full text-xs border-collapse border border-gray-200">
          <thead className="bg-gray-100 sticky top-0 z-10">
            <tr>
              <th className="border p-1 w-9 text-center text-gray-600 font-semibold">#</th>
              <th className="border p-1 whitespace-nowrap">Location ID</th>
              <th className="border p-1 whitespace-nowrap">Company</th>
              <th className="border p-1 whitespace-nowrap">Region</th>
              <th className="border p-1 whitespace-nowrap">Location Name *</th>
              <th className="border p-1 whitespace-nowrap">Street</th>
              <th className="border p-1 whitespace-nowrap">City</th>
              <th className="border p-1 whitespace-nowrap">State</th>
              <th className="border p-1 whitespace-nowrap">Country</th>
              <th className="border p-1 whitespace-nowrap">Zip</th>
              <th className="border p-1 whitespace-nowrap">Phone</th>
              <th className="border p-1 whitespace-nowrap">Email</th>
              <th className="border p-1 whitespace-nowrap">Lat</th>
              <th className="border p-1 whitespace-nowrap">Lng</th>
              <th className="border p-1 whitespace-nowrap">Active</th>
            </tr>
          </thead>
          <tbody>
            {rows.map((r, idx) => (
              <tr key={`${r.id}-${idx}`} className="bg-white">
                <td className="border p-1 text-center align-middle text-gray-700 tabular-nums text-xs font-medium">
                  {idx + 1}
                </td>
                <td className="border p-1 align-top">
                  <Input
                    className="h-7 text-xs min-w-[100px]"
                    value={r.locationCodeReadonly}
                    readOnly
                    title="Location ID is not changed in bulk edit"
                  />
                </td>
                <td className="border p-1 align-top">
                  <Input
                    className="h-7 text-xs min-w-[80px]"
                    value={r.partner ?? ""}
                    onChange={(e) => updateRow(idx, { partner: e.target.value })}
                  />
                </td>
                <td className="border p-1 align-top">
                  <Input
                    className="h-7 text-xs min-w-[80px]"
                    value={r.groupName ?? ""}
                    onChange={(e) => updateRow(idx, { groupName: e.target.value })}
                  />
                </td>
                <td className="border p-1 align-top">
                  <Input
                    className="h-7 text-xs min-w-[100px]"
                    value={r.locationName}
                    onChange={(e) => updateRow(idx, { locationName: e.target.value })}
                  />
                </td>
                <td className="border p-1 align-top">
                  <Input className="h-7 text-xs min-w-[80px]" value={r.street ?? ""} onChange={(e) => updateRow(idx, { street: e.target.value })} />
                </td>
                <td className="border p-1 align-top">
                  <Input className="h-7 text-xs min-w-[72px]" value={r.city ?? ""} onChange={(e) => updateRow(idx, { city: e.target.value })} />
                </td>
                <td className="border p-1 align-top">
                  <Input className="h-7 text-xs min-w-[48px]" value={r.stateCode ?? ""} onChange={(e) => updateRow(idx, { stateCode: e.target.value })} />
                </td>
                <td className="border p-1 align-top">
                  <Input className="h-7 text-xs min-w-[56px]" value={r.country ?? ""} onChange={(e) => updateRow(idx, { country: e.target.value })} />
                </td>
                <td className="border p-1 align-top">
                  <Input className="h-7 text-xs min-w-[56px]" value={r.zipCode ?? ""} onChange={(e) => updateRow(idx, { zipCode: e.target.value })} />
                </td>
                <td className="border p-1 align-top">
                  <Input className="h-7 text-xs min-w-[88px]" value={r.phone ?? ""} onChange={(e) => updateRow(idx, { phone: e.target.value })} />
                </td>
                <td className="border p-1 align-top">
                  <Input className="h-7 text-xs min-w-[120px]" value={r.email ?? ""} onChange={(e) => updateRow(idx, { email: e.target.value })} />
                </td>
                <td className="border p-1 align-top">
                  <Input
                    className="h-7 text-xs min-w-[64px]"
                    value={r.latitude === null || r.latitude === undefined ? "" : String(r.latitude)}
                    onChange={(e) => {
                      const v = e.target.value.trim();
                      updateRow(idx, { latitude: v === "" ? null : Number(v) });
                    }}
                  />
                </td>
                <td className="border p-1 align-top">
                  <Input
                    className="h-7 text-xs min-w-[64px]"
                    value={r.longitude === null || r.longitude === undefined ? "" : String(r.longitude)}
                    onChange={(e) => {
                      const v = e.target.value.trim();
                      updateRow(idx, { longitude: v === "" ? null : Number(v) });
                    }}
                  />
                </td>
                <td className="border p-1 align-middle text-center">
                  <div className="flex justify-center">
                    <Switch checked={r.state !== false} onCheckedChange={(c) => updateRow(idx, { state: !!c })} />
                  </div>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <div className="px-4 py-3 border-t border-gray-100 text-center text-xs text-gray-500 shrink-0 space-y-1">
        <p>Only the locations you selected in the list are shown here.</p>
        <p>You can copy and paste from Excel or Google Sheets.</p>
        <p>Columns marked * are required for each row.</p>
      </div>
    </div>
  );
}