206 lines
6.8 KiB
JavaScript
206 lines
6.8 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Tooltip from "@/components/ui/Tooltip";
|
|
import { formatDate } from "@/lib/utils";
|
|
import { useTranslation } from "@/lib/i18n";
|
|
import { formatDistanceToNow } from "date-fns";
|
|
import { pl, enUS } from "date-fns/locale";
|
|
|
|
export default function FieldWithHistory({
|
|
tableName,
|
|
recordId,
|
|
fieldName,
|
|
currentValue,
|
|
displayValue = null,
|
|
label = null,
|
|
className = "",
|
|
}) {
|
|
const { t, language } = useTranslation();
|
|
const [hasHistory, setHasHistory] = useState(false);
|
|
const [history, setHistory] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Get locale for date-fns
|
|
const locale = language === 'pl' ? pl : enUS;
|
|
|
|
useEffect(() => {
|
|
const fetchHistory = async () => {
|
|
try {
|
|
const response = await fetch(
|
|
`/api/field-history?table_name=${tableName}&record_id=${recordId}&field_name=${fieldName}`
|
|
);
|
|
if (response.ok) {
|
|
const historyData = await response.json();
|
|
setHistory(historyData);
|
|
setHasHistory(historyData.length > 0);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch field history:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (tableName && recordId && fieldName) {
|
|
fetchHistory();
|
|
} else {
|
|
setLoading(false);
|
|
}
|
|
}, [tableName, recordId, fieldName]);
|
|
|
|
// Format value for display
|
|
const getDisplayValue = (value) => {
|
|
if (displayValue !== null) return displayValue;
|
|
if (value && fieldName.includes("date")) {
|
|
try {
|
|
return formatDate(value);
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
return value || t("common.na");
|
|
};
|
|
|
|
// Create tooltip content
|
|
const tooltipContent = history.length > 0 && (
|
|
<div className="space-y-2 max-w-sm">
|
|
<div className="flex items-center gap-2 font-semibold text-white mb-3 pb-2 border-b border-gray-600">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{t("common.changeHistory")} ({history.length})
|
|
</div>
|
|
{history.map((change, index) => (
|
|
<div
|
|
key={change.id}
|
|
className="text-xs border-b border-gray-700 pb-2 last:border-b-0 hover:bg-gray-800/30 p-2 rounded transition-colors"
|
|
>
|
|
<div className="flex justify-between items-start gap-3 mb-2">
|
|
<div className="flex-1">
|
|
{/* New Value */}
|
|
<div className="flex items-start gap-2 mb-1">
|
|
<svg className="w-3.5 h-3.5 text-green-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<div>
|
|
<span className="text-green-400 font-medium text-[10px] uppercase tracking-wide block mb-0.5">
|
|
{t("common.changedTo")}
|
|
</span>
|
|
<span className="text-white font-semibold">
|
|
{getDisplayValue(change.new_value)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Old Value */}
|
|
{change.old_value && (
|
|
<div className="flex items-start gap-2 mb-1">
|
|
<svg className="w-3.5 h-3.5 text-red-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
<div>
|
|
<span className="text-red-400 font-medium text-[10px] uppercase tracking-wide block mb-0.5">
|
|
{t("common.from")}
|
|
</span>
|
|
<span className="text-gray-300 line-through">
|
|
{getDisplayValue(change.old_value)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Changed By */}
|
|
{change.changed_by_name && (
|
|
<div className="flex items-center gap-1.5 mt-2 text-gray-400">
|
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
<span className="text-xs">
|
|
{t("common.by")} <span className="text-gray-200 font-medium">{change.changed_by_name}</span>
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Timestamp */}
|
|
<div className="text-right flex-shrink-0">
|
|
<div className="text-gray-400 text-[10px] whitespace-nowrap">
|
|
{formatDistanceToNow(new Date(change.changed_at), {
|
|
addSuffix: true,
|
|
locale: locale
|
|
})}
|
|
</div>
|
|
<div className="text-gray-500 text-[9px] mt-0.5">
|
|
{formatDate(change.changed_at)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Change Reason */}
|
|
{change.change_reason && (
|
|
<div className="mt-2 pt-2 border-t border-gray-700">
|
|
<div className="flex items-start gap-1.5">
|
|
<svg className="w-3 h-3 text-amber-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<div>
|
|
<span className="text-amber-400 font-medium text-[10px] uppercase tracking-wide block mb-0.5">
|
|
{t("common.reason")}
|
|
</span>
|
|
<span className="text-gray-300 text-xs italic">
|
|
{change.change_reason}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className={className}>
|
|
{label && <span className="text-sm font-medium text-gray-500 block mb-1">{label}</span>}
|
|
<p className="text-gray-900 font-medium">{getDisplayValue(currentValue)}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={className}>
|
|
{label && <span className="text-sm font-medium text-gray-500 block mb-1">{label}</span>}
|
|
<div className="flex items-center gap-2">
|
|
<p className="text-gray-900 font-medium">{getDisplayValue(currentValue)}</p>
|
|
{hasHistory && (
|
|
<Tooltip content={tooltipContent}>
|
|
<div className="relative group cursor-help">
|
|
{/* History Icon with Badge */}
|
|
<div className="flex items-center gap-1 px-2 py-1 rounded-full bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
|
|
<svg
|
|
className="w-3.5 h-3.5 text-blue-600 group-hover:text-blue-700 transition-colors"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
<span className="text-[10px] font-semibold text-blue-600 group-hover:text-blue-700 transition-colors">
|
|
{history.length}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|