@burdenoff/microfe-housingvista 2026.1030.1 → 2026.1031.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/housingvista/hooks/platform.d.ts.map +1 -1
- package/dist/housingvista/hooks/platform.js +1 -1
- package/dist/housingvista/hooks/platform.js.map +1 -1
- package/dist/housingvista/pages/finance/HomeLoanDetailPage.d.ts.map +1 -1
- package/dist/housingvista/pages/finance/HomeLoanDetailPage.js +1 -1
- package/dist/housingvista/pages/finance/HomeLoanDetailPage.js.map +1 -1
- package/dist/housingvista/pages/legal/LegalServiceDetailPage.d.ts.map +1 -1
- package/dist/housingvista/pages/legal/LegalServiceDetailPage.js +1 -1
- package/dist/housingvista/pages/legal/LegalServiceDetailPage.js.map +1 -1
- package/dist/housingvista/pages/marketplace/ServiceDetailPage.d.ts.map +1 -1
- package/dist/housingvista/pages/marketplace/ServiceDetailPage.js +1 -1
- package/dist/housingvista/pages/marketplace/ServiceDetailPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../../src/housingvista/hooks/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AA2EH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0FAA0F;AAC1F,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,mBAAmB,EAAE,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AASD,UAAU,kBAAkB;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,YAAY,EAAE;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,EAAE;YACR,KAAK,EAAE;gBACL,IAAI,EAAE;oBACJ,EAAE,EAAE,MAAM,CAAC;oBACX,QAAQ,EAAE,MAAM,CAAC;oBACjB,UAAU,EAAE,MAAM,CAAC;oBACnB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;oBACnC,SAAS,EAAE,MAAM,CAAC;oBAClB,WAAW,EAAE,mBAAmB,EAAE,CAAC;iBACpC,CAAC;aACH,EAAE,CAAC;SACL,CAAC;KACH,GAAG,IAAI,CAAC;CACV;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;+
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../../src/housingvista/hooks/platform.ts"],"names":[],"mappings":"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"}
|
|
@@ -60,7 +60,7 @@ var a = n`
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
`, l = () => {
|
|
63
|
-
let { workspaceId: n } = e(), [i, { loading: o }] = r(a
|
|
63
|
+
let { workspaceId: n } = e(), [i, { loading: o }] = r(a);
|
|
64
64
|
return {
|
|
65
65
|
openConversation: t(async (e) => {
|
|
66
66
|
let t = `housingvista:${e.entityType}:${e.entityId}`, r = await i({ variables: { input: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.js","names":[],"sources":["../../../src/housingvista/hooks/platform.ts"],"sourcesContent":["/**\n * Platform-service reuse hooks for HousingVista.\n *\n * These call *other* wspace subgraphs through the SAME Apollo client / wspace\n * gateway the MFE already uses — cross-SERVICE GraphQL (allowed), never a\n * cross-MFE import (forbidden):\n *\n * - `wspace-conversations-svc` — direct messaging (Contact / Message / Apply\n * CTAs) via `resolveContextConversation` + `sendMessage` (and\n * `sendContextMessage` when a message carries file attachments), and\n * per-listing Q&A / reviews via `getEntityComments` + `createEntityComment`.\n * - `wspace-files-svc` — real uploads via the presigned `initiateUpload` →\n * PUT → `completeUpload` flow, returning a durable File id + download URL.\n *\n * No `any`, no mock data. `workspaceId` is read from the HousingVista context.\n * @module housingvista\n */\n\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { useCallback } from 'react';\nimport { useHousingVista } from '../context/HousingVistaContext';\n\n// ---------------------------------------------------------------------------\n// Conversations — direct messaging\n// ---------------------------------------------------------------------------\n\nconst RESOLVE_CONTEXT_CONVERSATION = gql`\n mutation HvResolveContextConversation($input: ResolveContextConversationInput!) {\n resolveContextConversation(input: $input) {\n created\n conversation {\n id\n title\n }\n }\n }\n`;\n\nconst SEND_MESSAGE = gql`\n mutation HvSendMessage($input: SendMessageInput!) {\n sendMessage(input: $input) {\n success\n message {\n id\n createdAt\n }\n }\n }\n`;\n\nconst SEND_CONTEXT_MESSAGE = gql`\n mutation HvSendContextMessage($input: SendContextConversationMessageInput!) {\n sendContextMessage(input: $input) {\n success\n message {\n id\n createdAt\n }\n }\n }\n`;\n\nconst GET_CONVERSATION_MESSAGES = gql`\n query HvConversationMessages($id: ID!, $first: Int) {\n conversation(id: $id) {\n id\n title\n messages(first: $first) {\n edges {\n node {\n id\n authorId\n authorName\n content\n createdAt\n attachments {\n id\n fileId\n fileName\n fileSize\n mimeType\n downloadUrl\n }\n }\n }\n }\n }\n }\n`;\n\nexport interface HvContextTarget {\n /** Stable identity of the thing being discussed, e.g. a property id. */\n entityId: string;\n /** e.g. `housingvista-property`, `housingvista-marketplace-service`. */\n entityType: string;\n /** Human title used when a conversation is first created. */\n title: string;\n /** Deep link back into the MFE surface, e.g. `/housingvista/properties/:id`. */\n contextHref: string;\n /** Router path portion of `contextHref`. */\n pagePath: string;\n}\n\n/** A file attached to a message (`Message.attachments` on the conversations subgraph). */\nexport interface HvMessageAttachment {\n id: string;\n fileId: string;\n fileName: string;\n fileSize: number;\n mimeType: string;\n /** Null when the attachment row carries no resolvable download metadata. */\n downloadUrl: string | null;\n}\n\nexport interface HvConversationMessage {\n id: string;\n authorId: string;\n /** Resolved display name of the sender (`Message.authorName`). */\n authorName: string;\n /** Conversations stores content as JSON; the plain text is under `.text`. */\n text: string;\n createdAt: string;\n attachments: HvMessageAttachment[];\n}\n\n/**\n * The `sourceContext` a HousingVista thread was created with, read back off the\n * conversation's persisted `settings.metadata`. Present only when every field\n * `SendContextConversationMessageInput` requires survived; callers must treat a\n * `null` as \"this thread cannot take the context-message path\".\n */\nexport interface HvConversationSourceContext {\n product: string;\n threadKey: string;\n widgetKey: string;\n contextHref: string;\n pagePath: string;\n contextTitle?: string;\n entityId?: string;\n entityType?: string;\n}\n\ninterface ResolveContextConversationResult {\n resolveContextConversation: {\n created: boolean;\n conversation: { id: string; title: string | null };\n };\n}\n\ninterface MessageContentJson {\n text?: string;\n}\n\ninterface ConversationMessagesResult {\n conversation: {\n id: string;\n title: string | null;\n messages: {\n edges: {\n node: {\n id: string;\n authorId: string;\n authorName: string;\n content: MessageContentJson | null;\n createdAt: string;\n attachments: HvMessageAttachment[];\n };\n }[];\n };\n } | null;\n}\n\n/**\n * Resolve (get-or-create) the conversation attached to a HousingVista entity,\n * then return its id. Idempotent per (workspace, entityType, entityId) via the\n * derived `threadKey`, so repeated \"Contact\" clicks reuse one thread.\n */\nexport const useContactConversation = () => {\n const { workspaceId } = useHousingVista();\n // Get-or-create changes nothing a page displays, so skip the client's refetch\n // of every active query after a successful mutation.\n const [resolve, { loading }] = useMutation<ResolveContextConversationResult>(RESOLVE_CONTEXT_CONVERSATION, {\n context: { skipMutationAutoRefresh: true },\n });\n\n const openConversation = useCallback(\n async (target: HvContextTarget): Promise<{ id: string; created: boolean }> => {\n const threadKey = `housingvista:${target.entityType}:${target.entityId}`;\n const res = await resolve({\n variables: {\n input: {\n workspaceId,\n title: target.title,\n sourceContext: {\n product: 'housingvista',\n entityId: target.entityId,\n entityType: target.entityType,\n threadKey,\n widgetKey: 'housingvista-contact',\n contextHref: target.contextHref,\n pagePath: target.pagePath,\n contextTitle: target.title,\n },\n },\n },\n });\n const conversation = res.data?.resolveContextConversation.conversation;\n if (!conversation) throw new Error('Could not open the conversation');\n return { id: conversation.id, created: res.data?.resolveContextConversation.created ?? false };\n },\n [resolve, workspaceId]\n );\n\n return { openConversation, loading };\n};\n\nexport const useSendMessage = () => {\n const [send, { loading }] = useMutation<{ sendMessage: { success: boolean } }>(SEND_MESSAGE);\n const sendMessage = useCallback(\n async (conversationId: string, text: string): Promise<void> => {\n const res = await send({\n variables: { input: { conversationId, content: { text } } },\n refetchQueries: ['HvConversationMessages'],\n awaitRefetchQueries: true,\n });\n if (!res.data?.sendMessage.success) throw new Error('Message was not sent');\n },\n [send]\n );\n return { sendMessage, loading };\n};\n\n/**\n * Send a message WITH file attachments.\n *\n * `sendMessage`'s `attachmentIds` only stores bare file ids — the subgraph's\n * `Attachment.fileName`/`downloadUrl` resolvers read their values out of the\n * attachment row's metadata, so an id-only attachment comes back rendering its\n * own uuid with no download link. `sendContextMessage` is the mutation that\n * accepts full `ContextMessageAttachmentInput` details, so it is the only path\n * that yields a readable attachment. It resolves the conversation from the\n * thread's `sourceContext` (idempotent on `threadKey`), which is why the caller\n * must hand back the SAME sourceContext the thread was created with.\n */\nexport const useSendContextMessage = () => {\n const { workspaceId } = useHousingVista();\n const [send, { loading }] = useMutation<{ sendContextMessage: { success: boolean } }>(SEND_CONTEXT_MESSAGE);\n\n const sendContextMessage = useCallback(\n async (args: {\n sourceContext: HvConversationSourceContext;\n text: string;\n attachments: HvUploadedFile[];\n }): Promise<void> => {\n const res = await send({\n variables: {\n input: {\n workspaceId,\n sourceContext: args.sourceContext,\n content: { text: args.text },\n type: args.attachments.length > 0 ? 'FILE' : 'TEXT',\n attachments: args.attachments.map((file) => ({\n fileId: file.id,\n fileName: file.name,\n fileSize: file.size,\n mimeType: file.mimeType,\n downloadUrl: file.url || undefined,\n })),\n },\n },\n refetchQueries: ['HvConversationMessages'],\n awaitRefetchQueries: true,\n });\n if (!res.data?.sendContextMessage.success) throw new Error('Message was not sent');\n },\n [send, workspaceId]\n );\n\n return { sendContextMessage, loading };\n};\n\nexport const useConversationMessages = (conversationId: string | undefined, first = 50) => {\n const { data, loading, error, refetch } = useQuery<ConversationMessagesResult>(GET_CONVERSATION_MESSAGES, {\n variables: { id: conversationId ?? '', first },\n skip: !conversationId,\n });\n const messages: HvConversationMessage[] = (data?.conversation?.messages.edges ?? []).map((edge) => ({\n id: edge.node.id,\n authorId: edge.node.authorId,\n authorName: edge.node.authorName,\n text: edge.node.content?.text ?? '',\n createdAt: edge.node.createdAt,\n attachments: edge.node.attachments ?? [],\n }));\n return { data: messages, title: data?.conversation?.title ?? null, loading, error, refetch };\n};\n\n// ---------------------------------------------------------------------------\n// Entity comments — per-listing Q&A + reviews\n// ---------------------------------------------------------------------------\n\nconst GET_ENTITY_COMMENTS = gql`\n query HvEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n isAnonymous\n createdAt\n metadata\n content {\n text\n }\n }\n }\n }\n`;\n\nconst CREATE_ENTITY_COMMENT = gql`\n mutation HvCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) {\n id\n createdAt\n }\n }\n`;\n\nexport interface HvEntityComment {\n id: string;\n authorId: string;\n text: string;\n createdAt: string;\n /** Free-form; reviews carry `{ stars: number }`. */\n metadata: Record<string, unknown> | null;\n}\n\ninterface EntityCommentsResult {\n getEntityComments: {\n total: number;\n items: {\n id: string;\n authorId: string;\n authorType: string;\n isAnonymous: boolean;\n createdAt: string;\n metadata: Record<string, unknown> | null;\n content: { text: string | null } | null;\n }[];\n };\n}\n\nexport const useEntityComments = (entityType: string, entityId: string | undefined, take = 50) => {\n const { data, loading, error, refetch } = useQuery<EntityCommentsResult>(GET_ENTITY_COMMENTS, {\n variables: { input: { entityType, entityId: entityId ?? '', take, sortBy: 'createdAt', sortOrder: 'desc' } },\n skip: !entityId,\n });\n const comments: HvEntityComment[] = (data?.getEntityComments.items ?? []).map((item) => ({\n id: item.id,\n authorId: item.authorId,\n text: item.content?.text ?? '',\n createdAt: item.createdAt,\n metadata: item.metadata,\n }));\n return { data: comments, total: data?.getEntityComments.total ?? 0, loading, error, refetch };\n};\n\nexport const useCreateEntityComment = () => {\n const [mutate, { loading }] = useMutation<{ createEntityComment: { id: string } }>(CREATE_ENTITY_COMMENT);\n const createComment = useCallback(\n async (args: {\n entityType: string;\n entityId: string;\n text: string;\n metadata?: Record<string, unknown>;\n }): Promise<void> => {\n await mutate({\n variables: {\n input: {\n entityType: args.entityType,\n entityId: args.entityId,\n content: { text: args.text },\n metadata: args.metadata,\n },\n },\n refetchQueries: ['HvEntityComments'],\n awaitRefetchQueries: true,\n });\n },\n [mutate]\n );\n return { createComment, loading };\n};\n\n// ---------------------------------------------------------------------------\n// Files — presigned upload (initiateUpload → PUT → completeUpload)\n// ---------------------------------------------------------------------------\n\nconst INITIATE_UPLOAD = gql`\n mutation HvInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session {\n id\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD = gql`\n mutation HvCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }\n }\n`;\n\nconst GET_UPLOADED_FILE = gql`\n query HvGetUploadedFile($id: ID!) {\n getFile(id: $id) {\n id\n name\n mimeType\n downloadUrl\n }\n }\n`;\n\nconst DELETE_UPLOADED_FILE = gql`\n mutation HvDeleteUploadedFile($id: ID!) {\n deleteFile(id: $id)\n }\n`;\n\nexport interface HvUploadedFile {\n id: string;\n name: string;\n url: string;\n mimeType: string;\n size: number;\n}\n\ninterface InitiateUploadResult {\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n}\n\ninterface CompleteUploadResult {\n completeUpload: {\n fileId: string;\n file: { id: string; name: string; downloadUrl: string | null; mimeType: string; size: number };\n };\n}\n\nconst splitExtension = (fileName: string): string => {\n const dot = fileName.lastIndexOf('.');\n return dot > 0 && dot < fileName.length - 1 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n};\n\n/**\n * Upload a real browser `File` through wspace-files-svc and return the durable\n * File id + download URL. Single-PUT presigned flow (fine for the image /\n * document sizes HousingVista deals with).\n */\nexport const useFileUpload = () => {\n const [initiate] = useMutation<InitiateUploadResult>(INITIATE_UPLOAD);\n const [complete] = useMutation<CompleteUploadResult>(COMPLETE_UPLOAD);\n\n const uploadFile = useCallback(\n async (file: File): Promise<HvUploadedFile> => {\n const extension = splitExtension(file.name);\n const mimeType = file.type || 'application/octet-stream';\n const initiated = await initiate({\n variables: {\n input: {\n fileName: file.name,\n extension,\n fileSize: file.size,\n mimeType,\n visibility: 'WORKSPACE',\n },\n },\n });\n const uploadUrl = initiated.data?.initiateUpload.uploadUrl;\n const sessionId = initiated.data?.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) throw new Error('Upload could not be started');\n\n const putRes = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': mimeType },\n });\n if (!putRes.ok) throw new Error(`Upload failed (${putRes.status})`);\n\n const completed = await complete({\n variables: { input: { sessionId, fileSize: file.size } },\n });\n const uploaded = completed.data?.completeUpload.file;\n if (!uploaded) throw new Error('Upload could not be finalized');\n return {\n id: uploaded.id,\n name: uploaded.name,\n url: uploaded.downloadUrl ?? '',\n mimeType: uploaded.mimeType,\n size: Number(uploaded.size),\n };\n },\n [initiate, complete]\n );\n\n return { uploadFile };\n};\n\n/**\n * Soft-delete files that were uploaded but never attached to a record (a\n * cancelled form, a removed chip) so abandoned uploads do not linger in the\n * workspace. Best-effort: resolves with the ids that could not be deleted.\n */\nexport const useDeleteUploadedFile = () => {\n const [mutate] = useMutation<{ deleteFile: boolean }, { id: string }>(DELETE_UPLOADED_FILE);\n\n const deleteUploadedFiles = useCallback(\n async (fileIds: string[]): Promise<string[]> => {\n const results = await Promise.allSettled(fileIds.map((id) => mutate({ variables: { id } })));\n return fileIds.filter((_, index) => {\n const result = results[index];\n return result.status === 'rejected' || result.value.data?.deleteFile !== true;\n });\n },\n [mutate]\n );\n\n return { deleteUploadedFiles };\n};\n\nexport interface HvStoredFile {\n id: string;\n name: string;\n mimeType: string;\n downloadUrl: string | null;\n}\n\n/**\n * Read one stored file's metadata + a fresh download URL from wspace-files-svc.\n * `downloadUrl` is a presigned URL (~1h), so every mount revalidates against\n * the network (`cache-and-network` still paints the cached name immediately).\n */\nexport const useUploadedFile = (fileId: string) => {\n const { data, loading, error } = useQuery<{ getFile: HvStoredFile | null }, { id: string }>(GET_UPLOADED_FILE, {\n variables: { id: fileId },\n fetchPolicy: 'cache-and-network',\n skip: !fileId,\n });\n return { file: data?.getFile ?? null, loading, error };\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"platform.js","names":[],"sources":["../../../src/housingvista/hooks/platform.ts"],"sourcesContent":["/**\n * Platform-service reuse hooks for HousingVista.\n *\n * These call *other* wspace subgraphs through the SAME Apollo client / wspace\n * gateway the MFE already uses — cross-SERVICE GraphQL (allowed), never a\n * cross-MFE import (forbidden):\n *\n * - `wspace-conversations-svc` — direct messaging (Contact / Message / Apply\n * CTAs) via `resolveContextConversation` + `sendMessage` (and\n * `sendContextMessage` when a message carries file attachments), and\n * per-listing Q&A / reviews via `getEntityComments` + `createEntityComment`.\n * - `wspace-files-svc` — real uploads via the presigned `initiateUpload` →\n * PUT → `completeUpload` flow, returning a durable File id + download URL.\n *\n * No `any`, no mock data. `workspaceId` is read from the HousingVista context.\n * @module housingvista\n */\n\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { useCallback } from 'react';\nimport { useHousingVista } from '../context/HousingVistaContext';\n\n// ---------------------------------------------------------------------------\n// Conversations — direct messaging\n// ---------------------------------------------------------------------------\n\nconst RESOLVE_CONTEXT_CONVERSATION = gql`\n mutation HvResolveContextConversation($input: ResolveContextConversationInput!) {\n resolveContextConversation(input: $input) {\n created\n conversation {\n id\n title\n }\n }\n }\n`;\n\nconst SEND_MESSAGE = gql`\n mutation HvSendMessage($input: SendMessageInput!) {\n sendMessage(input: $input) {\n success\n message {\n id\n createdAt\n }\n }\n }\n`;\n\nconst SEND_CONTEXT_MESSAGE = gql`\n mutation HvSendContextMessage($input: SendContextConversationMessageInput!) {\n sendContextMessage(input: $input) {\n success\n message {\n id\n createdAt\n }\n }\n }\n`;\n\nconst GET_CONVERSATION_MESSAGES = gql`\n query HvConversationMessages($id: ID!, $first: Int) {\n conversation(id: $id) {\n id\n title\n messages(first: $first) {\n edges {\n node {\n id\n authorId\n authorName\n content\n createdAt\n attachments {\n id\n fileId\n fileName\n fileSize\n mimeType\n downloadUrl\n }\n }\n }\n }\n }\n }\n`;\n\nexport interface HvContextTarget {\n /** Stable identity of the thing being discussed, e.g. a property id. */\n entityId: string;\n /** e.g. `housingvista-property`, `housingvista-marketplace-service`. */\n entityType: string;\n /** Human title used when a conversation is first created. */\n title: string;\n /** Deep link back into the MFE surface, e.g. `/housingvista/properties/:id`. */\n contextHref: string;\n /** Router path portion of `contextHref`. */\n pagePath: string;\n}\n\n/** A file attached to a message (`Message.attachments` on the conversations subgraph). */\nexport interface HvMessageAttachment {\n id: string;\n fileId: string;\n fileName: string;\n fileSize: number;\n mimeType: string;\n /** Null when the attachment row carries no resolvable download metadata. */\n downloadUrl: string | null;\n}\n\nexport interface HvConversationMessage {\n id: string;\n authorId: string;\n /** Resolved display name of the sender (`Message.authorName`). */\n authorName: string;\n /** Conversations stores content as JSON; the plain text is under `.text`. */\n text: string;\n createdAt: string;\n attachments: HvMessageAttachment[];\n}\n\n/**\n * The `sourceContext` a HousingVista thread was created with, read back off the\n * conversation's persisted `settings.metadata`. Present only when every field\n * `SendContextConversationMessageInput` requires survived; callers must treat a\n * `null` as \"this thread cannot take the context-message path\".\n */\nexport interface HvConversationSourceContext {\n product: string;\n threadKey: string;\n widgetKey: string;\n contextHref: string;\n pagePath: string;\n contextTitle?: string;\n entityId?: string;\n entityType?: string;\n}\n\ninterface ResolveContextConversationResult {\n resolveContextConversation: {\n created: boolean;\n conversation: { id: string; title: string | null };\n };\n}\n\ninterface MessageContentJson {\n text?: string;\n}\n\ninterface ConversationMessagesResult {\n conversation: {\n id: string;\n title: string | null;\n messages: {\n edges: {\n node: {\n id: string;\n authorId: string;\n authorName: string;\n content: MessageContentJson | null;\n createdAt: string;\n attachments: HvMessageAttachment[];\n };\n }[];\n };\n } | null;\n}\n\n/**\n * Resolve (get-or-create) the conversation attached to a HousingVista entity,\n * then return its id. Idempotent per (workspace, entityType, entityId) via the\n * derived `threadKey`, so repeated \"Contact\" clicks reuse one thread.\n */\nexport const useContactConversation = () => {\n const { workspaceId } = useHousingVista();\n const [resolve, { loading }] = useMutation<ResolveContextConversationResult>(RESOLVE_CONTEXT_CONVERSATION);\n\n const openConversation = useCallback(\n async (target: HvContextTarget): Promise<{ id: string; created: boolean }> => {\n const threadKey = `housingvista:${target.entityType}:${target.entityId}`;\n const res = await resolve({\n variables: {\n input: {\n workspaceId,\n title: target.title,\n sourceContext: {\n product: 'housingvista',\n entityId: target.entityId,\n entityType: target.entityType,\n threadKey,\n widgetKey: 'housingvista-contact',\n contextHref: target.contextHref,\n pagePath: target.pagePath,\n contextTitle: target.title,\n },\n },\n },\n });\n const conversation = res.data?.resolveContextConversation.conversation;\n if (!conversation) throw new Error('Could not open the conversation');\n return { id: conversation.id, created: res.data?.resolveContextConversation.created ?? false };\n },\n [resolve, workspaceId]\n );\n\n return { openConversation, loading };\n};\n\nexport const useSendMessage = () => {\n const [send, { loading }] = useMutation<{ sendMessage: { success: boolean } }>(SEND_MESSAGE);\n const sendMessage = useCallback(\n async (conversationId: string, text: string): Promise<void> => {\n const res = await send({\n variables: { input: { conversationId, content: { text } } },\n refetchQueries: ['HvConversationMessages'],\n awaitRefetchQueries: true,\n });\n if (!res.data?.sendMessage.success) throw new Error('Message was not sent');\n },\n [send]\n );\n return { sendMessage, loading };\n};\n\n/**\n * Send a message WITH file attachments.\n *\n * `sendMessage`'s `attachmentIds` only stores bare file ids — the subgraph's\n * `Attachment.fileName`/`downloadUrl` resolvers read their values out of the\n * attachment row's metadata, so an id-only attachment comes back rendering its\n * own uuid with no download link. `sendContextMessage` is the mutation that\n * accepts full `ContextMessageAttachmentInput` details, so it is the only path\n * that yields a readable attachment. It resolves the conversation from the\n * thread's `sourceContext` (idempotent on `threadKey`), which is why the caller\n * must hand back the SAME sourceContext the thread was created with.\n */\nexport const useSendContextMessage = () => {\n const { workspaceId } = useHousingVista();\n const [send, { loading }] = useMutation<{ sendContextMessage: { success: boolean } }>(SEND_CONTEXT_MESSAGE);\n\n const sendContextMessage = useCallback(\n async (args: {\n sourceContext: HvConversationSourceContext;\n text: string;\n attachments: HvUploadedFile[];\n }): Promise<void> => {\n const res = await send({\n variables: {\n input: {\n workspaceId,\n sourceContext: args.sourceContext,\n content: { text: args.text },\n type: args.attachments.length > 0 ? 'FILE' : 'TEXT',\n attachments: args.attachments.map((file) => ({\n fileId: file.id,\n fileName: file.name,\n fileSize: file.size,\n mimeType: file.mimeType,\n downloadUrl: file.url || undefined,\n })),\n },\n },\n refetchQueries: ['HvConversationMessages'],\n awaitRefetchQueries: true,\n });\n if (!res.data?.sendContextMessage.success) throw new Error('Message was not sent');\n },\n [send, workspaceId]\n );\n\n return { sendContextMessage, loading };\n};\n\nexport const useConversationMessages = (conversationId: string | undefined, first = 50) => {\n const { data, loading, error, refetch } = useQuery<ConversationMessagesResult>(GET_CONVERSATION_MESSAGES, {\n variables: { id: conversationId ?? '', first },\n skip: !conversationId,\n });\n const messages: HvConversationMessage[] = (data?.conversation?.messages.edges ?? []).map((edge) => ({\n id: edge.node.id,\n authorId: edge.node.authorId,\n authorName: edge.node.authorName,\n text: edge.node.content?.text ?? '',\n createdAt: edge.node.createdAt,\n attachments: edge.node.attachments ?? [],\n }));\n return { data: messages, title: data?.conversation?.title ?? null, loading, error, refetch };\n};\n\n// ---------------------------------------------------------------------------\n// Entity comments — per-listing Q&A + reviews\n// ---------------------------------------------------------------------------\n\nconst GET_ENTITY_COMMENTS = gql`\n query HvEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n isAnonymous\n createdAt\n metadata\n content {\n text\n }\n }\n }\n }\n`;\n\nconst CREATE_ENTITY_COMMENT = gql`\n mutation HvCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) {\n id\n createdAt\n }\n }\n`;\n\nexport interface HvEntityComment {\n id: string;\n authorId: string;\n text: string;\n createdAt: string;\n /** Free-form; reviews carry `{ stars: number }`. */\n metadata: Record<string, unknown> | null;\n}\n\ninterface EntityCommentsResult {\n getEntityComments: {\n total: number;\n items: {\n id: string;\n authorId: string;\n authorType: string;\n isAnonymous: boolean;\n createdAt: string;\n metadata: Record<string, unknown> | null;\n content: { text: string | null } | null;\n }[];\n };\n}\n\nexport const useEntityComments = (entityType: string, entityId: string | undefined, take = 50) => {\n const { data, loading, error, refetch } = useQuery<EntityCommentsResult>(GET_ENTITY_COMMENTS, {\n variables: { input: { entityType, entityId: entityId ?? '', take, sortBy: 'createdAt', sortOrder: 'desc' } },\n skip: !entityId,\n });\n const comments: HvEntityComment[] = (data?.getEntityComments.items ?? []).map((item) => ({\n id: item.id,\n authorId: item.authorId,\n text: item.content?.text ?? '',\n createdAt: item.createdAt,\n metadata: item.metadata,\n }));\n return { data: comments, total: data?.getEntityComments.total ?? 0, loading, error, refetch };\n};\n\nexport const useCreateEntityComment = () => {\n const [mutate, { loading }] = useMutation<{ createEntityComment: { id: string } }>(CREATE_ENTITY_COMMENT);\n const createComment = useCallback(\n async (args: {\n entityType: string;\n entityId: string;\n text: string;\n metadata?: Record<string, unknown>;\n }): Promise<void> => {\n await mutate({\n variables: {\n input: {\n entityType: args.entityType,\n entityId: args.entityId,\n content: { text: args.text },\n metadata: args.metadata,\n },\n },\n refetchQueries: ['HvEntityComments'],\n awaitRefetchQueries: true,\n });\n },\n [mutate]\n );\n return { createComment, loading };\n};\n\n// ---------------------------------------------------------------------------\n// Files — presigned upload (initiateUpload → PUT → completeUpload)\n// ---------------------------------------------------------------------------\n\nconst INITIATE_UPLOAD = gql`\n mutation HvInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session {\n id\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD = gql`\n mutation HvCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }\n }\n`;\n\nconst GET_UPLOADED_FILE = gql`\n query HvGetUploadedFile($id: ID!) {\n getFile(id: $id) {\n id\n name\n mimeType\n downloadUrl\n }\n }\n`;\n\nconst DELETE_UPLOADED_FILE = gql`\n mutation HvDeleteUploadedFile($id: ID!) {\n deleteFile(id: $id)\n }\n`;\n\nexport interface HvUploadedFile {\n id: string;\n name: string;\n url: string;\n mimeType: string;\n size: number;\n}\n\ninterface InitiateUploadResult {\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n}\n\ninterface CompleteUploadResult {\n completeUpload: {\n fileId: string;\n file: { id: string; name: string; downloadUrl: string | null; mimeType: string; size: number };\n };\n}\n\nconst splitExtension = (fileName: string): string => {\n const dot = fileName.lastIndexOf('.');\n return dot > 0 && dot < fileName.length - 1 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n};\n\n/**\n * Upload a real browser `File` through wspace-files-svc and return the durable\n * File id + download URL. Single-PUT presigned flow (fine for the image /\n * document sizes HousingVista deals with).\n */\nexport const useFileUpload = () => {\n const [initiate] = useMutation<InitiateUploadResult>(INITIATE_UPLOAD);\n const [complete] = useMutation<CompleteUploadResult>(COMPLETE_UPLOAD);\n\n const uploadFile = useCallback(\n async (file: File): Promise<HvUploadedFile> => {\n const extension = splitExtension(file.name);\n const mimeType = file.type || 'application/octet-stream';\n const initiated = await initiate({\n variables: {\n input: {\n fileName: file.name,\n extension,\n fileSize: file.size,\n mimeType,\n visibility: 'WORKSPACE',\n },\n },\n });\n const uploadUrl = initiated.data?.initiateUpload.uploadUrl;\n const sessionId = initiated.data?.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) throw new Error('Upload could not be started');\n\n const putRes = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': mimeType },\n });\n if (!putRes.ok) throw new Error(`Upload failed (${putRes.status})`);\n\n const completed = await complete({\n variables: { input: { sessionId, fileSize: file.size } },\n });\n const uploaded = completed.data?.completeUpload.file;\n if (!uploaded) throw new Error('Upload could not be finalized');\n return {\n id: uploaded.id,\n name: uploaded.name,\n url: uploaded.downloadUrl ?? '',\n mimeType: uploaded.mimeType,\n size: Number(uploaded.size),\n };\n },\n [initiate, complete]\n );\n\n return { uploadFile };\n};\n\n/**\n * Soft-delete files that were uploaded but never attached to a record (a\n * cancelled form, a removed chip) so abandoned uploads do not linger in the\n * workspace. Best-effort: resolves with the ids that could not be deleted.\n */\nexport const useDeleteUploadedFile = () => {\n const [mutate] = useMutation<{ deleteFile: boolean }, { id: string }>(DELETE_UPLOADED_FILE);\n\n const deleteUploadedFiles = useCallback(\n async (fileIds: string[]): Promise<string[]> => {\n const results = await Promise.allSettled(fileIds.map((id) => mutate({ variables: { id } })));\n return fileIds.filter((_, index) => {\n const result = results[index];\n return result.status === 'rejected' || result.value.data?.deleteFile !== true;\n });\n },\n [mutate]\n );\n\n return { deleteUploadedFiles };\n};\n\nexport interface HvStoredFile {\n id: string;\n name: string;\n mimeType: string;\n downloadUrl: string | null;\n}\n\n/**\n * Read one stored file's metadata + a fresh download URL from wspace-files-svc.\n * `downloadUrl` is a presigned URL (~1h), so every mount revalidates against\n * the network (`cache-and-network` still paints the cached name immediately).\n */\nexport const useUploadedFile = (fileId: string) => {\n const { data, loading, error } = useQuery<{ getFile: HvStoredFile | null }, { id: string }>(GET_UPLOADED_FILE, {\n variables: { id: fileId },\n fetchPolicy: 'cache-and-network',\n skip: !fileId,\n });\n return { file: data?.getFile ?? null, loading, error };\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomeLoanDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/finance/HomeLoanDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8CH,wBAAgB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"HomeLoanDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/finance/HomeLoanDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8CH,wBAAgB,kBAAkB,gCAuQjC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomeLoanDetailPage.js","names":[],"sources":["../../../../src/housingvista/pages/finance/HomeLoanDetailPage.tsx"],"sourcesContent":["/**\n * HousingVista Home Loan Detail / Apply Page\n * Single loan offer backed by `housingvistaHomeLoanOffer(id)` — full terms plus\n * a live EMI summary for the offer's headline rate.\n *\n * \"Apply\" opens the real `ContactOwnerDialog` — a persisted conversation with\n * the bank. No fabricated approval; the EMI calculator stays for modelling.\n * @module housingvista\n */\n\nimport { useMemo, useState } from 'react';\nimport { useNavigate, useParams, useLocation } from 'react-router';\nimport {\n ArrowLeft,\n Building2,\n Star,\n CheckCircle,\n Gift,\n Calculator,\n Percent,\n Wallet,\n CalendarClock,\n} from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { Breadcrumbs, StatusBadge, EmptyState, LoadingSkeleton, ContactOwnerDialog } from '../../components';\nimport type { BreadcrumbItem } from '../../components';\nimport { useHomeLoanOffer } from '../../hooks';\nimport { cn, formatCurrency, formatINRCompact } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\n/** Branded emerald range slider (matches the mock's green sliders). */\nconst SLIDER_CLASS = 'w-full accent-[var(--color-action-primary-bg)]';\n\nfunction calculateEMI(principal: number, annualRate: number, tenureYears: number): number {\n if (principal <= 0 || annualRate <= 0 || tenureYears <= 0) return 0;\n const monthlyRate = annualRate / 12 / 100;\n const months = tenureYears * 12;\n const emi = (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1);\n return Math.round(emi);\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5 text-status-warning-text\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn('h-4 w-4', i < Math.floor(rating) ? 'fill-current' : 'fill-none text-text-secondary/30')}\n />\n ))}\n </div>\n );\n}\n\nexport function HomeLoanDetailPage() {\n const { id } = useParams<{ id: string }>();\n\n useTrackPageView('housingvista.home_loan.viewed', id);\n const navigate = useNavigate();\n const location = useLocation();\n const { data: loan, loading } = useHomeLoanOffer(id);\n\n // Model a representative loan against the offer's headline rate so the borrower\n // can see the real monthly cost before enquiring.\n const [amount, setAmount] = useState(500000);\n const [tenure, setTenure] = useState(20);\n const [contactOpen, setContactOpen] = useState(false);\n\n const rate = loan?.interestRate ?? 0;\n const emi = useMemo(() => calculateEMI(amount, rate, tenure), [amount, rate, tenure]);\n const totalPayment = emi * tenure * 12;\n const totalInterest = totalPayment - amount;\n\n // Initial load only: a background refetch must not unmount the page, or the\n // Apply dialog loses its conversation and re-resolves in a loop.\n if (loading && !loan) {\n return (\n <div className=\"p-6\">\n <LoadingSkeleton variant=\"detail\" />\n </div>\n );\n }\n\n if (!loan) {\n return (\n <div className=\"p-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title=\"Loan Offer Not Found\"\n description=\"The loan offer you're looking for doesn't exist or is no longer available.\"\n action={\n <button\n onClick={() => navigate('../home-loans')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Home Loans\n </button>\n }\n />\n </div>\n );\n }\n\n const features = loan.features ?? [];\n const breadcrumbs: BreadcrumbItem[] = [{ label: 'Home Loans', href: '../' }, { label: loan.bankName }];\n\n return (\n <div className=\"flex flex-col gap-6 p-6\">\n <Breadcrumbs items={breadcrumbs} />\n\n {/* Header */}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n <Building2 className=\"h-6 w-6\" />\n </div>\n <div>\n <button\n onClick={() => navigate('../home-loans')}\n className=\"mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Home Loans\n </button>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{loan.bankName}</h1>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n {loan.rating != null && (\n <span className=\"inline-flex items-center gap-1.5\">\n <RatingStars rating={loan.rating} />\n <span className=\"text-sm text-text-secondary\">{loan.rating}</span>\n </span>\n )}\n {loan.specialOffer && <StatusBadge label=\"Special Offer\" variant=\"warning\" />}\n </div>\n </div>\n </div>\n <Button type=\"button\" variant=\"default\" onClick={() => setContactOpen(true)}>\n Apply Now\n </Button>\n </div>\n\n <PagePurpose className=\"max-w-3xl\">\n The full terms of this offer with a live EMI estimate at the bank's headline rate. Model your loan amount and\n tenure to see the real monthly cost, then apply to connect with {loan.bankName} through Messages.\n </PagePurpose>\n\n {loan.specialOffer && (\n <div className=\"flex items-center gap-2 rounded-lg bg-status-warning-bg-subtle p-3 text-sm text-status-warning-text\">\n <Gift className=\"h-4 w-4 shrink-0\" />\n {loan.specialOffer}\n </div>\n )}\n\n {/* EMI summary — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Calculator className=\"h-5 w-5 text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">EMI Estimate at {loan.interestRate}%</h2>\n </div>\n <div className=\"grid gap-6 md:grid-cols-2\">\n <div className=\"space-y-5\">\n <div>\n <div className=\"mb-2 flex items-center justify-between\">\n <label className=\"text-sm font-medium text-text-primary\">Loan Amount</label>\n <span className=\"text-sm font-semibold text-text-primary\">{formatCurrency(amount)}</span>\n </div>\n <input\n type=\"range\"\n min={50000}\n max={Math.max(loan.maxAmount, 100000)}\n step={10000}\n value={Math.min(amount, loan.maxAmount)}\n onChange={(e) => setAmount(Number(e.target.value))}\n aria-label=\"Loan amount\"\n className={SLIDER_CLASS}\n />\n <div className=\"mt-1 flex justify-between text-xs text-text-secondary\">\n <span>₹50K</span>\n <span>Max {formatINRCompact(loan.maxAmount)}</span>\n </div>\n </div>\n <div>\n <div className=\"mb-2 flex items-center justify-between\">\n <label className=\"text-sm font-medium text-text-primary\">Tenure (years)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{tenure} years</span>\n </div>\n <input\n type=\"range\"\n min={1}\n max={loan.maxTenure}\n step={1}\n value={Math.min(tenure, loan.maxTenure)}\n onChange={(e) => setTenure(Number(e.target.value))}\n aria-label=\"Tenure in years\"\n className={SLIDER_CLASS}\n />\n <div className=\"mt-1 flex justify-between text-xs text-text-secondary\">\n <span>1 yr</span>\n <span>Max {loan.maxTenure} yrs</span>\n </div>\n </div>\n </div>\n <div className=\"flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6\">\n <p className=\"text-sm text-text-secondary\">Monthly EMI</p>\n <p className=\"my-2 text-4xl font-bold text-text-primary\">{formatCurrency(emi)}</p>\n <div className=\"mt-4 grid w-full grid-cols-2 gap-4\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Payment</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(totalPayment)}</p>\n </div>\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Interest</p>\n <p className=\"text-sm font-semibold text-status-error-text\">{formatCurrency(totalInterest)}</p>\n </div>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Terms */}\n <div className=\"space-y-6 lg:col-span-2\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Loan Terms</h2>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3\">\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Percent className=\"h-3.5 w-3.5\" />\n Interest Rate\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">{loan.interestRate}%</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Wallet className=\"h-3.5 w-3.5\" />\n EMI / Lakh\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">\n {loan.emiPerLakh != null ? formatINRCompact(loan.emiPerLakh) : '—'}\n </div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <CalendarClock className=\"h-3.5 w-3.5\" />\n Max Tenure\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">{loan.maxTenure} yrs</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Max Amount</div>\n <div className=\"mt-1 text-sm font-semibold text-text-primary\">{formatINRCompact(loan.maxAmount)}</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Processing Fee</div>\n <div className=\"mt-1 text-sm font-semibold text-text-primary\">{loan.processingFee ?? '—'}</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Prepayment</div>\n <div\n className={cn(\n 'mt-1 text-sm font-semibold',\n loan.prepaymentCharges === 'None' ? 'text-status-success-text' : 'text-text-primary'\n )}\n >\n {loan.prepaymentCharges ?? '—'}\n </div>\n </div>\n </div>\n </GlassCard>\n\n {features.length > 0 && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Features</h2>\n <ul className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n {features.map((feature, i) => (\n <li key={i} className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <CheckCircle className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n {feature}\n </li>\n ))}\n </ul>\n </GlassCard>\n )}\n </div>\n\n {/* Apply card */}\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h3 className=\"mb-2 text-base font-semibold text-text-primary\">Ready to apply?</h3>\n <p className=\"mb-4 text-sm text-text-secondary\">\n Applications are handled directly with {loan.bankName} through HousingVista Messages. Start an enquiry and\n the bank will guide you through eligibility and paperwork.\n </p>\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"w-full justify-center\"\n onClick={() => setContactOpen(true)}\n >\n Apply Now\n </Button>\n </GlassCard>\n </div>\n </div>\n\n <ContactOwnerDialog\n open={contactOpen}\n onOpenChange={setContactOpen}\n entityId={loan.id}\n entityType=\"housingvista-home-loan\"\n title={loan.bankName}\n pagePath={location.pathname}\n heading={`Apply with ${loan.bankName}`}\n description={`Start your application with ${loan.bankName}. Messages are saved and delivered in your inbox.`}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HomeLoanDetailPage.js","names":[],"sources":["../../../../src/housingvista/pages/finance/HomeLoanDetailPage.tsx"],"sourcesContent":["/**\n * HousingVista Home Loan Detail / Apply Page\n * Single loan offer backed by `housingvistaHomeLoanOffer(id)` — full terms plus\n * a live EMI summary for the offer's headline rate.\n *\n * \"Apply\" opens the real `ContactOwnerDialog` — a persisted conversation with\n * the bank. No fabricated approval; the EMI calculator stays for modelling.\n * @module housingvista\n */\n\nimport { useMemo, useState } from 'react';\nimport { useNavigate, useParams, useLocation } from 'react-router';\nimport {\n ArrowLeft,\n Building2,\n Star,\n CheckCircle,\n Gift,\n Calculator,\n Percent,\n Wallet,\n CalendarClock,\n} from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { Breadcrumbs, StatusBadge, EmptyState, LoadingSkeleton, ContactOwnerDialog } from '../../components';\nimport type { BreadcrumbItem } from '../../components';\nimport { useHomeLoanOffer } from '../../hooks';\nimport { cn, formatCurrency, formatINRCompact } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\n/** Branded emerald range slider (matches the mock's green sliders). */\nconst SLIDER_CLASS = 'w-full accent-[var(--color-action-primary-bg)]';\n\nfunction calculateEMI(principal: number, annualRate: number, tenureYears: number): number {\n if (principal <= 0 || annualRate <= 0 || tenureYears <= 0) return 0;\n const monthlyRate = annualRate / 12 / 100;\n const months = tenureYears * 12;\n const emi = (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1);\n return Math.round(emi);\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5 text-status-warning-text\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn('h-4 w-4', i < Math.floor(rating) ? 'fill-current' : 'fill-none text-text-secondary/30')}\n />\n ))}\n </div>\n );\n}\n\nexport function HomeLoanDetailPage() {\n const { id } = useParams<{ id: string }>();\n\n useTrackPageView('housingvista.home_loan.viewed', id);\n const navigate = useNavigate();\n const location = useLocation();\n const { data: loan, loading } = useHomeLoanOffer(id);\n\n // Model a representative loan against the offer's headline rate so the borrower\n // can see the real monthly cost before enquiring.\n const [amount, setAmount] = useState(500000);\n const [tenure, setTenure] = useState(20);\n const [contactOpen, setContactOpen] = useState(false);\n\n const rate = loan?.interestRate ?? 0;\n const emi = useMemo(() => calculateEMI(amount, rate, tenure), [amount, rate, tenure]);\n const totalPayment = emi * tenure * 12;\n const totalInterest = totalPayment - amount;\n\n if (loading) {\n return (\n <div className=\"p-6\">\n <LoadingSkeleton variant=\"detail\" />\n </div>\n );\n }\n\n if (!loan) {\n return (\n <div className=\"p-6\">\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title=\"Loan Offer Not Found\"\n description=\"The loan offer you're looking for doesn't exist or is no longer available.\"\n action={\n <button\n onClick={() => navigate('../home-loans')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Home Loans\n </button>\n }\n />\n </div>\n );\n }\n\n const features = loan.features ?? [];\n const breadcrumbs: BreadcrumbItem[] = [{ label: 'Home Loans', href: '../' }, { label: loan.bankName }];\n\n return (\n <div className=\"flex flex-col gap-6 p-6\">\n <Breadcrumbs items={breadcrumbs} />\n\n {/* Header */}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n <Building2 className=\"h-6 w-6\" />\n </div>\n <div>\n <button\n onClick={() => navigate('../home-loans')}\n className=\"mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Home Loans\n </button>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{loan.bankName}</h1>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n {loan.rating != null && (\n <span className=\"inline-flex items-center gap-1.5\">\n <RatingStars rating={loan.rating} />\n <span className=\"text-sm text-text-secondary\">{loan.rating}</span>\n </span>\n )}\n {loan.specialOffer && <StatusBadge label=\"Special Offer\" variant=\"warning\" />}\n </div>\n </div>\n </div>\n <Button type=\"button\" variant=\"default\" onClick={() => setContactOpen(true)}>\n Apply Now\n </Button>\n </div>\n\n <PagePurpose className=\"max-w-3xl\">\n The full terms of this offer with a live EMI estimate at the bank's headline rate. Model your loan amount and\n tenure to see the real monthly cost, then apply to connect with {loan.bankName} through Messages.\n </PagePurpose>\n\n {loan.specialOffer && (\n <div className=\"flex items-center gap-2 rounded-lg bg-status-warning-bg-subtle p-3 text-sm text-status-warning-text\">\n <Gift className=\"h-4 w-4 shrink-0\" />\n {loan.specialOffer}\n </div>\n )}\n\n {/* EMI summary — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Calculator className=\"h-5 w-5 text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">EMI Estimate at {loan.interestRate}%</h2>\n </div>\n <div className=\"grid gap-6 md:grid-cols-2\">\n <div className=\"space-y-5\">\n <div>\n <div className=\"mb-2 flex items-center justify-between\">\n <label className=\"text-sm font-medium text-text-primary\">Loan Amount</label>\n <span className=\"text-sm font-semibold text-text-primary\">{formatCurrency(amount)}</span>\n </div>\n <input\n type=\"range\"\n min={50000}\n max={Math.max(loan.maxAmount, 100000)}\n step={10000}\n value={Math.min(amount, loan.maxAmount)}\n onChange={(e) => setAmount(Number(e.target.value))}\n aria-label=\"Loan amount\"\n className={SLIDER_CLASS}\n />\n <div className=\"mt-1 flex justify-between text-xs text-text-secondary\">\n <span>₹50K</span>\n <span>Max {formatINRCompact(loan.maxAmount)}</span>\n </div>\n </div>\n <div>\n <div className=\"mb-2 flex items-center justify-between\">\n <label className=\"text-sm font-medium text-text-primary\">Tenure (years)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{tenure} years</span>\n </div>\n <input\n type=\"range\"\n min={1}\n max={loan.maxTenure}\n step={1}\n value={Math.min(tenure, loan.maxTenure)}\n onChange={(e) => setTenure(Number(e.target.value))}\n aria-label=\"Tenure in years\"\n className={SLIDER_CLASS}\n />\n <div className=\"mt-1 flex justify-between text-xs text-text-secondary\">\n <span>1 yr</span>\n <span>Max {loan.maxTenure} yrs</span>\n </div>\n </div>\n </div>\n <div className=\"flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6\">\n <p className=\"text-sm text-text-secondary\">Monthly EMI</p>\n <p className=\"my-2 text-4xl font-bold text-text-primary\">{formatCurrency(emi)}</p>\n <div className=\"mt-4 grid w-full grid-cols-2 gap-4\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Payment</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(totalPayment)}</p>\n </div>\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Interest</p>\n <p className=\"text-sm font-semibold text-status-error-text\">{formatCurrency(totalInterest)}</p>\n </div>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Terms */}\n <div className=\"space-y-6 lg:col-span-2\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Loan Terms</h2>\n <div className=\"grid grid-cols-2 gap-4 sm:grid-cols-3\">\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Percent className=\"h-3.5 w-3.5\" />\n Interest Rate\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">{loan.interestRate}%</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Wallet className=\"h-3.5 w-3.5\" />\n EMI / Lakh\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">\n {loan.emiPerLakh != null ? formatINRCompact(loan.emiPerLakh) : '—'}\n </div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <CalendarClock className=\"h-3.5 w-3.5\" />\n Max Tenure\n </div>\n <div className=\"mt-1 text-lg font-bold text-text-primary\">{loan.maxTenure} yrs</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Max Amount</div>\n <div className=\"mt-1 text-sm font-semibold text-text-primary\">{formatINRCompact(loan.maxAmount)}</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Processing Fee</div>\n <div className=\"mt-1 text-sm font-semibold text-text-primary\">{loan.processingFee ?? '—'}</div>\n </div>\n <div className=\"rounded-lg bg-bg-sunken/50 p-4\">\n <div className=\"text-xs text-text-secondary\">Prepayment</div>\n <div\n className={cn(\n 'mt-1 text-sm font-semibold',\n loan.prepaymentCharges === 'None' ? 'text-status-success-text' : 'text-text-primary'\n )}\n >\n {loan.prepaymentCharges ?? '—'}\n </div>\n </div>\n </div>\n </GlassCard>\n\n {features.length > 0 && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Features</h2>\n <ul className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n {features.map((feature, i) => (\n <li key={i} className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <CheckCircle className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n {feature}\n </li>\n ))}\n </ul>\n </GlassCard>\n )}\n </div>\n\n {/* Apply card */}\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h3 className=\"mb-2 text-base font-semibold text-text-primary\">Ready to apply?</h3>\n <p className=\"mb-4 text-sm text-text-secondary\">\n Applications are handled directly with {loan.bankName} through HousingVista Messages. Start an enquiry and\n the bank will guide you through eligibility and paperwork.\n </p>\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"w-full justify-center\"\n onClick={() => setContactOpen(true)}\n >\n Apply Now\n </Button>\n </GlassCard>\n </div>\n </div>\n\n <ContactOwnerDialog\n open={contactOpen}\n onOpenChange={setContactOpen}\n entityId={loan.id}\n entityType=\"housingvista-home-loan\"\n title={loan.bankName}\n pagePath={location.pathname}\n heading={`Apply with ${loan.bankName}`}\n description={`Start your application with ${loan.bankName}. Messages are saved and delivered in your inbox.`}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LegalServiceDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/legal/LegalServiceDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA2EH,wBAAgB,sBAAsB,
|
|
1
|
+
{"version":3,"file":"LegalServiceDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/legal/LegalServiceDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA2EH,wBAAgB,sBAAsB,gCAiOrC"}
|
|
@@ -42,7 +42,7 @@ function F() {
|
|
|
42
42
|
let { id: e } = T();
|
|
43
43
|
o("housingvista.legal.viewed", e);
|
|
44
44
|
let f = w(), g = C(), [b, F] = u(!1), { data: I, loading: L } = s(e);
|
|
45
|
-
if (L
|
|
45
|
+
if (L) return /* @__PURE__ */ E("div", {
|
|
46
46
|
className: "p-6",
|
|
47
47
|
children: /* @__PURE__ */ E(a, { variant: "detail" })
|
|
48
48
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LegalServiceDetailPage.js","names":[],"sources":["../../../../src/housingvista/pages/legal/LegalServiceDetailPage.tsx"],"sourcesContent":["/**\n * HousingVista Legal Service Detail Page\n * Single legal-service provider backed by `housingvistaLegalService(id)` —\n * category, provider, rating, price, duration, documents required.\n *\n * \"Book Now\" / \"Request a consultation\" opens the real `ContactOwnerDialog` — a\n * persisted conversation with the provider. Reviews are real, backed by\n * EntityComments.\n * @module housingvista\n */\n\nimport { useState } from 'react';\nimport { useNavigate, useParams, useLocation } from 'react-router';\nimport {\n ArrowLeft,\n Scale,\n Star,\n Clock,\n FileText,\n Search as SearchIcon,\n Gavel,\n BookOpen,\n ClipboardCheck,\n MessageCircle,\n ShieldCheck,\n IndianRupee,\n} from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n Breadcrumbs,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n ContactOwnerDialog,\n ReviewsSection,\n} from '../../components';\nimport type { BreadcrumbItem } from '../../components';\nimport { useLegalService } from '../../hooks';\nimport { cn, formatCurrency } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\nfunction getCategoryIcon(category: string) {\n switch (category) {\n case 'title_search':\n return <SearchIcon className=\"h-5 w-5\" />;\n case 'lease_review':\n return <FileText className=\"h-5 w-5\" />;\n case 'dispute_resolution':\n return <Gavel className=\"h-5 w-5\" />;\n case 'registration':\n return <ClipboardCheck className=\"h-5 w-5\" />;\n case 'compliance':\n return <BookOpen className=\"h-5 w-5\" />;\n default:\n return <Scale className=\"h-5 w-5\" />;\n }\n}\n\nfunction getStatusVariant(status: string): 'success' | 'warning' | 'error' | 'muted' {\n switch (status) {\n case 'available':\n return 'success';\n case 'busy':\n return 'warning';\n case 'unavailable':\n return 'error';\n default:\n return 'muted';\n }\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5 text-status-warning-text\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn('h-4 w-4', i < Math.floor(rating) ? 'fill-current' : 'fill-none text-text-secondary/30')}\n />\n ))}\n </div>\n );\n}\n\nexport function LegalServiceDetailPage() {\n const { id } = useParams<{ id: string }>();\n\n useTrackPageView('housingvista.legal.viewed', id);\n const navigate = useNavigate();\n const location = useLocation();\n const [contactOpen, setContactOpen] = useState(false);\n const { data: service, loading } = useLegalService(id);\n\n // Initial load only: a background refetch must not unmount the page, or the\n // consultation dialog loses its conversation and re-resolves in a loop.\n if (loading && !service) {\n return (\n <div className=\"p-6\">\n <LoadingSkeleton variant=\"detail\" />\n </div>\n );\n }\n\n if (!service) {\n return (\n <div className=\"p-6\">\n <EmptyState\n illustration=\"empty-search\"\n icon={<Scale className=\"h-12 w-12\" />}\n title=\"Legal Service Not Found\"\n description=\"The legal service you're looking for doesn't exist or is no longer offered.\"\n action={\n <button\n onClick={() => navigate('../legal')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Legal Services\n </button>\n }\n />\n </div>\n );\n }\n\n const unavailable = service.status === 'unavailable';\n const documents = service.documentsRequired ?? [];\n const breadcrumbs: BreadcrumbItem[] = [{ label: 'Legal', href: '../' }, { label: service.title }];\n\n return (\n <div className=\"flex flex-col gap-6 p-6\">\n <Breadcrumbs items={breadcrumbs} />\n\n {/* Header */}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n {getCategoryIcon(service.category)}\n </div>\n <div>\n <button\n onClick={() => navigate('../legal')}\n className=\"mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Legal Services\n </button>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{service.title}</h1>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <StatusBadge label={service.status} variant={getStatusVariant(service.status)} dot />\n <StatusBadge label={service.category.replace(/_/g, ' ')} variant=\"muted\" />\n <span className=\"text-sm text-text-secondary\">{service.providerName}</span>\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {unavailable ? (\n <Button type=\"button\" disabled>\n Currently Unavailable\n </Button>\n ) : (\n <Button type=\"button\" variant=\"default\" onClick={() => setContactOpen(true)}>\n <MessageCircle className=\"mr-1.5 h-4 w-4\" />\n Book Now\n </Button>\n )}\n </div>\n </div>\n\n <PagePurpose className=\"max-w-3xl\">\n Everything you need to decide whether to engage this legal provider — what the service covers, their rating and\n fees, how long it takes, and which documents to prepare. When you're ready, request a consultation to reach them\n through Messages.\n </PagePurpose>\n\n {/* Key facts — primary insight zone (one EmphasisPanel per page) */}\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Star className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Provider Rating</span>\n </div>\n <div className=\"mt-2 flex items-center gap-2\">\n <span className=\"text-xl font-bold text-text-primary\">{service.providerRating ?? '—'}</span>\n {service.providerRating != null && <RatingStars rating={service.providerRating} />}\n </div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <IndianRupee className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Price Range</span>\n </div>\n <div className=\"mt-2 text-xl font-bold text-text-primary\">{service.priceRange ?? '—'}</div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Est. Duration</span>\n </div>\n <div className=\"mt-2 text-sm font-bold text-text-primary\">{service.estimatedDuration ?? '—'}</div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <ShieldCheck className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Consultation</span>\n </div>\n <div className=\"mt-2 text-sm font-bold text-text-primary\">\n {service.consultationFee && service.consultationFee > 0\n ? formatCurrency(service.consultationFee)\n : 'Free'}\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Overview */}\n <div className=\"space-y-6 lg:col-span-2\">\n {service.description && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">About this service</h2>\n <p className=\"text-sm leading-relaxed text-text-secondary\">{service.description}</p>\n </GlassCard>\n )}\n\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <FileText className=\"h-5 w-5\" />\n Documents Required\n </h2>\n {documents.length > 0 ? (\n <ul className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n {documents.map((doc, i) => (\n <li\n key={i}\n className=\"flex items-center gap-2 rounded-lg bg-bg-sunken/50 p-3 text-sm text-text-primary\"\n >\n <ClipboardCheck className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n {doc}\n </li>\n ))}\n </ul>\n ) : (\n <EmptyState\n illustration=\"empty-files\"\n title=\"No documents listed\"\n description=\"The provider will confirm what to prepare during your consultation.\"\n className=\"border-0 bg-transparent p-4\"\n />\n )}\n </GlassCard>\n </div>\n\n {/* Provider card */}\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h3 className=\"mb-4 text-base font-semibold text-text-primary\">Provider</h3>\n <dl className=\"space-y-3 text-sm\">\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Name</dt>\n <dd className=\"font-medium text-text-primary\">{service.providerName}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Rating</dt>\n <dd className=\"font-medium text-text-primary\">{service.providerRating ?? '—'}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Availability</dt>\n <dd>\n <StatusBadge label={service.status} variant={getStatusVariant(service.status)} />\n </dd>\n </div>\n </dl>\n {unavailable ? (\n <Button type=\"button\" className=\"mt-4 w-full justify-center\" disabled>\n Currently Unavailable\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"mt-4 w-full justify-center\"\n onClick={() => setContactOpen(true)}\n >\n Request a consultation\n </Button>\n )}\n </GlassCard>\n </div>\n </div>\n\n {/* Reviews — real, backed by EntityComments */}\n <ReviewsSection\n entityId={service.id}\n entityType=\"housingvista-legal-service\"\n aggregateRating={service.providerRating}\n />\n\n <ContactOwnerDialog\n open={contactOpen}\n onOpenChange={setContactOpen}\n entityId={service.id}\n entityType=\"housingvista-legal-service\"\n title={service.title}\n pagePath={location.pathname}\n heading={`Contact ${service.providerName}`}\n description={`Request a consultation about ${service.title}. Messages are saved and delivered in your inbox.`}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LegalServiceDetailPage.js","names":[],"sources":["../../../../src/housingvista/pages/legal/LegalServiceDetailPage.tsx"],"sourcesContent":["/**\n * HousingVista Legal Service Detail Page\n * Single legal-service provider backed by `housingvistaLegalService(id)` —\n * category, provider, rating, price, duration, documents required.\n *\n * \"Book Now\" / \"Request a consultation\" opens the real `ContactOwnerDialog` — a\n * persisted conversation with the provider. Reviews are real, backed by\n * EntityComments.\n * @module housingvista\n */\n\nimport { useState } from 'react';\nimport { useNavigate, useParams, useLocation } from 'react-router';\nimport {\n ArrowLeft,\n Scale,\n Star,\n Clock,\n FileText,\n Search as SearchIcon,\n Gavel,\n BookOpen,\n ClipboardCheck,\n MessageCircle,\n ShieldCheck,\n IndianRupee,\n} from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n Breadcrumbs,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n ContactOwnerDialog,\n ReviewsSection,\n} from '../../components';\nimport type { BreadcrumbItem } from '../../components';\nimport { useLegalService } from '../../hooks';\nimport { cn, formatCurrency } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\nfunction getCategoryIcon(category: string) {\n switch (category) {\n case 'title_search':\n return <SearchIcon className=\"h-5 w-5\" />;\n case 'lease_review':\n return <FileText className=\"h-5 w-5\" />;\n case 'dispute_resolution':\n return <Gavel className=\"h-5 w-5\" />;\n case 'registration':\n return <ClipboardCheck className=\"h-5 w-5\" />;\n case 'compliance':\n return <BookOpen className=\"h-5 w-5\" />;\n default:\n return <Scale className=\"h-5 w-5\" />;\n }\n}\n\nfunction getStatusVariant(status: string): 'success' | 'warning' | 'error' | 'muted' {\n switch (status) {\n case 'available':\n return 'success';\n case 'busy':\n return 'warning';\n case 'unavailable':\n return 'error';\n default:\n return 'muted';\n }\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5 text-status-warning-text\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn('h-4 w-4', i < Math.floor(rating) ? 'fill-current' : 'fill-none text-text-secondary/30')}\n />\n ))}\n </div>\n );\n}\n\nexport function LegalServiceDetailPage() {\n const { id } = useParams<{ id: string }>();\n\n useTrackPageView('housingvista.legal.viewed', id);\n const navigate = useNavigate();\n const location = useLocation();\n const [contactOpen, setContactOpen] = useState(false);\n const { data: service, loading } = useLegalService(id);\n\n if (loading) {\n return (\n <div className=\"p-6\">\n <LoadingSkeleton variant=\"detail\" />\n </div>\n );\n }\n\n if (!service) {\n return (\n <div className=\"p-6\">\n <EmptyState\n illustration=\"empty-search\"\n icon={<Scale className=\"h-12 w-12\" />}\n title=\"Legal Service Not Found\"\n description=\"The legal service you're looking for doesn't exist or is no longer offered.\"\n action={\n <button\n onClick={() => navigate('../legal')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Legal Services\n </button>\n }\n />\n </div>\n );\n }\n\n const unavailable = service.status === 'unavailable';\n const documents = service.documentsRequired ?? [];\n const breadcrumbs: BreadcrumbItem[] = [{ label: 'Legal', href: '../' }, { label: service.title }];\n\n return (\n <div className=\"flex flex-col gap-6 p-6\">\n <Breadcrumbs items={breadcrumbs} />\n\n {/* Header */}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n {getCategoryIcon(service.category)}\n </div>\n <div>\n <button\n onClick={() => navigate('../legal')}\n className=\"mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Legal Services\n </button>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{service.title}</h1>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <StatusBadge label={service.status} variant={getStatusVariant(service.status)} dot />\n <StatusBadge label={service.category.replace(/_/g, ' ')} variant=\"muted\" />\n <span className=\"text-sm text-text-secondary\">{service.providerName}</span>\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {unavailable ? (\n <Button type=\"button\" disabled>\n Currently Unavailable\n </Button>\n ) : (\n <Button type=\"button\" variant=\"default\" onClick={() => setContactOpen(true)}>\n <MessageCircle className=\"mr-1.5 h-4 w-4\" />\n Book Now\n </Button>\n )}\n </div>\n </div>\n\n <PagePurpose className=\"max-w-3xl\">\n Everything you need to decide whether to engage this legal provider — what the service covers, their rating and\n fees, how long it takes, and which documents to prepare. When you're ready, request a consultation to reach them\n through Messages.\n </PagePurpose>\n\n {/* Key facts — primary insight zone (one EmphasisPanel per page) */}\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Star className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Provider Rating</span>\n </div>\n <div className=\"mt-2 flex items-center gap-2\">\n <span className=\"text-xl font-bold text-text-primary\">{service.providerRating ?? '—'}</span>\n {service.providerRating != null && <RatingStars rating={service.providerRating} />}\n </div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <IndianRupee className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Price Range</span>\n </div>\n <div className=\"mt-2 text-xl font-bold text-text-primary\">{service.priceRange ?? '—'}</div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Est. Duration</span>\n </div>\n <div className=\"mt-2 text-sm font-bold text-text-primary\">{service.estimatedDuration ?? '—'}</div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <ShieldCheck className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Consultation</span>\n </div>\n <div className=\"mt-2 text-sm font-bold text-text-primary\">\n {service.consultationFee && service.consultationFee > 0\n ? formatCurrency(service.consultationFee)\n : 'Free'}\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Overview */}\n <div className=\"space-y-6 lg:col-span-2\">\n {service.description && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">About this service</h2>\n <p className=\"text-sm leading-relaxed text-text-secondary\">{service.description}</p>\n </GlassCard>\n )}\n\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <FileText className=\"h-5 w-5\" />\n Documents Required\n </h2>\n {documents.length > 0 ? (\n <ul className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n {documents.map((doc, i) => (\n <li\n key={i}\n className=\"flex items-center gap-2 rounded-lg bg-bg-sunken/50 p-3 text-sm text-text-primary\"\n >\n <ClipboardCheck className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n {doc}\n </li>\n ))}\n </ul>\n ) : (\n <EmptyState\n illustration=\"empty-files\"\n title=\"No documents listed\"\n description=\"The provider will confirm what to prepare during your consultation.\"\n className=\"border-0 bg-transparent p-4\"\n />\n )}\n </GlassCard>\n </div>\n\n {/* Provider card */}\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h3 className=\"mb-4 text-base font-semibold text-text-primary\">Provider</h3>\n <dl className=\"space-y-3 text-sm\">\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Name</dt>\n <dd className=\"font-medium text-text-primary\">{service.providerName}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Rating</dt>\n <dd className=\"font-medium text-text-primary\">{service.providerRating ?? '—'}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Availability</dt>\n <dd>\n <StatusBadge label={service.status} variant={getStatusVariant(service.status)} />\n </dd>\n </div>\n </dl>\n {unavailable ? (\n <Button type=\"button\" className=\"mt-4 w-full justify-center\" disabled>\n Currently Unavailable\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"mt-4 w-full justify-center\"\n onClick={() => setContactOpen(true)}\n >\n Request a consultation\n </Button>\n )}\n </GlassCard>\n </div>\n </div>\n\n {/* Reviews — real, backed by EntityComments */}\n <ReviewsSection\n entityId={service.id}\n entityType=\"housingvista-legal-service\"\n aggregateRating={service.providerRating}\n />\n\n <ContactOwnerDialog\n open={contactOpen}\n onOpenChange={setContactOpen}\n entityId={service.id}\n entityType=\"housingvista-legal-service\"\n title={service.title}\n pagePath={location.pathname}\n heading={`Contact ${service.providerName}`}\n description={`Request a consultation about ${service.title}. Messages are saved and delivered in your inbox.`}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServiceDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/marketplace/ServiceDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA4EH,wBAAgB,iBAAiB,
|
|
1
|
+
{"version":3,"file":"ServiceDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/housingvista/pages/marketplace/ServiceDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA4EH,wBAAgB,iBAAiB,gCA4NhC"}
|
|
@@ -36,7 +36,7 @@ function I() {
|
|
|
36
36
|
let { id: e } = D();
|
|
37
37
|
a("housingvista.marketplace.viewed", e);
|
|
38
38
|
let h = E(), _ = T(), [v, y] = l(!1), { data: x, loading: C } = o(e);
|
|
39
|
-
if (C
|
|
39
|
+
if (C) return /* @__PURE__ */ O("div", {
|
|
40
40
|
className: "p-6",
|
|
41
41
|
children: /* @__PURE__ */ O(i, { variant: "detail" })
|
|
42
42
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServiceDetailPage.js","names":[],"sources":["../../../../src/housingvista/pages/marketplace/ServiceDetailPage.tsx"],"sourcesContent":["/**\n * HousingVista Marketplace Service Detail Page\n * Single service provider backed by `housingvistaMarketplaceService(id)` —\n * rating, review count, response time, service areas, verified status.\n *\n * \"Contact\" opens the real `ContactOwnerDialog` — a persisted conversation with\n * the provider. Reviews are real, backed by EntityComments; the denormalized\n * rating/reviewCount stays for at-a-glance display.\n * @module housingvista\n */\n\nimport { useState } from 'react';\nimport { useNavigate, useParams, useLocation } from 'react-router';\nimport {\n ArrowLeft,\n Star,\n MapPin,\n Clock,\n CheckCircle,\n MessageCircle,\n Phone,\n Wrench,\n Zap,\n SprayCanIcon,\n Paintbrush,\n Shield,\n ShieldCheck,\n Trees,\n Users,\n} from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n Breadcrumbs,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n ContactOwnerDialog,\n ReviewsSection,\n} from '../../components';\nimport type { BreadcrumbItem } from '../../components';\nimport { useMarketplaceService } from '../../hooks';\nimport { cn } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\nfunction getCategoryIcon(category: string) {\n switch (category) {\n case 'repair':\n return <Wrench className=\"h-5 w-5\" />;\n case 'maintenance':\n return <Zap className=\"h-5 w-5\" />;\n case 'cleaning':\n return <SprayCanIcon className=\"h-5 w-5\" />;\n case 'renovation':\n return <Paintbrush className=\"h-5 w-5\" />;\n case 'security':\n return <Shield className=\"h-5 w-5\" />;\n case 'landscaping':\n return <Trees className=\"h-5 w-5\" />;\n case 'pest_control':\n return <ShieldCheck className=\"h-5 w-5\" />;\n default:\n return <Wrench className=\"h-5 w-5\" />;\n }\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5 text-status-warning-text\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'h-4 w-4',\n i < Math.floor(rating)\n ? 'fill-current'\n : i < rating\n ? 'fill-current opacity-50'\n : 'fill-none text-text-secondary/30'\n )}\n />\n ))}\n </div>\n );\n}\n\nexport function ServiceDetailPage() {\n const { id } = useParams<{ id: string }>();\n\n useTrackPageView('housingvista.marketplace.viewed', id);\n const navigate = useNavigate();\n const location = useLocation();\n const [contactOpen, setContactOpen] = useState(false);\n const { data: service, loading } = useMarketplaceService(id);\n\n // Initial load only: a background refetch must not unmount the page, or the\n // contact dialog loses its conversation and re-resolves in a loop.\n if (loading && !service) {\n return (\n <div className=\"p-6\">\n <LoadingSkeleton variant=\"detail\" />\n </div>\n );\n }\n\n if (!service) {\n return (\n <div className=\"p-6\">\n <EmptyState\n illustration=\"empty-search\"\n icon={<Users className=\"h-12 w-12\" />}\n title=\"Service Not Found\"\n description=\"The service provider you're looking for doesn't exist or is no longer listed.\"\n action={\n <button\n onClick={() => navigate('../marketplace')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Marketplace\n </button>\n }\n />\n </div>\n );\n }\n\n const areas = service.availableAreas ?? [];\n const breadcrumbs: BreadcrumbItem[] = [{ label: 'Marketplace', href: '../' }, { label: service.name }];\n\n return (\n <div className=\"flex flex-col gap-6 p-6\">\n <Breadcrumbs items={breadcrumbs} />\n\n {/* Header */}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n {getCategoryIcon(service.category)}\n </div>\n <div>\n <button\n onClick={() => navigate('../marketplace')}\n className=\"mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Marketplace\n </button>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{service.name}</h1>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <StatusBadge label={service.category.replace(/_/g, ' ')} variant=\"muted\" />\n {service.verified && <StatusBadge label=\"Verified\" variant=\"success\" dot />}\n {service.featured && <StatusBadge label=\"Featured\" variant=\"warning\" />}\n <span className=\"text-sm text-text-secondary\">{service.providerName}</span>\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"default\" onClick={() => setContactOpen(true)}>\n <MessageCircle className=\"mr-1.5 h-4 w-4\" />\n Contact\n </Button>\n </div>\n </div>\n\n <PagePurpose className=\"max-w-3xl\">\n Everything you need to decide whether to hire this provider — their rating and reviews, how fast they respond,\n the areas they cover, and whether they're verified. When you're ready, contact them through Messages.\n </PagePurpose>\n\n {/* Key facts — primary insight zone (one EmphasisPanel per page) */}\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Star className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Rating</span>\n </div>\n <div className=\"mt-2 flex items-center gap-2\">\n <span className=\"text-xl font-bold text-text-primary\">{service.rating ?? '—'}</span>\n {service.rating != null && <RatingStars rating={service.rating} />}\n </div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Users className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Reviews</span>\n </div>\n <div className=\"mt-2 text-xl font-bold text-text-primary\">{service.reviewCount}</div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Response Time</span>\n </div>\n <div className=\"mt-2 text-sm font-bold text-text-primary\">{service.responseTime ?? '—'}</div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <MapPin className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Coverage</span>\n </div>\n <div className=\"mt-2 text-sm font-bold text-text-primary\">\n {areas.length} area{areas.length !== 1 ? 's' : ''}\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Overview */}\n <div className=\"space-y-6 lg:col-span-2\">\n {service.imageUrl && (\n <div className=\"overflow-hidden rounded-xl border border-border-subtle\">\n <img src={service.imageUrl} alt={service.name} className=\"h-64 w-full object-cover\" />\n </div>\n )}\n\n {service.description && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">About this provider</h2>\n <p className=\"text-sm leading-relaxed text-text-secondary\">{service.description}</p>\n </GlassCard>\n )}\n\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <MapPin className=\"h-5 w-5\" />\n Service Areas\n </h2>\n {areas.length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {areas.map((area, i) => (\n <span key={i} className=\"rounded-full bg-bg-sunken px-3 py-1 text-xs text-text-primary\">\n {area}\n </span>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">Service areas not specified — ask the provider directly.</p>\n )}\n </GlassCard>\n </div>\n\n {/* Provider card */}\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h3 className=\"mb-4 text-base font-semibold text-text-primary\">Provider</h3>\n <dl className=\"space-y-3 text-sm\">\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Name</dt>\n <dd className=\"font-medium text-text-primary\">{service.providerName}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Category</dt>\n <dd className=\"font-medium capitalize text-text-primary\">{service.category.replace(/_/g, ' ')}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Price Range</dt>\n <dd className=\"font-medium text-text-primary\">{service.priceRange ?? '—'}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Verified</dt>\n <dd>\n {service.verified ? (\n <span className=\"inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle className=\"h-4 w-4\" />\n Yes\n </span>\n ) : (\n <span className=\"text-text-secondary\">Not verified</span>\n )}\n </dd>\n </div>\n </dl>\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"mt-4 w-full justify-center\"\n onClick={() => setContactOpen(true)}\n >\n <Phone className=\"mr-1.5 h-4 w-4\" />\n Contact provider\n </Button>\n </GlassCard>\n </div>\n </div>\n\n {/* Reviews — real, backed by EntityComments */}\n <ReviewsSection\n entityId={service.id}\n entityType=\"housingvista-marketplace-service\"\n aggregateRating={service.rating}\n reviewCount={service.reviewCount}\n />\n\n <ContactOwnerDialog\n open={contactOpen}\n onOpenChange={setContactOpen}\n entityId={service.id}\n entityType=\"housingvista-marketplace-service\"\n title={service.name}\n pagePath={location.pathname}\n heading={`Contact ${service.providerName}`}\n description={`Start a conversation about ${service.name}. Messages are saved and delivered in your inbox.`}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ServiceDetailPage.js","names":[],"sources":["../../../../src/housingvista/pages/marketplace/ServiceDetailPage.tsx"],"sourcesContent":["/**\n * HousingVista Marketplace Service Detail Page\n * Single service provider backed by `housingvistaMarketplaceService(id)` —\n * rating, review count, response time, service areas, verified status.\n *\n * \"Contact\" opens the real `ContactOwnerDialog` — a persisted conversation with\n * the provider. Reviews are real, backed by EntityComments; the denormalized\n * rating/reviewCount stays for at-a-glance display.\n * @module housingvista\n */\n\nimport { useState } from 'react';\nimport { useNavigate, useParams, useLocation } from 'react-router';\nimport {\n ArrowLeft,\n Star,\n MapPin,\n Clock,\n CheckCircle,\n MessageCircle,\n Phone,\n Wrench,\n Zap,\n SprayCanIcon,\n Paintbrush,\n Shield,\n ShieldCheck,\n Trees,\n Users,\n} from 'lucide-react';\nimport { Button, EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport {\n Breadcrumbs,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n ContactOwnerDialog,\n ReviewsSection,\n} from '../../components';\nimport type { BreadcrumbItem } from '../../components';\nimport { useMarketplaceService } from '../../hooks';\nimport { cn } from '../../utils/helpers';\nimport { useTrackPageView } from '../../hooks/useHousingVistaAnalytics';\n\nfunction getCategoryIcon(category: string) {\n switch (category) {\n case 'repair':\n return <Wrench className=\"h-5 w-5\" />;\n case 'maintenance':\n return <Zap className=\"h-5 w-5\" />;\n case 'cleaning':\n return <SprayCanIcon className=\"h-5 w-5\" />;\n case 'renovation':\n return <Paintbrush className=\"h-5 w-5\" />;\n case 'security':\n return <Shield className=\"h-5 w-5\" />;\n case 'landscaping':\n return <Trees className=\"h-5 w-5\" />;\n case 'pest_control':\n return <ShieldCheck className=\"h-5 w-5\" />;\n default:\n return <Wrench className=\"h-5 w-5\" />;\n }\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5 text-status-warning-text\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'h-4 w-4',\n i < Math.floor(rating)\n ? 'fill-current'\n : i < rating\n ? 'fill-current opacity-50'\n : 'fill-none text-text-secondary/30'\n )}\n />\n ))}\n </div>\n );\n}\n\nexport function ServiceDetailPage() {\n const { id } = useParams<{ id: string }>();\n\n useTrackPageView('housingvista.marketplace.viewed', id);\n const navigate = useNavigate();\n const location = useLocation();\n const [contactOpen, setContactOpen] = useState(false);\n const { data: service, loading } = useMarketplaceService(id);\n\n if (loading) {\n return (\n <div className=\"p-6\">\n <LoadingSkeleton variant=\"detail\" />\n </div>\n );\n }\n\n if (!service) {\n return (\n <div className=\"p-6\">\n <EmptyState\n illustration=\"empty-search\"\n icon={<Users className=\"h-12 w-12\" />}\n title=\"Service Not Found\"\n description=\"The service provider you're looking for doesn't exist or is no longer listed.\"\n action={\n <button\n onClick={() => navigate('../marketplace')}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg-hover\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Marketplace\n </button>\n }\n />\n </div>\n );\n }\n\n const areas = service.availableAreas ?? [];\n const breadcrumbs: BreadcrumbItem[] = [{ label: 'Marketplace', href: '../' }, { label: service.name }];\n\n return (\n <div className=\"flex flex-col gap-6 p-6\">\n <Breadcrumbs items={breadcrumbs} />\n\n {/* Header */}\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n {getCategoryIcon(service.category)}\n </div>\n <div>\n <button\n onClick={() => navigate('../marketplace')}\n className=\"mb-2 inline-flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary transition-colors hover:bg-bg-sunken hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n Back to Marketplace\n </button>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{service.name}</h1>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <StatusBadge label={service.category.replace(/_/g, ' ')} variant=\"muted\" />\n {service.verified && <StatusBadge label=\"Verified\" variant=\"success\" dot />}\n {service.featured && <StatusBadge label=\"Featured\" variant=\"warning\" />}\n <span className=\"text-sm text-text-secondary\">{service.providerName}</span>\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <Button type=\"button\" variant=\"default\" onClick={() => setContactOpen(true)}>\n <MessageCircle className=\"mr-1.5 h-4 w-4\" />\n Contact\n </Button>\n </div>\n </div>\n\n <PagePurpose className=\"max-w-3xl\">\n Everything you need to decide whether to hire this provider — their rating and reviews, how fast they respond,\n the areas they cover, and whether they're verified. When you're ready, contact them through Messages.\n </PagePurpose>\n\n {/* Key facts — primary insight zone (one EmphasisPanel per page) */}\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Star className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Rating</span>\n </div>\n <div className=\"mt-2 flex items-center gap-2\">\n <span className=\"text-xl font-bold text-text-primary\">{service.rating ?? '—'}</span>\n {service.rating != null && <RatingStars rating={service.rating} />}\n </div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Users className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Reviews</span>\n </div>\n <div className=\"mt-2 text-xl font-bold text-text-primary\">{service.reviewCount}</div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Response Time</span>\n </div>\n <div className=\"mt-2 text-sm font-bold text-text-primary\">{service.responseTime ?? '—'}</div>\n </div>\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-4\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <MapPin className=\"h-4 w-4\" />\n <span className=\"text-xs font-medium\">Coverage</span>\n </div>\n <div className=\"mt-2 text-sm font-bold text-text-primary\">\n {areas.length} area{areas.length !== 1 ? 's' : ''}\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Overview */}\n <div className=\"space-y-6 lg:col-span-2\">\n {service.imageUrl && (\n <div className=\"overflow-hidden rounded-xl border border-border-subtle\">\n <img src={service.imageUrl} alt={service.name} className=\"h-64 w-full object-cover\" />\n </div>\n )}\n\n {service.description && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">About this provider</h2>\n <p className=\"text-sm leading-relaxed text-text-secondary\">{service.description}</p>\n </GlassCard>\n )}\n\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <h2 className=\"mb-4 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <MapPin className=\"h-5 w-5\" />\n Service Areas\n </h2>\n {areas.length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {areas.map((area, i) => (\n <span key={i} className=\"rounded-full bg-bg-sunken px-3 py-1 text-xs text-text-primary\">\n {area}\n </span>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-text-secondary\">Service areas not specified — ask the provider directly.</p>\n )}\n </GlassCard>\n </div>\n\n {/* Provider card */}\n <div className=\"space-y-6\">\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-5\">\n <h3 className=\"mb-4 text-base font-semibold text-text-primary\">Provider</h3>\n <dl className=\"space-y-3 text-sm\">\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Name</dt>\n <dd className=\"font-medium text-text-primary\">{service.providerName}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Category</dt>\n <dd className=\"font-medium capitalize text-text-primary\">{service.category.replace(/_/g, ' ')}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Price Range</dt>\n <dd className=\"font-medium text-text-primary\">{service.priceRange ?? '—'}</dd>\n </div>\n <div className=\"flex items-center justify-between\">\n <dt className=\"text-text-secondary\">Verified</dt>\n <dd>\n {service.verified ? (\n <span className=\"inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle className=\"h-4 w-4\" />\n Yes\n </span>\n ) : (\n <span className=\"text-text-secondary\">Not verified</span>\n )}\n </dd>\n </div>\n </dl>\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"mt-4 w-full justify-center\"\n onClick={() => setContactOpen(true)}\n >\n <Phone className=\"mr-1.5 h-4 w-4\" />\n Contact provider\n </Button>\n </GlassCard>\n </div>\n </div>\n\n {/* Reviews — real, backed by EntityComments */}\n <ReviewsSection\n entityId={service.id}\n entityType=\"housingvista-marketplace-service\"\n aggregateRating={service.rating}\n reviewCount={service.reviewCount}\n />\n\n <ContactOwnerDialog\n open={contactOpen}\n onOpenChange={setContactOpen}\n entityId={service.id}\n entityType=\"housingvista-marketplace-service\"\n title={service.name}\n pagePath={location.pathname}\n heading={`Contact ${service.providerName}`}\n description={`Start a conversation about ${service.name}. Messages are saved and delivered in your inbox.`}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA4CA,SAAS,EAAgB,GAAkB;AACzC,SAAQ,GAAR;EACE,KAAK,SACH,QAAO,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACvC,KAAK,cACH,QAAO,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;EACpC,KAAK,WACH,QAAO,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA;EAC7C,KAAK,aACH,QAAO,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;EAC3C,KAAK,WACH,QAAO,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;EACvC,KAAK,cACH,QAAO,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;EACtC,KAAK,eACH,QAAO,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;EAC5C,QACE,QAAO,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;;;AAI3C,SAAS,EAAY,EAAE,aAA8B;AACnD,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,MAAM,KAAK,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG,MACjC,kBAAC,GAAD,EAEE,WAAW,EACT,WACA,IAAI,KAAK,MAAM,EAAO,GAClB,iBACA,IAAI,IACF,4BACA,mCACP,EACD,EATK,EASL,CACF;EACE,CAAA;;AAIV,SAAgB,IAAoB;CAClC,IAAM,EAAE,UAAO,GAA2B;AAE1C,GAAiB,mCAAmC,EAAG;CACvD,IAAM,IAAW,GAAa,EACxB,IAAW,GAAa,EACxB,CAAC,GAAa,KAAkB,EAAS,GAAM,EAC/C,EAAE,MAAM,GAAS,eAAY,EAAsB,EAAG;AAE5D,KAAI,EACF,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAiB,SAAQ,UAAW,CAAA;EAChC,CAAA;AAIV,KAAI,CAAC,EACH,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAO,WAAU,aAAc,CAAA;GACrC,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,UAAD;IACE,eAAe,EAAS,iBAAiB;IACzC,WAAU;cAFZ,CAIE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,sBAE1B;;GAEX,CAAA;EACE,CAAA;CAIV,IAAM,IAAQ,EAAQ,kBAAkB,EAAE;AAG1C,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD,EAAa,OAJqB,CAAC;IAAE,OAAO;IAAe,MAAM;IAAO,EAAE,EAAE,OAAO,EAAQ,MAAM,CAAC,EAI/D,CAAA;GAGnC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAgB,EAAQ,SAAS;MAC9B,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;MACE,kBAAC,UAAD;OACE,eAAe,EAAS,iBAAiB;OACzC,WAAU;iBAFZ,CAIE,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA,EAAA,sBAE1B;;MACT,kBAAC,MAAD;OAAI,WAAU;iBAA4C,EAAQ;OAAU,CAAA;MAC5E,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAa,OAAO,EAAQ,SAAS,QAAQ,MAAM,IAAI;SAAE,SAAQ;SAAU,CAAA;QAC1E,EAAQ,YAAY,kBAAC,GAAD;SAAa,OAAM;SAAW,SAAQ;SAAU,KAAA;SAAM,CAAA;QAC1E,EAAQ,YAAY,kBAAC,GAAD;SAAa,OAAM;SAAW,SAAQ;SAAY,CAAA;QACvE,kBAAC,QAAD;SAAM,WAAU;mBAA+B,EAAQ;SAAoB,CAAA;QACvE;;MACF,EAAA,CAAA,CACF;QACN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAQ,MAAK;MAAS,SAAQ;MAAU,eAAe,EAAe,GAAK;gBAA3E,CACE,kBAAC,GAAD,EAAe,WAAU,kBAAmB,CAAA,EAAA,UAErC;;KACL,CAAA,CACF;;GAEN,kBAAC,GAAD;IAAa,WAAU;cAAY;IAGrB,CAAA;GAGd,kBAAC,GAAD;IAAe,WAAU;cACvB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC5B,kBAAC,QAAD;SAAM,WAAU;mBAAsB;SAAa,CAAA,CAC/C;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAuC,EAAQ,UAAU;SAAW,CAAA,EACnF,EAAQ,UAAU,QAAQ,kBAAC,GAAD,EAAa,QAAQ,EAAQ,QAAU,CAAA,CAC9D;UACF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,EAC7B,kBAAC,QAAD;SAAM,WAAU;mBAAsB;SAAc,CAAA,CAChD;WACN,kBAAC,OAAD;QAAK,WAAU;kBAA4C,EAAQ;QAAkB,CAAA,CACjF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,EAC7B,kBAAC,QAAD;SAAM,WAAU;mBAAsB;SAAoB,CAAA,CACtD;WACN,kBAAC,OAAD;QAAK,WAAU;kBAA4C,EAAQ,gBAAgB;QAAU,CAAA,CACzF;;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAC9B,kBAAC,QAAD;SAAM,WAAU;mBAAsB;SAAe,CAAA,CACjD;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACG,EAAM;SAAO;SAAM,EAAM,WAAW,IAAU,KAAN;SACrC;UACF;;MACF;;IACQ,CAAA;GAEhB,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,EAAQ,YACP,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,OAAD;QAAK,KAAK,EAAQ;QAAU,KAAK,EAAQ;QAAM,WAAU;QAA6B,CAAA;OAClF,CAAA;MAGP,EAAQ,eACP,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBAAvC,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAiD;QAAwB,CAAA,EACvF,kBAAC,KAAD;QAAG,WAAU;kBAA+C,EAAQ;QAAgB,CAAA,CAC1E;;MAGd,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBAAvC,CACE,kBAAC,MAAD;QAAI,WAAU;kBAAd,CACE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAAA,gBAE3B;WACJ,EAAM,SAAS,IACd,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAM,KAAK,GAAM,MAChB,kBAAC,QAAD;SAAc,WAAU;mBACrB;SACI,EAFI,EAEJ,CACP;QACE,CAAA,GAEN,kBAAC,KAAD;QAAG,WAAU;kBAA8B;QAA4D,CAAA,CAE/F;;MACR;QAGN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAW,WAAU;MAAQ,WAAU;gBAAvC;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAiD;QAAa,CAAA;OAC5E,kBAAC,MAAD;QAAI,WAAU;kBAAd;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,MAAD;WAAI,WAAU;qBAAsB;WAAS,CAAA,EAC7C,kBAAC,MAAD;WAAI,WAAU;qBAAiC,EAAQ;WAAkB,CAAA,CACrE;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,MAAD;WAAI,WAAU;qBAAsB;WAAa,CAAA,EACjD,kBAAC,MAAD;WAAI,WAAU;qBAA4C,EAAQ,SAAS,QAAQ,MAAM,IAAI;WAAM,CAAA,CAC/F;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,MAAD;WAAI,WAAU;qBAAsB;WAAgB,CAAA,EACpD,kBAAC,MAAD;WAAI,WAAU;qBAAiC,EAAQ,cAAc;WAAS,CAAA,CAC1E;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,MAAD;WAAI,WAAU;qBAAsB;WAAa,CAAA,EACjD,kBAAC,MAAD,EAAA,UACG,EAAQ,WACP,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CACE,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA,EAAA,MAE9B;eAEP,kBAAC,QAAD;WAAM,WAAU;qBAAsB;WAAmB,CAAA,EAExD,CAAA,CACD;;SACH;;OACL,kBAAC,GAAD;QACE,MAAK;QACL,SAAQ;QACR,WAAU;QACV,eAAe,EAAe,GAAK;kBAJrC,CAME,kBAAC,GAAD,EAAO,WAAU,kBAAmB,CAAA,EAAA,mBAE7B;;OACC;;KACR,CAAA,CACF;;GAGN,kBAAC,GAAD;IACE,UAAU,EAAQ;IAClB,YAAW;IACX,iBAAiB,EAAQ;IACzB,aAAa,EAAQ;IACrB,CAAA;GAEF,kBAAC,GAAD;IACE,MAAM;IACN,cAAc;IACd,UAAU,EAAQ;IAClB,YAAW;IACX,OAAO,EAAQ;IACf,UAAU,EAAS;IACnB,SAAS,WAAW,EAAQ;IAC5B,aAAa,8BAA8B,EAAQ,KAAK;IACxD,CAAA;GACE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-housingvista",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1031.1",
|
|
4
4
|
"description": "Burdenoff HousingVista Microfrontend - Comprehensive real estate platform for property management, investment, and transactions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|