Audit bilgileri yapılan değişiklikler kaydediliyor.

This commit is contained in:
Sedat Öztürk 2025-10-18 19:02:58 +03:00
parent 0f9fe71e6b
commit a299ae099d
11 changed files with 451 additions and 196 deletions

View file

@ -3,6 +3,7 @@ using Kurs.Notifications.Application;
using Kurs.Settings;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Account;
using Volo.Abp.Auditing;
using Volo.Abp.AutoMapper;
using Volo.Abp.FeatureManagement;
using Volo.Abp.Identity;
@ -39,5 +40,11 @@ public class PlatformApplicationModule : AbpModule
options.IsDynamicPermissionStoreEnabled = true;
options.SaveStaticPermissionsToDatabase = true;
});
// ListFormCustomization için audit kaydı kapatılıyor
Configure<AbpAuditingOptions>(options =>
{
options.IgnoredTypes.Add(typeof(Entities.ListFormCustomization));
});
}
}

View file

@ -9,7 +9,6 @@ public class Sector : FullAuditedEntity<Guid>, IMultiTenant
public Guid? TenantId { get; set; }
public string Name { get; set; }
public string FullName { get; set; }
Guid? IMultiTenant.TenantId => TenantId;
}

View file

@ -750,7 +750,6 @@ public class PlatformDbContext :
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
b.Property(x => x.FullName).HasMaxLength(256);
});
builder.Entity<ContactTag>(b =>

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20251016215302_Initial")]
[Migration("20251018150230_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -6070,10 +6070,6 @@ namespace Kurs.Platform.Migrations
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("FullName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")

View file

@ -1068,7 +1068,6 @@ namespace Kurs.Platform.Migrations
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
FullName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),

View file

@ -6067,10 +6067,6 @@ namespace Kurs.Platform.Migrations
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("FullName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")

View file

@ -100,7 +100,9 @@
"props": null,
"description": null,
"isActive": true,
"dependencies": ["AxiosListComponent"]
"dependencies": [
"AxiosListComponent"
]
}
],
"ReportCategories": [
@ -120,7 +122,6 @@
"icon": "📜"
}
],
"Abouts": [
{
"stats": [
@ -623,65 +624,97 @@
],
"Sectors": [
{
"Name": "Ambalaj",
"FullName": ""
"Name": "Adalet ve Güvenlik"
},
{
"Name": "Demir Çelik",
"FullName": ""
"Name": "Ağaç İşleri, Kağıt ve Kağıt Ürünleri"
},
{
"Name": "Ambalaj"
},
{
"Name": "Bilişim Teknolojileri"
},
{
"Name": "Cam, Çimento ve Toprak"
},
{
"Name": "Çevre"
},
{
"Name": "Demir Çelik"
},
{
"Name": "Diğer",
"FullName": ""
},
{
"Name": "Eğitim"
},
{
"Name": "Elektrik ve Elektronik",
"FullName": ""
},
{
"Name": "Giyim",
"Name": "Enerji"
},
{
"Name": "Finans",
"FullName": ""
},
{
"Name": "Güvenlik",
"FullName": ""
"Name": "Gıda"
},
{
"Name": "Gıda",
"FullName": ""
"Name": "Giyim"
},
{
"Name": "Güvenlik"
},
{
"Name": "Hizmet-servis",
"FullName": ""
},
{
"Name": "Hırdavat ve Nalburiye",
"FullName": ""
"Name": "Hırdavat ve Nalburiye"
},
{
"Name": "Isıtma, Soğutma ve Havalandırma",
"FullName": ""
},
{
"Name": "İnşaat",
"FullName": ""
"Name": "İnşaat"
},
{
"Name": "İş ve Yönetim"
},
{
"Name": "Kantar",
"FullName": ""
},
{
"Name": "Kimya, Petrol, Lastik ve Plastik"
},
{
"Name": "Kimyasal",
"FullName": ""
},
{
"Name": "Kırtasiye",
"FullName": ""
"Name": "Kırtasiye"
},
{
"Name": "Kültür, Sanat ve Tasarım"
},
{
"Name": "Laboratuar ve Test Ürünleri",
"FullName": ""
},
{
"Name": "Maden"
},
{
"Name": "Makine"
},
{
"Name": "Makina",
"FullName": ""
@ -690,37 +723,67 @@
"Name": "Matbaa",
"FullName": ""
},
{
"Name": "Medya, İletişim ve Yayıncılık"
},
{
"Name": "Metal"
},
{
"Name": "Ofis",
"FullName": ""
},
{
"Name": "Otomotiv"
},
{
"Name": "Oto Tamir-Servis",
"FullName": ""
},
{
"Name": "Pnomatik",
"FullName": ""
},
{
"Name": "Sarf",
"FullName": ""
"Name": "Pnomatik"
},
{
"Name": "Sağlık",
"FullName": ""
},
{
"Name": "Tartı",
"Name": "Sağlık ve Sosyal Hizmetler"
},
{
"Name": "Sarf"
},
{
"Name": "Spor ve Rekreasyon",
"FullName": ""
},
{
"Name": "Transpalet",
"Name": "Tarım, Avcılık ve Balıılık"
},
{
"Name": "Tartı"
},
{
"Name": "Tekstil, Hazır Giyim, Deri",
"FullName": ""
},
{
"Name": "Yedek Parça",
"FullName": ""
"Name": "Ticaret (Satış ve Pazarlama)"
},
{
"Name": "Toplumsal ve Kişisel Hizmetler"
},
{
"Name": "Transpalet"
},
{
"Name": "Turizm, Konaklama, Yiyecek-İçecek Hizmetleri"
},
{
"Name": "Ulaştırma, Lojistik ve Haberleşme"
},
{
"Name": "Yedek Parça"
}
],
"SkillTypes": [

View file

@ -192,7 +192,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
await _sectorRepository.InsertAsync(new Sector
{
Name = item.Name,
FullName = item.FullName
});
}
}

View file

@ -62,7 +62,6 @@ public class GlobalSearchSeedDto
public class SectorSeedDto
{
public string Name { get; set; }
public string FullName { get; set; }
}
public class UomCategorySeedDto

View file

@ -27,6 +27,7 @@ using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore;
using Volo.Abp;
using Volo.Abp.Account.Web;
using Volo.Abp.AspNetCore.Auditing;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.AspNetCore.Mvc;
@ -35,6 +36,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Auditing;
using Volo.Abp.Autofac;
using Volo.Abp.BackgroundWorkers.Hangfire;
using Volo.Abp.BlobStoring;
@ -112,6 +114,7 @@ public class PlatformHttpApiHostModule : AbpModule
ConfigureCache();
ConfigureHangfire(context, configuration);
ConfigureBlobStoring(configuration);
ConfigureAuditing();
context.Services.AddSignalR();
@ -351,6 +354,13 @@ public class PlatformHttpApiHostModule : AbpModule
});
}
private void ConfigureAuditing()
{
Configure<AbpAspNetCoreAuditingOptions>(options =>
{
options.IgnoredUrls.Add("/api/app/list-form-customization");
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{

View file

@ -3,10 +3,19 @@ import Dialog from '@/components/ui/Dialog'
import TabList from '@/components/ui/Tabs/TabList'
import TabNav from '@/components/ui/Tabs/TabNav'
import TabContent from '@/components/ui/Tabs/TabContent'
import { Tabs } from '@/components/ui'
import { Tabs, Badge, Spinner } from '@/components/ui'
import { AdaptableCard } from '@/components/shared'
import { AuditLogDto } from '@/proxy/auditLog/audit-log'
import { getAuditLogs } from '@/services/identity.service'
import {
HiOutlineClock,
HiOutlineGlobe,
HiOutlineUser,
HiOutlineCode,
HiOutlineDocumentText,
HiOutlineExclamationCircle,
HiOutlineCheckCircle,
} from 'react-icons/hi'
function AuditLogs({
open,
@ -18,6 +27,7 @@ function AuditLogs({
id: string
}) {
const [selectedLog, setSelectedLog] = useState<AuditLogDto>()
const [loading, setLoading] = useState(false)
useEffect(() => {
if (open && id) {
@ -26,184 +36,362 @@ function AuditLogs({
}, [open, id])
const fetchAuditLog = async (logId: string) => {
const response = await getAuditLogs(logId)
setSelectedLog(response.data)
setLoading(true)
try {
const response = await getAuditLogs(logId)
setSelectedLog(response.data)
} catch (error) {
console.error('Failed to fetch audit log:', error)
} finally {
setLoading(false)
}
}
return (
<Dialog width="80%" isOpen={open} onClose={onDialogClose} onRequestClose={onDialogClose}>
<h5 className="text-lg font-semibold mb-4">
Audit Log Detail
{selectedLog?.id && (
<span className="text-xs font-normal text-slate-500 ml-2">({selectedLog.id})</span>
)}
</h5>
const getStatusBadge = (statusCode?: number) => {
if (!statusCode) return <Badge className="bg-gray-500">Unknown</Badge>
if (statusCode >= 200 && statusCode < 300)
return <Badge className="bg-green-500" content={statusCode}></Badge>
if (statusCode >= 400 && statusCode < 500)
return <Badge className="bg-yellow-500" content={statusCode}></Badge>
if (statusCode >= 500) return <Badge className="bg-red-500" content={statusCode}></Badge>
return <Badge className="bg-blue-500" content={statusCode}></Badge>
}
{!selectedLog ? (
<div className="text-center py-6 text-gray-500">Loading...</div>
const getChangeTypeBadge = (changeType: number) => {
const types = ['Created', 'Updated', 'Deleted']
const colors = ['bg-green-600', 'bg-blue-600', 'bg-red-600']
return (
<Badge
className={colors[changeType] || 'bg-gray-600'}
content={types[changeType] || 'Unknown'}
></Badge>
)
}
const formatDuration = (ms: number) => {
if (ms < 1000) return `${ms}ms`
return `${(ms / 1000).toFixed(2)}s`
}
const InfoRow = ({ icon: Icon, label, value, valueClassName = '' }: any) => (
<div className="flex items-start gap-3 py-2 border-b border-gray-100 last:border-0">
<Icon className="w-5 h-5 text-gray-400 mt-0.5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-gray-500 mb-0.5">{label}</div>
<div className={`text-sm ${valueClassName} break-words`}>{value}</div>
</div>
</div>
)
return (
<Dialog width="90%" isOpen={open} onClose={onDialogClose} onRequestClose={onDialogClose}>
{/* Header */}
<div className="flex items-center justify-between mb-6 pb-4 border-b border-gray-200">
<div>
<h4 className="text-xl font-bold text-gray-800">Audit Log Details</h4>
{selectedLog?.id && <p className="text-sm text-gray-500 mt-1">ID: {selectedLog.id}</p>}
</div>
{selectedLog?.httpStatusCode && (
<div className="flex items-center gap-2">
{getStatusBadge(selectedLog.httpStatusCode)}
<Badge
className="border border-gray-300 bg-white text-gray-700"
content={selectedLog.applicationName}
></Badge>
</div>
)}
</div>
{loading ? (
<div className="flex items-center justify-center py-16">
<Spinner size={40} />
</div>
) : !selectedLog ? (
<div className="text-center py-16">
<HiOutlineExclamationCircle className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No audit log found</p>
</div>
) : (
<Tabs defaultValue="log" variant="pill">
<TabList className="flex-wrap border-b mb-4 bg-slate-50 rounded-t">
<TabNav value="log">Log</TabNav>
<TabList className="mb-6 bg-gray-50 p-1 rounded-lg">
<TabNav value="log">
<HiOutlineDocumentText className="w-4 h-4 mr-2" />
Overview
</TabNav>
<TabNav value="actions">
Actions {selectedLog.actions?.length > 0 && `(${selectedLog.actions.length})`}
<HiOutlineCode className="w-4 h-4 mr-2" />
Actions
{selectedLog.actions?.length > 0 && (
<Badge
className="ml-2 bg-blue-500"
content={`${selectedLog.actions.length}`}
></Badge>
)}
</TabNav>
<TabNav value="changes">
Entity Changes{' '}
{selectedLog.entityChanges?.length > 0 && `(${selectedLog.entityChanges.length})`}
<HiOutlineCheckCircle className="w-4 h-4 mr-2" />
Entity Changes
{selectedLog.entityChanges?.length > 0 && (
<Badge
className="ml-2 bg-purple-500"
content={`${selectedLog.entityChanges.length}`}
></Badge>
)}
</TabNav>
</TabList>
{/* LOG DETAILS */}
{/* OVERVIEW TAB */}
<TabContent value="log">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-2">
<AdaptableCard>
<h6 className="font-medium mb-3 text-slate-600 text-sm">Request Info</h6>
<table className="w-full text-xs">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all">
<tr>
<td>User</td>
<td>{selectedLog.userName}</td>
</tr>
<tr>
<td>Time</td>
<td>{new Date(selectedLog.executionTime).toLocaleString()}</td>
</tr>
<tr>
<td>Duration</td>
<td>{selectedLog.executionDuration} ms</td>
</tr>
<tr>
<td>IP</td>
<td>{selectedLog.clientIpAddress}</td>
</tr>
<tr>
<td>Method</td>
<td>{selectedLog.httpMethod}</td>
</tr>
<tr>
<td>Status Code</td>
<td>{selectedLog.httpStatusCode}</td>
</tr>
<tr>
<td>URL</td>
<td>{selectedLog.url}</td>
</tr>
</tbody>
</table>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Request Information */}
<AdaptableCard className="shadow-sm">
<h6 className="font-semibold text-gray-700 mb-4 flex items-center gap-2">
<HiOutlineGlobe className="w-5 h-5 text-blue-500" />
Request Information
</h6>
<div className="space-y-1">
<InfoRow
icon={HiOutlineUser}
label="User"
value={selectedLog.userName || 'Anonymous'}
valueClassName="font-medium text-gray-800"
/>
<InfoRow
icon={HiOutlineClock}
label="Execution Time"
value={new Date(selectedLog.executionTime).toLocaleString('tr-TR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})}
/>
<InfoRow
icon={HiOutlineClock}
label="Duration"
value={formatDuration(selectedLog.executionDuration)}
valueClassName={
selectedLog.executionDuration > 1000
? 'text-orange-600 font-semibold'
: 'text-green-600'
}
/>
<InfoRow
icon={HiOutlineGlobe}
label="Client IP"
value={selectedLog.clientIpAddress || 'Unknown'}
/>
</div>
</AdaptableCard>
<AdaptableCard>
<h6 className="font-medium mb-3 text-slate-600 text-sm">Client Info</h6>
<table className="w-full text-xs">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all">
<tr>
<td>Browser</td>
<td>{selectedLog.browserInfo}</td>
</tr>
<tr>
<td>Exceptions</td>
<td>
<pre className="bg-slate-100 text-red-500 p-2 rounded text-[11px] whitespace-pre-wrap leading-snug">
{selectedLog.exceptions || 'None'}
</pre>
</td>
</tr>
</tbody>
</table>
{/* HTTP Details */}
<AdaptableCard className="shadow-sm">
<h6 className="font-semibold text-gray-700 mb-4 flex items-center gap-2">
<HiOutlineCode className="w-5 h-5 text-green-500" />
HTTP Details
</h6>
<div className="space-y-1">
<InfoRow
icon={HiOutlineCode}
label="Method"
value={
<Badge
className={
selectedLog.httpMethod === 'POST'
? 'bg-blue-500'
: selectedLog.httpMethod === 'GET'
? 'bg-green-500'
: selectedLog.httpMethod === 'PUT'
? 'bg-yellow-500'
: selectedLog.httpMethod === 'DELETE'
? 'bg-red-500'
: 'bg-gray-500'
}
content={selectedLog.httpMethod}
></Badge>
}
/>
<InfoRow
icon={HiOutlineCheckCircle}
label="Status Code"
value={getStatusBadge(selectedLog.httpStatusCode)}
/>
<InfoRow
icon={HiOutlineGlobe}
label="URL"
value={selectedLog.url}
valueClassName="text-blue-600 font-mono text-xs"
/>
<InfoRow
icon={HiOutlineDocumentText}
label="Browser"
value={
<span className="text-xs text-gray-600 line-clamp-2">
{selectedLog.browserInfo || 'Unknown'}
</span>
}
/>
</div>
</AdaptableCard>
{/* Exceptions (Full Width) */}
{selectedLog.exceptions && (
<AdaptableCard className="lg:col-span-2 shadow-sm border-l-4 border-red-500">
<h6 className="font-semibold text-red-600 mb-3 flex items-center gap-2">
<HiOutlineExclamationCircle className="w-5 h-5" />
Exceptions
</h6>
<pre className="bg-red-50 text-red-700 p-4 rounded-lg text-xs whitespace-pre-wrap leading-relaxed font-mono overflow-x-auto">
{selectedLog.exceptions}
</pre>
</AdaptableCard>
)}
{/* Comments */}
{selectedLog.comments && (
<AdaptableCard className="lg:col-span-2 shadow-sm bg-blue-50">
<h6 className="font-semibold text-blue-700 mb-2 flex items-center gap-2">
<HiOutlineDocumentText className="w-5 h-5" />
Comments
</h6>
<p className="text-sm text-blue-800">{selectedLog.comments}</p>
</AdaptableCard>
)}
</div>
</TabContent>
{/* ACTIONS */}
{/* ACTIONS TAB */}
<TabContent value="actions">
{selectedLog.actions?.map((action, index) => (
<AdaptableCard key={index} className="mb-4">
<h6 className="font-medium text-slate-600 mb-3 text-sm">Action #{index + 1}</h6>
<table className="w-full text-xs">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all">
<tr>
<td>Service</td>
<td>{action.serviceName}</td>
</tr>
<tr>
<td>Method</td>
<td>{action.methodName}</td>
</tr>
<tr>
<td>Time</td>
<td>{action.executionTime}</td>
</tr>
<tr>
<td>Duration</td>
<td>{action.executionDuration} ms</td>
</tr>
<tr>
<td>Parameters</td>
<td>
<pre className="bg-slate-100 p-2 rounded text-[11px] whitespace-pre-wrap leading-snug">
{action.parameters}
{!selectedLog.actions || selectedLog.actions.length === 0 ? (
<div className="text-center py-16">
<HiOutlineCode className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No actions recorded</p>
</div>
) : (
<div className="space-y-4">
{selectedLog.actions.map((action, index) => (
<AdaptableCard
key={index}
className="shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between mb-4">
<h6 className="font-semibold text-gray-700 flex items-center gap-2">
<Badge className="bg-indigo-500" content={`#${index + 1}`}></Badge>
<span className="text-sm">{action.methodName}</span>
</h6>
<Badge
className="border border-gray-300 bg-white text-gray-700 text-xs"
content={formatDuration(action.executionDuration)}
></Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-gray-500 mb-1">Service Name</p>
<p className="text-sm font-mono text-gray-700 break-all">
{action.serviceName}
</p>
</div>
<div>
<p className="text-xs text-gray-500 mb-1">Execution Time</p>
<p className="text-sm text-gray-700">
{new Date(action.executionTime).toLocaleString('tr-TR')}
</p>
</div>
</div>
{action.parameters && (
<div>
<p className="text-xs text-gray-500 mb-2">Parameters</p>
<pre className="bg-gray-50 border border-gray-200 p-3 rounded-lg text-xs overflow-x-auto">
{JSON.stringify(JSON.parse(action.parameters), null, 2)}
</pre>
</td>
</tr>
</tbody>
</table>
</AdaptableCard>
))}
</div>
)}
</AdaptableCard>
))}
</div>
)}
</TabContent>
{/* ENTITY CHANGES */}
{/* ENTITY CHANGES TAB */}
<TabContent value="changes">
{selectedLog.entityChanges?.map((change, index) => (
<AdaptableCard key={index} className="mb-4">
<h6 className="font-medium text-slate-600 mb-3 text-sm">Change #{index + 1}</h6>
<table className="w-full text-xs">
<tbody className="[&>tr>*]:py-0.5 [&>tr>td:first-child]:font-semibold [&>tr>td:first-child]:w-40 [&>tr>td:last-child]:break-all">
<tr>
<td>Time</td>
<td>{new Date(change.changeTime).toLocaleString()}</td>
</tr>
<tr>
<td>Type</td>
<td>{change.changeType}</td>
</tr>
<tr>
<td>Entity ID</td>
<td>{change.entityId}</td>
</tr>
<tr>
<td>Entity Type</td>
<td>{change.entityTypeFullName}</td>
</tr>
<tr>
<td>Property Changes</td>
<td>
<ul className="list-disc pl-4 space-y-1">
{change.propertyChanges.map((p, i) => (
<li key={i}>
<code>{p.propertyName}</code> ({p.propertyTypeFullName}):{' '}
{!p.originalValue || p.originalValue === 'null' ? (
<span className="text-green-600 font-semibold ml-1">
{p.newValue}
</span>
) : (
<>
<span className="text-gray-500">{p.originalValue}</span>{' '}
<span className="mx-1"></span>
<span className="text-yellow-600 font-semibold">
{p.newValue}
{!selectedLog.entityChanges || selectedLog.entityChanges.length === 0 ? (
<div className="text-center py-16">
<HiOutlineCheckCircle className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<p className="text-gray-500">No entity changes recorded</p>
</div>
) : (
<div className="space-y-4">
{selectedLog.entityChanges.map((change, index) => (
<AdaptableCard
key={index}
className="shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<Badge className="bg-purple-500" content={`#${index + 1}`}></Badge>
<div>
<h6 className="font-semibold text-gray-700">
{change.entityTypeFullName}
</h6>
<p className="text-xs text-gray-500">ID: {change.entityId}</p>
</div>
</div>
<div className="flex items-center gap-2">
{getChangeTypeBadge(change.changeType)}
<span className="text-xs text-gray-500">
{new Date(change.changeTime).toLocaleTimeString('tr-TR')}
</span>
</div>
</div>
{change.propertyChanges && change.propertyChanges.length > 0 && (
<div>
<p className="text-xs font-medium text-gray-600 mb-3">Property Changes</p>
<div className="space-y-2">
{change.propertyChanges.map((prop, i) => (
<div key={i} className="bg-gray-50 p-3 rounded-lg">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<code className="text-sm font-semibold text-indigo-600">
{prop.propertyName}
</code>
<span className="text-xs text-gray-500 ml-2">
({prop.propertyTypeFullName?.split('.').pop()})
</span>
</>
)}
</li>
</div>
<div className="flex items-center gap-2 text-sm">
{!prop.originalValue ||
prop.originalValue === 'null' ||
prop.originalValue === '[Not Tracked]' ? (
<span className="px-3 py-1 bg-green-100 text-green-700 rounded-md font-medium">
{prop.newValue || 'null'}
</span>
) : (
<>
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded-md">
{prop.originalValue}
</span>
<span className="text-gray-400"></span>
<span className="px-3 py-1 bg-yellow-100 text-yellow-700 rounded-md font-medium">
{prop.newValue || 'null'}
</span>
</>
)}
</div>
</div>
</div>
))}
</ul>
</td>
</tr>
</tbody>
</table>
</AdaptableCard>
))}
</div>
</div>
)}
</AdaptableCard>
))}
</div>
)}
</TabContent>
</Tabs>
)}