fix: restore Velocity-OS pillar data contracts

This commit is contained in:
2026-05-01 18:43:38 +05:30
parent 55d0c3a8c4
commit 103900c58a
3 changed files with 190 additions and 6 deletions

View File

@@ -1,26 +1,27 @@
/* ControlRoom */
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-5) var(--space-8); border-bottom: var(--glass-border); flex-shrink: 0; }
.root { display: flex; flex-direction: column; min-height: 100vh; height: 100vh; overflow: hidden; background: radial-gradient(circle at 16% 10%, rgba(124,58,237,0.22), transparent 34%), radial-gradient(circle at 76% 12%, rgba(59,130,246,0.14), transparent 30%), var(--color-bg-primary); color: var(--color-text-primary); }
.header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-5) var(--space-8); border-bottom: var(--glass-border); background: rgba(9, 13, 24, 0.72); backdrop-filter: blur(22px); flex-shrink: 0; }
.headerLeft { display: flex; align-items: center; gap: var(--space-4); }
.headerIcon { font-size: 28px; color: var(--color-text-tertiary); }
.title { font-size: var(--text-xl); font-weight: var(--font-bold); color: var(--color-text-primary); margin: 0; }
.subtitle { font-size: var(--text-xs); color: var(--color-text-tertiary); margin: 0; }
.body { display: flex; flex: 1; overflow: hidden; }
/* Sidebar */
.sidebar { width: 200px; border-right: var(--glass-border); display: flex; flex-direction: column; padding: var(--space-4); gap: var(--space-1); flex-shrink: 0; }
.sidebar { width: 220px; border-right: var(--glass-border); display: flex; flex-direction: column; padding: var(--space-4); gap: var(--space-1); flex-shrink: 0; background: rgba(7, 11, 20, 0.62); }
.sideItem { position: relative; display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3); border-radius: var(--radius-md); background: none; border: none; cursor: pointer; font-family: var(--font-sans); font-size: var(--text-sm); color: var(--color-text-secondary); text-align: left; transition: all var(--duration-fast) var(--ease-standard); width: 100%; }
.sideItem:hover { background: var(--glass-bg); color: var(--color-text-primary); }
.sideActive { color: var(--color-text-primary) !important; background: var(--glass-bg); }
.sideIcon { font-size: 14px; flex-shrink: 0; }
.indicator { position: absolute; left: 0; top: 20%; bottom: 20%; width: 2px; background: var(--color-violet); border-radius: 1px; }
/* Content */
.content { flex: 1; overflow-y: auto; padding: var(--space-8); }
.content { flex: 1; overflow-y: auto; padding: var(--space-8); background: linear-gradient(135deg, rgba(15,23,42,0.48), rgba(2,6,23,0.88)); }
.panelWrap { display: flex; flex-direction: column; gap: 0; }
/* Panel */
.panel { display: flex; flex-direction: column; gap: var(--space-6); max-width: 800px; }
.panelTitle { font-size: var(--text-xl); font-weight: var(--font-bold); color: var(--color-text-primary); margin: 0; }
.panelSubtitle { font-size: var(--text-sm); color: var(--color-text-secondary); margin: -var(--space-4) 0 0; }
.subTitle { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text-secondary); margin: 0; }
.muted { font-size: var(--text-sm); color: var(--color-text-secondary); margin: 0; }
.adminSection { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-5); background: var(--glass-bg); border: var(--glass-border); border-radius: var(--radius-lg); }
/* Service grid */
.serviceGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: var(--space-3); }

View File

