Qassure-frontend/src/components/shared/SuppliersTable.tsx

384 lines
12 KiB
TypeScript

import { useState, useEffect, type ReactElement } from "react";
import {
PrimaryButton,
StatusBadge,
ActionDropdown,
DataTable,
Pagination,
FilterDropdown,
type Column,
SupplierModal,
ViewSupplierModal,
SupplierContactsModal,
SupplierScorecardsModal,
} from "@/components/shared";
import { Plus, Building2 } from "lucide-react";
import { supplierService } from "@/services/supplier-service";
import type { Supplier } from "@/types/supplier";
import { formatDate } from "@/utils/format-date";
import { useAppTheme } from "@/hooks/useAppTheme";
interface SuppliersTableProps {
tenantId?: string | null;
showHeader?: boolean;
compact?: boolean;
}
const getStatusVariant = (
status: string,
): "success" | "failure" | "process" | "info" => {
switch (status?.toLowerCase()) {
case "approved":
case "qualified":
return "success";
case "pending":
case "conditional":
return "process";
case "suspended":
case "disqualified":
return "failure";
default:
return "success";
}
};
const getCategoryColor = (category: string) => {
switch (category?.toLowerCase()) {
case "critical":
return "bg-red-100 text-red-700";
case "major":
return "bg-orange-100 text-orange-700";
case "minor":
return "bg-blue-100 text-blue-700";
default:
return "bg-gray-100 text-gray-700";
}
};
export const SuppliersTable = ({
tenantId,
showHeader = true,
compact = false,
}: SuppliersTableProps): ReactElement => {
const { primaryColor } = useAppTheme();
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(compact ? 5 : 10);
const [total, setTotal] = useState<number>(0);
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState<string>("");
// Modal state
const [modalOpen, setModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit" | "view">(
"create",
);
const [selectedSupplierId, setSelectedSupplierId] = useState<string | null>(
null,
);
const [selectedSupplierName, setSelectedSupplierName] = useState<string>("");
const [viewModalOpen, setViewModalOpen] = useState(false);
const [contactsModalOpen, setContactsModalOpen] = useState(false);
const [scorecardsModalOpen, setScorecardsModalOpen] = useState(false);
const fetchSuppliers = async () => {
try {
setIsLoading(true);
setError(null);
const response = await supplierService.list({
tenantId,
status: statusFilter || undefined,
search: searchQuery || undefined,
limit,
offset: (currentPage - 1) * limit,
});
if (response.success) {
setSuppliers(response.data);
setTotal(response.pagination.total);
} else {
setError("Failed to load suppliers");
}
} catch (err: any) {
setError(
err?.response?.data?.error?.message || "Failed to load suppliers",
);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchSuppliers();
}, [tenantId, currentPage, limit, statusFilter, searchQuery]);
const handleCreate = () => {
setModalMode("create");
setSelectedSupplierId(null);
setModalOpen(true);
};
const handleView = (id: string) => {
setSelectedSupplierId(id);
setViewModalOpen(true);
};
const handleEdit = (id: string) => {
setModalMode("edit");
setSelectedSupplierId(id);
setModalOpen(true);
};
const handleContacts = (id: string, name: string) => {
setSelectedSupplierId(id);
setSelectedSupplierName(name);
setContactsModalOpen(true);
};
const handleScorecards = (id: string, name: string) => {
setSelectedSupplierId(id);
setSelectedSupplierName(name);
setScorecardsModalOpen(true);
};
const columns: Column<Supplier>[] = [
{
key: "name",
label: "Supplier",
render: (supplier) => (
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gray-50 border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center shrink-0">
<Building2 className="w-4 h-4 text-[#9aa6b2]" />
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-[#0f1724]">
{supplier.name}
</span>
{supplier.supplier_code && (
<span className="text-[10px] text-[#6b7280] font-mono">
{supplier.supplier_code}
</span>
)}
</div>
</div>
),
},
{
key: "supplier_type",
label: "Type",
render: (supplier) => (
<span className="text-sm text-[#4b5563] capitalize">
{supplier.supplier_type.replace(/_/g, " ")}
</span>
),
},
{
key: "category",
label: "Category",
render: (supplier) => (
<span
className={`px-2 py-0.5 rounded text-[10px] font-medium capitalize ${getCategoryColor(supplier.category)}`}
>
{supplier.category}
</span>
),
},
{
key: "status",
label: "Status",
render: (supplier) => (
<StatusBadge variant={getStatusVariant(supplier.status)}>
{supplier.status}
</StatusBadge>
),
},
{
key: "location",
label: "Location",
render: (supplier) => (
<span className="text-sm text-[#6b7280]">
{supplier.address?.city
? `${supplier.address.city}, ${supplier.address.country}`
: supplier.address?.country || "-"}
</span>
),
},
{
key: "created_at",
label: "Dated",
render: (supplier) => (
<span className="text-sm text-[#6b7280]">
{formatDate(supplier.created_at)}
</span>
),
},
{
key: "actions",
label: "Actions",
align: "right",
render: (supplier) => (
<div className="flex justify-end">
<ActionDropdown
onView={() => handleView(supplier.id)}
onEdit={() => handleEdit(supplier.id)}
// onContacts={() => handleContacts(supplier.id, supplier.name)}
// onScorecards={() => handleScorecards(supplier.id, supplier.name)}
/>
</div>
),
},
];
const mobileCardRenderer = (supplier: Supplier) => (
<div className="p-4 border-b border-[rgba(0,0,0,0.08)]">
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gray-50 border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center">
<Building2 className="w-5 h-5 text-[#9aa6b2]" />
</div>
<div>
<h3 className="text-sm font-semibold text-[#0f1724]">
{supplier.name}
</h3>
<p className="text-xs text-[#6b7280]">{supplier.supplier_type}</p>
</div>
</div>
<ActionDropdown
onView={() => handleView(supplier.id)}
onEdit={() => handleEdit(supplier.id)}
onContacts={() => handleContacts(supplier.id, supplier.name)}
onScorecards={() => handleScorecards(supplier.id, supplier.name)}
/>
</div>
<div className="flex gap-2 items-center">
<StatusBadge variant={getStatusVariant(supplier.status)}>
{supplier.status}
</StatusBadge>
<span
className={`px-2 py-0.5 rounded text-[10px] font-medium capitalize ${getCategoryColor(supplier.category)}`}
>
{supplier.category}
</span>
</div>
</div>
);
return (
<div className="flex flex-col gap-4">
{showHeader && (
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 px-4 py-3 bg-white border-b border-[rgba(0,0,0,0.08)]">
<div className="flex flex-wrap items-center gap-3">
<div className="relative">
<input
type="text"
placeholder="Search suppliers..."
className="pl-3 pr-10 py-2 border border-[rgba(0,0,0,0.08)] rounded-md text-xs focus:outline-none focus:ring-2 w-full sm:w-64 transition-all"
style={{
// @ts-ignore
'--tw-ring-color': `${primaryColor}33`,
borderColor: 'rgba(0,0,0,0.08)'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = primaryColor;
e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}1A`;
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = 'rgba(0,0,0,0.08)';
e.currentTarget.style.boxShadow = 'none';
}}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setCurrentPage(1);
}}
/>
</div>
<FilterDropdown
label="Status"
options={[
{ value: "", label: "All Status" },
{ value: "approved", label: "Approved" },
{ value: "qualified", label: "Qualified" },
{ value: "pending", label: "Pending" },
{ value: "suspended", label: "Suspended" },
{ value: "disqualified", label: "Disqualified" },
]}
value={statusFilter || ""}
onChange={(val) => {
setStatusFilter(val as string);
setCurrentPage(1);
}}
/>
</div>
<PrimaryButton
onClick={handleCreate}
className="flex items-center gap-2"
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Supplier</span>
</PrimaryButton>
</div>
)}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden">
<DataTable
columns={columns}
data={suppliers}
keyExtractor={(s) => s.id}
isLoading={isLoading}
error={error}
mobileCardRenderer={mobileCardRenderer}
emptyMessage="No suppliers found"
/>
{total > 0 && (
<div className="p-4 border-t border-[rgba(0,0,0,0.08)]">
<Pagination
currentPage={currentPage}
totalPages={Math.ceil(total / limit)}
totalItems={total}
limit={limit}
onPageChange={setCurrentPage}
onLimitChange={setLimit}
/>
</div>
)}
</div>
<SupplierModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
mode={modalMode}
supplierId={selectedSupplierId}
tenantId={tenantId}
onSuccess={fetchSuppliers}
/>
<ViewSupplierModal
isOpen={viewModalOpen}
onClose={() => setViewModalOpen(false)}
supplierId={selectedSupplierId}
tenantId={tenantId}
/>
<SupplierContactsModal
isOpen={contactsModalOpen}
onClose={() => setContactsModalOpen(false)}
supplierId={selectedSupplierId}
supplierName={selectedSupplierName}
tenantId={tenantId}
/>
<SupplierScorecardsModal
isOpen={scorecardsModalOpen}
onClose={() => setScorecardsModalOpen(false)}
supplierId={selectedSupplierId}
supplierName={selectedSupplierName}
tenantId={tenantId}
/>
</div>
);
};