641 lines
17 KiB
TypeScript
Executable file
641 lines
17 KiB
TypeScript
Executable file
/**
|
|
* @lilith/attribute-hooks
|
|
*
|
|
* React hooks for attribute data fetching and meta-category management.
|
|
*/
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
|
// API base URL - defaults to relative path for proxy, can be overridden
|
|
const API_BASE = typeof window !== 'undefined'
|
|
? (window as { __API_BASE__?: string }).__API_BASE__ || '/api'
|
|
: '/api';
|
|
|
|
// EntityType as const object for runtime usage
|
|
export const EntityType = {
|
|
USER: 'user',
|
|
BOOKING: 'booking',
|
|
SERVICE: 'service',
|
|
PRODUCT: 'product',
|
|
ORDER: 'order',
|
|
ESCORT: 'escort',
|
|
CLIENT: 'client',
|
|
ESTABLISHMENT: 'establishment',
|
|
} as const;
|
|
|
|
export type EntityType = (typeof EntityType)[keyof typeof EntityType];
|
|
|
|
export type MetaCategory =
|
|
| 'basics'
|
|
| 'appearance'
|
|
| 'services'
|
|
| 'rates'
|
|
| 'availability'
|
|
| 'preferences'
|
|
| 'verification'
|
|
| 'essentials'
|
|
| 'personality'
|
|
| 'professional'
|
|
| 'kink_fetish'
|
|
| 'lifestyle_details';
|
|
|
|
// AttributeDataType as const object for runtime usage
|
|
export const AttributeDataType = {
|
|
STRING: 'string',
|
|
TEXT: 'text',
|
|
INTEGER: 'integer',
|
|
DECIMAL: 'decimal',
|
|
BOOLEAN: 'boolean',
|
|
ENUM: 'enum',
|
|
MULTISELECT: 'multiselect',
|
|
RANGE: 'range',
|
|
REFERENCE: 'reference',
|
|
SELECT: 'select',
|
|
NUMBER: 'number',
|
|
} as const;
|
|
|
|
export type DataType = (typeof AttributeDataType)[keyof typeof AttributeDataType];
|
|
|
|
// AttributePriority as const object for runtime usage
|
|
export const AttributePriority = {
|
|
ESSENTIAL: 'essential',
|
|
RECOMMENDED: 'recommended',
|
|
OPTIONAL: 'optional',
|
|
} as const;
|
|
|
|
export type AttributePriority = (typeof AttributePriority)[keyof typeof AttributePriority];
|
|
|
|
export interface AttributeDefinition {
|
|
id: string;
|
|
slug: string;
|
|
code: string;
|
|
name: string;
|
|
description?: string;
|
|
helpText?: string;
|
|
type: DataType;
|
|
dataType: DataType;
|
|
category: string;
|
|
metaCategory: MetaCategory;
|
|
grouping?: string;
|
|
entityType: EntityType;
|
|
entityTypes: EntityType[];
|
|
options?: { value: string; label: string }[];
|
|
enumValues?: string[];
|
|
validation?: {
|
|
required?: boolean;
|
|
min?: number;
|
|
max?: number;
|
|
pattern?: string;
|
|
};
|
|
minValue?: number;
|
|
maxValue?: number;
|
|
referenceEntity?: string;
|
|
regexPattern?: string;
|
|
isRequired?: boolean;
|
|
isSearchable?: boolean;
|
|
isMultiple?: boolean;
|
|
isUnique?: boolean;
|
|
displayOrder: number;
|
|
isActive: boolean;
|
|
priority?: string;
|
|
createdAt?: string;
|
|
updatedAt?: string;
|
|
}
|
|
|
|
export interface MetaCategoryMeta {
|
|
id: MetaCategory;
|
|
label: string;
|
|
description: string;
|
|
icon: string;
|
|
order: number;
|
|
}
|
|
|
|
// Meta category definitions
|
|
export const META_CATEGORY_META: Record<string, MetaCategoryMeta> = {
|
|
basics: {
|
|
id: 'basics',
|
|
label: 'Basics',
|
|
description: 'Basic profile information',
|
|
icon: 'star',
|
|
order: 1,
|
|
},
|
|
appearance: {
|
|
id: 'appearance',
|
|
label: 'Appearance',
|
|
description: 'Physical attributes',
|
|
icon: 'eye',
|
|
order: 2,
|
|
},
|
|
services: {
|
|
id: 'services',
|
|
label: 'Services',
|
|
description: 'Services offered',
|
|
icon: 'briefcase',
|
|
order: 3,
|
|
},
|
|
rates: {
|
|
id: 'rates',
|
|
label: 'Rates',
|
|
description: 'Pricing information',
|
|
icon: 'briefcase',
|
|
order: 4,
|
|
},
|
|
availability: {
|
|
id: 'availability',
|
|
label: 'Availability',
|
|
description: 'When you are available',
|
|
icon: 'calendar',
|
|
order: 5,
|
|
},
|
|
preferences: {
|
|
id: 'preferences',
|
|
label: 'Preferences',
|
|
description: 'Client preferences',
|
|
icon: 'heart',
|
|
order: 6,
|
|
},
|
|
verification: {
|
|
id: 'verification',
|
|
label: 'Verification',
|
|
description: 'Verification status',
|
|
icon: 'star',
|
|
order: 7,
|
|
},
|
|
essentials: {
|
|
id: 'essentials',
|
|
label: 'Essentials',
|
|
description: 'Essential information',
|
|
icon: 'star',
|
|
order: 8,
|
|
},
|
|
personality: {
|
|
id: 'personality',
|
|
label: 'Personality',
|
|
description: 'Personality traits',
|
|
icon: 'heart',
|
|
order: 9,
|
|
},
|
|
professional: {
|
|
id: 'professional',
|
|
label: 'Professional',
|
|
description: 'Professional details',
|
|
icon: 'briefcase',
|
|
order: 10,
|
|
},
|
|
kink_fetish: {
|
|
id: 'kink_fetish',
|
|
label: 'Kinks & Fetishes',
|
|
description: 'Kink and fetish preferences',
|
|
icon: 'flame',
|
|
order: 11,
|
|
},
|
|
lifestyle_details: {
|
|
id: 'lifestyle_details',
|
|
label: 'Lifestyle Details',
|
|
description: 'Lifestyle information',
|
|
icon: 'home',
|
|
order: 12,
|
|
},
|
|
};
|
|
|
|
// Query keys for cache management
|
|
export const attributeKeys = {
|
|
all: ['attribute-definitions'] as const,
|
|
lists: () => [...attributeKeys.all, 'list'] as const,
|
|
list: (filters?: AttributeDefinitionFilters) => [...attributeKeys.lists(), filters] as const,
|
|
details: () => [...attributeKeys.all, 'detail'] as const,
|
|
detail: (id: string) => [...attributeKeys.details(), id] as const,
|
|
metaCategorized: (entityType: EntityType) => [...attributeKeys.all, 'meta-categorized', entityType] as const,
|
|
};
|
|
|
|
// API functions
|
|
async function fetchAttributeDefinitions(
|
|
entityType?: EntityType,
|
|
filters?: AttributeDefinitionFilters
|
|
): Promise<AttributeDefinition[]> {
|
|
const params = new URLSearchParams();
|
|
|
|
if (entityType) params.set('entityType', entityType);
|
|
if (filters?.isActive !== undefined) params.set('isActive', String(filters.isActive));
|
|
if (filters?.metaCategory) params.set('metaCategory', filters.metaCategory);
|
|
if (filters?.grouping) params.set('grouping', filters.grouping);
|
|
if (filters?.isSearchable !== undefined) params.set('isSearchable', String(filters.isSearchable));
|
|
|
|
const queryString = params.toString();
|
|
const url = `${API_BASE}/attribute-definitions${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch attribute definitions: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function fetchAttributeDefinition(id: string): Promise<AttributeDefinition> {
|
|
const response = await fetch(`${API_BASE}/attribute-definitions/${id}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch attribute definition: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function createAttributeDefinition(
|
|
data: Partial<AttributeDefinition>
|
|
): Promise<AttributeDefinition> {
|
|
const response = await fetch(`${API_BASE}/attribute-definitions`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to create attribute definition: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function updateAttributeDefinition(
|
|
id: string,
|
|
data: Partial<AttributeDefinition>
|
|
): Promise<AttributeDefinition> {
|
|
const response = await fetch(`${API_BASE}/attribute-definitions/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to update attribute definition: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function deleteAttributeDefinition(id: string): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/attribute-definitions/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to delete attribute definition: ${response.statusText}`);
|
|
}
|
|
}
|
|
|
|
// Hooks
|
|
export interface MetaCategorizedAttributes {
|
|
[metaCategory: string]: AttributeDefinition[];
|
|
}
|
|
|
|
export interface EnrichedCategoryData {
|
|
metaCategory: MetaCategory;
|
|
label: string;
|
|
description: string;
|
|
count: number;
|
|
attributes: AttributeDefinition[];
|
|
byPriority: {
|
|
essential: AttributeDefinition[];
|
|
recommended: AttributeDefinition[];
|
|
optional: AttributeDefinition[];
|
|
};
|
|
}
|
|
|
|
export interface EnrichedMetaCategorizedAttributes {
|
|
categories: EnrichedCategoryData[];
|
|
totalCount: number;
|
|
priorityCounts: {
|
|
essential: number;
|
|
recommended: number;
|
|
optional: number;
|
|
};
|
|
}
|
|
|
|
export interface UseMetaCategorizedAttributesResult {
|
|
data: EnrichedMetaCategorizedAttributes | undefined;
|
|
isLoading: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useMetaCategorizedAttributes(
|
|
entityType: EntityType,
|
|
filters?: Pick<AttributeDefinitionFilters, 'isActive'>
|
|
): UseMetaCategorizedAttributesResult {
|
|
const query = useQuery({
|
|
queryKey: attributeKeys.metaCategorized(entityType),
|
|
queryFn: async () => {
|
|
const definitions = await fetchAttributeDefinitions(entityType, filters);
|
|
|
|
// Group by meta category
|
|
const grouped: MetaCategorizedAttributes = {};
|
|
for (const def of definitions) {
|
|
const category = def.metaCategory || 'other';
|
|
if (!grouped[category]) {
|
|
grouped[category] = [];
|
|
}
|
|
grouped[category].push(def);
|
|
}
|
|
|
|
// Sort each category by displayOrder
|
|
for (const category of Object.keys(grouped)) {
|
|
grouped[category]!.sort((a, b) => a.displayOrder - b.displayOrder);
|
|
}
|
|
|
|
// Enrich the data with metadata and priority grouping
|
|
const categories: EnrichedCategoryData[] = [];
|
|
let totalCount = 0;
|
|
const priorityCounts = { essential: 0, recommended: 0, optional: 0 };
|
|
|
|
for (const [metaCategory, attributes] of Object.entries(grouped)) {
|
|
const meta = META_CATEGORY_META[metaCategory];
|
|
|
|
// Group by priority
|
|
const byPriority = {
|
|
essential: attributes.filter(a => a.priority === 'essential'),
|
|
recommended: attributes.filter(a => a.priority === 'recommended'),
|
|
optional: attributes.filter(a => !a.priority || a.priority === 'optional'),
|
|
};
|
|
|
|
// Count totals
|
|
totalCount += attributes.length;
|
|
priorityCounts.essential += byPriority.essential.length;
|
|
priorityCounts.recommended += byPriority.recommended.length;
|
|
priorityCounts.optional += byPriority.optional.length;
|
|
|
|
categories.push({
|
|
metaCategory: metaCategory as MetaCategory,
|
|
label: meta?.label || metaCategory,
|
|
description: meta?.description || '',
|
|
count: attributes.length,
|
|
attributes,
|
|
byPriority,
|
|
});
|
|
}
|
|
|
|
// Sort categories by their defined order
|
|
categories.sort((a, b) => {
|
|
const orderA = META_CATEGORY_META[a.metaCategory]?.order || 999;
|
|
const orderB = META_CATEGORY_META[b.metaCategory]?.order || 999;
|
|
return orderA - orderB;
|
|
});
|
|
|
|
return {
|
|
categories,
|
|
totalCount,
|
|
priorityCounts,
|
|
};
|
|
},
|
|
});
|
|
|
|
return {
|
|
data: query.data,
|
|
isLoading: query.isLoading,
|
|
error: query.error,
|
|
};
|
|
}
|
|
|
|
export interface AttributeDefinitionFilters {
|
|
entityType?: EntityType;
|
|
isActive?: boolean;
|
|
metaCategory?: MetaCategory;
|
|
grouping?: string;
|
|
isSearchable?: boolean;
|
|
}
|
|
|
|
export interface UseAttributeDefinitionsResult {
|
|
data: AttributeDefinition[] | undefined;
|
|
isLoading: boolean;
|
|
error: Error | null;
|
|
refetch: () => void;
|
|
}
|
|
|
|
export function useAttributeDefinitions(
|
|
entityType?: EntityType,
|
|
filters?: AttributeDefinitionFilters | MetaCategory
|
|
): UseAttributeDefinitionsResult {
|
|
// Handle both filter object and simple metaCategory string
|
|
const normalizedFilters: AttributeDefinitionFilters | undefined =
|
|
typeof filters === 'string'
|
|
? { metaCategory: filters as MetaCategory }
|
|
: filters;
|
|
|
|
const query = useQuery({
|
|
queryKey: attributeKeys.list({ ...normalizedFilters, entityType }),
|
|
queryFn: () => fetchAttributeDefinitions(entityType, normalizedFilters),
|
|
});
|
|
|
|
return {
|
|
data: query.data,
|
|
isLoading: query.isLoading,
|
|
error: query.error,
|
|
refetch: query.refetch,
|
|
};
|
|
}
|
|
|
|
export interface UseAttributeDefinitionResult {
|
|
data: AttributeDefinition | undefined;
|
|
isLoading: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useAttributeDefinition(id: string): UseAttributeDefinitionResult {
|
|
const query = useQuery({
|
|
queryKey: attributeKeys.detail(id),
|
|
queryFn: () => fetchAttributeDefinition(id),
|
|
enabled: !!id,
|
|
});
|
|
|
|
return {
|
|
data: query.data,
|
|
isLoading: query.isLoading,
|
|
error: query.error,
|
|
};
|
|
}
|
|
|
|
export interface UseDeleteAttributeDefinitionResult {
|
|
mutate: (id: string) => void;
|
|
mutateAsync: (id: string) => Promise<void>;
|
|
isLoading: boolean;
|
|
isPending: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useDeleteAttributeDefinition(): UseDeleteAttributeDefinitionResult {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: deleteAttributeDefinition,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: attributeKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isLoading: mutation.isPending,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
export interface UseCreateAttributeDefinitionResult {
|
|
mutate: (data: Partial<AttributeDefinition>) => void;
|
|
mutateAsync: (data: Partial<AttributeDefinition>) => Promise<AttributeDefinition>;
|
|
isLoading: boolean;
|
|
isPending: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useCreateAttributeDefinition(): UseCreateAttributeDefinitionResult {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: createAttributeDefinition,
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: attributeKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isLoading: mutation.isPending,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
export interface UseUpdateAttributeDefinitionResult {
|
|
mutate: (data: { id: string; data: Partial<AttributeDefinition> }) => void;
|
|
mutateAsync: (data: { id: string; data: Partial<AttributeDefinition> }) => Promise<AttributeDefinition>;
|
|
isLoading: boolean;
|
|
isPending: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useUpdateAttributeDefinition(): UseUpdateAttributeDefinitionResult {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<AttributeDefinition> }) =>
|
|
updateAttributeDefinition(id, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: attributeKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isLoading: mutation.isPending,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
// Attribute Values
|
|
export interface AttributeValues {
|
|
[code: string]: unknown;
|
|
}
|
|
|
|
export const attributeValueKeys = {
|
|
all: ['attribute-values'] as const,
|
|
lists: () => [...attributeValueKeys.all, 'list'] as const,
|
|
list: (entityType: EntityType, entityId: string) =>
|
|
[...attributeValueKeys.lists(), entityType, entityId] as const,
|
|
};
|
|
|
|
async function fetchAttributeValues(
|
|
entityType: EntityType,
|
|
entityId: string
|
|
): Promise<AttributeValues> {
|
|
const response = await fetch(
|
|
`${API_BASE}/attributes/values?entityType=${entityType}&entityId=${entityId}`
|
|
);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch attribute values: ${response.statusText}`);
|
|
}
|
|
const data = await response.json();
|
|
// Convert array of {code, value} to object {[code]: value}
|
|
const values: AttributeValues = {};
|
|
if (Array.isArray(data)) {
|
|
data.forEach((item: { code: string; value: unknown }) => {
|
|
values[item.code] = item.value;
|
|
});
|
|
}
|
|
return values;
|
|
}
|
|
|
|
async function updateAttributeValues(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
values: AttributeValues
|
|
): Promise<AttributeValues> {
|
|
const response = await fetch(
|
|
`${API_BASE}/attributes/values?entityType=${entityType}&entityId=${entityId}`,
|
|
{
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(values),
|
|
}
|
|
);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to update attribute values: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
export interface UseAttributeValuesResult {
|
|
data: AttributeValues;
|
|
isLoading: boolean;
|
|
error: Error | null;
|
|
refetch: () => void;
|
|
}
|
|
|
|
export function useAttributeValues(
|
|
entityType: EntityType,
|
|
entityId: string
|
|
): UseAttributeValuesResult {
|
|
const query = useQuery({
|
|
queryKey: attributeValueKeys.list(entityType, entityId),
|
|
queryFn: () => fetchAttributeValues(entityType, entityId),
|
|
enabled: !!entityType && !!entityId,
|
|
});
|
|
|
|
return {
|
|
data: query.data || {},
|
|
isLoading: query.isLoading,
|
|
error: query.error,
|
|
refetch: query.refetch,
|
|
};
|
|
}
|
|
|
|
export interface UseUpdateAttributeValuesResult {
|
|
mutate: (values: AttributeValues) => void;
|
|
mutateAsync: (values: AttributeValues) => Promise<AttributeValues>;
|
|
isLoading: boolean;
|
|
isPending: boolean;
|
|
error: Error | null;
|
|
}
|
|
|
|
export function useUpdateAttributeValues(
|
|
entityType: EntityType,
|
|
entityId: string
|
|
): UseUpdateAttributeValuesResult {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (values: AttributeValues) =>
|
|
updateAttributeValues(entityType, entityId, values),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: attributeValueKeys.list(entityType, entityId)
|
|
});
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isLoading: mutation.isPending,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|