@@ -7,7 +7,15 @@ import { api } from '@/shared/lib/apiClient';
export function useStudioProperties() {
const query = useQuery({
queryKey: ['studio-properties'],
queryFn: () => api.get<StudioProperty[]>('/inventory/properties'),
queryFn: async () => {
const response = await api.get<InventoryPropertiesResponse | StudioProperty[]>('/inventory/properties');
const rawProperties = Array.isArray(response)
? response
: Array.isArray(response.properties)
? response.properties
: [];
return rawProperties.map(mapInventoryProperty);
},
staleTime: 120_000,
});
return { properties: query.data ?? [], isLoading: query.isLoading };
@@ -19,7 +27,9 @@ export function useStudioProperties() {
export function useProperty(propertyId: string) {
const query = useQuery({
queryKey: ['property', propertyId],
queryFn: () => api.get<PropertyDetail>(`/inventory/properties/${propertyId}`),
queryFn: async () => mapInventoryPropertyDetail(
await api.get<InventoryPropertyRecord>(`/inventory/properties/${propertyId}`)
),
staleTime: 120_000,
enabled: !!propertyId,
});
@@ -49,3 +59,82 @@ export interface PropertyDetail {
images?: string[];
amenities?: string[];
}
interface InventoryPropertiesResponse {
properties?: InventoryPropertyRecord[];
}
interface InventoryPropertyRecord {
property_id?: string;
id?: string;
project_name?: string;
name?: string;
developer_name?: string;
property_type?: string;
location?: {
city?: string;
district?: string;
area?: string;
address?: string;
};
price_bands?: Array<{ label?: string; min?: number; max?: number; currency?: string }>;
unit_mix?: Array<{ configuration?: string; count?: number; available?: number; area?: string; price?: string }>;
amenities?: string[];
media?: Array<{ url?: string; thumbnail_url?: string }>;
images?: string[];
thumbnailUrl?: string;
availableUnits?: number;
}
function mapLocation(location: InventoryPropertyRecord['location']): string {
if (!location) return 'Location pending';
return [location.district, location.area, location.city, location.address].filter(Boolean).join(', ') || 'Location pending';
}
function mapPriceRange(priceBands: InventoryPropertyRecord['price_bands']): string | undefined {
if (!Array.isArray(priceBands) || priceBands.length === 0) return undefined;
const mins = priceBands.map((band) => Number(band.min)).filter(Number.isFinite);
const maxes = priceBands.map((band) => Number(band.max)).filter(Number.isFinite);
if (!mins.length && !maxes.length) return priceBands[0]?.label;
const currency = priceBands[0]?.currency ?? 'INR';
const min = mins.length ? Math.min(...mins) : undefined;
const max = maxes.length ? Math.max(...maxes) : undefined;
if (min !== undefined && max !== undefined) return `${currency} ${min} - ${max}`;
if (min !== undefined) return `From ${currency} ${min}`;
return `Up to ${currency} ${max}`;
}
function mapInventoryProperty(record: InventoryPropertyRecord): StudioProperty {
const mediaThumb = Array.isArray(record.media) ? record.media.find((item) => item.thumbnail_url || item.url) : undefined;
const availableUnits = Array.isArray(record.unit_mix)
? record.unit_mix.reduce((sum, unit) => sum + Number(unit.available ?? unit.count ?? 0), 0)
: record.availableUnits;
const stableId = record.property_id ?? record.id ?? record.project_name ?? record.name ?? 'unknown-property';
return {
id: String(stableId),
name: record.project_name ?? record.name ?? 'Unnamed property',
location: mapLocation(record.location),
priceRange: mapPriceRange(record.price_bands),
thumbnailUrl: record.thumbnailUrl ?? mediaThumb?.thumbnail_url ?? mediaThumb?.url,
availableUnits,
};
}
function mapInventoryPropertyDetail(record: InventoryPropertyRecord): PropertyDetail {
const base = mapInventoryProperty(record);
const primaryUnit = Array.isArray(record.unit_mix) ? record.unit_mix[0] : undefined;
const images = Array.isArray(record.images)
? record.images
: Array.isArray(record.media)
? record.media.map((item) => item.url).filter((url): url is string => Boolean(url))
: [];
return {
...base,
config: primaryUnit?.configuration ?? record.property_type ?? 'Mixed configuration',
area: primaryUnit?.area ?? 'Area pending',
price: primaryUnit?.price ?? base.priceRange ?? 'Price pending',
description: `${record.developer_name ?? 'Developer'} property in ${base.location}`,
images,
amenities: Array.isArray(record.amenities) ? record.amenities : [],
};
}