223 lines
7.6 KiB
TypeScript
223 lines
7.6 KiB
TypeScript
import { useState } from 'react';
|
|
import apiClient from '@app/services/apiClient';
|
|
import { mergePendingSettings, isFieldPending, hasPendingChanges } from '@app/utils/settingsPendingHelper';
|
|
|
|
interface UseAdminSettingsOptions<T> {
|
|
sectionName: string;
|
|
/**
|
|
* Optional transformer to combine data from multiple endpoints.
|
|
* If not provided, uses the section response directly.
|
|
*/
|
|
fetchTransformer?: () => Promise<T>;
|
|
/**
|
|
* Optional transformer to split settings before saving.
|
|
* Returns an object with sectionData and optionally deltaSettings.
|
|
*/
|
|
saveTransformer?: (settings: T) => {
|
|
sectionData: any;
|
|
deltaSettings?: Record<string, any>;
|
|
};
|
|
}
|
|
|
|
interface UseAdminSettingsReturn<T> {
|
|
settings: T;
|
|
rawSettings: any;
|
|
loading: boolean;
|
|
saving: boolean;
|
|
setSettings: (settings: T) => void;
|
|
fetchSettings: () => Promise<void>;
|
|
saveSettings: () => Promise<void>;
|
|
isFieldPending: (fieldPath: string) => boolean;
|
|
hasPendingChanges: () => boolean;
|
|
}
|
|
|
|
/**
|
|
* Hook for managing admin settings with automatic pending changes support.
|
|
* Includes delta detection to only send changed fields.
|
|
*
|
|
* @example
|
|
* const { settings, setSettings, saveSettings, isFieldPending } = useAdminSettings({
|
|
* sectionName: 'legal'
|
|
* });
|
|
*/
|
|
export function useAdminSettings<T = any>(
|
|
options: UseAdminSettingsOptions<T>
|
|
): UseAdminSettingsReturn<T> {
|
|
const { sectionName, fetchTransformer, saveTransformer } = options;
|
|
|
|
const [settings, setSettings] = useState<T>({} as T);
|
|
const [rawSettings, setRawSettings] = useState<any>(null);
|
|
const [originalSettings, setOriginalSettings] = useState<T>({} as T); // Track original active values
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const fetchSettings = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
let rawData: any;
|
|
|
|
if (fetchTransformer) {
|
|
// Use custom fetch logic for complex sections
|
|
rawData = await fetchTransformer();
|
|
} else {
|
|
// Simple single-endpoint fetch
|
|
const response = await apiClient.get(`/api/v1/admin/settings/section/${sectionName}`);
|
|
rawData = response.data || {};
|
|
}
|
|
|
|
console.log(`[useAdminSettings:${sectionName}] Raw response:`, JSON.stringify(rawData, null, 2));
|
|
|
|
// Store raw settings (includes _pending if present)
|
|
setRawSettings(rawData);
|
|
|
|
// Merge pending changes into settings for display
|
|
const mergedSettings = mergePendingSettings(rawData);
|
|
console.log(`[useAdminSettings:${sectionName}] Merged settings:`, JSON.stringify(mergedSettings, null, 2));
|
|
|
|
// Store merged settings as original for delta comparison
|
|
// This ensures we compare against what the user SAW (with pending), not raw active values
|
|
setOriginalSettings(mergedSettings as T);
|
|
console.log(`[useAdminSettings:${sectionName}] Original settings (for comparison):`, JSON.stringify(mergedSettings, null, 2));
|
|
|
|
setSettings(mergedSettings as T);
|
|
} catch (error) {
|
|
console.error(`[useAdminSettings:${sectionName}] Failed to fetch:`, error);
|
|
throw error;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const saveSettings = async () => {
|
|
try {
|
|
setSaving(true);
|
|
|
|
// Compute delta: only include fields that changed from original
|
|
const delta = computeDelta(originalSettings, settings);
|
|
console.log(`[useAdminSettings:${sectionName}] Delta (changed fields):`, JSON.stringify(delta, null, 2));
|
|
|
|
if (Object.keys(delta).length === 0) {
|
|
console.log(`[useAdminSettings:${sectionName}] No changes detected, skipping save`);
|
|
return;
|
|
}
|
|
|
|
if (saveTransformer) {
|
|
// Use custom save logic for complex sections
|
|
const { sectionData, deltaSettings } = saveTransformer(settings);
|
|
|
|
// Get original sectionData using same transformer for fair comparison
|
|
const { sectionData: originalSectionData } = saveTransformer(originalSettings);
|
|
|
|
// Save section data (with delta applied) - compare transformed vs transformed
|
|
const sectionDelta = computeDelta(originalSectionData, sectionData);
|
|
if (Object.keys(sectionDelta).length > 0) {
|
|
await apiClient.put(`/api/v1/admin/settings/section/${sectionName}`, sectionDelta);
|
|
}
|
|
|
|
// Save delta settings if provided (filter to only changed values)
|
|
if (deltaSettings && Object.keys(deltaSettings).length > 0) {
|
|
// Build deltaSettings from original using same transformer to get correct structure
|
|
const { deltaSettings: originalDeltaSettings } = saveTransformer(originalSettings);
|
|
|
|
console.log(`[useAdminSettings:${sectionName}] Comparing deltaSettings:`, {
|
|
original: originalDeltaSettings,
|
|
current: deltaSettings
|
|
});
|
|
|
|
// Compare current vs original deltaSettings (both have same backend paths)
|
|
const changedDeltaSettings: Record<string, any> = {};
|
|
for (const [key, value] of Object.entries(deltaSettings)) {
|
|
const originalValue = originalDeltaSettings?.[key];
|
|
|
|
// Only include if value actually changed
|
|
if (JSON.stringify(value) !== JSON.stringify(originalValue)) {
|
|
changedDeltaSettings[key] = value;
|
|
console.log(`[useAdminSettings:${sectionName}] Delta field changed: ${key}`, {
|
|
original: originalValue,
|
|
new: value
|
|
});
|
|
}
|
|
}
|
|
|
|
if (Object.keys(changedDeltaSettings).length > 0) {
|
|
console.log(`[useAdminSettings:${sectionName}] Sending delta settings:`, changedDeltaSettings);
|
|
await apiClient.put('/api/v1/admin/settings', { settings: changedDeltaSettings });
|
|
} else {
|
|
console.log(`[useAdminSettings:${sectionName}] No delta settings changed, skipping`);
|
|
}
|
|
}
|
|
} else {
|
|
// Simple single-endpoint save with delta
|
|
await apiClient.put(`/api/v1/admin/settings/section/${sectionName}`, delta);
|
|
}
|
|
|
|
// Refetch to get updated _pending block
|
|
await fetchSettings();
|
|
} catch (error) {
|
|
console.error(`[useAdminSettings:${sectionName}] Failed to save:`, error);
|
|
throw error;
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
settings,
|
|
rawSettings,
|
|
loading,
|
|
saving,
|
|
setSettings,
|
|
fetchSettings,
|
|
saveSettings,
|
|
isFieldPending: (fieldPath: string) => isFieldPending(rawSettings, fieldPath),
|
|
hasPendingChanges: () => hasPendingChanges(rawSettings),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compute delta between original and current settings.
|
|
* Returns only fields that have changed.
|
|
*/
|
|
function computeDelta(original: any, current: any): any {
|
|
const delta: any = {};
|
|
|
|
for (const key in current) {
|
|
if (!Object.prototype.hasOwnProperty.call(current, key)) continue;
|
|
|
|
const originalValue = original[key];
|
|
const currentValue = current[key];
|
|
|
|
// Handle nested objects
|
|
if (isPlainObject(currentValue) && isPlainObject(originalValue)) {
|
|
const nestedDelta = computeDelta(originalValue, currentValue);
|
|
if (Object.keys(nestedDelta).length > 0) {
|
|
delta[key] = nestedDelta;
|
|
}
|
|
}
|
|
// Handle arrays
|
|
else if (Array.isArray(currentValue) && Array.isArray(originalValue)) {
|
|
if (JSON.stringify(currentValue) !== JSON.stringify(originalValue)) {
|
|
delta[key] = currentValue;
|
|
}
|
|
}
|
|
// Handle primitives
|
|
else if (currentValue !== originalValue) {
|
|
delta[key] = currentValue;
|
|
}
|
|
}
|
|
|
|
return delta;
|
|
}
|
|
|
|
/**
|
|
* Check if value is a plain object (not array, not null, not Date, etc.)
|
|
*/
|
|
function isPlainObject(value: any): boolean {
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
value.constructor === Object
|
|
);
|
|
}
|