feat: Implement sorting functionality in inspection management page

- Added sorting capabilities for equipment listings, allowing users to sort by various columns such as equipment code, name, type, and more.
- Introduced visual indicators for sort direction using icons (ArrowUp, ArrowDown, ArrowUpDown).
- Enhanced filtering logic to accommodate sorting, improving data retrieval and user experience.

These changes aim to provide users with better control over equipment data presentation and enhance overall usability in the inspection management workflow.
This commit is contained in:
kjs
2026-04-24 09:29:41 +09:00
parent 4a9e3768a9
commit c01166263b
7 changed files with 511 additions and 189 deletions

View File

@@ -30,6 +30,9 @@ import {
Inbox,
Settings2,
Upload,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ImageUpload } from "@/components/common/ImageUpload";
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
const [eqForm, setEqForm] = useState<Record<string, any>>({});
const [eqSaving, setEqSaving] = useState(false);
const [eqKeyword, setEqKeyword] = useState("");
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
/* ───── 채번 ───── */
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
)
: defects;
const filteredEquipments = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
const filteredEquipments = useMemo(() => {
const base = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
if (!eqSortKey) return base;
const key = eqSortKey;
const dir = eqSortDir;
return [...base].sort((a, b) => {
const av = a[key];
const bv = b[key];
const aEmpty = av === null || av === undefined || av === "";
const bEmpty = bv === null || bv === undefined || bv === "";
if (aEmpty && bEmpty) return 0;
if (aEmpty) return 1;
if (bEmpty) return -1;
const na = Number(av);
const nb = Number(bv);
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
const sa = String(av).toLowerCase();
const sb = String(bv).toLowerCase();
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
});
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
const handleEqSort = useCallback((key: string) => {
setEqSortKey((prev) => {
if (prev === key) {
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
return prev;
}
setEqSortDir("asc");
return key;
});
}, []);
const renderEqSortIcon = (key: string) => {
if (eqSortKey !== key)
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
return eqSortDir === "asc" ? (
<ArrowUp className="ml-1 inline h-3 w-3" />
) : (
<ArrowDown className="ml-1 inline h-3 w-3" />
);
};
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
const openInspCreate = async () => {
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
{renderEqSortIcon("equipment_code")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
{renderEqSortIcon("equipment_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
{renderEqSortIcon("equipment_type")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
{renderEqSortIcon("model_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
{renderEqSortIcon("manufacturer")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
{renderEqSortIcon("installation_location")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
{renderEqSortIcon("last_calibration_date")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
()
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
(){renderEqSortIcon("calibration_period")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
{renderEqSortIcon("equipment_status")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
{renderEqSortIcon("manager_id")}
</TableHead>
</TableRow>
</TableHeader>

View File

@@ -30,6 +30,9 @@ import {
Inbox,
Settings2,
Upload,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ImageUpload } from "@/components/common/ImageUpload";
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
const [eqForm, setEqForm] = useState<Record<string, any>>({});
const [eqSaving, setEqSaving] = useState(false);
const [eqKeyword, setEqKeyword] = useState("");
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
/* ───── 채번 ───── */
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
)
: defects;
const filteredEquipments = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
const filteredEquipments = useMemo(() => {
const base = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
if (!eqSortKey) return base;
const key = eqSortKey;
const dir = eqSortDir;
return [...base].sort((a, b) => {
const av = a[key];
const bv = b[key];
const aEmpty = av === null || av === undefined || av === "";
const bEmpty = bv === null || bv === undefined || bv === "";
if (aEmpty && bEmpty) return 0;
if (aEmpty) return 1;
if (bEmpty) return -1;
const na = Number(av);
const nb = Number(bv);
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
const sa = String(av).toLowerCase();
const sb = String(bv).toLowerCase();
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
});
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
const handleEqSort = useCallback((key: string) => {
setEqSortKey((prev) => {
if (prev === key) {
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
return prev;
}
setEqSortDir("asc");
return key;
});
}, []);
const renderEqSortIcon = (key: string) => {
if (eqSortKey !== key)
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
return eqSortDir === "asc" ? (
<ArrowUp className="ml-1 inline h-3 w-3" />
) : (
<ArrowDown className="ml-1 inline h-3 w-3" />
);
};
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
const openInspCreate = async () => {
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
{renderEqSortIcon("equipment_code")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
{renderEqSortIcon("equipment_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
{renderEqSortIcon("equipment_type")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
{renderEqSortIcon("model_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
{renderEqSortIcon("manufacturer")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
{renderEqSortIcon("installation_location")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
{renderEqSortIcon("last_calibration_date")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
()
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
(){renderEqSortIcon("calibration_period")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
{renderEqSortIcon("equipment_status")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
{renderEqSortIcon("manager_id")}
</TableHead>
</TableRow>
</TableHeader>

View File

@@ -30,6 +30,9 @@ import {
Inbox,
Settings2,
Upload,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ImageUpload } from "@/components/common/ImageUpload";
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
const [eqForm, setEqForm] = useState<Record<string, any>>({});
const [eqSaving, setEqSaving] = useState(false);
const [eqKeyword, setEqKeyword] = useState("");
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
/* ───── 채번 ───── */
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
)
: defects;
const filteredEquipments = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
const filteredEquipments = useMemo(() => {
const base = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
if (!eqSortKey) return base;
const key = eqSortKey;
const dir = eqSortDir;
return [...base].sort((a, b) => {
const av = a[key];
const bv = b[key];
const aEmpty = av === null || av === undefined || av === "";
const bEmpty = bv === null || bv === undefined || bv === "";
if (aEmpty && bEmpty) return 0;
if (aEmpty) return 1;
if (bEmpty) return -1;
const na = Number(av);
const nb = Number(bv);
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
const sa = String(av).toLowerCase();
const sb = String(bv).toLowerCase();
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
});
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
const handleEqSort = useCallback((key: string) => {
setEqSortKey((prev) => {
if (prev === key) {
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
return prev;
}
setEqSortDir("asc");
return key;
});
}, []);
const renderEqSortIcon = (key: string) => {
if (eqSortKey !== key)
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
return eqSortDir === "asc" ? (
<ArrowUp className="ml-1 inline h-3 w-3" />
) : (
<ArrowDown className="ml-1 inline h-3 w-3" />
);
};
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
const openInspCreate = async () => {
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
{renderEqSortIcon("equipment_code")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
{renderEqSortIcon("equipment_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
{renderEqSortIcon("equipment_type")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
{renderEqSortIcon("model_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
{renderEqSortIcon("manufacturer")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
{renderEqSortIcon("installation_location")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
{renderEqSortIcon("last_calibration_date")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
()
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
(){renderEqSortIcon("calibration_period")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
{renderEqSortIcon("equipment_status")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
{renderEqSortIcon("manager_id")}
</TableHead>
</TableRow>
</TableHeader>

View File

@@ -30,6 +30,9 @@ import {
Inbox,
Settings2,
Upload,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ImageUpload } from "@/components/common/ImageUpload";
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
const [eqForm, setEqForm] = useState<Record<string, any>>({});
const [eqSaving, setEqSaving] = useState(false);
const [eqKeyword, setEqKeyword] = useState("");
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
/* ───── 채번 ───── */
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
)
: defects;
const filteredEquipments = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
const filteredEquipments = useMemo(() => {
const base = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
if (!eqSortKey) return base;
const key = eqSortKey;
const dir = eqSortDir;
return [...base].sort((a, b) => {
const av = a[key];
const bv = b[key];
const aEmpty = av === null || av === undefined || av === "";
const bEmpty = bv === null || bv === undefined || bv === "";
if (aEmpty && bEmpty) return 0;
if (aEmpty) return 1;
if (bEmpty) return -1;
const na = Number(av);
const nb = Number(bv);
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
const sa = String(av).toLowerCase();
const sb = String(bv).toLowerCase();
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
});
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
const handleEqSort = useCallback((key: string) => {
setEqSortKey((prev) => {
if (prev === key) {
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
return prev;
}
setEqSortDir("asc");
return key;
});
}, []);
const renderEqSortIcon = (key: string) => {
if (eqSortKey !== key)
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
return eqSortDir === "asc" ? (
<ArrowUp className="ml-1 inline h-3 w-3" />
) : (
<ArrowDown className="ml-1 inline h-3 w-3" />
);
};
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
const openInspCreate = async () => {
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
{renderEqSortIcon("equipment_code")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
{renderEqSortIcon("equipment_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
{renderEqSortIcon("equipment_type")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
{renderEqSortIcon("model_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
{renderEqSortIcon("manufacturer")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
{renderEqSortIcon("installation_location")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
{renderEqSortIcon("last_calibration_date")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
()
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
(){renderEqSortIcon("calibration_period")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
{renderEqSortIcon("equipment_status")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
{renderEqSortIcon("manager_id")}
</TableHead>
</TableRow>
</TableHeader>

View File

@@ -30,6 +30,9 @@ import {
Inbox,
Settings2,
Upload,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ImageUpload } from "@/components/common/ImageUpload";
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
const [eqForm, setEqForm] = useState<Record<string, any>>({});
const [eqSaving, setEqSaving] = useState(false);
const [eqKeyword, setEqKeyword] = useState("");
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
/* ───── 채번 ───── */
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
)
: defects;
const filteredEquipments = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
const filteredEquipments = useMemo(() => {
const base = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
if (!eqSortKey) return base;
const key = eqSortKey;
const dir = eqSortDir;
return [...base].sort((a, b) => {
const av = a[key];
const bv = b[key];
const aEmpty = av === null || av === undefined || av === "";
const bEmpty = bv === null || bv === undefined || bv === "";
if (aEmpty && bEmpty) return 0;
if (aEmpty) return 1;
if (bEmpty) return -1;
const na = Number(av);
const nb = Number(bv);
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
const sa = String(av).toLowerCase();
const sb = String(bv).toLowerCase();
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
});
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
const handleEqSort = useCallback((key: string) => {
setEqSortKey((prev) => {
if (prev === key) {
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
return prev;
}
setEqSortDir("asc");
return key;
});
}, []);
const renderEqSortIcon = (key: string) => {
if (eqSortKey !== key)
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
return eqSortDir === "asc" ? (
<ArrowUp className="ml-1 inline h-3 w-3" />
) : (
<ArrowDown className="ml-1 inline h-3 w-3" />
);
};
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
const openInspCreate = async () => {
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
{renderEqSortIcon("equipment_code")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
{renderEqSortIcon("equipment_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
{renderEqSortIcon("equipment_type")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
{renderEqSortIcon("model_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
{renderEqSortIcon("manufacturer")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
{renderEqSortIcon("installation_location")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
{renderEqSortIcon("last_calibration_date")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
()
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
(){renderEqSortIcon("calibration_period")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
{renderEqSortIcon("equipment_status")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
{renderEqSortIcon("manager_id")}
</TableHead>
</TableRow>
</TableHeader>

View File

@@ -30,6 +30,9 @@ import {
Inbox,
Settings2,
Upload,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ImageUpload } from "@/components/common/ImageUpload";
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
const [eqForm, setEqForm] = useState<Record<string, any>>({});
const [eqSaving, setEqSaving] = useState(false);
const [eqKeyword, setEqKeyword] = useState("");
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
/* ───── 채번 ───── */
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
)
: defects;
const filteredEquipments = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
const filteredEquipments = useMemo(() => {
const base = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
if (!eqSortKey) return base;
const key = eqSortKey;
const dir = eqSortDir;
return [...base].sort((a, b) => {
const av = a[key];
const bv = b[key];
const aEmpty = av === null || av === undefined || av === "";
const bEmpty = bv === null || bv === undefined || bv === "";
if (aEmpty && bEmpty) return 0;
if (aEmpty) return 1;
if (bEmpty) return -1;
const na = Number(av);
const nb = Number(bv);
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
const sa = String(av).toLowerCase();
const sb = String(bv).toLowerCase();
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
});
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
const handleEqSort = useCallback((key: string) => {
setEqSortKey((prev) => {
if (prev === key) {
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
return prev;
}
setEqSortDir("asc");
return key;
});
}, []);
const renderEqSortIcon = (key: string) => {
if (eqSortKey !== key)
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
return eqSortDir === "asc" ? (
<ArrowUp className="ml-1 inline h-3 w-3" />
) : (
<ArrowDown className="ml-1 inline h-3 w-3" />
);
};
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
const openInspCreate = async () => {
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
{renderEqSortIcon("equipment_code")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
{renderEqSortIcon("equipment_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
{renderEqSortIcon("equipment_type")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
{renderEqSortIcon("model_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
{renderEqSortIcon("manufacturer")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
{renderEqSortIcon("installation_location")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
{renderEqSortIcon("last_calibration_date")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
()
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
(){renderEqSortIcon("calibration_period")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
{renderEqSortIcon("equipment_status")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
{renderEqSortIcon("manager_id")}
</TableHead>
</TableRow>
</TableHeader>

View File

@@ -30,6 +30,9 @@ import {
Inbox,
Settings2,
Upload,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from "lucide-react";
import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter";
import { ImageUpload } from "@/components/common/ImageUpload";
@@ -113,6 +116,8 @@ export default function InspectionManagementPage() {
const [eqForm, setEqForm] = useState<Record<string, any>>({});
const [eqSaving, setEqSaving] = useState(false);
const [eqKeyword, setEqKeyword] = useState("");
const [eqSortKey, setEqSortKey] = useState<string | null>(null);
const [eqSortDir, setEqSortDir] = useState<"asc" | "desc">("asc");
/* ───── 채번 ───── */
const [numberingRuleId, setNumberingRuleId] = useState<string | null>(null);
@@ -288,13 +293,54 @@ export default function InspectionManagementPage() {
)
: defects;
const filteredEquipments = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
const filteredEquipments = useMemo(() => {
const base = eqKeyword.trim()
? equipments.filter(
(r) =>
(r.equipment_name || "").toLowerCase().includes(eqKeyword.toLowerCase()) ||
(r.model_name || "").toLowerCase().includes(eqKeyword.toLowerCase()),
)
: equipments;
if (!eqSortKey) return base;
const key = eqSortKey;
const dir = eqSortDir;
return [...base].sort((a, b) => {
const av = a[key];
const bv = b[key];
const aEmpty = av === null || av === undefined || av === "";
const bEmpty = bv === null || bv === undefined || bv === "";
if (aEmpty && bEmpty) return 0;
if (aEmpty) return 1;
if (bEmpty) return -1;
const na = Number(av);
const nb = Number(bv);
if (!isNaN(na) && !isNaN(nb)) return dir === "asc" ? na - nb : nb - na;
const sa = String(av).toLowerCase();
const sb = String(bv).toLowerCase();
return dir === "asc" ? sa.localeCompare(sb) : sb.localeCompare(sa);
});
}, [equipments, eqKeyword, eqSortKey, eqSortDir]);
const handleEqSort = useCallback((key: string) => {
setEqSortKey((prev) => {
if (prev === key) {
setEqSortDir((d) => (d === "asc" ? "desc" : "asc"));
return prev;
}
setEqSortDir("asc");
return key;
});
}, []);
const renderEqSortIcon = (key: string) => {
if (eqSortKey !== key)
return <ArrowUpDown className="ml-1 inline h-3 w-3 opacity-40" />;
return eqSortDir === "asc" ? (
<ArrowUp className="ml-1 inline h-3 w-3" />
) : (
<ArrowDown className="ml-1 inline h-3 w-3" />
);
};
/* ═══════════════════ 검사기준 CRUD ═══════════════════ */
const openInspCreate = async () => {
@@ -1586,35 +1632,35 @@ export default function InspectionManagementPage() {
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase w-[60px] text-center">
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_code")}>
{renderEqSortIcon("equipment_code")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_name")}>
{renderEqSortIcon("equipment_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_type")}>
{renderEqSortIcon("equipment_type")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("model_name")}>
{renderEqSortIcon("model_name")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manufacturer")}>
{renderEqSortIcon("manufacturer")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("installation_location")}>
{renderEqSortIcon("installation_location")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("last_calibration_date")}>
{renderEqSortIcon("last_calibration_date")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
()
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("calibration_period")}>
(){renderEqSortIcon("calibration_period")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("equipment_status")}>
{renderEqSortIcon("equipment_status")}
</TableHead>
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase">
<TableHead className="text-muted-foreground text-[11px] font-bold tracking-wide uppercase cursor-pointer select-none hover:text-foreground" onClick={() => handleEqSort("manager_id")}>
{renderEqSortIcon("manager_id")}
</TableHead>
</TableRow>
</TableHeader>